mirror of
https://github.com/simon-ding/polaris.git
synced 2026-06-09 19:47:47 +08:00
feat: add movie tracking feature
This commit is contained in:
@@ -5,7 +5,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:quiver/strings.dart';
|
||||
import 'package:ui/providers/login.dart';
|
||||
import 'package:ui/weclome.dart';
|
||||
import 'package:ui/tv_watchlist.dart';
|
||||
|
||||
class LoginScreen extends ConsumerWidget {
|
||||
static const route = '/login';
|
||||
@@ -30,7 +30,7 @@ class LoginScreen extends ConsumerWidget {
|
||||
ref.read(authSettingProvider.notifier).login(data.name, data.password);
|
||||
},
|
||||
onSubmitAnimationCompleted: () {
|
||||
context.go(WelcomePage.route);
|
||||
context.go(TvWatchlistPage.route);
|
||||
},
|
||||
onRecoverPassword: _recoverPassword,
|
||||
userValidator: (value) => isBlank(value)? "不能为空":null,
|
||||
|
||||
@@ -4,12 +4,13 @@ import 'package:go_router/go_router.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:ui/activity.dart';
|
||||
import 'package:ui/login_page.dart';
|
||||
import 'package:ui/movie_watchlist.dart';
|
||||
import 'package:ui/navdrawer.dart';
|
||||
import 'package:ui/providers/APIs.dart';
|
||||
import 'package:ui/search.dart';
|
||||
import 'package:ui/system_settings.dart';
|
||||
import 'package:ui/tv_details.dart';
|
||||
import 'package:ui/weclome.dart';
|
||||
import 'package:ui/tv_watchlist.dart';
|
||||
|
||||
void main() {
|
||||
runApp(const MyApp());
|
||||
@@ -76,11 +77,25 @@ class MyApp extends StatelessWidget {
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: "/",
|
||||
redirect: (context, state) => WelcomePage.route,
|
||||
redirect: (context, state) => TvWatchlistPage.route,
|
||||
),
|
||||
GoRoute(
|
||||
path: WelcomePage.route,
|
||||
builder: (context, state) => const WelcomePage(),
|
||||
path: TvWatchlistPage.route,
|
||||
builder: (context, state) => const TvWatchlistPage(),
|
||||
),
|
||||
GoRoute(
|
||||
path: TvDetailsPage.route,
|
||||
builder: (context, state) =>
|
||||
TvDetailsPage(seriesId: state.pathParameters['id']!),
|
||||
),
|
||||
GoRoute(
|
||||
path: MovieWatchlistPage.route,
|
||||
builder: (context, state) => const MovieWatchlistPage(),
|
||||
),
|
||||
GoRoute(
|
||||
path: MovieDetailsPage.route,
|
||||
builder: (context, state) =>
|
||||
MovieDetailsPage(id: state.pathParameters['id']!),
|
||||
),
|
||||
GoRoute(
|
||||
path: SearchPage.route,
|
||||
@@ -90,11 +105,6 @@ class MyApp extends StatelessWidget {
|
||||
path: SystemSettingsPage.route,
|
||||
builder: (context, state) => const SystemSettingsPage(),
|
||||
),
|
||||
GoRoute(
|
||||
path: TvDetailsPage.route,
|
||||
builder: (context, state) =>
|
||||
TvDetailsPage(seriesId: state.pathParameters['id']!),
|
||||
),
|
||||
GoRoute(
|
||||
path: ActivityPage.route,
|
||||
builder: (context, state) => const ActivityPage(),
|
||||
@@ -104,7 +114,7 @@ class MyApp extends StatelessWidget {
|
||||
|
||||
final _router = GoRouter(
|
||||
navigatorKey: APIs.navigatorKey,
|
||||
initialLocation: WelcomePage.route,
|
||||
initialLocation: TvWatchlistPage.route,
|
||||
routes: [
|
||||
_shellRoute,
|
||||
GoRoute(
|
||||
|
||||
203
ui/lib/movie_watchlist.dart
Normal file
203
ui/lib/movie_watchlist.dart
Normal file
@@ -0,0 +1,203 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:ui/providers/APIs.dart';
|
||||
import 'package:ui/providers/series_details.dart';
|
||||
import 'package:ui/providers/settings.dart';
|
||||
import 'package:ui/providers/welcome_data.dart';
|
||||
import 'package:ui/utils.dart';
|
||||
import 'package:ui/widgets/progress_indicator.dart';
|
||||
|
||||
class MovieWatchlistPage extends ConsumerWidget {
|
||||
static const route = "/movie";
|
||||
|
||||
const MovieWatchlistPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final data = ref.watch(movieWatchlistDataProvider);
|
||||
|
||||
return switch (data) {
|
||||
AsyncData(:final value) => GridView.builder(
|
||||
padding: const EdgeInsets.all(30),
|
||||
itemCount: value.length,
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 6),
|
||||
itemBuilder: (context, i) {
|
||||
var item = value[i];
|
||||
return Card(
|
||||
margin: const EdgeInsets.all(4),
|
||||
clipBehavior: Clip.hardEdge,
|
||||
child: InkWell(
|
||||
//splashColor: Colors.blue.withAlpha(30),
|
||||
onTap: () {
|
||||
context.go(MovieDetailsPage.toRoute(item.id!));
|
||||
//showDialog(context: context, builder: builder)
|
||||
},
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Flexible(
|
||||
child: Image.network(
|
||||
"${APIs.imagesUrl}/${item.id}/poster.jpg",
|
||||
fit: BoxFit.contain,
|
||||
headers: APIs.authHeaders,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
item.name!,
|
||||
style: const TextStyle(
|
||||
fontSize: 14, fontWeight: FontWeight.bold),
|
||||
),
|
||||
],
|
||||
),
|
||||
));
|
||||
}),
|
||||
_ => const MyProgressIndicator(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class MovieDetailsPage extends ConsumerStatefulWidget {
|
||||
static const route = "/movie/:id";
|
||||
|
||||
static String toRoute(int id) {
|
||||
return "/movie/$id";
|
||||
}
|
||||
|
||||
final String id;
|
||||
|
||||
const MovieDetailsPage({super.key, required this.id});
|
||||
|
||||
@override
|
||||
ConsumerState<ConsumerStatefulWidget> createState() {
|
||||
return _MovieDetailsPageState(id: id);
|
||||
}
|
||||
}
|
||||
|
||||
class _MovieDetailsPageState extends ConsumerState<MovieDetailsPage> {
|
||||
final String id;
|
||||
|
||||
_MovieDetailsPageState({required this.id});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var seriesDetails = ref.watch(mediaDetailsProvider(id));
|
||||
var torrents = ref.watch(movieTorrentsDataProvider(id));
|
||||
var storage = ref.watch(storageSettingProvider);
|
||||
|
||||
return seriesDetails.when(
|
||||
data: (details) {
|
||||
return ListView(
|
||||
children: [
|
||||
Card(
|
||||
margin: const EdgeInsets.all(4),
|
||||
clipBehavior: Clip.hardEdge,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Flexible(
|
||||
flex: 1,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: Image.network(
|
||||
"${APIs.imagesUrl}/${details.id}/poster.jpg",
|
||||
fit: BoxFit.contain,
|
||||
headers: APIs.authHeaders,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 6,
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text("${details.resolution}"),
|
||||
const SizedBox(
|
||||
width: 30,
|
||||
),
|
||||
storage.when(
|
||||
data: (value) {
|
||||
for (final s in value) {
|
||||
if (s.id == details.storageId) {
|
||||
return Text(
|
||||
"${s.name}(${s.implementation})");
|
||||
}
|
||||
}
|
||||
return const Text("未知存储");
|
||||
},
|
||||
error: (error, stackTrace) =>
|
||||
Text("$error"),
|
||||
loading: () =>
|
||||
const MyProgressIndicator()),
|
||||
],
|
||||
),
|
||||
const Divider(thickness: 1, height: 1),
|
||||
Text(
|
||||
"${details.name} (${details.airDate!.split("-")[0]})",
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold),
|
||||
),
|
||||
const Text(""),
|
||||
Text(
|
||||
details.overview!,
|
||||
),
|
||||
],
|
||||
)),
|
||||
Column(
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
ref
|
||||
.read(
|
||||
mediaDetailsProvider(id).notifier)
|
||||
.delete();
|
||||
context.go(MovieDetailsPage.route);
|
||||
},
|
||||
icon: const Icon(Icons.delete))
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
torrents.when(
|
||||
data: (v) {
|
||||
return DataTable(
|
||||
columns: const [
|
||||
DataColumn(label: Text("名称")),
|
||||
DataColumn(label: Text("大小")),
|
||||
DataColumn(label: Text("seeders")),
|
||||
DataColumn(label: Text("peers")),
|
||||
],
|
||||
rows: List.generate(v.length, (i) {
|
||||
final torrent = v[i];
|
||||
return DataRow(cells: [
|
||||
DataCell(Text("${torrent.name}")),
|
||||
DataCell(Text("${torrent.size?.readableFileSize()}")),
|
||||
DataCell(Text("${torrent.seeders}")),
|
||||
DataCell(Text("${torrent.peers}")),
|
||||
]);
|
||||
}),
|
||||
);
|
||||
},
|
||||
error: (error, trace) => Text("$error"),
|
||||
loading: () => const MyProgressIndicator()),
|
||||
],
|
||||
);
|
||||
},
|
||||
error: (err, trace) {
|
||||
return Text("$err");
|
||||
},
|
||||
loading: () => const MyProgressIndicator());
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:ui/activity.dart';
|
||||
import 'package:ui/movie_watchlist.dart';
|
||||
import 'package:ui/search.dart';
|
||||
import 'package:ui/system_settings.dart';
|
||||
import 'package:ui/weclome.dart';
|
||||
import 'package:ui/tv_watchlist.dart';
|
||||
|
||||
class NavDrawer extends StatefulWidget {
|
||||
const NavDrawer({super.key});
|
||||
@@ -33,18 +34,24 @@ class _NavDrawerState extends State<NavDrawer> {
|
||||
_counter = value;
|
||||
});
|
||||
if (value == 0) {
|
||||
context.go(WelcomePage.route);
|
||||
context.go(MovieWatchlistPage.route);
|
||||
} else if (value == 1) {
|
||||
context.go(SearchPage.route);
|
||||
context.go(TvWatchlistPage.route);
|
||||
} else if (value == 2) {
|
||||
context.go(ActivityPage.route);
|
||||
context.go(SearchPage.route);
|
||||
} else if (value == 3) {
|
||||
context.go(ActivityPage.route);
|
||||
} else if (value == 4) {
|
||||
context.go(SystemSettingsPage.route);
|
||||
}
|
||||
},
|
||||
extended: MediaQuery.of(context).size.width >= 850,
|
||||
unselectedIconTheme: const IconThemeData(color: Colors.grey),
|
||||
destinations: const <NavigationRailDestination>[
|
||||
NavigationRailDestination(
|
||||
icon: Icon(Icons.movie),
|
||||
label: Text('电影'),
|
||||
),
|
||||
NavigationRailDestination(
|
||||
icon: Icon(Icons.live_tv),
|
||||
label: Text('电视剧'),
|
||||
|
||||
@@ -7,10 +7,12 @@ import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class APIs {
|
||||
static final _baseUrl = baseUrl();
|
||||
static final searchUrl = "$_baseUrl/api/v1/tv/search";
|
||||
static final searchUrl = "$_baseUrl/api/v1/media/search";
|
||||
static final settingsUrl = "$_baseUrl/api/v1/setting/do";
|
||||
static final watchlistUrl = "$_baseUrl/api/v1/tv/watchlist";
|
||||
static final seriesDetailUrl = "$_baseUrl/api/v1/tv/series/";
|
||||
static final watchlistTvUrl = "$_baseUrl/api/v1/media/tv/watchlist";
|
||||
static final watchlistMovieUrl = "$_baseUrl/api/v1/media/movie/watchlist";
|
||||
static final availableMoviesUrl = "$_baseUrl/api/v1/media/movie/resources/";
|
||||
static final seriesDetailUrl = "$_baseUrl/api/v1/media/record/";
|
||||
static final searchAndDownloadUrl = "$_baseUrl/api/v1/indexer/download";
|
||||
static final allIndexersUrl = "$_baseUrl/api/v1/indexer/";
|
||||
static final addIndexerUrl = "$_baseUrl/api/v1/indexer/add";
|
||||
@@ -24,7 +26,7 @@ class APIs {
|
||||
static final activityUrl = "$_baseUrl/api/v1/activity/";
|
||||
static final imagesUrl = "$_baseUrl/api/v1/img";
|
||||
|
||||
static const tmdbImgBaseUrl = "https://image.tmdb.org/t/p/w500/";
|
||||
static final tmdbImgBaseUrl = "$_baseUrl/api/v1/posters";
|
||||
|
||||
static const tmdbApiKey = "tmdb_api_key";
|
||||
static const downloadDirKey = "download_dir";
|
||||
|
||||
@@ -40,7 +40,7 @@ class ActivityData extends AutoDisposeAsyncNotifier<List<Activity>> {
|
||||
class Activity {
|
||||
Activity(
|
||||
{required this.id,
|
||||
required this.seriesId,
|
||||
required this.mediaId,
|
||||
required this.episodeId,
|
||||
required this.sourceTitle,
|
||||
required this.date,
|
||||
@@ -50,7 +50,7 @@ class Activity {
|
||||
required this.progress});
|
||||
|
||||
final int? id;
|
||||
final int? seriesId;
|
||||
final int? mediaId;
|
||||
final int? episodeId;
|
||||
final String? sourceTitle;
|
||||
final DateTime? date;
|
||||
@@ -62,7 +62,7 @@ class Activity {
|
||||
factory Activity.fromJson(Map<String, dynamic> json) {
|
||||
return Activity(
|
||||
id: json["id"],
|
||||
seriesId: json["series_id"],
|
||||
mediaId: json["media_id"],
|
||||
episodeId: json["episode_id"],
|
||||
sourceTitle: json["source_title"],
|
||||
date: DateTime.tryParse(json["date"] ?? ""),
|
||||
|
||||
@@ -4,7 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:ui/providers/APIs.dart';
|
||||
import 'package:ui/providers/server_response.dart';
|
||||
|
||||
var seriesDetailsProvider = AsyncNotifierProvider.autoDispose
|
||||
var mediaDetailsProvider = AsyncNotifierProvider.autoDispose
|
||||
.family<SeriesDetailData, SeriesDetails, String>(SeriesDetailData.new);
|
||||
|
||||
class SeriesDetailData
|
||||
|
||||
@@ -4,23 +4,39 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:ui/providers/APIs.dart';
|
||||
import 'package:ui/providers/server_response.dart';
|
||||
|
||||
final welcomePageDataProvider = FutureProvider.autoDispose((ref) async {
|
||||
final tvWatchlistDataProvider = FutureProvider.autoDispose((ref) async {
|
||||
final dio = await APIs.getDio();
|
||||
var resp = await dio.get(APIs.watchlistUrl);
|
||||
var resp = await dio.get(APIs.watchlistTvUrl);
|
||||
var sp = ServerResponse.fromJson(resp.data);
|
||||
List<TvSeries> favList = List.empty(growable: true);
|
||||
List<MediaDetail> favList = List.empty(growable: true);
|
||||
for (var item in sp.data as List) {
|
||||
var tv = TvSeries.fromJson(item);
|
||||
var tv = MediaDetail.fromJson(item);
|
||||
favList.add(tv);
|
||||
}
|
||||
return favList;
|
||||
});
|
||||
|
||||
var searchPageDataProvider = AsyncNotifierProvider.autoDispose
|
||||
<SearchPageData, List<SearchResult>>(SearchPageData.new);
|
||||
final movieWatchlistDataProvider = FutureProvider.autoDispose((ref) async {
|
||||
final dio = await APIs.getDio();
|
||||
var resp = await dio.get(APIs.watchlistMovieUrl);
|
||||
var sp = ServerResponse.fromJson(resp.data);
|
||||
List<MediaDetail> favList = List.empty(growable: true);
|
||||
for (var item in sp.data as List) {
|
||||
var tv = MediaDetail.fromJson(item);
|
||||
favList.add(tv);
|
||||
}
|
||||
return favList;
|
||||
});
|
||||
|
||||
var searchPageDataProvider =
|
||||
AsyncNotifierProvider.autoDispose<SearchPageData, List<SearchResult>>(
|
||||
SearchPageData.new);
|
||||
|
||||
var movieTorrentsDataProvider = AsyncNotifierProvider.autoDispose
|
||||
.family<MovieTorrentResource, List<TorrentResource>, String>(
|
||||
MovieTorrentResource.new);
|
||||
|
||||
class SearchPageData extends AutoDisposeAsyncNotifier<List<SearchResult>> {
|
||||
|
||||
List<SearchResult> list = List.empty(growable: true);
|
||||
|
||||
@override
|
||||
@@ -28,19 +44,32 @@ class SearchPageData extends AutoDisposeAsyncNotifier<List<SearchResult>> {
|
||||
return list;
|
||||
}
|
||||
|
||||
Future<void> submit2Watchlist(int tmdbId, int storageId, String resolution) async {
|
||||
Future<void> submit2Watchlist(
|
||||
int tmdbId, int storageId, String resolution, String mediaType) async {
|
||||
final dio = await APIs.getDio();
|
||||
var resp = await dio
|
||||
.post(APIs.watchlistUrl, data: {
|
||||
"tmdb_id": tmdbId,
|
||||
"storage_id": storageId,
|
||||
"resolution": resolution
|
||||
});
|
||||
var sp = ServerResponse.fromJson(resp.data);
|
||||
if (sp.code != 0) {
|
||||
throw sp.message;
|
||||
if (mediaType == "tv") {
|
||||
var resp = await dio.post(APIs.watchlistTvUrl, data: {
|
||||
"tmdb_id": tmdbId,
|
||||
"storage_id": storageId,
|
||||
"resolution": resolution
|
||||
});
|
||||
var sp = ServerResponse.fromJson(resp.data);
|
||||
if (sp.code != 0) {
|
||||
throw sp.message;
|
||||
}
|
||||
ref.invalidate(tvWatchlistDataProvider);
|
||||
} else {
|
||||
var resp = await dio.post(APIs.watchlistMovieUrl, data: {
|
||||
"tmdb_id": tmdbId,
|
||||
"storage_id": storageId,
|
||||
"resolution": resolution
|
||||
});
|
||||
var sp = ServerResponse.fromJson(resp.data);
|
||||
if (sp.code != 0) {
|
||||
throw sp.message;
|
||||
}
|
||||
ref.invalidate(movieWatchlistDataProvider);
|
||||
}
|
||||
ref.invalidate(welcomePageDataProvider);
|
||||
}
|
||||
|
||||
Future<void> queryResults(String q) async {
|
||||
@@ -65,78 +94,149 @@ class SearchPageData extends AutoDisposeAsyncNotifier<List<SearchResult>> {
|
||||
}
|
||||
}
|
||||
|
||||
class SearchResult {
|
||||
String? originalName;
|
||||
int? id;
|
||||
String? name;
|
||||
int? voteCount;
|
||||
double? voteAverage;
|
||||
String? posterPath;
|
||||
String? firstAirDate;
|
||||
double? popularity;
|
||||
List<int>? genreIds;
|
||||
String? originalLanguage;
|
||||
String? backdropPath;
|
||||
String? overview;
|
||||
List<String>? originCountry;
|
||||
|
||||
SearchResult(
|
||||
{this.originalName,
|
||||
this.id,
|
||||
this.name,
|
||||
this.voteCount,
|
||||
this.voteAverage,
|
||||
this.posterPath,
|
||||
this.firstAirDate,
|
||||
this.popularity,
|
||||
this.genreIds,
|
||||
this.originalLanguage,
|
||||
this.backdropPath,
|
||||
this.overview,
|
||||
this.originCountry});
|
||||
|
||||
SearchResult.fromJson(Map<String, dynamic> json) {
|
||||
originalName = json['original_name'];
|
||||
id = json['id'];
|
||||
name = json['name'];
|
||||
voteCount = json['vote_count'];
|
||||
voteAverage = json['vote_average'];
|
||||
posterPath = json['poster_path'];
|
||||
firstAirDate = json['first_air_date'];
|
||||
popularity = json['popularity'];
|
||||
genreIds = json['genre_ids'].cast<int>();
|
||||
originalLanguage = json['original_language'];
|
||||
backdropPath = json['backdrop_path'];
|
||||
overview = json['overview'];
|
||||
originCountry = json['origin_country'].cast<String>();
|
||||
}
|
||||
}
|
||||
|
||||
class TvSeries {
|
||||
class MediaDetail {
|
||||
int? id;
|
||||
int? tmdbId;
|
||||
String? mediaType;
|
||||
String? name;
|
||||
String? originalName;
|
||||
String? overview;
|
||||
String? path;
|
||||
String? posterPath;
|
||||
String? createdAt;
|
||||
String? resolution;
|
||||
int? storageId;
|
||||
String? airDate;
|
||||
|
||||
TvSeries(
|
||||
{this.id,
|
||||
this.tmdbId,
|
||||
this.name,
|
||||
this.originalName,
|
||||
this.overview,
|
||||
this.path,
|
||||
this.posterPath});
|
||||
MediaDetail({
|
||||
this.id,
|
||||
this.tmdbId,
|
||||
this.mediaType,
|
||||
this.name,
|
||||
this.originalName,
|
||||
this.overview,
|
||||
this.posterPath,
|
||||
this.createdAt,
|
||||
this.resolution,
|
||||
this.storageId,
|
||||
this.airDate,
|
||||
});
|
||||
|
||||
TvSeries.fromJson(Map<String, dynamic> json) {
|
||||
MediaDetail.fromJson(Map<String, dynamic> json) {
|
||||
id = json['id'];
|
||||
tmdbId = json['tmdb_id'];
|
||||
mediaType = json["media_type"];
|
||||
name = json['name_cn'];
|
||||
originalName = json['original_name'];
|
||||
overview = json['overview'];
|
||||
path = json['path'];
|
||||
posterPath = json["poster_path"];
|
||||
posterPath = json['poster_path'];
|
||||
createdAt = json['created_at'];
|
||||
resolution = json["resolution"];
|
||||
storageId = json["storage_id"];
|
||||
airDate = json["air_date"];
|
||||
}
|
||||
}
|
||||
|
||||
class SearchResult {
|
||||
SearchResult({
|
||||
required this.backdropPath,
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.originalName,
|
||||
required this.overview,
|
||||
required this.posterPath,
|
||||
required this.mediaType,
|
||||
required this.adult,
|
||||
required this.originalLanguage,
|
||||
required this.genreIds,
|
||||
required this.popularity,
|
||||
required this.firstAirDate,
|
||||
required this.voteAverage,
|
||||
required this.voteCount,
|
||||
required this.originCountry,
|
||||
});
|
||||
|
||||
final String? backdropPath;
|
||||
final int? id;
|
||||
final String? name;
|
||||
final String? originalName;
|
||||
final String? overview;
|
||||
final String? posterPath;
|
||||
final String? mediaType;
|
||||
final bool? adult;
|
||||
final String? originalLanguage;
|
||||
final List<int> genreIds;
|
||||
final double? popularity;
|
||||
final DateTime? firstAirDate;
|
||||
final double? voteAverage;
|
||||
final int? voteCount;
|
||||
final List<String> originCountry;
|
||||
|
||||
factory SearchResult.fromJson(Map<String, dynamic> json) {
|
||||
return SearchResult(
|
||||
backdropPath: json["backdrop_path"],
|
||||
id: json["id"],
|
||||
name: json["name"],
|
||||
originalName: json["original_name"],
|
||||
overview: json["overview"],
|
||||
posterPath: json["poster_path"],
|
||||
mediaType: json["media_type"],
|
||||
adult: json["adult"],
|
||||
originalLanguage: json["original_language"],
|
||||
genreIds: json["genre_ids"] == null
|
||||
? []
|
||||
: List<int>.from(json["genre_ids"]!.map((x) => x)),
|
||||
popularity: json["popularity"],
|
||||
firstAirDate: DateTime.tryParse(json["first_air_date"] ?? ""),
|
||||
voteAverage: json["vote_average"],
|
||||
voteCount: json["vote_count"],
|
||||
originCountry: json["origin_country"] == null
|
||||
? []
|
||||
: List<String>.from(json["origin_country"]!.map((x) => x)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MovieTorrentResource
|
||||
extends AutoDisposeFamilyAsyncNotifier<List<TorrentResource>, String> {
|
||||
String? mediaId;
|
||||
@override
|
||||
FutureOr<List<TorrentResource>> build(String id) async {
|
||||
mediaId = id;
|
||||
final dio = await APIs.getDio();
|
||||
var resp = await dio.get(APIs.availableMoviesUrl + id);
|
||||
var rsp = ServerResponse.fromJson(resp.data);
|
||||
if (rsp.code != 0) {
|
||||
throw rsp.message;
|
||||
}
|
||||
return (resp.data as List).map((v) => TorrentResource.fromJson(v)).toList();
|
||||
}
|
||||
|
||||
Future<void> download(String link) async {
|
||||
final dio = await APIs.getDio();
|
||||
var resp = await dio.post(APIs.availableMoviesUrl,
|
||||
data: {"media_id": int.parse(mediaId!), "link": link});
|
||||
var rsp = ServerResponse.fromJson(resp.data);
|
||||
if (rsp.code != 0) {
|
||||
throw rsp.message;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class TorrentResource {
|
||||
TorrentResource({this.name, this.size, this.seeders, this.peers, this.link});
|
||||
|
||||
String? name;
|
||||
int? size;
|
||||
int? seeders;
|
||||
int? peers;
|
||||
String? link;
|
||||
|
||||
factory TorrentResource.fromJson(Map<String, dynamic> json) {
|
||||
return TorrentResource(
|
||||
name: json["name"],
|
||||
size: json["size"],
|
||||
seeders: json["seeders"],
|
||||
peers: json["peers"],
|
||||
link: json["link"]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ class _SearchPageState extends ConsumerState<SearchPage> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"${item.name} (${item.firstAirDate?.split("-")[0]})",
|
||||
"${item.name} (${item.firstAirDate?.year})",
|
||||
style: const TextStyle(
|
||||
fontSize: 14, fontWeight: FontWeight.bold),
|
||||
),
|
||||
@@ -174,7 +174,7 @@ class _SearchPageState extends ConsumerState<SearchPage> {
|
||||
ref
|
||||
.read(searchPageDataProvider.notifier)
|
||||
.submit2Watchlist(
|
||||
item.id!, _storageSelected, _resSelected);
|
||||
item.id!, _storageSelected, _resSelected, item.mediaType!);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
|
||||
@@ -5,7 +5,7 @@ import 'package:ui/providers/APIs.dart';
|
||||
import 'package:ui/providers/series_details.dart';
|
||||
import 'package:ui/providers/settings.dart';
|
||||
import 'package:ui/utils.dart';
|
||||
import 'package:ui/weclome.dart';
|
||||
import 'package:ui/tv_watchlist.dart';
|
||||
import 'package:ui/widgets/progress_indicator.dart';
|
||||
|
||||
class TvDetailsPage extends ConsumerStatefulWidget {
|
||||
@@ -38,7 +38,7 @@ class _TvDetailsPageState extends ConsumerState<TvDetailsPage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var seriesDetails = ref.watch(seriesDetailsProvider(seriesId));
|
||||
var seriesDetails = ref.watch(mediaDetailsProvider(seriesId));
|
||||
var storage = ref.watch(storageSettingProvider);
|
||||
return FutureBuilder(
|
||||
// We listen to the pending operation, to update the UI accordingly.
|
||||
@@ -70,7 +70,7 @@ class _TvDetailsPageState extends ConsumerState<TvDetailsPage> {
|
||||
onPressed: () async {
|
||||
var f = ref
|
||||
.read(
|
||||
seriesDetailsProvider(seriesId).notifier)
|
||||
mediaDetailsProvider(seriesId).notifier)
|
||||
.searchAndDownload(seriesId, ep.seasonNumber!,
|
||||
ep.episodeNumber!);
|
||||
setState(() {
|
||||
@@ -189,11 +189,11 @@ class _TvDetailsPageState extends ConsumerState<TvDetailsPage> {
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
ref
|
||||
.read(seriesDetailsProvider(
|
||||
.read(mediaDetailsProvider(
|
||||
seriesId)
|
||||
.notifier)
|
||||
.delete();
|
||||
context.go(WelcomePage.route);
|
||||
context.go(TvWatchlistPage.route);
|
||||
},
|
||||
icon: const Icon(Icons.delete))
|
||||
],
|
||||
|
||||
@@ -6,14 +6,16 @@ import 'package:ui/providers/welcome_data.dart';
|
||||
import 'package:ui/tv_details.dart';
|
||||
import 'package:ui/widgets/progress_indicator.dart';
|
||||
|
||||
class WelcomePage extends ConsumerWidget {
|
||||
class TvWatchlistPage
|
||||
extends ConsumerWidget {
|
||||
static const route = "/series";
|
||||
|
||||
const WelcomePage({super.key});
|
||||
const TvWatchlistPage
|
||||
({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final data = ref.watch(welcomePageDataProvider);
|
||||
final data = ref.watch(tvWatchlistDataProvider);
|
||||
|
||||
return switch (data) {
|
||||
AsyncData(:final value) => GridView.builder(
|
||||
@@ -1,4 +1,7 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
class Utils {
|
||||
static Future<void> showAlertDialog(BuildContext context, String msg) async {
|
||||
@@ -42,3 +45,14 @@ class Utils {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
extension FileFormatter on num {
|
||||
String readableFileSize({bool base1024 = true}) {
|
||||
final base = base1024 ? 1024 : 1000;
|
||||
if (this <= 0) return "0";
|
||||
final units = ["B", "kB", "MB", "GB", "TB"];
|
||||
int digitGroups = (log(this) / log(base)).round();
|
||||
return "${NumberFormat("#,##0.#").format(this / pow(base, digitGroups))} ${units[digitGroups]}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,6 +168,14 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "4.0.2"
|
||||
intl:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: intl
|
||||
sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.19.0"
|
||||
intl_phone_number_input:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -42,6 +42,7 @@ dependencies:
|
||||
flutter_login: ^5.0.0
|
||||
shared_preferences: ^2.2.3
|
||||
percent_indicator: ^4.2.3
|
||||
intl: ^0.19.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
Reference in New Issue
Block a user