Skip to content

Instantly share code, notes, and snippets.

@GeekTree0101
Created August 20, 2025 07:52
Show Gist options
  • Save GeekTree0101/559b887b23777009feec535566a311d8 to your computer and use it in GitHub Desktop.
Save GeekTree0101/559b887b23777009feec535566a311d8 to your computer and use it in GitHub Desktop.
David vs AI UI Programming
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),
),
),
),
],
);
}
}
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;
}
}
}
@GeekTree0101
Copy link
Author

david ai
Simulator Screenshot - iPhone 15 Pro - 2025-08-20 at 16 53 13 Simulator Screenshot - iPhone 15 Pro - 2025-08-20 at 16 53 18

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment