use riverpod

This commit is contained in:
Simon Ding
2024-07-09 13:51:30 +08:00
parent a76adfdd29
commit 684846fd88
8 changed files with 327 additions and 293 deletions

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:ui/navdrawer.dart';
import 'package:ui/search.dart';
@@ -46,7 +47,7 @@ class MyApp extends StatelessWidget {
// Center is a layout widget. It takes a single child and positions it
// in the middle of the parent.
child: Row(children: <Widget>[
NavDrawer(),
const NavDrawer(),
const VerticalDivider(thickness: 1, width: 1),
Expanded(child: child)
]))),
@@ -81,29 +82,31 @@ class MyApp extends StatelessWidget {
],
);
return MaterialApp.router(
title: 'Flutter Demo',
theme: ThemeData(
// This is the theme of your application.
//
// TRY THIS: Try running your application with "flutter run". You'll see
// the application has a purple toolbar. Then, without quitting the app,
// try changing the seedColor in the colorScheme below to Colors.green
// and then invoke "hot reload" (save your changes or press the "hot
// reload" button in a Flutter-supported IDE, or press "r" if you used
// the command line to start the app).
//
// Notice that the counter didn't reset back to zero; the application
// state is not lost during the reload. To reset the state, use hot
// restart instead.
//
// This works for code too, not just values: Most code changes can be
// tested with just a hot reload.
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.blue, brightness: Brightness.dark),
useMaterial3: true,
return ProviderScope(
child: MaterialApp.router(
title: 'Flutter Demo',
theme: ThemeData(
// This is the theme of your application.
//
// TRY THIS: Try running your application with "flutter run". You'll see
// the application has a purple toolbar. Then, without quitting the app,
// try changing the seedColor in the colorScheme below to Colors.green
// and then invoke "hot reload" (save your changes or press the "hot
// reload" button in a Flutter-supported IDE, or press "r" if you used
// the command line to start the app).
//
// Notice that the counter didn't reset back to zero; the application
// state is not lost during the reload. To reset the state, use hot
// restart instead.
//
// This works for code too, not just values: Most code changes can be
// tested with just a hot reload.
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.blue, brightness: Brightness.dark),
useMaterial3: true,
),
routerConfig: _router,
),
routerConfig: _router,
);
}
}

View File

@@ -0,0 +1,82 @@
import 'package:dio/dio.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:ui/APIs.dart';
import 'package:ui/server_response.dart';
var seriesDetailsProvider = FutureProvider.family((ref, seriesId) async {
var resp = await Dio().get("${APIs.seriesDetailUrl}$seriesId");
var rsp = ServerResponse.fromJson(resp.data);
if (rsp.code != 0) {
throw rsp.message;
}
return SeriesDetails.fromJson(rsp.data);
});
class SeriesDetails {
int? id;
int? tmdbId;
String? name;
String? originalName;
String? overview;
String? path;
String? posterPath;
String? createdAt;
List<Episodes>? episodes;
SeriesDetails(
{this.id,
this.tmdbId,
this.name,
this.originalName,
this.overview,
this.path,
this.posterPath,
this.createdAt,
this.episodes});
SeriesDetails.fromJson(Map<String, dynamic> json) {
id = json['id'];
tmdbId = json['tmdb_id'];
name = json['name'];
originalName = json['original_name'];
overview = json['overview'];
path = json['path'];
posterPath = json['poster_path'];
createdAt = json['created_at'];
if (json['episodes'] != null) {
episodes = <Episodes>[];
json['episodes'].forEach((v) {
episodes!.add(Episodes.fromJson(v));
});
}
}
}
class Episodes {
int? id;
int? seriesId;
int? episodeNumber;
String? title;
String? airDate;
int? seasonNumber;
String? overview;
Episodes(
{this.id,
this.seriesId,
this.episodeNumber,
this.title,
this.airDate,
this.seasonNumber,
this.overview});
Episodes.fromJson(Map<String, dynamic> json) {
id = json['id'];
seriesId = json['series_id'];
episodeNumber = json['episode_number'];
title = json['title'];
airDate = json['air_date'];
seasonNumber = json['season_number'];
overview = json['overview'];
}
}

View File

@@ -0,0 +1,58 @@
import 'package:dio/dio.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:ui/APIs.dart';
import 'package:ui/server_response.dart';
final welcomePageDataProvider = FutureProvider((ref) async {
var resp = await Dio().get(APIs.watchlistUrl);
var sp = ServerResponse.fromJson(resp.data);
List<TvSeries> favList = List.empty(growable: true);
for (var item in sp.data as List) {
var tv = TvSeries.fromJson(item);
favList.add(tv);
}
return favList;
});
class TvSeries {
int? id;
int? tmdbId;
String? name;
String? originalName;
String? overview;
String? path;
String? posterPath;
TvSeries(
{this.id,
this.tmdbId,
this.name,
this.originalName,
this.overview,
this.path,
this.posterPath});
TvSeries.fromJson(Map<String, dynamic> json) {
id = json['id'];
tmdbId = json['tmdb_id'];
name = json['name'];
originalName = json['original_name'];
overview = json['overview'];
path = json['path'];
posterPath = json["poster_path"];
}
}
var tmdbApiSettingProvider = FutureProvider(
(ref) async {
final dio = Dio();
var resp = await dio
.get(APIs.settingsUrl, queryParameters: {"key": APIs.tmdbApiKey});
var rrr = resp.data as Map<String, dynamic>;
var data = rrr["data"] as Map<String, dynamic>;
var key = data[APIs.tmdbApiKey] as String;
return key;
},
);

View File

