INSIDE · STATION · 참여형 번역 · 알림 · MY 화면
This commit is contained in:
@@ -0,0 +1,201 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../core/theme/tokens.dart';
|
||||
import '../../core/ui/app_icons.dart';
|
||||
import 'notification_data.dart';
|
||||
import '../../core/i18n/app_locale.dart';
|
||||
|
||||
/// 알림 종류·표기·그룹화와 저장소는 `notification_data.dart` 에 있다.
|
||||
/// 이 화면을 임포트한 곳이 모델까지 함께 쓰므로 그대로 내보낸다.
|
||||
export 'notification_data.dart';
|
||||
|
||||
/// 알림 — 화면 대조 원장 F0 · Figma `229:11265`.
|
||||
class NotificationScreen extends StatefulWidget {
|
||||
const NotificationScreen({super.key, required this.repository});
|
||||
|
||||
final NotificationRepository repository;
|
||||
|
||||
@override
|
||||
State<NotificationScreen> createState() => _NotificationScreenState();
|
||||
}
|
||||
|
||||
class _NotificationScreenState extends State<NotificationScreen> {
|
||||
List<AppNotification>? _items;
|
||||
final Set<String> _read = {};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_load();
|
||||
}
|
||||
|
||||
Future<void> _load() async {
|
||||
final items = await widget.repository.load();
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_items = items;
|
||||
_read
|
||||
..clear()
|
||||
..addAll(items.where((i) => i.read).map((i) => i.id));
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final t = Theme.of(context).textTheme;
|
||||
final items = _items;
|
||||
if (items == null) {
|
||||
return const Scaffold(body: Center(child: CircularProgressIndicator()));
|
||||
}
|
||||
final groups = groupNotifications(items);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(context.l10n.homeNotificationsTooltip),
|
||||
// 명세 — 뒤로가기 시 읽음 상태값을 돌려준다.
|
||||
leading: BackButton(
|
||||
onPressed: () => Navigator.of(context).pop({..._read}),
|
||||
),
|
||||
),
|
||||
body: items.isEmpty
|
||||
? Center(child: Text(context.l10n.notifEmpty, style: t.bodyMedium))
|
||||
: ListView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: AppSpace.s4),
|
||||
children: [
|
||||
for (final group in groups)
|
||||
// 시안 날짜 묶음 — 위아래 20 · 머리글과 항목 사이 20 · 항목 사이 24 · 아래 #1a1a1a 줄.
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 20),
|
||||
foregroundDecoration: const BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(color: Color(0xFF1A1A1A)),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
group.label,
|
||||
style: _style(_grey, height: 24 / 14),
|
||||
),
|
||||
for (final (i, item) in group.items.indexed) ...[
|
||||
SizedBox(height: i == 0 ? 20 : 24),
|
||||
_tile(item),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _tile(AppNotification item) {
|
||||
final read = _read.contains(item.id);
|
||||
final (fill, icon) = _badge(item.kind);
|
||||
return InkWell(
|
||||
onTap: () => _open(item),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
width: 30,
|
||||
height: 30,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: fill,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: const Color(0xFF333333)),
|
||||
),
|
||||
child: ImageIcon(icon, size: 14, color: AppColors.onCosmic),
|
||||
),
|
||||
const SizedBox(width: 13),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
item.title,
|
||||
style: _style(
|
||||
read ? AppColors.onCosmicVariant : AppColors.onCosmic,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
'${item.sourceLabel} · ${notificationTimestamp(item.at)}',
|
||||
style: _style(_grey),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// 이동할 곳이 없는 종류에는 화살표를 그리지 않는다.
|
||||
if (item.kind.navigable) ...[
|
||||
const SizedBox(width: 13),
|
||||
const ImageIcon(
|
||||
AppIcons.chevronRight,
|
||||
size: 24,
|
||||
color: AppColors.onCosmicMuted,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _open(AppNotification item) async {
|
||||
// 처음 읽은 것만 서버에 알린다. 이미 읽은 알림을 다시 눌러도 쓰기가 없다.
|
||||
final first = _read.add(item.id);
|
||||
setState(() {});
|
||||
if (item.kind.navigable && mounted) {
|
||||
ScaffoldMessenger.of(context)
|
||||
..hideCurrentSnackBar()
|
||||
..showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(context.l10n.notifGoTo('${item.kind.target}')),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (!first) return;
|
||||
try {
|
||||
await widget.repository.markRead(item.id);
|
||||
} on Exception {
|
||||
// 읽음 표시가 실패해도 화면을 막지 않는다. 다음 진입에서 서버 값으로
|
||||
// 되돌아오므로 잃는 것이 없고, 여기서 막으면 알림 자체를 못 읽는다.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const _grey = Color(0xFF777777);
|
||||
|
||||
/// 시안 알림 글자 — 42dot 14/600 · 자간 -2.5% · 행간 130%.
|
||||
TextStyle _style(Color color, {double height = 1.3}) => TextStyle(
|
||||
fontFamily: AppFonts.body,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
height: height,
|
||||
letterSpacing: -0.35,
|
||||
color: color,
|
||||
);
|
||||
|
||||
/// 종류별 원 배지 — 시안 30 원 · #333333 획. 댓글·이클립스는 검정, 좋아요는 #ff5151,
|
||||
/// 아이템은 #cebf3b, 퀘스트는 #1a1a1c. 캠페인 달성은 시안이 없어 댓글과 같은 검정이다.
|
||||
(Color, ImageProvider) _badge(NotificationKind kind) => switch (kind) {
|
||||
NotificationKind.comment => (Colors.black, _icon('message_circle')),
|
||||
NotificationKind.campaign => (Colors.black, _icon('message_circle')),
|
||||
NotificationKind.like => (const Color(0xFFFF5151), _icon('heart')),
|
||||
NotificationKind.eclipse => (Colors.black, _icon('planet')),
|
||||
NotificationKind.itemUsed => (const Color(0xFFCEBF3B), _icon('water_ec')),
|
||||
NotificationKind.quest => (
|
||||
AppColors.cosmicSurfaceContainer,
|
||||
_icon('book_open'),
|
||||
),
|
||||
};
|
||||
|
||||
AssetImage _icon(String name) => AssetImage('assets/icons/$name.png');
|
||||
Reference in New Issue
Block a user