Created
August 20, 2025 07:52
-
-
Save GeekTree0101/559b887b23777009feec535566a311d8 to your computer and use it in GitHub Desktop.
David vs AI UI Programming
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'package:flutter/material.dart'; | |
import 'package:hexcolor/hexcolor.dart'; | |
import 'package:huaian/core/time/time_helper.dart'; | |
import 'package:huaian/domain/base_font_family.dart'; | |
import 'package:huaian/features/emotion/home/components/diary/emotion_home_diary_widget.dart'; | |
import 'package:huaian/i18n/strings.g.dart'; | |
import 'package:huaian/ui/color_extension.dart'; | |
import 'package:huaian/ui/providers/couple_color_provider.dart'; | |
import 'package:huaian/ui/providers/profile_view_model/profile_view_model_provider.dart'; | |
import 'package:intl/intl.dart'; | |
import 'package:provider/provider.dart'; | |
class EmotionHomeBookWidget extends StatelessWidget { | |
final EmotionHomeDiaryItem item; | |
final DateTime dateTime; | |
const EmotionHomeBookWidget({ | |
super.key, | |
required this.item, | |
required this.dateTime, | |
}); | |
@override | |
Widget build(BuildContext context) { | |
return Consumer<ProfileViewModelProvider>( | |
builder: (context, profileViewModel, _) { | |
return Consumer<CoupleColorProvider>( | |
builder: (context, colorProvider, __) { | |
final Color coverColor; | |
final String title; | |
final String subtitle; | |
switch (item) { | |
case EmotionHomeDiaryItem.my: | |
coverColor = colorProvider.me; | |
final nickname = profileViewModel.myNickname; | |
title = t.couple_emotion.diary.title(nickname: nickname); | |
subtitle = DateFormat(t.couple_emotion.diary.datetime, "ko") | |
.format(dateTime); | |
case EmotionHomeDiaryItem.partner: | |
coverColor = colorProvider.partner; | |
final nickname = profileViewModel.partnerNickname; | |
title = t.couple_emotion.diary.title(nickname: nickname); | |
subtitle = DateFormat(t.couple_emotion.diary.datetime, "ko") | |
.format(dateTime); | |
case EmotionHomeDiaryItem.nextMonth: | |
coverColor = Colors.grey; | |
title = t.couple_emotion.diary.move_to_month( | |
M: dateTime.nextMonth.month, | |
); | |
subtitle = t.couple_emotion.diary.touch; | |
case EmotionHomeDiaryItem.prevMonth: | |
coverColor = Colors.grey; | |
title = t.couple_emotion.diary.move_to_month( | |
M: dateTime.prevMonth.month, | |
); | |
subtitle = t.couple_emotion.diary.touch; | |
} | |
final textColor = _adaptiveForegroundColor(coverColor); | |
return AspectRatio( | |
aspectRatio: 3 / 4, | |
child: LayoutBuilder( | |
builder: (context, c) { | |
final r = BorderRadius.circular(38); | |
final pageOffset = c.maxWidth * 0.055; | |
return Stack( | |
clipBehavior: Clip.none, | |
children: [ | |
// 뒤쪽 페이지(회색) | |
Positioned( | |
right: -pageOffset * 0.2, | |
top: pageOffset * 0.9, | |
bottom: pageOffset * 0.9, | |
child: _PageLayer( | |
width: c.maxWidth * 0.90, | |
color: const Color(0xFFDADADA), | |
borderRadius: r, | |
rightBorderWidth: 6, | |
), | |
), | |
// 뒤쪽 페이지(파랑) | |
Positioned( | |
right: pageOffset * 0.25, | |
top: pageOffset * 0.65, | |
bottom: pageOffset * 0.65, | |
child: _PageLayer( | |
width: c.maxWidth * 0.94, | |
color: const Color(0xFFBFD9FF), | |
borderRadius: r, | |
rightBorderWidth: 6, | |
), | |
), | |
// 메인 커버 | |
Positioned.fill( | |
child: Container( | |
decoration: BoxDecoration( | |
borderRadius: r, | |
color: coverColor, | |
gradient: LinearGradient( | |
begin: Alignment.topLeft, | |
end: Alignment.bottomRight, | |
colors: [ | |
_tint(coverColor, 0.06), | |
_tint(coverColor, -0.02), | |
], | |
), | |
), | |
child: ClipRRect( | |
borderRadius: r, | |
child: Stack( | |
children: [ | |
// 왼쪽 등(spine) 음영 | |
Align( | |
alignment: Alignment.centerLeft, | |
child: Container( | |
width: c.maxWidth * 0.16, | |
decoration: BoxDecoration( | |
gradient: LinearGradient( | |
begin: Alignment.centerLeft, | |
end: Alignment.centerRight, | |
colors: [ | |
Colors.black.withOpacity(0.06), | |
Colors.black.withOpacity(0.00), | |
], | |
), | |
), | |
), | |
), | |
// 왼쪽 가장자리 하이라이트 | |
Align( | |
alignment: Alignment.centerLeft, | |
child: Container( | |
width: c.maxWidth * 0.04, | |
decoration: BoxDecoration( | |
gradient: LinearGradient( | |
begin: Alignment.centerLeft, | |
end: Alignment.centerRight, | |
colors: [ | |
Colors.white.withOpacity(0.32), | |
Colors.white.withOpacity(0.00), | |
], | |
), | |
), | |
), | |
), | |
// 아래 페이지 밴드 | |
Align( | |
alignment: Alignment.bottomCenter, | |
child: _BottomBand( | |
height: c.maxHeight * 0.09, | |
edgeHeight: c.maxHeight * 0.012, | |
edgeColor: coverColor.darkenBlend(0.10), | |
), | |
), | |
// 타이틀/서브타이틀 | |
Center( | |
child: Padding( | |
padding: EdgeInsets.only( | |
top: c.maxHeight * 0.22, | |
left: 20, | |
right: 20, | |
), | |
child: Column( | |
mainAxisSize: MainAxisSize.min, | |
children: [ | |
Text( | |
title, | |
textAlign: TextAlign.center, | |
style: TextStyle( | |
fontSize: 28, | |
fontWeight: FontWeight.bold, | |
fontFamily: | |
BaseFontFamily.playfulJ.value, | |
color: textColor, | |
height: 1.2, | |
), | |
), | |
const SizedBox(height: 8), | |
Text( | |
subtitle, | |
textAlign: TextAlign.center, | |
style: TextStyle( | |
fontSize: 14, | |
fontFamily: | |
BaseFontFamily.playfulJ.value, | |
color: textColor.withOpacity(0.85), | |
), | |
), | |
], | |
), | |
), | |
), | |
], | |
), | |
), | |
), | |
), | |
// 좌상단 둥근 모서리 하이라이트 | |
Positioned( | |
left: 8, | |
top: 8, | |
child: Container( | |
width: 18, | |
height: 18, | |
decoration: BoxDecoration( | |
borderRadius: BorderRadius.circular(10), | |
gradient: LinearGradient( | |
begin: Alignment.topLeft, | |
end: Alignment.bottomRight, | |
colors: [ | |
Colors.white.withOpacity(0.55), | |
Colors.white.withOpacity(0.00), | |
], | |
), | |
), | |
), | |
), | |
], | |
); | |
}, | |
), | |
); | |
}, | |
); | |
}, | |
); | |
} | |
static Color _tint(Color c, double amount) { | |
final h = HSLColor.fromColor(c); | |
final l = (h.lightness + amount).clamp(0.0, 1.0); | |
return h.withLightness(l).toColor(); | |
} | |
static Color _adaptiveForegroundColor(Color color) { | |
// 0~255 정규화된 RGB가 아니라면 color_extension에서 r/g/b가 0~1일 수 있어 보정 | |
final r = (color.r <= 1 ? color.r * 255 : color.r); | |
final g = (color.g <= 1 ? color.g * 255 : color.g); | |
final b = (color.b <= 1 ? color.b * 255 : color.b); | |
final brightness = (r * 299 + g * 587 + b * 114) / 1000; | |
return brightness > 165 ? HexColor("#4b4b4b") : Colors.white; | |
} | |
} | |
class _PageLayer extends StatelessWidget { | |
const _PageLayer({ | |
required this.width, | |
required this.color, | |
required this.borderRadius, | |
this.rightBorderWidth = 0, | |
}); | |
final double width; | |
final Color color; | |
final BorderRadius borderRadius; | |
final double rightBorderWidth; | |
@override | |
Widget build(BuildContext context) { | |
return Container( | |
width: width, | |
decoration: BoxDecoration( | |
color: color, | |
borderRadius: borderRadius, | |
boxShadow: [ | |
BoxShadow( | |
color: Colors.black.withOpacity(0.06), | |
blurRadius: 10, | |
offset: const Offset(0, 6), | |
), | |
], | |
border: Border( | |
left: BorderSide(color: Colors.black.withOpacity(0.04), width: 0.5), | |
top: BorderSide(color: Colors.black.withOpacity(0.02), width: 0.5), | |
right: BorderSide( | |
color: Colors.black.withOpacity(0.08), | |
width: rightBorderWidth, | |
), | |
bottom: | |
BorderSide(color: Colors.black.withOpacity(0.04), width: 0.5), | |
), | |
), | |
); | |
} | |
} | |
class _BottomBand extends StatelessWidget { | |
const _BottomBand({ | |
required this.height, | |
required this.edgeHeight, | |
required this.edgeColor, | |
}); | |
final double height; | |
final double edgeHeight; | |
final Color edgeColor; | |
@override | |
Widget build(BuildContext context) { | |
return Column( | |
children: [ | |
// 상단 어두운 엣지 라인 | |
Container( | |
height: edgeHeight, | |
decoration: BoxDecoration( | |
color: edgeColor, | |
borderRadius: const BorderRadius.only( | |
topLeft: Radius.circular(24), | |
bottomLeft: Radius.circular(0), | |
bottomRight: Radius.circular(0), | |
), | |
), | |
), | |
// 페이지 밴드(아이보리) | |
Container( | |
height: height, | |
decoration: const BoxDecoration( | |
gradient: LinearGradient( | |
begin: Alignment.topLeft, | |
end: Alignment.bottomRight, | |
colors: [ | |
Color(0xFFFAF7F1), | |
Color(0xFFE9E1D2), | |
], | |
), | |
borderRadius: BorderRadius.only( | |
bottomLeft: Radius.circular(24), | |
bottomRight: Radius.circular(8), | |
), | |
), | |
), | |
], | |
); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'package:flutter/material.dart'; | |
import 'package:hexcolor/hexcolor.dart'; | |
import 'package:huaian/core/time/time_helper.dart'; | |
import 'package:huaian/domain/base_font_family.dart'; | |
import 'package:huaian/features/emotion/home/components/diary/emotion_home_diary_widget.dart'; | |
import 'package:huaian/i18n/strings.g.dart'; | |
import 'package:huaian/ui/color_extension.dart'; | |
import 'package:huaian/ui/providers/couple_color_provider.dart'; | |
import 'package:huaian/ui/providers/profile_view_model/profile_view_model_provider.dart'; | |
import 'package:intl/intl.dart'; | |
import 'package:provider/provider.dart'; | |
class EmotionHomeBookWidget extends StatelessWidget { | |
final EmotionHomeDiaryItem item; | |
final DateTime dateTime; | |
const EmotionHomeBookWidget({ | |
super.key, | |
required this.item, | |
required this.dateTime, | |
}); | |
@override | |
Widget build(BuildContext context) { | |
return Consumer<ProfileViewModelProvider>( | |
builder: (context, profileViewModel, child) { | |
return Consumer<CoupleColorProvider>( | |
builder: (context, colorProvider, child) { | |
final Color color; | |
final String title; | |
final String subtitle; | |
switch (item) { | |
case EmotionHomeDiaryItem.my: | |
color = colorProvider.me; | |
final nickname = profileViewModel.myNickname; | |
title = t.couple_emotion.diary.title( | |
nickname: nickname, | |
); | |
subtitle = DateFormat( | |
t.couple_emotion.diary.datetime, | |
"ko", | |
).format(dateTime); | |
case EmotionHomeDiaryItem.partner: | |
color = colorProvider.partner; | |
final nickname = profileViewModel.partnerNickname; | |
title = t.couple_emotion.diary.title( | |
nickname: nickname, | |
); | |
subtitle = DateFormat( | |
t.couple_emotion.diary.datetime, | |
"ko", | |
).format(dateTime); | |
case EmotionHomeDiaryItem.nextMonth: | |
color = Colors.grey; | |
title = t.couple_emotion.diary.move_to_month( | |
M: dateTime.nextMonth.month, | |
); | |
subtitle = t.couple_emotion.diary.touch; | |
case EmotionHomeDiaryItem.prevMonth: | |
color = Colors.grey; | |
title = t.couple_emotion.diary.move_to_month( | |
M: dateTime.prevMonth.month, | |
); | |
subtitle = t.couple_emotion.diary.touch; | |
} | |
return Container( | |
decoration: BoxDecoration( | |
color: color, | |
borderRadius: const BorderRadius.only( | |
topLeft: Radius.circular(38.0), | |
bottomLeft: Radius.circular(38.0), | |
topRight: Radius.circular(8.0), | |
bottomRight: Radius.circular(8.0), | |
), | |
), | |
child: Stack( | |
fit: StackFit.expand, | |
children: [ | |
Row( | |
children: [ | |
Container( | |
width: 24.0, | |
height: double.infinity, | |
decoration: BoxDecoration( | |
color: color.darkenBlend(0.05), | |
borderRadius: const BorderRadius.only( | |
topLeft: Radius.circular(38.0), | |
bottomLeft: Radius.circular(38.0), | |
), | |
), | |
), | |
Expanded( | |
child: Container( | |
decoration: BoxDecoration( | |
color: color, | |
borderRadius: const BorderRadius.only( | |
topRight: Radius.circular(8.0), | |
bottomRight: Radius.circular(4.0), | |
), | |
), | |
child: LayoutBuilder( | |
builder: (context, constraints) { | |
return Padding( | |
padding: EdgeInsets.only( | |
top: constraints.maxHeight * 0.2, | |
left: 20.0, | |
right: 20.0, | |
), | |
child: Column( | |
mainAxisAlignment: MainAxisAlignment.start, | |
crossAxisAlignment: CrossAxisAlignment.center, | |
spacing: 4.0, | |
children: [ | |
Text( | |
title, | |
textAlign: TextAlign.center, | |
style: TextStyle( | |
fontSize: 28.0, | |
fontWeight: FontWeight.bold, | |
fontFamily: | |
BaseFontFamily.playfulJ.value, | |
color: _adaptiveForegroundColor( | |
color, | |
), | |
), | |
), | |
Text( | |
subtitle, | |
textAlign: TextAlign.center, | |
style: TextStyle( | |
fontSize: 14.0, | |
fontFamily: | |
BaseFontFamily.playfulJ.value, | |
color: _adaptiveForegroundColor(color), | |
), | |
), | |
], | |
), | |
); | |
}, | |
), | |
), | |
), | |
], | |
), | |
Align( | |
alignment: Alignment.bottomCenter, | |
child: Container( | |
decoration: BoxDecoration( | |
borderRadius: const BorderRadius.only( | |
topLeft: Radius.circular(24.0), | |
bottomLeft: Radius.circular(24.0), | |
bottomRight: Radius.circular(4.0), | |
), | |
border: Border( | |
top: BorderSide( | |
width: 8.0, | |
color: color.darkenBlend(0.1), | |
), | |
left: BorderSide( | |
width: 8.0, | |
color: color.darkenBlend(0.1), | |
), | |
bottom: BorderSide( | |
width: 8.0, | |
color: color.darkenBlend(0.1), | |
), | |
), | |
gradient: const LinearGradient( | |
begin: Alignment.topCenter, | |
end: Alignment.bottomCenter, | |
colors: [ | |
Color(0xFFFAF7F1), | |
Color(0xFFE9E1D2), | |
], | |
), | |
), | |
height: 42.0, | |
), | |
) | |
], | |
), | |
); | |
}, | |
); | |
}, | |
); | |
} | |
Color _adaptiveForegroundColor(Color color) { | |
double brightness = (color.r * 299 + color.g * 587 + color.b * 114) / 1000; | |
if (brightness > 0.65) { | |
return HexColor("#4b4b4b"); | |
} else { | |
return Colors.white; | |
} | |
} | |
} |
Author
GeekTree0101
commented
Aug 20, 2025
david | ai |
---|---|
![]() |
![]() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment