Files
fanuzu/app/lib/features/station/station_screen.dart

206 lines
6.5 KiB
Dart

import 'package:flutter/material.dart';
import '../../core/theme/tokens.dart';
import 'station_data.dart';
import 'station_suggest_screen.dart';
import '../../core/i18n/app_locale.dart';
/// STATION 탭 루트 — S-S-HUB.
///
/// **Figma 에 이 화면의 시안이 없다.** 근거는 견적서 v2.1 의 5탭 구성과
/// Firestore 설계서의 `stationLinks` · `stationSuggestions` 다. 그래서 데이터가
/// 정의한 것까지만 그렸고, 시안이 확정되면 그때 맞춘다.
class StationScreen extends StatefulWidget {
const StationScreen({
super.key,
this.repository = const SampleStationRepository(),
});
final StationRepository repository;
@override
State<StationScreen> createState() => _StationScreenState();
}
class _StationScreenState extends State<StationScreen> {
StationSnapshot? _snapshot;
bool _loading = true;
String? _error;
StationCategory? _filter;
@override
void initState() {
super.initState();
_load();
}
Future<void> _load() async {
setState(() {
_loading = true;
_error = null;
});
try {
final snap = await widget.repository.load();
if (!mounted) return;
setState(() {
_snapshot = snap;
_loading = false;
});
} on Object {
if (!mounted) return;
setState(() {
_loading = false;
_error = context.l10n.stationLoadFailed;
});
}
}
List<StationLink> get _visible {
final links = _snapshot?.links ?? const <StationLink>[];
final filter = _filter;
if (filter == null) return links;
return links.where((l) => l.category == filter).toList();
}
@override
Widget build(BuildContext context) {
final t = Theme.of(context).textTheme;
final snapshot = _snapshot;
return Scaffold(
appBar: AppBar(
title: const Text('STATION'),
bottom: PreferredSize(
preferredSize: const Size.fromHeight(48),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: AppSpace.s4),
child: Row(
children: [
ChoiceChip(
label: Text(context.l10n.commonAll),
selected: _filter == null,
onSelected: (_) => setState(() => _filter = null),
),
const SizedBox(width: AppSpace.s2),
for (final c in StationCategory.values) ...[
ChoiceChip(
label: Text(c.label),
selected: _filter == c,
onSelected: (_) => setState(() => _filter = c),
),
const SizedBox(width: AppSpace.s2),
],
],
),
),
),
),
floatingActionButton: (snapshot?.canSuggest ?? false)
? FloatingActionButton.extended(
onPressed: () => Navigator.of(context, rootNavigator: true).push<void>(
MaterialPageRoute(builder: (_) => const StationSuggestScreen()),
),
icon: const Icon(Icons.add_link),
label: Text(context.l10n.stationSuggest),
)
: null,
body: switch ((_loading, _error, _visible.isEmpty)) {
(true, _, _) => const Center(child: CircularProgressIndicator()),
(_, final String message, _) => Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(message, style: t.titleMedium),
const SizedBox(height: AppSpace.s4),
FilledButton(onPressed: _load, child: Text(context.l10n.commonRetry)),
],
),
),
(_, _, true) => Center(
child: Padding(
padding: const EdgeInsets.all(AppSpace.s5),
child: Text(
snapshot == null || snapshot.links.isEmpty
? context.l10n.stationEmpty
: context.l10n.stationCategoryEmpty,
style: t.bodyMedium,
textAlign: TextAlign.center,
),
),
),
_ => RefreshIndicator(
onRefresh: _load,
child: ListView.separated(
padding: const EdgeInsets.fromLTRB(
AppSpace.s4,
AppSpace.s4,
AppSpace.s4,
AppSpace.s8,
),
itemCount: _visible.length,
separatorBuilder: (_, _) => const SizedBox(height: AppSpace.s3),
itemBuilder: (context, i) => StationLinkTile(link: _visible[i]),
),
),
},
);
}
}
/// 링크 한 줄.
class StationLinkTile extends StatelessWidget {
const StationLinkTile({super.key, required this.link, this.onTap});
final StationLink link;
final VoidCallback? onTap;
@override
Widget build(BuildContext context) {
final t = Theme.of(context).textTheme;
return Material(
color: AppColors.cosmicSurfaceContainer,
borderRadius: BorderRadius.circular(AppRadius.md),
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(AppRadius.md),
child: Padding(
padding: const EdgeInsets.all(AppSpace.s4),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(link.category.label, style: t.labelSmall),
if (link.verified) ...[
const SizedBox(width: AppSpace.s2),
Icon(
Icons.verified_outlined,
size: 14,
color: AppColors.neonCyan,
),
],
],
),
const SizedBox(height: AppSpace.s2),
Text(link.title, style: t.titleMedium),
if (link.description.isNotEmpty) ...[
const SizedBox(height: AppSpace.s1),
Text(link.description, style: t.bodySmall),
],
],
),
),
const SizedBox(width: AppSpace.s3),
Icon(Icons.open_in_new, size: 18, color: t.labelSmall?.color),
],
),
),
),
);
}
}