feat: 逻辑与页面布局分离

This commit is contained in:
Vicky committed 2026-10-13 16:52:14 +08:00
1 parent b1f6ce4bf9
commit 1e61ae7a0b
11 files changed
+1233 -907

No files matched your search

+125 -1
View File
@@ -2,8 +2,11 @@ import 'package:flutter/material.dart';
import '../../core/data/models/drama.dart';
import '../../core/data/repositories/watch_history_repository.dart';
import '../../l10n/app_localizations.dart';
class DiscoverLogic {
enum DiscoverPageAction { home, my, watchlist, detail, resume }
class DiscoverLogic extends ChangeNotifier {
DiscoverLogic({required this.history, required this.drama}) {
refreshPosition();
}
@@ -15,6 +18,11 @@ class DiscoverLogic {
final FocusNode searchFocus = FocusNode();
final ScrollController scroll = ScrollController();
int category = 0;
bool _disposed = false;
void notify() {
if (!_disposed) notifyListeners();
}
void refreshPosition() {
position = history.positionFor(
@@ -24,9 +32,125 @@ class DiscoverLogic {
);
}
void selectCategory(int index) {
category = index;
notify();
}
void onSearchChanged() => notify();
void syncDrama({
required bool active,
required bool wasActive,
required Drama nextDrama,
required bool dramaChanged,
required int searchRequest,
required int previousSearchRequest,
required bool Function() isActive,
}) {
if (active && (!wasActive || dramaChanged)) {
drama = nextDrama;
refreshPosition();
notify();
}
if (active && searchRequest != previousSearchRequest) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (isActive()) searchFocus.requestFocus();
});
}
}
void act(
BuildContext context,
DiscoverPageAction action, {
required bool embedded,
ValueChanged<DiscoverPageAction>? onAction,
}) {
if (embedded) {
onAction?.call(action);
} else {
Navigator.pop(context, action);
}
}
Drama? dramaFor(String? dramaId, List<Drama> dramas) {
for (final item in dramas) {
if (item.id == dramaId) return item;
}
return null;
}
void openCard(
BuildContext context,
Drama? selected, {
required bool embedded,
ValueChanged<Drama>? onDramaSelected,
ValueChanged<DiscoverPageAction>? onAction,
}) {
if (selected == null) {
unavailable(context);
} else if (embedded && onDramaSelected != null) {
onDramaSelected(selected);
} else {
act(context, DiscoverPageAction.detail, embedded: embedded, onAction: onAction);
}
}
void unavailable(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(l10n.artworkOnlyNotice)));
}
void catalogInfo(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(l10n.demoCatalogNotice)));
}
void showFilters(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final labels = [
l10n.featured,
l10n.suspense,
l10n.romance,
l10n.actionGenre,
];
showModalBottomSheet<void>(
context: context,
backgroundColor: const Color(0xFF202126),
showDragHandle: true,
builder: (sheetContext) => SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(title: Text(l10n.filter)),
for (var index = 0; index < labels.length; index++)
ListTile(
title: Text(labels[index]),
trailing: index == category
? const Icon(Icons.check_rounded, color: Color(0xFFFF5B3D))
: null,
onTap: () {
selectCategory(index);
Navigator.pop(sheetContext);
},
),
const SizedBox(height: 8),
],
),
),
);
}
@override
void dispose() {
_disposed = true;
search.dispose();
searchFocus.dispose();
scroll.dispose();
super.dispose();
}
}
+65 -100
View File
@@ -8,8 +8,6 @@ import '../../l10n/app_localizations.dart';
import 'discover_binding.dart';
import 'discover_logic.dart';
enum DiscoverPageAction { home, my, watchlist, detail, resume }
class DiscoverPage extends StatefulWidget {
const DiscoverPage({
super.key,
@@ -55,42 +53,15 @@ class _DiscoverPageState extends State<DiscoverPage> {
@override
void didUpdateWidget(covariant DiscoverPage oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.active &&
(!oldWidget.active || oldWidget.drama.id != widget.drama.id)) {
logic.drama = widget.drama;
setState(logic.refreshPosition);
}
if (widget.active && widget.searchRequest != oldWidget.searchRequest) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted && widget.active) logic.searchFocus.requestFocus();
});
}
}
void _act(DiscoverPageAction action) {
if (widget.embedded) {
widget.onAction?.call(action);
} else {
Navigator.pop(context, action);
}
}
Drama? _dramaFor(_ShowcaseCard card) {
for (final drama in widget.dramas ?? [logic.drama]) {
if (drama.id == card.dramaId) return drama;
}
return null;
}
void _openCard(_ShowcaseCard card) {
final drama = _dramaFor(card);
if (drama == null) {
_unavailable();
} else if (widget.embedded && widget.onDramaSelected != null) {
widget.onDramaSelected!(drama);
} else {
_act(DiscoverPageAction.detail);
}
logic.syncDrama(
active: widget.active,
wasActive: oldWidget.active,
nextDrama: widget.drama,
dramaChanged: oldWidget.drama.id != widget.drama.id,
searchRequest: widget.searchRequest,
previousSearchRequest: oldWidget.searchRequest,
isActive: () => mounted && widget.active,
);
}
@override
@@ -99,57 +70,15 @@ class _DiscoverPageState extends State<DiscoverPage> {
super.dispose();
}
void _unavailable() {
final l10n = AppLocalizations.of(context)!;
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(l10n.artworkOnlyNotice)));
}
void _catalogInfo() {
final l10n = AppLocalizations.of(context)!;
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(l10n.demoCatalogNotice)));
}
void _showFilters() {
final l10n = AppLocalizations.of(context)!;
final labels = [
l10n.featured,
l10n.suspense,
l10n.romance,
l10n.actionGenre,
];
showModalBottomSheet<void>(
context: context,
backgroundColor: const Color(0xFF202126),
showDragHandle: true,
builder: (sheetContext) => SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(title: Text(l10n.filter)),
for (var index = 0; index < labels.length; index++)
ListTile(
title: Text(labels[index]),
trailing: index == logic.category
? const Icon(Icons.check_rounded, color: _orange)
: null,
onTap: () {
setState(() => logic.category = index);
Navigator.pop(sheetContext);
},
),
const SizedBox(height: 8),
],
),
),
@override
Widget build(BuildContext context) {
return ListenableBuilder(
listenable: logic,
builder: (context, _) => _page(context),
);
}
@override
Widget build(BuildContext context) {
Widget _page(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final cards = _cardsFor(l10n, logic.category, logic.search.text);
final content = SafeArea(
@@ -241,7 +170,12 @@ class _DiscoverPageState extends State<DiscoverPage> {
),
const SizedBox(width: 4),
InkWell(
onTap: () => _act(DiscoverPageAction.my),
onTap: () => logic.act(
context,
DiscoverPageAction.my,
embedded: widget.embedded,
onAction: widget.onAction,
),
child: const CircleAvatar(
radius: 14,
backgroundColor: Color(0xFF34353B),
@@ -269,7 +203,7 @@ class _DiscoverPageState extends State<DiscoverPage> {
controller: logic.search,
focusNode: logic.searchFocus,
autofocus: widget.focusSearch,
onChanged: (_) => setState(() {}),
onChanged: (_) => logic.onSearchChanged(),
style: const TextStyle(fontSize: 12),
decoration: InputDecoration(
hintText: l10n.searchDiscover,
@@ -284,7 +218,7 @@ class _DiscoverPageState extends State<DiscoverPage> {
),
IconButton(
tooltip: l10n.filter,
onPressed: _showFilters,
onPressed: () => logic.showFilters(context),
icon: const Icon(Icons.tune_rounded, size: 20),
),
],
@@ -307,7 +241,7 @@ class _DiscoverPageState extends State<DiscoverPage> {
itemBuilder: (context, index) => ChoiceChip(
label: Text(labels[index]),
selected: logic.category == index,
onSelected: (_) => setState(() => logic.category = index),
onSelected: (_) => logic.selectCategory(index),
showCheckmark: false,
visualDensity: VisualDensity.compact,
padding: const EdgeInsets.symmetric(horizontal: 7),
@@ -413,7 +347,12 @@ class _DiscoverPageState extends State<DiscoverPage> {
),
const SizedBox(width: 9),
FilledButton.icon(
onPressed: () => _act(DiscoverPageAction.resume),
onPressed: () => logic.act(
context,
DiscoverPageAction.resume,
embedded: widget.embedded,
onAction: widget.onAction,
),
style: FilledButton.styleFrom(
backgroundColor: const Color(0xFFFFBD5C),
foregroundColor: const Color(0xFF2A160B),
@@ -435,14 +374,23 @@ class _DiscoverPageState extends State<DiscoverPage> {
Widget _quickActions(AppLocalizations l10n) {
final actions = [
(Icons.tune_rounded, l10n.filter, _showFilters),
(Icons.bar_chart_rounded, l10n.topCharts, _catalogInfo),
(Icons.auto_awesome_rounded, l10n.newReleases, _catalogInfo),
(Icons.tune_rounded, l10n.filter, () => logic.showFilters(context)),
(Icons.bar_chart_rounded, l10n.topCharts, () => logic.catalogInfo(context)),
(
Icons.auto_awesome_rounded,
l10n.newReleases,
() => logic.catalogInfo(context),
),
(
Icons.bookmark_outline_rounded,
l10n.watchlist,
() {
_act(DiscoverPageAction.watchlist);
logic.act(
context,
DiscoverPageAction.watchlist,
embedded: widget.embedded,
onAction: widget.onAction,
);
},
),
];
@@ -509,7 +457,7 @@ class _DiscoverPageState extends State<DiscoverPage> {
),
),
TextButton(
onPressed: () => setState(() => logic.category = 0),
onPressed: () => logic.selectCategory(0),
child: Text(
l10n.seeAll,
style: const TextStyle(color: Color(0xFFFFB3A1)),
@@ -519,9 +467,16 @@ class _DiscoverPageState extends State<DiscoverPage> {
);
Widget _posterCard(AppLocalizations l10n, _ShowcaseCard card) {
final playable = _dramaFor(card) != null;
final playable =
logic.dramaFor(card.dramaId, widget.dramas ?? [logic.drama]) != null;
return InkWell(
onTap: () => _openCard(card),
onTap: () => logic.openCard(
context,
logic.dramaFor(card.dramaId, widget.dramas ?? [logic.drama]),
embedded: widget.embedded,
onDramaSelected: widget.onDramaSelected,
onAction: widget.onAction,
),
borderRadius: BorderRadius.circular(11),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -609,7 +564,12 @@ class _DiscoverPageState extends State<DiscoverPage> {
_DiscoverNavItem(
icon: Icons.play_circle_outline_rounded,
label: l10n.homeTab,
onTap: () => Navigator.pop(context, DiscoverPageAction.home),
onTap: () => logic.act(
context,
DiscoverPageAction.home,
embedded: widget.embedded,
onAction: widget.onAction,
),
),
_DiscoverNavItem(
icon: Icons.explore_outlined,
@@ -620,7 +580,12 @@ class _DiscoverPageState extends State<DiscoverPage> {
_DiscoverNavItem(
icon: Icons.video_library_outlined,
label: l10n.myTab,
onTap: () => Navigator.pop(context, DiscoverPageAction.my),
onTap: () => logic.act(
context,
DiscoverPageAction.my,
embedded: widget.embedded,
onAction: widget.onAction,
),
),
],
),