Files
fanuzu/app/lib/features/my/my_lists_screen.dart

208 lines
7.1 KiB
Dart

import 'package:flutter/material.dart';
import '../campaign/campaign_data.dart';
import '../campaign/campaign_detail_screen.dart';
import '../campaign/widgets/campaign_list_card.dart';
import '../inside/inside_data.dart';
import '../inside/inside_detail_screen.dart';
import '../translation/translation_data.dart' show TranslationRepository;
import '../inside/inside_screen.dart' show InsideQuestionCard;
import 'widgets/my_cards.dart';
import '../../core/i18n/app_locale.dart';
/// 내 인사이드 · 내 캠페인 — 화면 대조 원장 F6.
///
/// Figma `370:228289`(내 인사이드) · `370:229665`(내 캠페인).
/// 명세 `554:22382`(게시글 리스트 영역) 계열이며 **머리글과 빈 상태만 다르고**
/// 구조가 같아 한 화면에 종류를 받아 쓴다.
enum MyListKind {
inside,
campaign;
String get title => switch (this) {
inside => appL10n.myListInside,
campaign => appL10n.myListCampaign,
};
String get emptyMessage => switch (this) {
inside => appL10n.myListInsideEmpty,
campaign => appL10n.myListCampaignEmpty,
};
}
/// 정렬 칩 — 시안 `370:228289` 「최신순 · POP순 · 채택 완료」 · `370:229665` 「최신순 · 달성 완료」.
enum _MySort { latest, pop, solved, achieved }
class MyListsScreen extends StatefulWidget {
const MyListsScreen({
super.key,
required this.kind,
this.campaigns,
this.insides,
this.translation,
});
final MyListKind kind;
/// 내 인사이드의 원천. null 이면 표본(목 모드). 저장소 선택은 repositories.dart.
final InsideRepository? insides;
/// 내 질문 상세의 번역 원천. null 이면 번역 기능을 끈다.
final TranslationRepository? translation;
/// 내 캠페인의 원천. null 이면 표본(목 모드). 저장소 선택은 repositories.dart.
final CampaignRepository? campaigns;
@override
State<MyListsScreen> createState() => _MyListsScreenState();
}
class _MyListsScreenState extends State<MyListsScreen> {
_MySort _sort = _MySort.latest;
List<_MySort> get _sorts => switch (widget.kind) {
MyListKind.inside => const [_MySort.latest, _MySort.pop, _MySort.solved],
MyListKind.campaign => const [_MySort.latest, _MySort.achieved],
};
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(
title: Text(widget.kind.title),
bottom: PreferredSize(
preferredSize: const Size.fromHeight(36),
child: Align(
alignment: Alignment.centerLeft,
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 2, 16, 2),
child: Row(
children: [
for (final (i, sort) in _sorts.indexed) ...[
if (i > 0) const SizedBox(width: 8),
MySortChip(
label: switch (sort) {
_MySort.latest => context.l10n.sortLatest,
_MySort.pop => context.l10n.sortPop,
_MySort.solved => context.l10n.sortSolved,
_MySort.achieved => context.l10n.sortAchieved,
},
icon: switch (sort) {
_MySort.latest => MySortChip.clock,
_MySort.pop => MySortChip.bolt,
_ => MySortChip.fileCheck,
},
selected: _sort == sort,
onTap: () => setState(() => _sort = sort),
),
],
],
),
),
),
),
),
body: switch (widget.kind) {
MyListKind.inside => _insideList(context),
MyListKind.campaign => _campaignList(context),
},
);
/// 내가 쓴 질문 — 화면을 열 때 한 번 읽고, 정렬 칩은 그 목록을 다시 거른다.
late final Future<List<InsideQuestion>> _mine =
(widget.insides ?? const SampleInsideRepository()).mine();
Widget _insideList(BuildContext context) =>
FutureBuilder<List<InsideQuestion>>(
future: _mine,
builder: (context, snap) {
if (snap.connectionState != ConnectionState.done) {
return const Center(child: CircularProgressIndicator());
}
return _insideItems(context, snap.data ?? const []);
},
);
Widget _insideItems(BuildContext context, List<InsideQuestion> mine) {
final items = switch (_sort) {
_MySort.pop => [
...mine,
]..sort((a, b) => b.bountyPop.compareTo(a.bountyPop)),
_MySort.solved => mine.where((q) => q.isSolved).toList(),
_ => mine,
};
if (items.isEmpty) {
return Center(
child: Text(
widget.kind.emptyMessage,
style: Theme.of(context).textTheme.bodyMedium,
),
);
}
// 카드는 INSIDE 목록과 같은 것을 쓴다(시안도 같은 컴포넌트다).
return ListView.separated(
padding: const EdgeInsets.all(16),
itemCount: items.length,
separatorBuilder: (_, _) => const Padding(
padding: EdgeInsets.symmetric(vertical: 2),
child: SizedBox(height: 1, child: ColoredBox(color: Color(0xFF1A1A1A))),
),
itemBuilder: (context, i) => InsideQuestionCard(
question: items[i],
onTap: () => Navigator.of(context, rootNavigator: true).push(
MaterialPageRoute<void>(
builder: (_) => InsideDetailScreen(
question: items[i],
repository: widget.insides,
translation: widget.translation,
),
),
),
),
);
}
Widget _campaignList(BuildContext context) =>
FutureBuilder<List<CampaignCardData>>(
future: (widget.campaigns ?? SampleCampaignRepository()).mine(),
builder: (context, snap) {
if (snap.connectionState != ConnectionState.done) {
return const Center(child: CircularProgressIndicator());
}
// 내가 만든 것만 남긴다. 「달성 완료」는 목표를 채운 것만.
final mine = (snap.data ?? const [])
.where((c) => c.mine)
.where(
(c) =>
_sort != _MySort.achieved ||
c.status == CampaignStatus.success,
)
.toList();
if (mine.isEmpty) {
return Center(
child: Text(
widget.kind.emptyMessage,
style: Theme.of(context).textTheme.bodyMedium,
),
);
}
return ListView.separated(
padding: const EdgeInsets.all(16),
itemCount: mine.length,
separatorBuilder: (_, _) => const SizedBox(height: 16),
itemBuilder: (context, i) => CampaignListCard(
card: mine[i],
mine: true,
onOpen: () => Navigator.of(context, rootNavigator: true).push(
MaterialPageRoute<void>(
builder: (_) => CampaignDetailScreen(
campaignId: mine[i].id,
repository: widget.campaigns,
),
),
),
),
);
},
);
}