- Extract business logic from screens into dedicated controllers - Split large screen files into smaller, reusable widget components - Add controllers for AddSubscriptionScreen and DetailScreen - Create modular widgets for subscription and detail features - Improve code organization and maintainability - Remove duplicated code and improve reusability 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
312 lines
8.5 KiB
Dart
312 lines
8.5 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter/rendering.dart';
|
|
import 'dart:math' as math;
|
|
import '../theme/app_colors.dart';
|
|
import 'floating_navigation_bar.dart';
|
|
|
|
/// 글래스모피즘 디자인이 적용된 통일된 스캐폴드
|
|
class GlassmorphicScaffold extends StatefulWidget {
|
|
final PreferredSizeWidget? appBar;
|
|
final Widget body;
|
|
final Widget? floatingActionButton;
|
|
final FloatingActionButtonLocation? floatingActionButtonLocation;
|
|
final List<Color>? backgroundGradient;
|
|
final bool extendBodyBehindAppBar;
|
|
final bool extendBody;
|
|
final Widget? bottomNavigationBar;
|
|
final bool useFloatingNavBar;
|
|
final int? floatingNavBarIndex;
|
|
final Function(int)? onFloatingNavBarTapped;
|
|
final bool resizeToAvoidBottomInset;
|
|
final Widget? drawer;
|
|
final Widget? endDrawer;
|
|
final Color? backgroundColor;
|
|
final bool enableParticles;
|
|
final bool enableWaveAnimation;
|
|
|
|
const GlassmorphicScaffold({
|
|
super.key,
|
|
this.appBar,
|
|
required this.body,
|
|
this.floatingActionButton,
|
|
this.floatingActionButtonLocation,
|
|
this.backgroundGradient,
|
|
this.extendBodyBehindAppBar = true,
|
|
this.extendBody = true,
|
|
this.bottomNavigationBar,
|
|
this.useFloatingNavBar = false,
|
|
this.floatingNavBarIndex,
|
|
this.onFloatingNavBarTapped,
|
|
this.resizeToAvoidBottomInset = true,
|
|
this.drawer,
|
|
this.endDrawer,
|
|
this.backgroundColor,
|
|
this.enableParticles = false,
|
|
this.enableWaveAnimation = false,
|
|
});
|
|
|
|
@override
|
|
State<GlassmorphicScaffold> createState() => _GlassmorphicScaffoldState();
|
|
}
|
|
|
|
class _GlassmorphicScaffoldState extends State<GlassmorphicScaffold>
|
|
with TickerProviderStateMixin {
|
|
late AnimationController _particleController;
|
|
late AnimationController _waveController;
|
|
ScrollController? _scrollController;
|
|
bool _isFloatingNavBarVisible = true;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_particleController = AnimationController(
|
|
duration: const Duration(seconds: 20),
|
|
vsync: this,
|
|
)..repeat();
|
|
|
|
_waveController = AnimationController(
|
|
duration: const Duration(seconds: 10),
|
|
vsync: this,
|
|
)..repeat();
|
|
|
|
if (widget.useFloatingNavBar) {
|
|
_scrollController = ScrollController();
|
|
_setupScrollListener();
|
|
}
|
|
}
|
|
|
|
void _setupScrollListener() {
|
|
_scrollController?.addListener(() {
|
|
final currentScroll = _scrollController!.position.pixels;
|
|
|
|
// 스크롤 방향에 따라 플로팅 네비게이션 바 표시/숨김
|
|
if (currentScroll > 50 && _scrollController!.position.userScrollDirection == ScrollDirection.reverse) {
|
|
if (_isFloatingNavBarVisible) {
|
|
setState(() => _isFloatingNavBarVisible = false);
|
|
}
|
|
} else if (_scrollController!.position.userScrollDirection == ScrollDirection.forward) {
|
|
if (!_isFloatingNavBarVisible) {
|
|
setState(() => _isFloatingNavBarVisible = true);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_particleController.dispose();
|
|
_waveController.dispose();
|
|
_scrollController?.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
List<Color> _getBackgroundGradient() {
|
|
if (widget.backgroundGradient != null) {
|
|
return widget.backgroundGradient!;
|
|
}
|
|
|
|
// 시간대별 기본 그라디언트
|
|
final hour = DateTime.now().hour;
|
|
if (hour >= 6 && hour < 10) {
|
|
return AppColors.morningGradient;
|
|
} else if (hour >= 10 && hour < 17) {
|
|
return AppColors.dayGradient;
|
|
} else if (hour >= 17 && hour < 20) {
|
|
return AppColors.eveningGradient;
|
|
} else {
|
|
return AppColors.nightGradient;
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final backgroundGradient = _getBackgroundGradient();
|
|
|
|
return Stack(
|
|
children: [
|
|
// 배경 그라디언트
|
|
_buildBackground(backgroundGradient),
|
|
|
|
// 파티클 효과 (선택적)
|
|
if (widget.enableParticles) _buildParticles(),
|
|
|
|
// 웨이브 애니메이션 (선택적)
|
|
if (widget.enableWaveAnimation) _buildWaveAnimation(),
|
|
|
|
// 메인 스캐폴드
|
|
Scaffold(
|
|
backgroundColor: widget.backgroundColor ?? Colors.transparent,
|
|
appBar: widget.appBar,
|
|
body: widget.body,
|
|
floatingActionButton: widget.floatingActionButton,
|
|
floatingActionButtonLocation: widget.floatingActionButtonLocation,
|
|
bottomNavigationBar: widget.bottomNavigationBar,
|
|
extendBodyBehindAppBar: widget.extendBodyBehindAppBar,
|
|
extendBody: widget.extendBody,
|
|
resizeToAvoidBottomInset: widget.resizeToAvoidBottomInset,
|
|
drawer: widget.drawer,
|
|
endDrawer: widget.endDrawer,
|
|
),
|
|
|
|
// 플로팅 네비게이션 바 (선택적)
|
|
if (widget.useFloatingNavBar && widget.floatingNavBarIndex != null)
|
|
FloatingNavigationBar(
|
|
selectedIndex: widget.floatingNavBarIndex!,
|
|
isVisible: _isFloatingNavBarVisible,
|
|
onItemTapped: widget.onFloatingNavBarTapped ?? (_) {},
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildBackground(List<Color> gradientColors) {
|
|
return Positioned.fill(
|
|
child: Container(
|
|
decoration: BoxDecoration(
|
|
gradient: LinearGradient(
|
|
begin: Alignment.topLeft,
|
|
end: Alignment.bottomRight,
|
|
colors: gradientColors.map((color) => color.withValues(alpha: 0.1)).toList(),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildParticles() {
|
|
return Positioned.fill(
|
|
child: AnimatedBuilder(
|
|
animation: _particleController,
|
|
builder: (context, child) {
|
|
return CustomPaint(
|
|
painter: ParticlePainter(
|
|
animation: _particleController,
|
|
particleCount: 30,
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildWaveAnimation() {
|
|
return Positioned(
|
|
bottom: 0,
|
|
left: 0,
|
|
right: 0,
|
|
height: 200,
|
|
child: AnimatedBuilder(
|
|
animation: _waveController,
|
|
builder: (context, child) {
|
|
return CustomPaint(
|
|
painter: WavePainter(
|
|
animation: _waveController,
|
|
waveColor: AppColors.primaryColor.withValues(alpha: 0.1),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// 파티클 페인터
|
|
class ParticlePainter extends CustomPainter {
|
|
final Animation<double> animation;
|
|
final int particleCount;
|
|
final List<Particle> particles = [];
|
|
|
|
ParticlePainter({
|
|
required this.animation,
|
|
this.particleCount = 50,
|
|
}) : super(repaint: animation) {
|
|
_initParticles();
|
|
}
|
|
|
|
void _initParticles() {
|
|
final random = math.Random();
|
|
for (int i = 0; i < particleCount; i++) {
|
|
particles.add(Particle(
|
|
x: random.nextDouble(),
|
|
y: random.nextDouble(),
|
|
size: random.nextDouble() * 3 + 1,
|
|
speed: random.nextDouble() * 0.5 + 0.1,
|
|
opacity: random.nextDouble() * 0.5 + 0.1,
|
|
));
|
|
}
|
|
}
|
|
|
|
@override
|
|
void paint(Canvas canvas, Size size) {
|
|
final paint = Paint()..style = PaintingStyle.fill;
|
|
|
|
for (final particle in particles) {
|
|
final progress = animation.value;
|
|
final y = (particle.y + progress * particle.speed) % 1.0;
|
|
|
|
paint.color = Colors.white.withValues(alpha: particle.opacity);
|
|
canvas.drawCircle(
|
|
Offset(particle.x * size.width, y * size.height),
|
|
particle.size,
|
|
paint,
|
|
);
|
|
}
|
|
}
|
|
|
|
@override
|
|
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
|
|
}
|
|
|
|
/// 웨이브 페인터
|
|
class WavePainter extends CustomPainter {
|
|
final Animation<double> animation;
|
|
final Color waveColor;
|
|
|
|
WavePainter({
|
|
required this.animation,
|
|
required this.waveColor,
|
|
}) : super(repaint: animation);
|
|
|
|
@override
|
|
void paint(Canvas canvas, Size size) {
|
|
final paint = Paint()
|
|
..color = waveColor
|
|
..style = PaintingStyle.fill;
|
|
|
|
final path = Path();
|
|
final progress = animation.value;
|
|
|
|
path.moveTo(0, size.height);
|
|
|
|
for (double x = 0; x <= size.width; x++) {
|
|
final y = math.sin((x / size.width * 2 * math.pi) + (progress * 2 * math.pi)) * 20 +
|
|
size.height * 0.5;
|
|
path.lineTo(x, y);
|
|
}
|
|
|
|
path.lineTo(size.width, size.height);
|
|
path.close();
|
|
|
|
canvas.drawPath(path, paint);
|
|
}
|
|
|
|
@override
|
|
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
|
|
}
|
|
|
|
/// 파티클 데이터 클래스
|
|
class Particle {
|
|
final double x;
|
|
final double y;
|
|
final double size;
|
|
final double speed;
|
|
final double opacity;
|
|
|
|
Particle({
|
|
required this.x,
|
|
required this.y,
|
|
required this.size,
|
|
required this.speed,
|
|
required this.opacity,
|
|
});
|
|
} |