fromis_9/app/lib/views/main_shell.dart

195 lines
6 KiB
Dart
Raw Normal View History

/// 메인 셸 - 툴바 + 바텀 네비게이션
library;
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:flutter_svg/flutter_svg.dart';
import '../core/constants.dart';
/// 메인 앱 셸 (툴바 + 바텀 네비게이션 + 콘텐츠)
class MainShell extends StatelessWidget {
final Widget child;
const MainShell({super.key, required this.child});
@override
Widget build(BuildContext context) {
final location = GoRouterState.of(context).uri.path;
final isMembersPage = location == '/members';
return Scaffold(
backgroundColor: AppColors.background,
// 앱바 (툴바) - 멤버 페이지에서는 그림자 제거 (인디케이터와 계단 효과 방지)
appBar: PreferredSize(
preferredSize: const Size.fromHeight(56),
child: Container(
decoration: BoxDecoration(
color: Colors.white,
boxShadow: isMembersPage
? null
: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.05),
blurRadius: 4,
offset: const Offset(0, 1),
),
],
),
child: SafeArea(
child: SizedBox(
height: 56,
child: Center(
child: Text(
_getTitle(context),
style: const TextStyle(
fontFamily: 'Pretendard',
color: AppColors.primary,
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
),
),
),
),
),
// 콘텐츠
body: child,
// 바텀 네비게이션
bottomNavigationBar: const _BottomNavBar(),
);
}
/// 현재 경로에 따른 타이틀 반환
String _getTitle(BuildContext context) {
final location = GoRouterState.of(context).uri.path;
switch (location) {
case '/':
return 'fromis_9';
case '/members':
return '멤버';
case '/album':
return '앨범';
case '/schedule':
return '일정';
default:
return 'fromis_9';
}
}
}
/// 바텀 네비게이션 바
class _BottomNavBar extends StatelessWidget {
const _BottomNavBar();
@override
Widget build(BuildContext context) {
final location = GoRouterState.of(context).uri.path;
return Container(
decoration: const BoxDecoration(
color: Colors.white,
border: Border(
top: BorderSide(color: AppColors.border, width: 1),
),
),
child: SafeArea(
child: SizedBox(
height: 64,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_NavItem(
iconName: 'home',
label: '',
isActive: location == '/',
onTap: () => context.go('/'),
),
_NavItem(
iconName: 'users',
label: '멤버',
isActive: location == '/members',
onTap: () => context.go('/members'),
),
_NavItem(
iconName: 'disc-3',
label: '앨범',
isActive: location.startsWith('/album'),
onTap: () => context.go('/album'),
),
_NavItem(
iconName: 'calendar',
label: '일정',
isActive: location.startsWith('/schedule'),
onTap: () => context.go('/schedule'),
),
],
),
),
),
);
}
}
/// 네비게이션 아이템 - SVG 아이콘 사용으로 strokeWidth 조절 가능
class _NavItem extends StatelessWidget {
final String iconName;
final String label;
final bool isActive;
final VoidCallback onTap;
const _NavItem({
required this.iconName,
required this.label,
required this.isActive,
required this.onTap,
});
/// SVG 아이콘 문자열 생성 (strokeWidth 동적 조절)
String _getSvgString(String name, double strokeWidth) {
const icons = {
'home': '<path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/>',
'users': '<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/>',
'disc-3': '<circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="2"/><path d="M6 12c0-1.7.7-3.2 1.8-4.2"/><path d="M18 12c0 1.7-.7 3.2-1.8 4.2"/>',
'calendar': '<path d="M8 2v4"/><path d="M16 2v4"/><rect width="18" height="18" x="3" y="4" rx="2"/><path d="M3 10h18"/>',
};
return '''<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="$strokeWidth" stroke-linecap="round" stroke-linejoin="round">${icons[name]}</svg>''';
}
@override
Widget build(BuildContext context) {
final color = isActive ? AppColors.primary : AppColors.textTertiary;
// 웹과 동일: 활성화 시 strokeWidth=2.5, 비활성화 시 strokeWidth=2
final strokeWidth = isActive ? 2.5 : 2.0;
return Expanded(
child: InkWell(
onTap: onTap,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
width: 22,
height: 22,
child: SvgPicture.string(
_getSvgString(iconName, strokeWidth),
colorFilter: ColorFilter.mode(color, BlendMode.srcIn),
),
),
const SizedBox(height: 4),
Text(
label,
style: TextStyle(
fontSize: 12,
fontWeight: isActive ? FontWeight.w600 : FontWeight.w500,
color: color,
),
),
],
),
),
);
}
}