헤더∙로그인 UI 정비하고 벤더 오류 메시지 안정화
- 로그인 화면 상단에 브랜드 아이콘과 안내 문구를 추가하고 카드/푸터 레이아웃을 정리 - 네비게이션 레일/앱바 구성 재배치, 테마 토글 상단 이동, 그라데이션/브랜드 아이콘 스타일 조정 - ScaffoldMessenger 의존성 문제를 피하도록 벤더 컨트롤러 에러 스낵바 호출 시점을 프레임 이후로 이연 - IMPLEMENTATION_TASKS.md에 이번 변경 내역을 요약하여 진행 상황을 문서화
This commit is contained in:
@@ -2,6 +2,12 @@
|
|||||||
|
|
||||||
본 체크리스트는 PRD(`doc/PRD_입출고_결재_v2.md`)를 기준으로 shadcn_ui 스타일과 반응형 패턴을 준수하여 화면을 구현하기 위한 단계별 작업 목록입니다. 작업 순서는 ① 코드 시작 전 최종 확인 → ② UI 스캐폴딩/상호작용 구현 → ③ 실제 API 연동(Dio/ApiClient/DI)입니다. Mock 데이터는 사용하지 않습니다.
|
본 체크리스트는 PRD(`doc/PRD_입출고_결재_v2.md`)를 기준으로 shadcn_ui 스타일과 반응형 패턴을 준수하여 화면을 구현하기 위한 단계별 작업 목록입니다. 작업 순서는 ① 코드 시작 전 최종 확인 → ② UI 스캐폴딩/상호작용 구현 → ③ 실제 API 연동(Dio/ApiClient/DI)입니다. Mock 데이터는 사용하지 않습니다.
|
||||||
|
|
||||||
|
## 진행 상황 업데이트 (2024-08-08)
|
||||||
|
- 로그인 화면 상단에 브랜드 아이콘과 메시지를 추가하고 카드·푸터 구성을 재정리하여 Superport v2 공통 UI와 톤을 통일했습니다.
|
||||||
|
- 사이드바 네비게이션을 가로 정렬 구조로 전환하고 AppBar 브랜드 타이틀/테마 토글/로그아웃 버튼 배치를 재구성해 헤더 UX를 개선했습니다.
|
||||||
|
- AppBar 전역 그라데이션과 브랜드 아이콘 박스를 정돈(수직 그라데이션, 단색 아이콘 배경)해 시각적 집중도와 일관성을 확보했습니다.
|
||||||
|
- 벤더 화면 에러 처리 시 `ScaffoldMessenger` 의존성 오류가 발생하지 않도록 메시지 표시를 프레임 이후에 수행하도록 보강했습니다.
|
||||||
|
|
||||||
## 0) 코드 시작 전 최종 확인(Repository/환경)
|
## 0) 코드 시작 전 최종 확인(Repository/환경)
|
||||||
- [x] Flutter 버전/채널 확인, `flutter pub get`
|
- [x] Flutter 버전/채널 확인, `flutter pub get`
|
||||||
- [x] `pubspec.yaml` 확인: `go_router`, `shadcn_ui`, `intl`, `two_dimensional_scrollables`, `lucide_icons_flutter` 포함
|
- [x] `pubspec.yaml` 확인: `go_router`, `shadcn_ui`, `intl`, `two_dimensional_scrollables`, `lucide_icons_flutter` 포함
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:shadcn_ui/shadcn_ui.dart';
|
import 'package:shadcn_ui/shadcn_ui.dart';
|
||||||
@@ -64,81 +66,188 @@ class _LoginPageState extends State<LoginPage> {
|
|||||||
final theme = ShadTheme.of(context);
|
final theme = ShadTheme.of(context);
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: Center(
|
backgroundColor: theme.colorScheme.background,
|
||||||
child: ConstrainedBox(
|
body: SafeArea(
|
||||||
constraints: const BoxConstraints(maxWidth: 460),
|
child: Center(
|
||||||
child: Padding(
|
child: SingleChildScrollView(
|
||||||
padding: const EdgeInsets.all(24),
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 40),
|
||||||
child: ShadCard(
|
child: Column(
|
||||||
title: Text('Superport v2 로그인', style: theme.textTheme.h3),
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
description: Text(
|
children: [
|
||||||
'사번 또는 이메일과 비밀번호를 입력하여 대시보드로 이동합니다.',
|
_buildHeader(theme),
|
||||||
style: theme.textTheme.muted,
|
const SizedBox(height: 32),
|
||||||
),
|
ConstrainedBox(
|
||||||
child: Column(
|
constraints: const BoxConstraints(maxWidth: 460),
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
child: _buildLoginCard(theme),
|
||||||
children: [
|
),
|
||||||
ShadInput(
|
const SizedBox(height: 24),
|
||||||
controller: idController,
|
_buildFooter(theme),
|
||||||
placeholder: const Text('사번 또는 이메일'),
|
],
|
||||||
autofillHints: const [AutofillHints.username],
|
|
||||||
leading: const Icon(LucideIcons.user),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
ShadInput(
|
|
||||||
controller: passwordController,
|
|
||||||
placeholder: const Text('비밀번호'),
|
|
||||||
obscureText: true,
|
|
||||||
autofillHints: const [AutofillHints.password],
|
|
||||||
leading: const Icon(LucideIcons.lock),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
ShadSwitch(
|
|
||||||
value: rememberMe,
|
|
||||||
onChanged: (value) =>
|
|
||||||
setState(() => rememberMe = value),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
Text('자동 로그인', style: theme.textTheme.small),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 24),
|
|
||||||
if (errorMessage != null)
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(bottom: 12),
|
|
||||||
child: Text(
|
|
||||||
errorMessage!,
|
|
||||||
style: theme.textTheme.small.copyWith(
|
|
||||||
color: theme.colorScheme.destructive,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
ShadButton(
|
|
||||||
onPressed: isLoading ? null : _handleSubmit,
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
if (isLoading) ...[
|
|
||||||
const SizedBox(
|
|
||||||
width: 16,
|
|
||||||
height: 16,
|
|
||||||
child: CircularProgressIndicator(strokeWidth: 2),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
],
|
|
||||||
const Text('로그인'),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 로그인 헤더에 브랜드 아이콘과 안내 문구를 배치한다.
|
||||||
|
Widget _buildHeader(ShadThemeData theme) {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
DecoratedBox(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
gradient: LinearGradient(
|
||||||
|
colors: [
|
||||||
|
theme.colorScheme.primary,
|
||||||
|
theme.colorScheme.primary.withValues(alpha: 0.8),
|
||||||
|
theme.colorScheme.primary.withValues(alpha: 0.6),
|
||||||
|
],
|
||||||
|
begin: Alignment.topLeft,
|
||||||
|
end: Alignment.bottomRight,
|
||||||
|
),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: theme.colorScheme.primary.withValues(alpha: 0.3),
|
||||||
|
blurRadius: 24,
|
||||||
|
offset: const Offset(0, 12),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
child: TweenAnimationBuilder<double>(
|
||||||
|
duration: const Duration(milliseconds: 1400),
|
||||||
|
tween: Tween(begin: 0, end: 1),
|
||||||
|
curve: Curves.easeInOut,
|
||||||
|
builder: (context, value, child) {
|
||||||
|
return Transform.rotate(
|
||||||
|
angle: value * 2 * math.pi * 0.08,
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: Icon(
|
||||||
|
LucideIcons.ship,
|
||||||
|
size: 48,
|
||||||
|
color: theme.colorScheme.primaryForeground,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
Text(
|
||||||
|
'Superport v2',
|
||||||
|
style: theme.textTheme.h2.copyWith(
|
||||||
|
foreground: Paint()
|
||||||
|
..shader = LinearGradient(
|
||||||
|
colors: [
|
||||||
|
theme.colorScheme.primary,
|
||||||
|
theme.colorScheme.primary.withValues(alpha: 0.7),
|
||||||
|
],
|
||||||
|
).createShader(const Rect.fromLTWH(0, 0, 220, 80)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
'포트 운영 전용 스마트 ERP',
|
||||||
|
style: theme.textTheme.muted,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 로그인 폼 카드 레이아웃을 구성한다.
|
||||||
|
Widget _buildLoginCard(ShadThemeData theme) {
|
||||||
|
return ShadCard(
|
||||||
|
title: Text('Superport v2 로그인', style: theme.textTheme.h3),
|
||||||
|
description: Text(
|
||||||
|
'사번 또는 이메일과 비밀번호를 입력하여 대시보드로 이동합니다.',
|
||||||
|
style: theme.textTheme.muted,
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
ShadInput(
|
||||||
|
controller: idController,
|
||||||
|
placeholder: const Text('사번 또는 이메일'),
|
||||||
|
autofillHints: const [AutofillHints.username],
|
||||||
|
leading: const Icon(LucideIcons.user),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
ShadInput(
|
||||||
|
controller: passwordController,
|
||||||
|
placeholder: const Text('비밀번호'),
|
||||||
|
obscureText: true,
|
||||||
|
autofillHints: const [AutofillHints.password],
|
||||||
|
leading: const Icon(LucideIcons.lock),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
ShadSwitch(
|
||||||
|
value: rememberMe,
|
||||||
|
onChanged: (value) => setState(() => rememberMe = value),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Text('자동 로그인', style: theme.textTheme.small),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
if (errorMessage != null)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 12),
|
||||||
|
child: Text(
|
||||||
|
errorMessage!,
|
||||||
|
style: theme.textTheme.small.copyWith(
|
||||||
|
color: theme.colorScheme.destructive,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ShadButton(
|
||||||
|
onPressed: isLoading ? null : _handleSubmit,
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
if (isLoading) ...[
|
||||||
|
const SizedBox(
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
],
|
||||||
|
const Text('로그인'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 로그인 하단에 간단한 안내 메시지를 노출한다.
|
||||||
|
Widget _buildFooter(ShadThemeData theme) {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: const [
|
||||||
|
Icon(LucideIcons.packagePlus, size: 18),
|
||||||
|
SizedBox(width: 12),
|
||||||
|
Icon(LucideIcons.packageMinus, size: 18),
|
||||||
|
SizedBox(width: 12),
|
||||||
|
Icon(LucideIcons.handshake, size: 18),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
'입고 · 출고 · 대여 전 과정을 한 곳에서 관리하세요.',
|
||||||
|
style: theme.textTheme.small,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -116,10 +116,17 @@ class _VendorEnabledPageState extends State<_VendorEnabledPage> {
|
|||||||
final error = _controller.errorMessage;
|
final error = _controller.errorMessage;
|
||||||
if (error != null && error != _lastError && mounted) {
|
if (error != null && error != _lastError && mounted) {
|
||||||
_lastError = error;
|
_lastError = error;
|
||||||
ScaffoldMessenger.of(
|
// 스캐폴드 메시지는 프레임 빌드 이후 안전하게 호출한다.
|
||||||
context,
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
).showSnackBar(SnackBar(content: Text(error)));
|
if (!mounted) {
|
||||||
_controller.clearError();
|
return;
|
||||||
|
}
|
||||||
|
final messenger = ScaffoldMessenger.maybeOf(context);
|
||||||
|
if (messenger != null) {
|
||||||
|
messenger.showSnackBar(SnackBar(content: Text(error)));
|
||||||
|
}
|
||||||
|
_controller.clearError();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,18 +29,27 @@ class AppShell extends StatelessWidget {
|
|||||||
if (manager.can(page.path, PermissionAction.view)) page,
|
if (manager.can(page.path, PermissionAction.view)) page,
|
||||||
];
|
];
|
||||||
final pages = filteredPages.isEmpty ? allAppPages : filteredPages;
|
final pages = filteredPages.isEmpty ? allAppPages : filteredPages;
|
||||||
|
final themeController = ThemeControllerScope.of(context);
|
||||||
|
final appBar = _GradientAppBar(
|
||||||
|
title: const _BrandTitle(),
|
||||||
|
actions: [
|
||||||
|
_ThemeMenuButton(
|
||||||
|
mode: themeController.mode,
|
||||||
|
onChanged: themeController.update,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
IconButton(
|
||||||
|
tooltip: '로그아웃',
|
||||||
|
icon: const Icon(lucide.LucideIcons.logOut),
|
||||||
|
onPressed: () => context.go(loginRoutePath),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
if (isWide) {
|
if (isWide) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: appBar,
|
||||||
title: const Text('Superport v2'),
|
|
||||||
actions: [
|
|
||||||
IconButton(
|
|
||||||
tooltip: '로그아웃',
|
|
||||||
icon: const Icon(lucide.LucideIcons.logOut),
|
|
||||||
onPressed: () => context.go(loginRoutePath),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
body: Row(
|
body: Row(
|
||||||
children: [
|
children: [
|
||||||
_NavigationRail(currentLocation: currentLocation, pages: pages),
|
_NavigationRail(currentLocation: currentLocation, pages: pages),
|
||||||
@@ -52,16 +61,7 @@ class AppShell extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: appBar,
|
||||||
title: const Text('Superport v2'),
|
|
||||||
actions: [
|
|
||||||
IconButton(
|
|
||||||
tooltip: '로그아웃',
|
|
||||||
icon: const Icon(lucide.LucideIcons.logOut),
|
|
||||||
onPressed: () => context.go(loginRoutePath),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
drawer: Drawer(
|
drawer: Drawer(
|
||||||
child: SafeArea(
|
child: SafeArea(
|
||||||
child: _NavigationList(
|
child: _NavigationList(
|
||||||
@@ -92,18 +92,14 @@ class _NavigationRail extends StatelessWidget {
|
|||||||
final selectedIndex = _selectedIndex(currentLocation, pages);
|
final selectedIndex = _selectedIndex(currentLocation, pages);
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final colorScheme = theme.colorScheme;
|
final colorScheme = theme.colorScheme;
|
||||||
final themeController = ThemeControllerScope.of(context);
|
|
||||||
final currentThemeMode = themeController.mode;
|
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
width: 104,
|
width: 220,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
border: Border(right: BorderSide(color: colorScheme.outlineVariant)),
|
border: Border(right: BorderSide(color: colorScheme.outlineVariant)),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(height: 24),
|
|
||||||
const FlutterLogo(size: 48),
|
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: ListView.builder(
|
child: ListView.builder(
|
||||||
@@ -134,11 +130,10 @@ class _NavigationRail extends StatelessWidget {
|
|||||||
},
|
},
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
vertical: 12,
|
vertical: 10,
|
||||||
horizontal: 8,
|
horizontal: 12,
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
children: [
|
||||||
Icon(
|
Icon(
|
||||||
page.icon,
|
page.icon,
|
||||||
@@ -147,13 +142,14 @@ class _NavigationRail extends StatelessWidget {
|
|||||||
? colorScheme.primary
|
? colorScheme.primary
|
||||||
: colorScheme.onSurfaceVariant,
|
: colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 6),
|
const SizedBox(width: 12),
|
||||||
Text(
|
Expanded(
|
||||||
page.label,
|
child: Text(
|
||||||
textAlign: TextAlign.center,
|
page.label,
|
||||||
maxLines: 2,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: textStyle,
|
style: textStyle,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -164,13 +160,6 @@ class _NavigationRail extends StatelessWidget {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.fromLTRB(12, 8, 12, 16),
|
|
||||||
child: _ThemeMenuButton(
|
|
||||||
mode: currentThemeMode,
|
|
||||||
onChanged: themeController.update,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -228,6 +217,82 @@ class _NavigationList extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _BrandTitle extends StatelessWidget {
|
||||||
|
const _BrandTitle();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final colorScheme = theme.colorScheme;
|
||||||
|
|
||||||
|
return Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
DecoratedBox(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
color: colorScheme.primary,
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
child: Icon(
|
||||||
|
lucide.LucideIcons.ship,
|
||||||
|
size: 28,
|
||||||
|
color: colorScheme.onPrimary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Text(
|
||||||
|
'Superport v2',
|
||||||
|
style: theme.textTheme.titleLarge?.copyWith(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _GradientAppBar extends StatelessWidget implements PreferredSizeWidget {
|
||||||
|
const _GradientAppBar({required this.title, required this.actions});
|
||||||
|
|
||||||
|
final Widget title;
|
||||||
|
final List<Widget> actions;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final colorScheme = theme.colorScheme;
|
||||||
|
|
||||||
|
return AppBar(
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
surfaceTintColor: Colors.transparent,
|
||||||
|
automaticallyImplyLeading: false,
|
||||||
|
titleSpacing: 16,
|
||||||
|
toolbarHeight: kToolbarHeight,
|
||||||
|
title: title,
|
||||||
|
actions: actions,
|
||||||
|
flexibleSpace: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment.bottomCenter,
|
||||||
|
end: Alignment.topCenter,
|
||||||
|
colors: [
|
||||||
|
colorScheme.primary.withValues(alpha: 0),
|
||||||
|
colorScheme.primary.withValues(alpha: 0.2),
|
||||||
|
],
|
||||||
|
stops: const [0, 1],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class _ThemeMenuButton extends StatelessWidget {
|
class _ThemeMenuButton extends StatelessWidget {
|
||||||
const _ThemeMenuButton({required this.mode, required this.onChanged});
|
const _ThemeMenuButton({required this.mode, required this.onChanged});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user