feat: 逻辑与页面布局分离
This commit is contained in:
1 parent
b1f6ce4bf9
commit
1e61ae7a0b
11 files changed
+1233
-907
No files matched your search
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
Reference in new issue
Block a user