155 lines
4.5 KiB
Dart
155 lines
4.5 KiB
Dart
import 'package:flutter/material.dart';
|
|
|
|
import '../../core/theme/tokens.dart';
|
|
import '../../core/ui/app_icons.dart';
|
|
import '../../core/ui/relative_time.dart';
|
|
import '../home/home_screen.dart' show formatPop;
|
|
import 'inside_data.dart';
|
|
|
|
/// INSIDE 화면군이 같이 쓰는 시안 조각 — 아이콘 · 글자 · 작성자 줄 · 현상금 칩 · 반응 줄.
|
|
///
|
|
/// 아이콘은 Figma Iconset(Lucide) 을 3배 PNG 로 둔 것이다. 공용 [AppIcons] 로 올릴 때까지
|
|
/// 여기 둔다(디자인 일치 2단계 「공유 변경 요청」).
|
|
/// 시안 INSIDE 의 회색 단계.
|
|
abstract final class InsideColors {
|
|
static const stat = Color(0xFF4F4F4F);
|
|
static const avatar = Color(0xFFD9D9D9);
|
|
static const rule = Color(0xFF1A1A1A);
|
|
static const hint = Color(0xFF6B6B6B);
|
|
static const chipLabel = Color(0xFFD5D7D9);
|
|
}
|
|
|
|
/// 42dot Sans · 자간 -2.5% · 행간 130%(시안 INSIDE 본문 기본값).
|
|
TextStyle insideText(
|
|
double size, {
|
|
FontWeight weight = FontWeight.w500,
|
|
Color color = AppColors.onCosmic,
|
|
double height = 1.3,
|
|
}) => TextStyle(
|
|
fontFamily: AppFonts.body,
|
|
fontSize: size,
|
|
fontWeight: weight,
|
|
height: height,
|
|
letterSpacing: -0.025 * size,
|
|
color: color,
|
|
);
|
|
|
|
/// 작성자 닉네임 — ABeeZee 12(한글은 42dot 대체).
|
|
const insideNameStyle = TextStyle(
|
|
fontFamily: AppFonts.display,
|
|
fontFamilyFallback: [AppFonts.body],
|
|
fontSize: 12,
|
|
height: 1.3,
|
|
letterSpacing: -0.3,
|
|
color: AppColors.onCosmic,
|
|
);
|
|
|
|
/// 첨부 이미지. `assets/` 로 시작하면 번들 표본이다.
|
|
ImageProvider insideImage(String url) =>
|
|
url.startsWith('assets/') ? AssetImage(url) : NetworkImage(url);
|
|
|
|
/// 아바타 26 · 이름 · (있으면) 시각 줄.
|
|
class InsideAuthor extends StatelessWidget {
|
|
const InsideAuthor({super.key, required this.name, this.meta});
|
|
|
|
final String name;
|
|
final String? meta;
|
|
|
|
@override
|
|
Widget build(BuildContext context) => Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Container(
|
|
width: 26,
|
|
height: 26,
|
|
decoration: const BoxDecoration(
|
|
color: InsideColors.avatar,
|
|
shape: BoxShape.circle,
|
|
),
|
|
),
|
|
const SizedBox(width: 9),
|
|
Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Text(name, style: insideNameStyle),
|
|
if (meta != null)
|
|
Text(meta!, style: insideText(10, color: AppColors.onCosmicMuted)),
|
|
],
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
/// 현상금 칩 — 시안 58×24 · 모서리 2 · 번개 + 「500 P」. 채택이 끝난 질문은 회색이다.
|
|
class InsideBountyChip extends StatelessWidget {
|
|
const InsideBountyChip({super.key, required this.question});
|
|
|
|
final InsideQuestion question;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final c = question.isSolved ? InsideColors.stat : AppColors.cosmicBlue;
|
|
return Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
|
decoration: BoxDecoration(
|
|
color: c.withValues(alpha: 0.1),
|
|
borderRadius: BorderRadius.circular(2),
|
|
),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
ImageIcon(AppIcons.bolt, size: 10, color: c),
|
|
const SizedBox(width: 4),
|
|
Text(
|
|
'${formatPop(question.bountyPop)} P',
|
|
style: insideText(12, color: c),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// 좋아요 · 댓글 · 조회 줄. 0 이면 숫자를 빼고 아이콘만 둔다(시안 「아미보이」 카드).
|
|
class InsideStats extends StatelessWidget {
|
|
const InsideStats({
|
|
super.key,
|
|
required this.question,
|
|
this.size = 14,
|
|
this.textSize = 12,
|
|
this.color = InsideColors.stat,
|
|
});
|
|
|
|
final InsideQuestion question;
|
|
final double size;
|
|
final double textSize;
|
|
final Color color;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
Widget stat(ImageProvider icon, int n) => Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
ImageIcon(icon, size: size, color: color),
|
|
if (n > 0) ...[
|
|
const SizedBox(width: 4),
|
|
Text(
|
|
formatReaction(n),
|
|
style: insideText(textSize, color: color, height: 1),
|
|
),
|
|
],
|
|
],
|
|
);
|
|
return Row(
|
|
children: [
|
|
stat(AppIcons.heart, question.likeCount),
|
|
const SizedBox(width: 12),
|
|
stat(AppIcons.messageCircle, question.answerCount),
|
|
const SizedBox(width: 12),
|
|
stat(AppIcons.eye, question.viewCount),
|
|
],
|
|
);
|
|
}
|
|
}
|