@@ -1,35 +1,38 @@
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:ui/APIs.dart';
import 'package:ui/providers/welcome_data.dart';
import 'package:ui/server_response.dart';
import 'package:ui/utils.dart';
class SystemSettingsPage extends StatefulWidget {
class SystemSettingsPage extends ConsumerStatefulWidget {
static const route = "/systemsettings";
const SystemSettingsPage({super.key});
@override
State<StatefulWidget> createState() {
ConsumerState<ConsumerStatefulWidget> createState() {
return _SystemSettingsPageState();
}
}
class _SystemSettingsPageState extends State<SystemSettingsPage> {
class _SystemSettingsPageState extends ConsumerState<SystemSettingsPage> {
final GlobalKey _formKey = GlobalKey<FormState>();
final TextEditingController _tmdbApiKeyController = TextEditingController();
List<dynamic> indexers = List.empty();
@override
void initState() {
super.initState();
_handleRefresh();
}
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.fromLTRB(40, 10, 40, 0),
child: RefreshIndicator(
onRefresh: _handleRefresh,
var key = ref.watch(tmdbApiSettingProvider);
return key.when(
data: (data ) => Container(
padding: const EdgeInsets.fromLTRB(40, 10, 40, 0),
child: Form(
key: _formKey, //设置globalKey用于后面获取FormState
autovalidateMode: AutovalidateMode.onUserInteraction,
@@ -37,7 +40,7 @@ class _SystemSettingsPageState extends State<SystemSettingsPage> {
children: [
TextFormField(
autofocus: true,
controller: _tmdbApiKeyController,
initialValue: data,
decoration: const InputDecoration(
labelText: "TMDB Api Key",
icon: Icon(Icons.key),
@@ -46,6 +49,9 @@ class _SystemSettingsPageState extends State<SystemSettingsPage> {
validator: (v) {
return v!.trim().isNotEmpty ? null : "ApiKey 不能为空";
},
onSaved: (newValue) {
_submitSettings(context, newValue!);
},
),
Center(
child: Padding(
@@ -60,7 +66,7 @@ class _SystemSettingsPageState extends State<SystemSettingsPage> {
// 调用validate()方法校验用户名密码是否合法,校验
// 通过后再提交数据。
if ((_formKey.currentState as FormState).validate()) {
_submitSettings(context, _tmdbApiKeyController.text);
(_formKey.currentState as FormState).save();
}
},
),
@@ -68,20 +74,10 @@ class _SystemSettingsPageState extends State<SystemSettingsPage> {
)
],
),
)),
);
}
Future<void> _handleRefresh() async {
final dio = Dio();
var resp = await dio
.get(APIs.settingsUrl, queryParameters: {"key": APIs.tmdbApiKey});
var rrr = resp.data as Map<String, dynamic>;
var data = rrr["data"] as Map<String, dynamic>;
var key = data[APIs.tmdbApiKey] as String;
_tmdbApiKeyController.text = key;
// Fetch new data and update the UI
),
),
error: (err, trace) => Text("$err"),
loading: () => const CircularProgressIndicator());
}
void _submitSettings(BuildContext context, String v) async {

View File

@@ -1,10 +1,12 @@
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:ui/APIs.dart';
import 'package:ui/providers/series_details.dart';
import 'package:ui/server_response.dart';
import 'package:ui/utils.dart';
class TvDetailsPage extends StatefulWidget {
class TvDetailsPage extends ConsumerStatefulWidget {
static const route = "/series/:id";
static String toRoute(int id) {
@@ -16,140 +18,125 @@ class TvDetailsPage extends StatefulWidget {
const TvDetailsPage({super.key, required this.seriesId});
@override
State<StatefulWidget> createState() {
ConsumerState<ConsumerStatefulWidget> createState() {
return _TvDetailsPageState(seriesId: seriesId);
}
}
class _TvDetailsPageState extends State<TvDetailsPage> {
class _TvDetailsPageState extends ConsumerState<TvDetailsPage> {
final String seriesId;
_TvDetailsPageState({required this.seriesId});
SeriesDetails? details;
@override
void initState() {
super.initState();
_querySeriesDetails();
}
@override
Widget build(BuildContext context) {
if (details == null) {
return const Center(
child: Text("nothing here"),
);
}
Map<int, List<Widget>> m = Map();
for (final ep in details!.episodes!) {
var w = Container(
alignment: Alignment.topLeft,
child: Row(
children: [
SizedBox(
width: 70,
child: Text("${ep.episodeNumber}"),
),
SizedBox(
width: 100,
child: Opacity(
opacity: 0.5,
child: Text("${ep.airDate}"),
),
),
Text("${ep.title}", textAlign: TextAlign.left),
const Expanded(child: Text("")),
IconButton(
onPressed: () {
_searchAndDownload(
context, seriesId, ep.seasonNumber!, ep.episodeNumber!);
},
icon: const Icon(Icons.search))
],
),
);
if (m[ep.seasonNumber] == null) {
m[ep.seasonNumber!] = List.empty(growable: true);
}
m[ep.seasonNumber!]!.add(w);
}
List<ExpansionTile> list = List.empty(growable: true);
for (final k in m.keys.toList().reversed) {
bool _customTileExpanded = false;
var seasonList = ExpansionTile(
tilePadding: const EdgeInsets.fromLTRB(10, 0, 10, 0),
childrenPadding: const EdgeInsets.fromLTRB(50, 0, 50, 0),
initiallyExpanded: k == 0 ? false : true,
title: Text("$k"),
trailing: Icon(
_customTileExpanded
? Icons.arrow_drop_down_circle
: Icons.arrow_drop_down,
),
children: m[k]!,
onExpansionChanged: (bool expanded) {
setState(() {
_customTileExpanded = expanded;
});
},
);
list.add(seasonList);
}
return ListView(
children: [
Card(
margin: const EdgeInsets.all(4),
clipBehavior: Clip.hardEdge,
child: Row(
children: <Widget>[
Flexible(
child: SizedBox(
width: 150,
height: 200,
child: Image.network(
APIs.tmdbImgBaseUrl + details!.posterPath!,
fit: BoxFit.contain,
var seriesDetails = ref.watch(seriesDetailsProvider(seriesId));
return seriesDetails.when(
data: (details) {
Map<int, List<Widget>> m = Map();
for (final ep in details.episodes!) {
var w = Container(
alignment: Alignment.topLeft,
child: Row(
children: [
SizedBox(
width: 70,
child: Text("${ep.episodeNumber}"),
),
),
),
Flexible(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"${details!.name}",
style: const TextStyle(
fontSize: 14, fontWeight: FontWeight.bold),
SizedBox(
width: 100,
child: Opacity(
opacity: 0.5,
child: Text("${ep.airDate}"),
),
),
Text("${ep.title}", textAlign: TextAlign.left),
const Expanded(child: Text("")),
IconButton(
onPressed: () {
_searchAndDownload(context, seriesId, ep.seasonNumber!,
ep.episodeNumber!);
},
icon: const Icon(Icons.search))
],
),
);
if (m[ep.seasonNumber] == null) {
m[ep.seasonNumber!] = List.empty(growable: true);
}
m[ep.seasonNumber!]!.add(w);
}
List<ExpansionTile> list = List.empty(growable: true);
for (final k in m.keys.toList().reversed) {
bool _customTileExpanded = false;
var seasonList = ExpansionTile(
tilePadding: const EdgeInsets.fromLTRB(10, 0, 10, 0),
childrenPadding: const EdgeInsets.fromLTRB(50, 0, 50, 0),
initiallyExpanded: k == 0 ? false : true,
title: Text("$k"),
trailing: Icon(
_customTileExpanded
? Icons.arrow_drop_down_circle
: Icons.arrow_drop_down,
),
children: m[k]!,
onExpansionChanged: (bool expanded) {
setState(() {
_customTileExpanded = expanded;
});
},
);
list.add(seasonList);
}
return ListView(
children: [
Card(
margin: const EdgeInsets.all(4),
clipBehavior: Clip.hardEdge,
child: Row(
children: <Widget>[
Flexible(
child: SizedBox(
width: 150,
height: 200,
child: Image.network(
APIs.tmdbImgBaseUrl + details!.posterPath!,
fit: BoxFit.contain,
),
),
),
Flexible(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"${details!.name}",
style: const TextStyle(
fontSize: 14, fontWeight: FontWeight.bold),
),
const Text(""),
Text(details!.overview!)
],
),
),
const Text(""),
Text(details!.overview!)
],
),
),
Column(
children: list,
),
],
),
),
Column(
children: list,
),
],
);
}
void _querySeriesDetails() async {
if (details != null) {
return;
}
var resp = await Dio().get("${APIs.seriesDetailUrl}$seriesId");
var rsp = ServerResponse.fromJson(resp.data);
setState(() {
details = SeriesDetails.fromJson(rsp.data);
});
);
},
error: (err, trace) {
return Text("$err");
},
loading: () => const CircularProgressIndicator());
}
void _searchAndDownload(BuildContext context, String seriesId, int seasonNum,
@@ -170,72 +157,3 @@ class _TvDetailsPageState extends State<TvDetailsPage> {
}
}
}
class SeriesDetails {
int? id;
int? tmdbId;
String? name;
String? originalName;
String? overview;
String? path;
String? posterPath;
String? createdAt;
List<Episodes>? episodes;
SeriesDetails(
{this.id,
this.tmdbId,
this.name,
this.originalName,
this.overview,
this.path,
this.posterPath,
this.createdAt,
this.episodes});
SeriesDetails.fromJson(Map<String, dynamic> json) {
id = json['id'];
tmdbId = json['tmdb_id'];
name = json['name'];
originalName = json['original_name'];
overview = json['overview'];
path = json['path'];
posterPath = json['poster_path'];
createdAt = json['created_at'];
if (json['episodes'] != null) {
episodes = <Episodes>[];
json['episodes'].forEach((v) {
episodes!.add(Episodes.fromJson(v));
});
}
}
}
class Episodes {
int? id;
int? seriesId;
int? episodeNumber;
String? title;
String? airDate;
int? seasonNumber;
String? overview;
Episodes(
{this.id,
this.seriesId,
this.episodeNumber,
this.title,
this.airDate,
this.seasonNumber,
this.overview});
Episodes.fromJson(Map<String, dynamic> json) {
id = json['id'];
seriesId = json['series_id'];
episodeNumber = json['episode_number'];
title = json['title'];
airDate = json['air_date'];
seasonNumber = json['season_number'];
overview = json['overview'];
}
}

View File

@@ -1,37 +1,26 @@
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:ui/APIs.dart';
import 'package:ui/server_response.dart';
import 'package:ui/providers/welcome_data.dart';
import 'package:ui/tv_details.dart';
class WelcomePage extends StatefulWidget {
const WelcomePage({super.key});
class WelcomePage extends ConsumerWidget {
static const route = "/welcome";
@override
State<StatefulWidget> createState() {
return _WeclomePageState();
}
}
class _WeclomePageState extends State<WelcomePage> {
var favList = List.empty(growable: true);
const WelcomePage({super.key});
@override
void initState() {
super.initState();
_onRefresh();
}
Widget build(BuildContext context, WidgetRef ref) {
final data = ref.watch(welcomePageDataProvider);
@override
Widget build(BuildContext context) {
return GridView.builder(
itemCount: favList.length,
return switch (data) {
AsyncData(:final value) => GridView.builder(
itemCount: value.length,
gridDelegate:
const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 6),
itemBuilder: (context, i) {
var item = TvSeries.fromJson(favList[i]);
var item = value[i];
return Card(
margin: const EdgeInsets.all(4),
clipBehavior: Clip.hardEdge,
@@ -63,46 +52,9 @@ class _WeclomePageState extends State<WelcomePage> {
],
),
));
});
}
Future<void> _onRefresh() async {
if (favList.isNotEmpty) {
return;
}
var resp = await Dio().get(APIs.watchlistUrl);
var sp = ServerResponse.fromJson(resp.data);
setState(() {
favList = sp.data as List;
});
}
}
class TvSeries {
int? id;
int? tmdbId;
String? name;
String? originalName;
String? overview;
String? path;
String? posterPath;
TvSeries(
{this.id,
this.tmdbId,
this.name,
this.originalName,
this.overview,
this.path,
this.posterPath});
TvSeries.fromJson(Map<String, dynamic> json) {
id = json['id'];
tmdbId = json['tmdb_id'];
name = json['name'];
originalName = json['original_name'];
overview = json['overview'];
path = json['path'];
posterPath = json["poster_path"];
}),
_ => const CircularProgressIndicator(),
};
}
}

View File

@@ -78,6 +78,14 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "4.0.0"
flutter_riverpod:
dependency: "direct main"
description:
name: flutter_riverpod
sha256: "0f1974eff5bbe774bf1d870e406fc6f29e3d6f1c46bd9c58e7172ff68a785d7d"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.5.1"
flutter_test:
dependency: "direct dev"
description: flutter
@@ -176,6 +184,14 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.9.0"
riverpod:
dependency: transitive
description:
name: riverpod
sha256: f21b32ffd26a36555e501b04f4a5dca43ed59e16343f1a30c13632b2351dfa4d
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.5.1"
sky_engine:
dependency: transitive
description: flutter
@@ -197,6 +213,14 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.11.1"
state_notifier:
dependency: transitive
description:
name: state_notifier
sha256: b8677376aa54f2d7c58280d5a007f9e8774f1968d1fb1c096adcb4792fba29bb
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.0.0"
stream_channel:
dependency: transitive
description:

View File

@@ -37,6 +37,7 @@ dependencies:
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.6
go_router: ^14.2.0
flutter_riverpod: ^2.5.1
dev_dependencies:
flutter_test: