From f93d83c75c66d739eb370a910890d49f3a7e265c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Mart=C3=ADn=20Seery?= Date: Mon, 20 Jan 2020 10:32:54 -0300 Subject: [PATCH 01/15] AIcon --- lib/screens/cast_player/controls.dart | 6 ++- lib/screens/player/controls.dart | 8 ++-- lib/widgets/aicon.dart | 59 +++++++++++++++++++++++++++ 3 files changed, 67 insertions(+), 6 deletions(-) create mode 100644 lib/widgets/aicon.dart diff --git a/lib/screens/cast_player/controls.dart b/lib/screens/cast_player/controls.dart index c5f63f0..5e5797c 100644 --- a/lib/screens/cast_player/controls.dart +++ b/lib/screens/cast_player/controls.dart @@ -1,5 +1,6 @@ import 'package:animu/utils/helpers.dart'; import 'package:animu/utils/notifiers.dart'; +import 'package:animu/widgets/aicon.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -42,8 +43,9 @@ class _CastPlayerControlsState extends State { vlc.send('pl_pause'); setState(() => isPlaying = !isPlaying); }, - child: Icon( - isPlaying ? Icons.pause : Icons.play_arrow, + child: AIcon( + isInitialState: !isPlaying, + icon: AnimatedIcons.play_pause, size: 100, ), ), diff --git a/lib/screens/player/controls.dart b/lib/screens/player/controls.dart index 10ba4d1..74ddf2d 100644 --- a/lib/screens/player/controls.dart +++ b/lib/screens/player/controls.dart @@ -1,5 +1,6 @@ import 'package:animu/utils/helpers.dart'; import 'package:animu/utils/models.dart'; +import 'package:animu/widgets/aicon.dart'; import 'package:animu/widgets/previous_next.dart'; import 'package:flutter/material.dart'; import 'package:video_player/video_player.dart'; @@ -39,10 +40,9 @@ class PlayerControls extends StatelessWidget { ), GestureDetector( onTap: togglePlay, - child: Icon( - controller.value.isPlaying - ? Icons.pause - : Icons.play_arrow, + child: AIcon( + isInitialState: !controller.value.isPlaying, + icon: AnimatedIcons.play_pause, size: 100, ), ), diff --git a/lib/widgets/aicon.dart b/lib/widgets/aicon.dart new file mode 100644 index 0000000..ebaea71 --- /dev/null +++ b/lib/widgets/aicon.dart @@ -0,0 +1,59 @@ +import 'package:flutter/material.dart'; + +// AIcon = AnimatedIcon +class AIcon extends StatefulWidget { + final bool isInitialState; + final AnimatedIconData icon; + final double size; + final Color color; + + const AIcon({ + Key key, + this.isInitialState, + this.icon, + this.size, + this.color, + }) : super(key: key); + + @override + _AIconState createState() => _AIconState(); +} + +class _AIconState extends State with SingleTickerProviderStateMixin { + AnimationController _animationController; + bool currentIsInitalState = true; + // Initial State -- End State + // Foward (Initial --> End) + // Reverse (Initial <-- End) + + @override + void initState() { + super.initState(); + _animationController = + AnimationController(vsync: this, duration: Duration(milliseconds: 200)); + } + + @override + void dispose() { + super.dispose(); + _animationController.dispose(); + } + + @override + Widget build(BuildContext context) { + if (widget.isInitialState != currentIsInitalState) { + currentIsInitalState = !currentIsInitalState; + + if (widget.isInitialState) + _animationController.reverse(); + else + _animationController.forward(); + } + return AnimatedIcon( + icon: widget.icon, + color: widget.color, + size: widget.size, + progress: _animationController, + ); + } +} From b45256089ae8cd6044b5cd8f41f32afb9b58e1fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Mart=C3=ADn=20Seery?= Date: Mon, 20 Jan 2020 12:00:32 -0300 Subject: [PATCH 02/15] Hero --- lib/screens/anime/anime.dart | 49 ++++++++++-------- lib/widgets/anime_list.dart | 98 ++++++++++++++++++++++-------------- pubspec.lock | 7 --- pubspec.yaml | 1 - 4 files changed, 87 insertions(+), 68 deletions(-) diff --git a/lib/screens/anime/anime.dart b/lib/screens/anime/anime.dart index b0d4ff8..5fff0f0 100644 --- a/lib/screens/anime/anime.dart +++ b/lib/screens/anime/anime.dart @@ -6,7 +6,6 @@ import 'package:animu/utils/helpers.dart'; import 'package:animu/utils/watching_states.dart'; import 'package:animu/widgets/dialog_button.dart'; import 'package:animu/widgets/spinner.dart'; -import 'package:auto_size_text/auto_size_text.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; @@ -61,27 +60,13 @@ class _AnimeScreenState extends State { child: Stack( overflow: Overflow.visible, children: [ - Image.network( - getImageURL(ImageURLType.cover, anime: anime), - fit: BoxFit.cover, - width: MediaQuery.of(context).size.width, - headers: requestsService.headers, - ), - Positioned( - bottom: 5, - left: 10, - width: MediaQuery.of(context).size.width * 0.8, - child: AutoSizeText( - anime.name, - style: TextStyle( - fontWeight: FontWeight.w800, - fontSize: 28, - letterSpacing: 1, - height: 1.25, - shadows: [ - Shadow(color: Colors.black87, blurRadius: 10), - ], - ), + Hero( + tag: 'AnimeCover-${anime.id}', + child: Image.network( + getImageURL(ImageURLType.cover, anime: anime), + fit: BoxFit.cover, + width: MediaQuery.of(context).size.width, + headers: requestsService.headers, ), ), Positioned( @@ -166,6 +151,26 @@ class _AnimeScreenState extends State { ], ), ), + Padding( + padding: EdgeInsets.symmetric(horizontal: 10, vertical: 10), + child: Row( + children: [ + SizedBox( + width: MediaQuery.of(context).size.width * .7, + child: Text( + anime.name, + textAlign: TextAlign.left, + style: TextStyle( + fontWeight: FontWeight.w800, + fontSize: 24, + letterSpacing: 1, + height: 1.25, + ), + ), + ), + ], + ), + ), Expanded( child: !loading ? EpisodeList( diff --git a/lib/widgets/anime_list.dart b/lib/widgets/anime_list.dart index 184ebf3..65d0406 100644 --- a/lib/widgets/anime_list.dart +++ b/lib/widgets/anime_list.dart @@ -2,7 +2,6 @@ import 'package:animu/screens/anime/anime.dart'; import 'package:animu/services/requests.dart'; import 'package:animu/utils/helpers.dart'; import 'package:animu/utils/models.dart'; -import 'package:auto_size_text/auto_size_text.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -15,46 +14,69 @@ class AnimeList extends StatelessWidget { Widget build(BuildContext context) { final requestsService = Provider.of(context); + List> rows = []; + for (var i = 0; i < animes.length; i++) { + if (i % 2 == 0) { + rows.add([animes[i]]); + } else { + rows.last.add(animes[i]); + } + } + if (animes.length > 0) { - return GridView.count( - crossAxisCount: 2, - childAspectRatio: .56, - crossAxisSpacing: 30, - children: List.generate( - animes.length, - (i) => GestureDetector( - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (_) => AnimeScreen(), - settings: RouteSettings(arguments: animes[i]), - ), - ); - }, - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - ClipRRect( - borderRadius: BorderRadius.circular(7.5), - child: Image.network( - getImageURL(ImageURLType.cover, anime: animes[i]), - headers: requestsService.headers, + return ListView.builder( + // crossAxisCount: 2, + // childAspectRatio: .56, + // crossAxisSpacing: 30, + itemCount: rows.length, + itemBuilder: (context, i) => Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: rows[i] + .map( + (anime) => GestureDetector( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => AnimeScreen(), + settings: RouteSettings(arguments: anime), + ), + ); + }, + child: Padding( + padding: EdgeInsets.only(bottom: 10), + child: SizedBox( + width: MediaQuery.of(context).size.width * .433, + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(7.5), + child: Hero( + tag: 'AnimeCover-${anime.id}', + child: Image.network( + getImageURL(ImageURLType.cover, anime: anime), + headers: requestsService.headers, + ), + ), + ), + SizedBox(height: 5), + Text( + anime.name, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ), + textAlign: TextAlign.center, + ), + ], + ), + ), ), ), - SizedBox(height: 5), - AutoSizeText( - animes[i].name, - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16, - ), - textAlign: TextAlign.center, - maxLines: 2, - ), - ], - ), - ), + ) + .toList(), ), ); } else { diff --git a/pubspec.lock b/pubspec.lock index 82c0eeb..a88d675 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -36,13 +36,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.4.0" - auto_size_text: - dependency: "direct main" - description: - name: auto_size_text - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.0" boolean_selector: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index eea8d2d..84766d7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -23,7 +23,6 @@ dependencies: # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. # cupertino_icons: ^0.1.2 - auto_size_text: ^2.1.0 connectivity: ^0.4.6+1 cookie_jar: ^1.0.1 device_info: ^0.4.1+4 From d2387a48cccea696202fee504573d4aa079494e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Mart=C3=ADn=20Seery?= Date: Mon, 20 Jan 2020 22:24:15 -0300 Subject: [PATCH 03/15] animeflv-graphql GraphQL Client (animeflv.juanm04.com) RequestsService simplier Images as Base64 --- lib/main.dart | 2 - lib/screens/anime/anime.dart | 21 +-- lib/screens/anime/episode_list.dart | 13 +- lib/screens/browse.dart | 13 +- lib/screens/cast_player/cast_player.dart | 8 +- lib/screens/player/player.dart | 9 +- lib/screens/splash_screen/splash_screen.dart | 8 -- lib/services/requests.dart | 135 ++++++++++--------- lib/services/sources.dart | 24 ++-- lib/utils/models.dart | 7 +- lib/widgets/anime_list.dart | 13 +- pubspec.lock | 51 ++++--- pubspec.yaml | 3 +- web/api/get-cloudflare-id.ts | 22 --- web/api/get-natsuki.ts | 8 ++ 15 files changed, 156 insertions(+), 181 deletions(-) delete mode 100644 web/api/get-cloudflare-id.ts create mode 100644 web/api/get-natsuki.ts diff --git a/lib/main.dart b/lib/main.dart index 87031cb..2ae7f36 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -2,7 +2,6 @@ import 'package:animu/screens/browse.dart'; import 'package:animu/screens/saved_animes.dart'; import 'package:animu/screens/settings/settings.dart'; import 'package:animu/screens/splash_screen/splash_screen.dart'; -import 'package:animu/services/requests.dart'; import 'package:animu/utils/notifiers.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -30,7 +29,6 @@ class MyApp extends StatelessWidget { return MultiProvider( providers: [ ChangeNotifierProvider.value(value: VLCNotifier()), - Provider.value(value: RequestsService()), ], child: MaterialApp( title: 'Animú', diff --git a/lib/screens/anime/anime.dart b/lib/screens/anime/anime.dart index 5fff0f0..4a35c25 100644 --- a/lib/screens/anime/anime.dart +++ b/lib/screens/anime/anime.dart @@ -1,14 +1,14 @@ +import 'dart:convert'; + import 'package:animu/screens/anime/episode_list.dart'; import 'package:animu/services/requests.dart'; import 'package:animu/utils/models.dart'; import 'package:animu/services/anime_database.dart'; -import 'package:animu/utils/helpers.dart'; import 'package:animu/utils/watching_states.dart'; import 'package:animu/widgets/dialog_button.dart'; import 'package:animu/widgets/spinner.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:provider/provider.dart'; import 'main_button.dart'; @@ -18,7 +18,6 @@ class AnimeScreen extends StatefulWidget { } class _AnimeScreenState extends State { - RequestsService requestsService; Anime anime; List episodes; bool loading = true; @@ -31,12 +30,18 @@ class _AnimeScreenState extends State { } void getEpisodes() async { - List response = await requestsService.getEpisodes(anime: anime); + List response = await RequestsService.getEpisodes(anime: anime); if (mounted) setState(() { episodes = new List.from( - response.map((list) => Episode(id: list[1], n: list[0])).toList(), + response + .map((map) => Episode( + id: map['id'], + n: map['n'], + thumbnail: base64Decode(map['thumbnail']), + )) + .toList(), ); loading = false; }); @@ -46,7 +51,6 @@ class _AnimeScreenState extends State { Widget build(BuildContext context) { if (anime == null) { anime = ModalRoute.of(context).settings.arguments; - requestsService = Provider.of(context); getAnimeDBData(); } if (episodes == null) getEpisodes(); @@ -62,11 +66,10 @@ class _AnimeScreenState extends State { children: [ Hero( tag: 'AnimeCover-${anime.id}', - child: Image.network( - getImageURL(ImageURLType.cover, anime: anime), + child: Image.memory( + anime.cover, fit: BoxFit.cover, width: MediaQuery.of(context).size.width, - headers: requestsService.headers, ), ), Positioned( diff --git a/lib/screens/anime/episode_list.dart b/lib/screens/anime/episode_list.dart index 9bd8eb4..2eb7329 100644 --- a/lib/screens/anime/episode_list.dart +++ b/lib/screens/anime/episode_list.dart @@ -2,8 +2,6 @@ import 'dart:math'; import 'package:animu/screens/cast_player/cast_player.dart'; import 'package:animu/screens/player/player.dart'; -import 'package:animu/services/requests.dart'; -import 'package:animu/utils/helpers.dart'; import 'package:animu/utils/models.dart'; import 'package:animu/utils/notifiers.dart'; import 'package:flutter/material.dart'; @@ -36,8 +34,6 @@ class EpisodeList extends StatelessWidget { @override Widget build(BuildContext context) { - final requestsService = Provider.of(context); - return Column( children: [ Expanded( @@ -56,14 +52,7 @@ class EpisodeList extends StatelessWidget { children: [ ClipRRect( borderRadius: BorderRadius.circular(5), - child: Image.network( - getImageURL( - ImageURLType.thumbnail, - anime: anime, - episode: episodes[i], - ), - headers: requestsService.headers, - ), + child: Image.memory(episodes[i].thumbnail), ), Align( alignment: Alignment.center, diff --git a/lib/screens/browse.dart b/lib/screens/browse.dart index a694a3c..9ed3252 100644 --- a/lib/screens/browse.dart +++ b/lib/screens/browse.dart @@ -1,10 +1,11 @@ +import 'dart:convert'; + import 'package:animu/services/requests.dart'; import 'package:animu/widgets/anime_list.dart'; import 'package:animu/widgets/search_bar.dart'; import 'package:animu/utils/models.dart'; import 'package:animu/widgets/spinner.dart'; import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; class Browse extends StatefulWidget { @override @@ -12,23 +13,23 @@ class Browse extends StatefulWidget { } class _BrowseState extends State { - RequestsService requestsService; List animes; bool loading = false; void getAnimes(String query) async { setState(() => loading = true); - List response = await requestsService.searchAnimes(query); + List response = await RequestsService.searchAnimes(query); if (mounted) setState(() { animes = new List.from(response .map( (map) => Anime( - id: int.parse(map['id']), - name: map['title'], + id: map['id'], + name: map['name'], slug: map['slug'], + cover: base64Decode(map['cover']), ), ) .toList()); @@ -56,8 +57,6 @@ class _BrowseState extends State { @override Widget build(BuildContext context) { - requestsService = Provider.of(context); - return Scaffold( body: SafeArea( child: Padding( diff --git a/lib/screens/cast_player/cast_player.dart b/lib/screens/cast_player/cast_player.dart index e936ff6..3f4a3ab 100644 --- a/lib/screens/cast_player/cast_player.dart +++ b/lib/screens/cast_player/cast_player.dart @@ -1,6 +1,5 @@ import 'dart:async'; -import 'package:animu/services/requests.dart'; import 'package:animu/services/sources.dart'; import 'package:animu/widgets/previous_next.dart'; import 'package:animu/utils/models.dart'; @@ -19,7 +18,6 @@ class CastPlayer extends StatefulWidget { class _CastPlayerState extends State { PlayerData data; VLCNotifier vlc; - RequestsService requestsService; Timer ticker; dynamic tickerData; @@ -30,10 +28,7 @@ class _CastPlayerState extends State { } void initPlayer() async { - final url = await getEpisodeURLFromData( - requestsService: requestsService, - data: data, - ); + final url = await getEpisodeURLFromData(data); if (!mounted) return; if (url == null) return initPlayer(); tickerData = await vlc.send('in_play', input: url); @@ -61,7 +56,6 @@ class _CastPlayerState extends State { if (data == null) { data = ModalRoute.of(context).settings.arguments; vlc = Provider.of(context); - requestsService = Provider.of(context); initPlayer(); } diff --git a/lib/screens/player/player.dart b/lib/screens/player/player.dart index 5b1af4b..3700ec9 100644 --- a/lib/screens/player/player.dart +++ b/lib/screens/player/player.dart @@ -1,10 +1,8 @@ -import 'package:animu/services/requests.dart'; import 'package:animu/services/sources.dart'; import 'package:animu/utils/models.dart'; import 'package:animu/widgets/spinner.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:provider/provider.dart'; import 'package:screen/screen.dart'; import 'package:video_player/video_player.dart'; @@ -16,16 +14,12 @@ class Player extends StatefulWidget { } class _PlayerState extends State { - RequestsService requestsService; PlayerData data; VideoPlayerController _controller; bool _showControls = true; void initPlayer() async { - final url = await getEpisodeURLFromData( - requestsService: requestsService, - data: data, - ); + final url = await getEpisodeURLFromData(data); if (!mounted) return; if (url == null) return initPlayer(); @@ -87,7 +81,6 @@ class _PlayerState extends State { Widget build(BuildContext context) { if (data == null) { data = ModalRoute.of(context).settings.arguments; - requestsService = Provider.of(context); } if (_controller == null) initPlayer(); diff --git a/lib/screens/splash_screen/splash_screen.dart b/lib/screens/splash_screen/splash_screen.dart index e209099..5a276d1 100644 --- a/lib/screens/splash_screen/splash_screen.dart +++ b/lib/screens/splash_screen/splash_screen.dart @@ -1,5 +1,4 @@ import 'package:animu/screens/splash_screen/updater.dart'; -import 'package:animu/services/requests.dart'; import 'package:connectivity/connectivity.dart'; import 'package:device_info/device_info.dart'; import 'package:dio/dio.dart'; @@ -7,7 +6,6 @@ import 'package:flutter/material.dart'; import 'package:hive/hive.dart'; import 'package:hive_flutter/hive_flutter.dart'; import 'package:package_info/package_info.dart'; -import 'package:provider/provider.dart'; import 'package:pub_semver/pub_semver.dart'; class SplashScreen extends StatefulWidget { @@ -81,16 +79,10 @@ class _SplashScreenState extends State { setDefaultSetting('mark_as_seen_when_next_episode', true); } - Future initRequestsService() async { - final requestsService = Provider.of(context); - await requestsService.init(); - } - void initApp() async { if (await isOnline() == false) return; await checkUpdates(); await setDefaultSettings(); - await initRequestsService(); Navigator.pushReplacementNamed(context, '/home'); } diff --git a/lib/services/requests.dart b/lib/services/requests.dart index 4017865..3263f4a 100644 --- a/lib/services/requests.dart +++ b/lib/services/requests.dart @@ -1,92 +1,105 @@ -import 'dart:convert'; -import 'dart:io'; - import 'package:animu/utils/models.dart'; -import 'package:cookie_jar/cookie_jar.dart'; import 'package:dio/dio.dart'; -import 'package:dio_cookie_manager/dio_cookie_manager.dart'; +import 'package:graphql/client.dart'; class RequestsService { - var dio = new Dio(); - var jar = CookieJar(); - final mainUri = Uri.parse('https://animeflv.net'); - - Map get headers => { - 'cookie': jar - .loadForRequest(mainUri) - .fold( - '', - (accum, cookie) => accum + '; ' + cookieToString(cookie), - ) - .substring(2), - }; + static final _httpLink = + HttpLink(uri: 'https://animeflv.juanm04.com/graphql'); + static final _client = GraphQLClient( + cache: InMemoryCache(), + link: _httpLink, + ); - Future init() { - return dio.get('https://animu.juanm04.com/api/get-cloudflare-id').then( - (res) { - jar.saveFromResponse( - mainUri, - [Cookie('__cfduid', res.data), Cookie('device', 'computer')], - ); - dio.interceptors.add(CookieManager(jar)); - }, - ).catchError(print); + static Future _query({String query, Map variables}) { + return _client.query(QueryOptions( + documentNode: gql(query), + variables: variables, + )); } - Future getEpisodeSources({String animeSlug, Episode episode}) async { + static Future getEpisodeSources({ + String animeSlug, + Episode episode, + }) async { try { - final response = await dio.get( - 'https://animeflv.net/ver/${episode.id}/$animeSlug-${episode.n}'); - - final String rawSources = - response.data.split('var videos = ')[1].split(';')[0]; - return jsonDecode(rawSources)['SUB']; + final response = await _query( + query: """ + query GetEpisodeSources(\$episodeId: Int!, \$episodeN: Int!, \$animeSlug: String!) { + episodeSources(episodeId: \$episodeId, episodeN: \$episodeN, animeSlug: \$animeSlug) { + server + code + } + } + """, + variables: { + 'episodeId': episode.id, + 'episodeN': episode.n, + 'animeSlug': animeSlug, + }, + ); + return response.data['episodeSources']; } catch (e) { - await init(); return await getEpisodeSources(animeSlug: animeSlug, episode: episode); } } - Future getEpisodes({Anime anime}) async { + static Future getEpisodes({Anime anime}) async { try { - final response = - await dio.get('https://animeflv.net/anime/${anime.id}/${anime.slug}'); - - final String rawSources = - response.data.split('var episodes = ')[1].split(';')[0]; - return jsonDecode(rawSources); + final response = await _query( + query: """ + query GetEpisodes(\$animeId: Int!, \$animeSlug: String!) { + anime(id: \$animeId, slug: \$animeSlug) { + episodes { + id + n + thumbnail + } + } + } + """, + variables: { + 'animeId': anime.id, + 'animeSlug': anime.slug, + }, + ); + return response.data['anime']['episodes']; } catch (e) { - await init(); - return await getEpisodes(anime: anime); + print(e); + return null; } } - Future getNatsiku(String url) async { + static Future getNatsiku(String url) async { try { final response = - await dio.get(url.replaceFirst('embed.php', 'check.php')); + await new Dio().get(url.replaceFirst('embed.php', 'check.php')); - return jsonDecode(response.data); + return response.data; } catch (e) { - await init(); - return await getNatsiku(url); + print(e); + return null; } } - Future searchAnimes(String query) async { + static Future searchAnimes(String query) async { try { - final response = await dio.post( - 'https://animeflv.net/api/animes/search', - options: Options(contentType: Headers.formUrlEncodedContentType), - data: {'value': query}, + final response = await _query( + query: """ + query Search(\$query: String!) { + search(query: \$query) { + id + name + slug + cover + } + } + """, + variables: {'query': query}, ); - - return response.data; + return response.data['search']; } catch (e) { - await init(); - return await searchAnimes(query); + print(e); + return null; } } } - -String cookieToString(Cookie cookie) => cookie.name + '=' + cookie.value; diff --git a/lib/services/sources.dart b/lib/services/sources.dart index a713817..f56285a 100644 --- a/lib/services/sources.dart +++ b/lib/services/sources.dart @@ -6,24 +6,21 @@ import 'package:hive/hive.dart'; class APIServer { final String title; final List names; - final Future Function( - RequestsService requestsService, String sourceCode) function; + final Future Function(String sourceCode) function; APIServer({this.title, this.names, this.function}); } List servers = [ APIServer( - title: 'Natsuki/Izanagi', - names: ['natsuki', 'amus'], - function: (requestsService, sourceCode) async { - final response = await requestsService.getNatsiku(sourceCode); - return response['file']; - }), + title: 'Natsuki/Izanagi', + names: ['natsuki', 'amus'], + function: RequestsService.getNatsiku, + ), APIServer( title: 'Fembed', names: ['fembed'], - function: (_, sourceCode) async { + function: (sourceCode) async { int _quality(Map video) => int.parse(video['label'].replaceFirst('p', '')); @@ -50,11 +47,8 @@ List servers = [ ), ]; -Future getEpisodeURLFromData({ - PlayerData data, - RequestsService requestsService, -}) async { - List sources = await requestsService.getEpisodeSources( +Future getEpisodeURLFromData(PlayerData data) async { + List sources = await RequestsService.getEpisodeSources( animeSlug: data.anime.slug, episode: data.currentEpisode, ); @@ -65,7 +59,7 @@ Future getEpisodeURLFromData({ sources.indexWhere((source) => server.names.contains(source['server'])); if (index > -1) - return await server.function(requestsService, sources[index]['code']); + return await server.function(sources[index]['code']); else return ''; } diff --git a/lib/utils/models.dart b/lib/utils/models.dart index cddec10..5c7f662 100644 --- a/lib/utils/models.dart +++ b/lib/utils/models.dart @@ -1,9 +1,12 @@ +import 'dart:typed_data'; + import 'package:animu/utils/watching_states.dart'; class Anime { final int id; final String name; final String slug; + final Uint8List cover; bool favorite; WatchingState watchingState; List episodesSeen; @@ -12,6 +15,7 @@ class Anime { this.id, this.name, this.slug, + this.cover, this.favorite = false, this.watchingState, this.episodesSeen, @@ -46,8 +50,9 @@ class Anime { class Episode { final int id; final int n; + final Uint8List thumbnail; - Episode({this.id, this.n}); + Episode({this.id, this.n, this.thumbnail}); } class PlayerData { diff --git a/lib/widgets/anime_list.dart b/lib/widgets/anime_list.dart index 65d0406..f10a458 100644 --- a/lib/widgets/anime_list.dart +++ b/lib/widgets/anime_list.dart @@ -1,9 +1,6 @@ import 'package:animu/screens/anime/anime.dart'; -import 'package:animu/services/requests.dart'; -import 'package:animu/utils/helpers.dart'; import 'package:animu/utils/models.dart'; import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; class AnimeList extends StatelessWidget { final List animes; @@ -12,8 +9,6 @@ class AnimeList extends StatelessWidget { @override Widget build(BuildContext context) { - final requestsService = Provider.of(context); - List> rows = []; for (var i = 0; i < animes.length; i++) { if (i % 2 == 0) { @@ -25,9 +20,6 @@ class AnimeList extends StatelessWidget { if (animes.length > 0) { return ListView.builder( - // crossAxisCount: 2, - // childAspectRatio: .56, - // crossAxisSpacing: 30, itemCount: rows.length, itemBuilder: (context, i) => Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -55,10 +47,7 @@ class AnimeList extends StatelessWidget { borderRadius: BorderRadius.circular(7.5), child: Hero( tag: 'AnimeCover-${anime.id}', - child: Image.network( - getImageURL(ImageURLType.cover, anime: anime), - headers: requestsService.headers, - ), + child: Image.memory(anime.cover), ), ), SizedBox(height: 5), diff --git a/pubspec.lock b/pubspec.lock index a88d675..c05ec34 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -148,13 +148,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.1" - cookie_jar: - dependency: "direct main" - description: - name: cookie_jar - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.1" crypto: dependency: transitive description: @@ -196,14 +189,7 @@ packages: name: dio url: "https://pub.dartlang.org" source: hosted - version: "3.0.7" - dio_cookie_manager: - dependency: "direct main" - description: - name: dio_cookie_manager - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.0" + version: "3.0.8" fixnum: dependency: transitive description: @@ -254,6 +240,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.2.0" + gql: + dependency: transitive + description: + name: gql + url: "https://pub.dartlang.org" + source: hosted + version: "0.12.0" + graphql: + dependency: "direct main" + description: + name: graphql + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.0" graphs: dependency: transitive description: @@ -485,6 +485,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.5" + rxdart: + dependency: transitive + description: + name: rxdart + url: "https://pub.dartlang.org" + source: hosted + version: "0.23.1" screen: dependency: "direct main" description: @@ -609,6 +616,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.1.6" + uuid_enhanced: + dependency: transitive + description: + name: uuid_enhanced + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.2" vector_math: dependency: transitive description: @@ -651,6 +665,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.1.0" + websocket: + dependency: transitive + description: + name: websocket + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.5" xml: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 84766d7..d03daf8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -24,12 +24,11 @@ dependencies: # Use with the CupertinoIcons class for iOS style icons. # cupertino_icons: ^0.1.2 connectivity: ^0.4.6+1 - cookie_jar: ^1.0.1 device_info: ^0.4.1+4 dio: ^3.0.7 - dio_cookie_manager: ^1.0.0 flutter_secure_storage: ^3.3.1+1 flutter_spinkit: ^4.1.1+1 + graphql: ^3.0.0 hive: ^1.3.0 hive_flutter: ^0.3.0 ota_update: ^2.1.1 diff --git a/web/api/get-cloudflare-id.ts b/web/api/get-cloudflare-id.ts deleted file mode 100644 index 511a72c..0000000 --- a/web/api/get-cloudflare-id.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { NowRequest, NowResponse } from "@now/node"; -import cloudscraper from "cloudscraper"; - -export default (req: NowRequest, res: NowResponse) => { - let jar = cloudscraper.jar(); - - cloudscraper({ - url: `https://animeflv.net`, - jar: jar - }).then(_ => { - const cfCookie = jar - .getCookies("https://animeflv.net") - .find(cookie => cookie.key == "__cfduid"); - - if (cfCookie == null) { - res.statusCode = 500; - res.send("Error"); - } else { - res.send(cfCookie.value); - } - }); -}; diff --git a/web/api/get-natsuki.ts b/web/api/get-natsuki.ts new file mode 100644 index 0000000..240dac4 --- /dev/null +++ b/web/api/get-natsuki.ts @@ -0,0 +1,8 @@ +import { NowRequest, NowResponse } from "@now/node"; +import cloudscraper from "cloudscraper"; + +export default (req: NowRequest, res: NowResponse) => { + cloudscraper + .get(req.body.replaceFirst("embed.php", "check.php")) + .then(res => res.send(res.data)); +}; From 714df1d1a23272c55e9cc06445028025689f4426 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Mart=C3=ADn=20Seery?= Date: Mon, 20 Jan 2020 23:07:08 -0300 Subject: [PATCH 04/15] Cast Options in Hive --- lib/screens/settings/cast/cast.dart | 221 +++++++++---------- lib/screens/splash_screen/splash_screen.dart | 15 +- pubspec.lock | 7 - pubspec.yaml | 1 - 4 files changed, 116 insertions(+), 128 deletions(-) diff --git a/lib/screens/settings/cast/cast.dart b/lib/screens/settings/cast/cast.dart index 656e709..64153d0 100644 --- a/lib/screens/settings/cast/cast.dart +++ b/lib/screens/settings/cast/cast.dart @@ -2,13 +2,15 @@ import 'package:animu/screens/settings/cast/guide.dart'; import 'package:animu/utils/notifiers.dart'; import 'package:animu/widgets/spinner.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:hive/hive.dart'; import 'package:provider/provider.dart'; class CastOptions { String ip; String port; String password; + + CastOptions({this.ip, this.port, this.password}); } class CastScreen extends StatefulWidget { @@ -17,22 +19,20 @@ class CastScreen extends StatefulWidget { } class _CastScreenState extends State { - final _storage = new FlutterSecureStorage(); final _form = new GlobalKey(); var options = new CastOptions(); - bool loading = true; bool connecting = false; - void getOptions() async { - options.ip = await _storage.read(key: 'cast_ip') ?? '192.168.0.1'; - options.port = await _storage.read(key: 'cast_port') ?? '8080'; - options.password = await _storage.read(key: 'cast_password') ?? ''; - setState(() => loading = false); - } - @override void initState() { - getOptions(); + final settingsBox = Hive.box('settings'); + setState(() { + options = CastOptions( + ip: settingsBox.get('cast_ip'), + port: settingsBox.get('cast_port'), + password: settingsBox.get('cast_password'), + ); + }); super.initState(); } @@ -42,117 +42,110 @@ class _CastScreenState extends State { return Form( key: _form, - child: loading - ? Spinner(size: 50) - : Column( - children: [ - GestureDetector( - onTap: () { - Navigator.push( - context, - MaterialPageRoute(builder: (_) => CastGuide()), - ); + child: Column( + children: [ + GestureDetector( + onTap: () { + Navigator.push( + context, + MaterialPageRoute(builder: (_) => CastGuide()), + ); + }, + child: Card( + child: ListTile( + leading: Icon(Icons.info, size: 30), + title: Text('Cómo configurar'), + subtitle: Text( + 'Tocá acá para ver cómo configurar el modo Transmitir'), + ), + ), + ), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + SizedBox( + width: MediaQuery.of(context).size.width * .6, + child: TextFormField( + decoration: InputDecoration(labelText: 'Dirección IP'), + initialValue: options.ip, + maxLines: 1, + maxLength: 15, + keyboardType: TextInputType.number, + enabled: !connecting, + onChanged: (val) { + setState(() => options.ip = val); }, - child: Card( - child: ListTile( - leading: Icon( - Icons.info, - size: 30, - ), - title: Text('Cómo configurar'), - subtitle: Text( - 'Tocá acá para ver cómo configurar el modo Transmitir'), - ), - ), - ), - Row( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - SizedBox( - width: MediaQuery.of(context).size.width * .6, - child: TextFormField( - decoration: InputDecoration(labelText: 'Dirección IP'), - initialValue: options.ip, - maxLines: 1, - maxLength: 15, - keyboardType: TextInputType.number, - enabled: !connecting, - onChanged: (val) { - setState(() => options.ip = val); - }, - validator: (val) => - RegExp(r'^(\d?\d?\d)\.(\d?\d?\d)\.(\d?\d?\d)\.(\d?\d?\d)$') - .hasMatch(val) - ? null - : 'Ingrese una IP válida', - ), - ), - SizedBox( - width: MediaQuery.of(context).size.width * .3, - child: TextFormField( - decoration: InputDecoration(labelText: 'Puerto'), - initialValue: options.port.toString(), - maxLines: 1, - maxLength: 5, - keyboardType: TextInputType.number, - enabled: !connecting, - onChanged: (val) { - setState(() => options.port = val); - }, - validator: (val) => RegExp(r'^\d+$').hasMatch(val) - ? null - : 'Ingrese un puerto válido', - ), - ), - ], + validator: (val) => + RegExp(r'^(\d?\d?\d)\.(\d?\d?\d)\.(\d?\d?\d)\.(\d?\d?\d)$') + .hasMatch(val) + ? null + : 'Ingrese una IP válida', ), - TextFormField( - decoration: InputDecoration(labelText: 'Contraseña'), - initialValue: options.password, - enabled: !connecting, - obscureText: true, + ), + SizedBox( + width: MediaQuery.of(context).size.width * .3, + child: TextFormField( + decoration: InputDecoration(labelText: 'Puerto'), + initialValue: options.port.toString(), maxLines: 1, + maxLength: 5, + keyboardType: TextInputType.number, + enabled: !connecting, onChanged: (val) { - setState(() => options.password = val); + setState(() => options.port = val); }, + validator: (val) => RegExp(r'^\d+$').hasMatch(val) + ? null + : 'Ingrese un puerto válido', ), - SizedBox(height: 50), - vlc.isConnected - ? RaisedButton( - onPressed: vlc.disconnect, - child: Text('Desconectar'), - ) - : (connecting - ? Spinner(size: 30) - : RaisedButton( - onPressed: () async { - if (!_form.currentState.validate()) return; - setState(() => connecting = true); + ), + ], + ), + TextFormField( + decoration: InputDecoration(labelText: 'Contraseña'), + initialValue: options.password, + enabled: !connecting, + obscureText: true, + maxLines: 1, + onChanged: (val) { + setState(() => options.password = val); + }, + ), + SizedBox(height: 50), + vlc.isConnected + ? RaisedButton( + onPressed: vlc.disconnect, + child: Text('Desconectar'), + ) + : (connecting + ? Spinner(size: 30) + : RaisedButton( + onPressed: () async { + if (!_form.currentState.validate()) return; + setState(() => connecting = true); - await _storage.write( - key: 'cast_ip', value: options.ip); - await _storage.write( - key: 'cast_port', value: options.port); - await _storage.write( - key: 'cast_password', - value: options.password); + Hive.box('settings').putAll({ + 'cast_ip': options.ip, + 'cast_port': options.port, + 'cast_password': options.password, + }); - final connected = await vlc.init( - ip: options.ip, - port: int.parse(options.port), - password: options.password, - ); - if (!connected) - Scaffold.of(context).showSnackBar(SnackBar( - content: Text('Error conectándose'), - )); - setState(() => connecting = false); - }, - child: Text('Guardar y conectar'), - )), - ], - ), + final connected = await vlc.init( + ip: options.ip, + port: int.parse(options.port), + password: options.password, + ); + if (!connected) + Scaffold.of(context).showSnackBar(SnackBar( + content: Text('Error conectándose'), + )); + setState(() => connecting = false); + }, + child: Text('Guardar y conectar'), + )), + ], + ), ); } } diff --git a/lib/screens/splash_screen/splash_screen.dart b/lib/screens/splash_screen/splash_screen.dart index 5a276d1..b4ab7df 100644 --- a/lib/screens/splash_screen/splash_screen.dart +++ b/lib/screens/splash_screen/splash_screen.dart @@ -68,15 +68,18 @@ class _SplashScreenState extends State { Future setDefaultSettings() async { await Hive.initFlutter(); - final settingsBox = await Hive.openBox('settings'); - void setDefaultSetting(String key, dynamic defaultValue) { - if (settingsBox.get(key) == null) settingsBox.put(key, defaultValue); + void setDefaultSetting(Box box, String key, dynamic defaultValue) { + if (box.get(key) == null) box.put(key, defaultValue); } - setDefaultSetting('default_category_index', 1); - setDefaultSetting('server_index', 0); - setDefaultSetting('mark_as_seen_when_next_episode', true); + final settingsBox = await Hive.openBox('settings'); + setDefaultSetting(settingsBox, 'cast_ip', '192.168.0.1'); + setDefaultSetting(settingsBox, 'cast_port', '8080'); + setDefaultSetting(settingsBox, 'cast_password', ''); + setDefaultSetting(settingsBox, 'default_category_index', 1); + setDefaultSetting(settingsBox, 'server_index', 0); + setDefaultSetting(settingsBox, 'mark_as_seen_when_next_episode', true); } void initApp() async { diff --git a/pubspec.lock b/pubspec.lock index c05ec34..b58f9c8 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -209,13 +209,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.7.4" - flutter_secure_storage: - dependency: "direct main" - description: - name: flutter_secure_storage - url: "https://pub.dartlang.org" - source: hosted - version: "3.3.1+1" flutter_spinkit: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index d03daf8..01ad1ca 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -26,7 +26,6 @@ dependencies: connectivity: ^0.4.6+1 device_info: ^0.4.1+4 dio: ^3.0.7 - flutter_secure_storage: ^3.3.1+1 flutter_spinkit: ^4.1.1+1 graphql: ^3.0.0 hive: ^1.3.0 From 6fb83f39f6e1947d38de52a75049a2631c89590b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Mart=C3=ADn=20Seery?= Date: Mon, 20 Jan 2020 23:22:26 -0300 Subject: [PATCH 05/15] Loading message in Splash Screen --- lib/screens/splash_screen/splash_screen.dart | 41 ++++++++++++++++---- 1 file changed, 34 insertions(+), 7 deletions(-) diff --git a/lib/screens/splash_screen/splash_screen.dart b/lib/screens/splash_screen/splash_screen.dart index b4ab7df..690c3c4 100644 --- a/lib/screens/splash_screen/splash_screen.dart +++ b/lib/screens/splash_screen/splash_screen.dart @@ -14,6 +14,13 @@ class SplashScreen extends StatefulWidget { } class _SplashScreenState extends State { + String message = ''; + + Future run({Future function(), String msg}) { + setState(() => message = msg); + return function(); + } + Future isOnline() async { var connection = await Connectivity().checkConnectivity(); bool connected = ConnectivityResult.none != connection; @@ -84,8 +91,16 @@ class _SplashScreenState extends State { void initApp() async { if (await isOnline() == false) return; - await checkUpdates(); - await setDefaultSettings(); + + await run( + function: checkUpdates, + msg: 'Buscando actualizaciones...', + ); + await run( + function: setDefaultSettings, + msg: 'Verificando configuración...', + ); + Navigator.pushReplacementNamed(context, '/home'); } @@ -98,11 +113,23 @@ class _SplashScreenState extends State { @override Widget build(BuildContext context) { return Scaffold( - body: Center( - child: Image.asset( - 'images/Name.png', - width: MediaQuery.of(context).size.width * 0.8, - ), + body: Stack( + children: [ + Align( + alignment: Alignment.center, + child: Image.asset( + 'images/Name.png', + width: MediaQuery.of(context).size.width * 0.8, + ), + ), + Align( + alignment: Alignment.bottomCenter, + child: Padding( + padding: EdgeInsets.only(bottom: 15), + child: Text(message), + ), + ), + ], ), ); } From 591377c5476fe8c49386e0d74814c68711492514 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Mart=C3=ADn=20Seery?= Date: Tue, 21 Jan 2020 16:35:30 -0300 Subject: [PATCH 06/15] Removed Anime Hero (temporary) --- lib/screens/anime/anime.dart | 11 ++++------- lib/widgets/anime_list.dart | 5 +---- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/lib/screens/anime/anime.dart b/lib/screens/anime/anime.dart index 4a35c25..0c0bf4d 100644 --- a/lib/screens/anime/anime.dart +++ b/lib/screens/anime/anime.dart @@ -64,13 +64,10 @@ class _AnimeScreenState extends State { child: Stack( overflow: Overflow.visible, children: [ - Hero( - tag: 'AnimeCover-${anime.id}', - child: Image.memory( - anime.cover, - fit: BoxFit.cover, - width: MediaQuery.of(context).size.width, - ), + Image.memory( + anime.cover, + fit: BoxFit.cover, + width: MediaQuery.of(context).size.width, ), Positioned( bottom: positionFromBottom(1), diff --git a/lib/widgets/anime_list.dart b/lib/widgets/anime_list.dart index f10a458..60be18f 100644 --- a/lib/widgets/anime_list.dart +++ b/lib/widgets/anime_list.dart @@ -45,10 +45,7 @@ class AnimeList extends StatelessWidget { children: [ ClipRRect( borderRadius: BorderRadius.circular(7.5), - child: Hero( - tag: 'AnimeCover-${anime.id}', - child: Image.memory(anime.cover), - ), + child: Image.memory(anime.cover), ), SizedBox(height: 5), Text( From bffb56cbca537aff0adfeb4d75960ef8a23245af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Mart=C3=ADn=20Seery?= Date: Tue, 21 Jan 2020 16:37:03 -0300 Subject: [PATCH 07/15] SQFlite --> Hive --- lib/screens/anime/anime.dart | 21 ++-- lib/screens/splash_screen/splash_screen.dart | 6 + lib/services/anime_database.dart | 109 +++---------------- lib/utils/categories.dart | 8 +- lib/utils/models.dart | 36 ++---- lib/utils/models.g.dart | 49 +++++++++ lib/utils/watching_states.dart | 39 +++---- lib/utils/watching_states.g.dart | 41 +++++++ lib/widgets/previous_next.dart | 4 +- pubspec.lock | 16 +-- pubspec.yaml | 2 - 11 files changed, 157 insertions(+), 174 deletions(-) create mode 100644 lib/utils/models.g.dart create mode 100644 lib/utils/watching_states.g.dart diff --git a/lib/screens/anime/anime.dart b/lib/screens/anime/anime.dart index 0c0bf4d..43efa37 100644 --- a/lib/screens/anime/anime.dart +++ b/lib/screens/anime/anime.dart @@ -25,7 +25,7 @@ class _AnimeScreenState extends State { double positionFromBottom(int n) => n * 50.00 + (n + 1) * 20; void getAnimeDBData() async { - dynamic dbAnime = await AnimeDatabaseService().getAnimeById(anime.id); + dynamic dbAnime = AnimeDatabaseService.getAnimeById(anime.id); if (dbAnime != null) setState(() => anime = dbAnime); } @@ -89,11 +89,11 @@ class _AnimeScreenState extends State { style: TextStyle(color: Colors.white), ), selected: anime.watchingState == state, - onSelected: (changed) async { + onSelected: (changed) { if (!changed) return; anime.watchingState = state; - await AnimeDatabaseService() - .updateAnime(anime); + AnimeDatabaseService.updateAnime( + anime); setState( () => Navigator.pop(context)); }, @@ -104,10 +104,9 @@ class _AnimeScreenState extends State { if (anime.watchingState != null) DialogButton( label: 'Eliminar estado', - onPressed: () async { + onPressed: () { anime.watchingState = null; - await AnimeDatabaseService() - .updateAnime(anime); + AnimeDatabaseService.updateAnime(anime); setState(() => Navigator.pop(context)); }, ), @@ -132,9 +131,9 @@ class _AnimeScreenState extends State { child: MainButton( backgroundColor: Theme.of(context).primaryColor, child: IconButton( - onPressed: () async { + onPressed: () { anime.favorite = !anime.favorite; - await AnimeDatabaseService().updateAnime(anime); + AnimeDatabaseService.updateAnime(anime); setState(() {}); }, icon: Icon(anime.favorite @@ -176,13 +175,13 @@ class _AnimeScreenState extends State { ? EpisodeList( anime: anime, episodes: episodes, - seenUnseen: (episode) async { + seenUnseen: (episode) { if (anime.episodesSeen == null) anime.episodesSeen = []; if (anime.episodesSeen.contains(episode.n)) anime.episodesSeen.remove(episode.n); else anime.episodesSeen.add(episode.n); - await AnimeDatabaseService().updateAnime(anime); + AnimeDatabaseService.updateAnime(anime); HapticFeedback.vibrate(); setState(() {}); }, diff --git a/lib/screens/splash_screen/splash_screen.dart b/lib/screens/splash_screen/splash_screen.dart index 690c3c4..e3be2eb 100644 --- a/lib/screens/splash_screen/splash_screen.dart +++ b/lib/screens/splash_screen/splash_screen.dart @@ -1,4 +1,6 @@ import 'package:animu/screens/splash_screen/updater.dart'; +import 'package:animu/utils/models.dart'; +import 'package:animu/utils/watching_states.dart'; import 'package:connectivity/connectivity.dart'; import 'package:device_info/device_info.dart'; import 'package:dio/dio.dart'; @@ -87,6 +89,10 @@ class _SplashScreenState extends State { setDefaultSetting(settingsBox, 'default_category_index', 1); setDefaultSetting(settingsBox, 'server_index', 0); setDefaultSetting(settingsBox, 'mark_as_seen_when_next_episode', true); + + Hive.registerAdapter(AnimeAdapter()); + Hive.registerAdapter(WatchingStateAdapter()); + await Hive.openBox('animes'); } void initApp() async { diff --git a/lib/services/anime_database.dart b/lib/services/anime_database.dart index 16b2ee9..c5eb280 100644 --- a/lib/services/anime_database.dart +++ b/lib/services/anime_database.dart @@ -1,111 +1,32 @@ import 'package:animu/utils/models.dart'; import 'package:animu/utils/watching_states.dart'; -import 'package:path/path.dart'; -import 'package:sqflite/sqflite.dart'; +import 'package:hive/hive.dart'; class AnimeDatabaseService { - Database db; + static final _box = Hive.box('animes'); - Future _init() async { - db = await openDatabase( - join(await getDatabasesPath(), 'anime.db'), - onCreate: (db, version) => db.execute( - 'CREATE TABLE animes(id INTEGER PRIMARY KEY, name TEXT, slug TEXT, favorite INTEGER, watching_state INTEGER, episodes_seen TEXT)'), - onUpgrade: (db, oldVersion, newVersion) { - bool applyUpgrade(int n) => oldVersion < n && newVersion >= n; - - if (applyUpgrade(2)) - db.execute('ALTER TABLE animes ADD COLUMN watching_state INTEGER'); - }, - version: 2, - ); - } - - Future _close() async => await db.close(); - - Future _insert(Anime anime) async { - await db.insert( - 'animes', - anime.toDBMap(), - ); - - return anime; - } - - Future _update(Anime anime) async { - await db.update( - 'animes', - anime.toDBMap(), - where: 'id = ?', - whereArgs: [anime.id], - ); - - return anime; - } - - Future _getById(int id) async { - return await db.query( - 'animes', - where: 'id = ?', - limit: 1, - whereArgs: [id], - ); - } - - Future updateAnime(Anime anime) async { - await _init(); - - bool exists = (await _getById(anime.id)).length == 1; - if (exists) - _update(anime); - else - _insert(anime); - - await _close(); + static Anime updateAnime(Anime anime) { + _box.put(anime.id, anime); return anime; } - Future getAnimeById(int id) async { - await _init(); - var rawRes = await _getById(id); - await _close(); - - return rawRes.isNotEmpty ? Anime.fromDBMap(rawRes[0]) : null; + static Anime getAnimeById(int id) { + return _box.get(id); } - Future> searchFavorites(String query) async { - await _init(); - var res = await db.query( - 'animes', - where: 'favorite = 1 AND name LIKE ?', - orderBy: 'name', - whereArgs: ['%$query%'], - ); - await _close(); - - return List.generate( - res.length, - (i) => Anime.fromDBMap(res[i]), - ); + static List searchFavorites(String query) { + return _box.values + .where((anime) => anime.name.contains(query ?? '') && anime.favorite) + .toList(); } - Future> searchByWatchingState( + static Future> searchByWatchingState( String query, WatchingState state, ) async { - await _init(); - - var res = await db.query( - 'animes', - where: 'watching_state = ${state.n} AND name LIKE ?', - orderBy: 'name', - whereArgs: ['%$query%'], - ); - await _close(); - - return List.generate( - res.length, - (i) => Anime.fromDBMap(res[i]), - ); + return _box.values + .where((anime) => + anime.name.contains(query ?? '') && anime.watchingState == state) + .toList(); } } diff --git a/lib/utils/categories.dart b/lib/utils/categories.dart index d07536a..9c19dab 100644 --- a/lib/utils/categories.dart +++ b/lib/utils/categories.dart @@ -25,8 +25,8 @@ final List categories = WatchingState.values icon: state.icon, searchBarLabel: state.categorySearchBarLabel, emptyLabel: state.categoryEmptyLabel, - dbFunction: (String query) async => await AnimeDatabaseService() - .searchByWatchingState(query, state), + dbFunction: (String query) => + AnimeDatabaseService.searchByWatchingState(query, state), ), ) .toList() + @@ -36,7 +36,7 @@ final List categories = WatchingState.values icon: Icons.favorite, searchBarLabel: 'Buscar tus animes favoritos', emptyLabel: 'No tenés corazón :c', - dbFunction: (String query) async => - await AnimeDatabaseService().searchFavorites(query), + dbFunction: (String query) => + AnimeDatabaseService.searchFavorites(query), ), ]; diff --git a/lib/utils/models.dart b/lib/utils/models.dart index 5c7f662..6736def 100644 --- a/lib/utils/models.dart +++ b/lib/utils/models.dart @@ -1,14 +1,25 @@ import 'dart:typed_data'; import 'package:animu/utils/watching_states.dart'; +import 'package:hive/hive.dart'; +part 'models.g.dart'; + +@HiveType(typeId: 0) class Anime { + @HiveField(0) final int id; + @HiveField(1) final String name; + @HiveField(2) final String slug; + @HiveField(3) final Uint8List cover; + @HiveField(4) bool favorite; + @HiveField(5) WatchingState watchingState; + @HiveField(6) List episodesSeen; Anime({ @@ -20,31 +31,6 @@ class Anime { this.watchingState, this.episodesSeen, }); - - factory Anime.fromDBMap(Map map) => Anime( - id: map['id'], - name: map['name'], - slug: map['slug'], - favorite: map['favorite'] == 1 ? true : false, - watchingState: intToWatchingState(map['watching_state']), - episodesSeen: map['episodes_seen'] != '' - ? List.from( - map['episodes_seen'].split(',').map((x) => int.parse(x))) - : [], - ); - - Map toDBMap() => { - 'id': id, - 'name': name, - 'slug': slug, - 'favorite': favorite ? 1 : 0, - 'watching_state': watchingState.n, - 'episodes_seen': episodesSeen != null && episodesSeen.length > 0 - ? episodesSeen - .fold('', (accum, value) => '$accum,$value') - .substring(1) - : '', - }; } class Episode { diff --git a/lib/utils/models.g.dart b/lib/utils/models.g.dart new file mode 100644 index 0000000..5ccb794 --- /dev/null +++ b/lib/utils/models.g.dart @@ -0,0 +1,49 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'models.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class AnimeAdapter extends TypeAdapter { + @override + final typeId = 0; + + @override + Anime read(BinaryReader reader) { + var numOfFields = reader.readByte(); + var fields = { + for (var i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return Anime( + id: fields[0] as int, + name: fields[1] as String, + slug: fields[2] as String, + cover: fields[3] as Uint8List, + favorite: fields[4] as bool, + watchingState: fields[5] as WatchingState, + episodesSeen: (fields[6] as List)?.cast(), + ); + } + + @override + void write(BinaryWriter writer, Anime obj) { + writer + ..writeByte(7) + ..writeByte(0) + ..write(obj.id) + ..writeByte(1) + ..write(obj.name) + ..writeByte(2) + ..write(obj.slug) + ..writeByte(3) + ..write(obj.cover) + ..writeByte(4) + ..write(obj.favorite) + ..writeByte(5) + ..write(obj.watchingState) + ..writeByte(6) + ..write(obj.episodesSeen); + } +} diff --git a/lib/utils/watching_states.dart b/lib/utils/watching_states.dart index 1eb3c15..9428623 100644 --- a/lib/utils/watching_states.dart +++ b/lib/utils/watching_states.dart @@ -1,6 +1,23 @@ import 'package:flutter/material.dart'; +import 'package:hive/hive.dart'; -enum WatchingState { toWatch, watching, watched } +part 'watching_states.g.dart'; + +@HiveType(typeId: 1) +enum WatchingState { + @HiveField(0) + toWatch, + @HiveField(1) + watching, + @HiveField(2) + watched +} + +const watchingStateString = { + WatchingState.toWatch: 'toWatch', + WatchingState.watching: 'watching', + WatchingState.watched: 'watched', +}; extension WatchingStateExtension on WatchingState { String get name { @@ -67,24 +84,4 @@ extension WatchingStateExtension on WatchingState { return null; } } - - int get n { - switch (this) { - case WatchingState.toWatch: - return 1; - case WatchingState.watching: - return 2; - case WatchingState.watched: - return 3; - default: - return 0; - } - } -} - -WatchingState intToWatchingState(int n) { - for (var state in WatchingState.values) { - if (state.n == n) return state; - } - return null; } diff --git a/lib/utils/watching_states.g.dart b/lib/utils/watching_states.g.dart new file mode 100644 index 0000000..a8d9282 --- /dev/null +++ b/lib/utils/watching_states.g.dart @@ -0,0 +1,41 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'watching_states.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class WatchingStateAdapter extends TypeAdapter { + @override + final typeId = 1; + + @override + WatchingState read(BinaryReader reader) { + switch (reader.readByte()) { + case 0: + return WatchingState.toWatch; + case 1: + return WatchingState.watching; + case 2: + return WatchingState.watched; + default: + return null; + } + } + + @override + void write(BinaryWriter writer, WatchingState obj) { + switch (obj) { + case WatchingState.toWatch: + writer.writeByte(0); + break; + case WatchingState.watching: + writer.writeByte(1); + break; + case WatchingState.watched: + writer.writeByte(2); + break; + } + } +} diff --git a/lib/widgets/previous_next.dart b/lib/widgets/previous_next.dart index 4701e53..2a98757 100644 --- a/lib/widgets/previous_next.dart +++ b/lib/widgets/previous_next.dart @@ -22,14 +22,14 @@ class PreviousNext extends StatelessWidget { return SizedBox(width: 50); else return GestureDetector( - onTap: () async { + onTap: () { var anime = data.anime; if (!isPrevious && Hive.box('settings').get('mark_as_seen_when_next_episode') && !anime.episodesSeen.contains(data.currentEpisode.n)) { anime.episodesSeen.add(data.currentEpisode.n); - await AnimeDatabaseService().updateAnime(anime); + AnimeDatabaseService.updateAnime(anime); } changeEpisode(data.episodes[index]); diff --git a/pubspec.lock b/pubspec.lock index b58f9c8..f0b6471 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -402,7 +402,7 @@ packages: source: hosted version: "1.0.10" path: - dependency: "direct main" + dependency: transitive description: name: path url: "https://pub.dartlang.org" @@ -532,13 +532,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.5.5" - sqflite: - dependency: "direct main" - description: - name: sqflite - url: "https://pub.dartlang.org" - source: hosted - version: "1.2.0" stack_trace: dependency: transitive description: @@ -567,13 +560,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.5" - synchronized: - dependency: transitive - description: - name: synchronized - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.1" term_glyph: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 01ad1ca..3e0ed53 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -32,12 +32,10 @@ dependencies: hive_flutter: ^0.3.0 ota_update: ^2.1.1 package_info: ^0.4.0+13 - path: ^1.6.4 provider: ^3.2.0 pub_semver: ^1.4.2 screen: ^0.0.5 share: ^0.6.3+5 - sqflite: ^1.2.0 video_player: ^0.10.5 dev_dependencies: From a3552bedcd5f24bdf8d3d71b7075b45b41a94961 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Mart=C3=ADn=20Seery?= Date: Tue, 21 Jan 2020 16:42:12 -0300 Subject: [PATCH 08/15] Updated README --- README.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 72cb446..1036a29 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ ![Banner](images/Banner.png) + ![Version](https://img.shields.io/github/v/release/JuanM04/animu?style=flat-square) ![License](https://img.shields.io/github/license/JuanM04/animu?style=flat-square) ![Flutter](https://img.shields.io/static/v1?label=Flutter&message=v1.12&logo=flutter&color=02569B&style=flat-square) @@ -10,11 +11,9 @@ Animú es una app para ver anime sin complicaciones. Funciona con los servidores ### ¿Cómo funciona? -Animú consta de la app (hecha en Flutter) y un servidor en ZEIT Now. - -Este último sirve para saltarse la seguridad de Cloudflare al momento de pedirle información a AnimeFLV. Esto se hace porque Animú usa Web Scraping, que básicamente es descargar el HTML y sacar el contenido de ahí (en vez de pedirlo a una API). Usa `web/api/get-cloudflare-id.ts` solamente para "fingir un PC real" y luego sigue haciendo peticiones desde la app. La página web está en `web/`. +Animú consta de la app (hecha en Flutter), [AnimeFLV GraphQL](https://github.com/JuanM04/animeflv-graphql) y una función en ZEIT Now. -Para el modo Transmitir, se usa la API proveida por VLC 3+. Más información [aquí](https://wiki.videolan.org/VLC_HTTP_requests/). +Todos los datos son guardados con [HiveDB](https://github.com/hivedb/hive). Para el modo Transmitir, se usa la API proveida por VLC 3+. Más información [aquí](https://wiki.videolan.org/VLC_HTTP_requests/). ### ¿Por qué está todo en inglés? From 9b344b87da414affd45a6fd2d0c3e87df22f7216 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Mart=C3=ADn=20Seery?= Date: Tue, 21 Jan 2020 18:22:19 -0300 Subject: [PATCH 09/15] Updated dependencies --- pubspec.lock | 15 +++++++++++---- pubspec.yaml | 10 +++++----- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index f0b6471..dfca93b 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -140,7 +140,7 @@ packages: name: connectivity url: "https://pub.dartlang.org" source: hosted - version: "0.4.6+1" + version: "0.4.6+2" convert: dependency: transitive description: @@ -359,6 +359,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.9.6+3" + nested: + dependency: transitive + description: + name: nested + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.4" node_interop: dependency: transitive description: @@ -456,7 +463,7 @@ packages: name: provider url: "https://pub.dartlang.org" source: hosted - version: "3.2.0" + version: "4.0.2" pub_semver: dependency: "direct main" description: @@ -615,7 +622,7 @@ packages: name: video_player url: "https://pub.dartlang.org" source: hosted - version: "0.10.5" + version: "0.10.5+2" video_player_platform_interface: dependency: transitive description: @@ -667,4 +674,4 @@ packages: version: "2.2.0" sdks: dart: ">=2.6.0 <3.0.0" - flutter: ">=1.12.13+hotfix.4 <2.0.0" + flutter: ">=1.12.13+hotfix.5 <2.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index 3e0ed53..da621bb 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -23,20 +23,20 @@ dependencies: # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. # cupertino_icons: ^0.1.2 - connectivity: ^0.4.6+1 + connectivity: ^0.4.6+2 device_info: ^0.4.1+4 - dio: ^3.0.7 + dio: ^3.0.8 flutter_spinkit: ^4.1.1+1 graphql: ^3.0.0 hive: ^1.3.0 - hive_flutter: ^0.3.0 + hive_flutter: ^0.3.0+1 ota_update: ^2.1.1 package_info: ^0.4.0+13 - provider: ^3.2.0 + provider: ^4.0.2 pub_semver: ^1.4.2 screen: ^0.0.5 share: ^0.6.3+5 - video_player: ^0.10.5 + video_player: ^0.10.5+2 dev_dependencies: flutter_test: From 468428c29bb053307362a49718beaaa98a8fee3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Mart=C3=ADn=20Seery?= Date: Tue, 21 Jan 2020 19:47:55 -0300 Subject: [PATCH 10/15] Remove loading in Saved Animes --- lib/screens/saved_animes.dart | 23 +++++++++-------------- lib/services/anime_database.dart | 5 +---- lib/utils/categories.dart | 7 ++++--- 3 files changed, 14 insertions(+), 21 deletions(-) diff --git a/lib/screens/saved_animes.dart b/lib/screens/saved_animes.dart index ac29a24..1661d94 100644 --- a/lib/screens/saved_animes.dart +++ b/lib/screens/saved_animes.dart @@ -3,7 +3,6 @@ import 'package:animu/widgets/anime_list.dart'; import 'package:animu/widgets/dialog_button.dart'; import 'package:animu/widgets/search_bar.dart'; import 'package:animu/utils/models.dart'; -import 'package:animu/widgets/spinner.dart'; import 'package:flutter/material.dart'; import 'package:hive/hive.dart'; @@ -14,14 +13,13 @@ class SavedAnimes extends StatefulWidget { class _SavedAnimesState extends State { List animes; - bool loading = true; Category categorySelected; bool categoryHasChanged = false; - void getAnimes(String query) async { - setState(() => loading = true); - animes = await categorySelected.dbFunction(query); - if (mounted) setState(() => loading = false); + void getAnimes(String query) { + setState(() { + animes = categorySelected.dbFunction(query); + }); } @override @@ -55,7 +53,7 @@ class _SavedAnimesState extends State { ? categorySelected.searchBarLabel : '', callback: getAnimes, - disabled: loading, + disabled: false, ), ), SizedBox(width: 20), @@ -97,13 +95,10 @@ class _SavedAnimesState extends State { ), SizedBox(height: 25), Expanded( - child: loading - ? Spinner(size: 50) - : AnimeList( - animes: animes, - emptyLabel: categorySelected.emptyLabel, - ), - ), + child: AnimeList( + animes: animes, + emptyLabel: categorySelected.emptyLabel, + )), ], ), ), diff --git a/lib/services/anime_database.dart b/lib/services/anime_database.dart index c5eb280..bf1c6a5 100644 --- a/lib/services/anime_database.dart +++ b/lib/services/anime_database.dart @@ -20,10 +20,7 @@ class AnimeDatabaseService { .toList(); } - static Future> searchByWatchingState( - String query, - WatchingState state, - ) async { + static List searchByWatchingState(String query, WatchingState state) { return _box.values .where((anime) => anime.name.contains(query ?? '') && anime.watchingState == state) diff --git a/lib/utils/categories.dart b/lib/utils/categories.dart index 9c19dab..4693895 100644 --- a/lib/utils/categories.dart +++ b/lib/utils/categories.dart @@ -2,12 +2,14 @@ import 'package:animu/services/anime_database.dart'; import 'package:animu/utils/watching_states.dart'; import 'package:flutter/material.dart'; +import 'models.dart'; + class Category { final String label; final IconData icon; final String searchBarLabel; final String emptyLabel; - final Function(String query) dbFunction; + final List Function(String query) dbFunction; Category({ this.label, @@ -36,7 +38,6 @@ final List categories = WatchingState.values icon: Icons.favorite, searchBarLabel: 'Buscar tus animes favoritos', emptyLabel: 'No tenés corazón :c', - dbFunction: (String query) => - AnimeDatabaseService.searchFavorites(query), + dbFunction: AnimeDatabaseService.searchFavorites, ), ]; From 94b47c75ddaac8e116fc117ee7d4bb34877d1c33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Mart=C3=ADn=20Seery?= Date: Tue, 21 Jan 2020 21:20:01 -0300 Subject: [PATCH 11/15] Firebase Auth And Analytics ;) --- .gitignore | 3 ++ README.md | 5 ++ android/app/build.gradle | 2 + android/build.gradle | 1 + lib/main.dart | 10 ++++ lib/screens/settings/backups.dart | 50 +++++++++++++++++ lib/screens/settings/settings.dart | 6 +++ lib/services/backup.dart | 36 +++++++++++++ pubspec.lock | 86 +++++++++++++++++++++++++++++- pubspec.yaml | 4 ++ 10 files changed, 202 insertions(+), 1 deletion(-) create mode 100644 lib/screens/settings/backups.dart create mode 100644 lib/services/backup.dart diff --git a/.gitignore b/.gitignore index 4b91960..f734bc5 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,9 @@ # is commented out by default. #.vscode/ +# Secrets +google-services.json + # Flutter/Dart/Pub related **/doc/api/ .dart_tool/ diff --git a/README.md b/README.md index 1036a29..356a402 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,11 @@ Todos los datos son guardados con [HiveDB](https://github.com/hivedb/hive). Para Tengo problemas, no me peguen. +## Setup + +1. Agregar `android/app/google-services.json` de Firebase. +2. Ejecturar `flutter pub get`. + ## To-do - Animaciones diff --git a/android/app/build.gradle b/android/app/build.gradle index ba65629..e04f0fe 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -24,6 +24,7 @@ if (flutterVersionName == null) { apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" +apply plugin: 'com.google.gms.google-services' android { compileSdkVersion 28 @@ -78,4 +79,5 @@ dependencies { testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test:runner:1.1.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' + implementation 'com.google.firebase:firebase-analytics:17.2.0' } diff --git a/android/build.gradle b/android/build.gradle index e1d9a1e..9b5605e 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -8,6 +8,7 @@ buildscript { dependencies { classpath 'com.android.tools.build:gradle:3.3.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + classpath 'com.google.gms:google-services:4.3.2' } } diff --git a/lib/main.dart b/lib/main.dart index 2ae7f36..2ffa582 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -2,7 +2,11 @@ import 'package:animu/screens/browse.dart'; import 'package:animu/screens/saved_animes.dart'; import 'package:animu/screens/settings/settings.dart'; import 'package:animu/screens/splash_screen/splash_screen.dart'; +import 'package:animu/services/backup.dart'; import 'package:animu/utils/notifiers.dart'; +import 'package:firebase_analytics/firebase_analytics.dart'; +import 'package:firebase_analytics/observer.dart'; +import 'package:firebase_auth/firebase_auth.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; @@ -26,9 +30,12 @@ class MyApp extends StatelessWidget { final primaryColor = Color(0xFFBF3030); // Strawberry Red + final analytics = FirebaseAnalytics(); + return MultiProvider( providers: [ ChangeNotifierProvider.value(value: VLCNotifier()), + StreamProvider.value(value: BackupService().userStream), ], child: MaterialApp( title: 'Animú', @@ -63,6 +70,9 @@ class MyApp extends StatelessWidget { '/home': (context) => TabsWrapper(), }, initialRoute: '/', + navigatorObservers: [ + FirebaseAnalyticsObserver(analytics: analytics), + ], ), ); } diff --git a/lib/screens/settings/backups.dart b/lib/screens/settings/backups.dart new file mode 100644 index 0000000..411ba0f --- /dev/null +++ b/lib/screens/settings/backups.dart @@ -0,0 +1,50 @@ +import 'package:animu/services/backup.dart'; +import 'package:animu/widgets/spinner.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class BackupsSettings extends StatefulWidget { + @override + _BackupsSettingsState createState() => _BackupsSettingsState(); +} + +class _BackupsSettingsState extends State { + bool loading = false; + + @override + Widget build(BuildContext context) { + final user = Provider.of(context); + final signedIn = user != null; + + return Center( + child: loading + ? Spinner(size: 50) + : Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Padding( + padding: EdgeInsets.fromLTRB(0, 20, 0, 10), + child: Text( + signedIn + ? ('Conectado: ' + user.displayName) + : 'Desconectado', + ), + ), + RaisedButton( + child: Text( + signedIn ? 'Desconectarse' : 'Conectarse con Google'), + onPressed: () async { + setState(() => loading = true); + if (signedIn) + await BackupService().signOut(); + else + await BackupService().signIn(); + setState(() => loading = false); + }, + ), + ], + ), + ); + } +} diff --git a/lib/screens/settings/settings.dart b/lib/screens/settings/settings.dart index 4383c07..58efe2a 100644 --- a/lib/screens/settings/settings.dart +++ b/lib/screens/settings/settings.dart @@ -1,3 +1,4 @@ +import 'package:animu/screens/settings/backups.dart'; import 'package:animu/screens/settings/cast/cast.dart'; import 'package:animu/screens/settings/about.dart'; import 'package:animu/screens/settings/faq.dart'; @@ -18,6 +19,11 @@ class SettingsIndex extends StatelessWidget { icon: Icons.settings_applications, widget: GeneralSettings(), ), + Setting( + name: 'Copias de seguridad', + icon: Icons.cloud, + widget: BackupsSettings(), + ), Setting( name: 'Trasmitir', icon: Icons.cast, diff --git a/lib/services/backup.dart b/lib/services/backup.dart new file mode 100644 index 0000000..54edda8 --- /dev/null +++ b/lib/services/backup.dart @@ -0,0 +1,36 @@ +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:google_sign_in/google_sign_in.dart'; + +class BackupService { + final _auth = FirebaseAuth.instance; + final _googleSignIn = GoogleSignIn(); + + Stream get userStream => _auth.onAuthStateChanged; + + Future signIn() async { + try { + final googleUser = await _googleSignIn.signIn(); + final googleAuth = await googleUser.authentication; + final googleCredential = GoogleAuthProvider.getCredential( + accessToken: googleAuth.accessToken, + idToken: googleAuth.idToken, + ); + + await _auth.signInWithCredential(googleCredential); + return await _auth.currentUser(); + } catch (e) { + print(e); + return null; + } + } + + Future signOut() async { + try { + await _auth.signOut(); + return true; + } catch (e) { + print(e); + return false; + } + } +} diff --git a/pubspec.lock b/pubspec.lock index dfca93b..bf1e706 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -120,6 +120,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.2" + cloud_firestore: + dependency: "direct main" + description: + name: cloud_firestore + url: "https://pub.dartlang.org" + source: hosted + version: "0.13.0+1" code_builder: dependency: transitive description: @@ -190,6 +197,62 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.0.8" + firebase: + dependency: transitive + description: + name: firebase + url: "https://pub.dartlang.org" + source: hosted + version: "7.2.0" + firebase_analytics: + dependency: "direct main" + description: + name: firebase_analytics + url: "https://pub.dartlang.org" + source: hosted + version: "5.0.9" + firebase_auth: + dependency: "direct main" + description: + name: firebase_auth + url: "https://pub.dartlang.org" + source: hosted + version: "0.15.3+1" + firebase_auth_platform_interface: + dependency: transitive + description: + name: firebase_auth_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.5" + firebase_auth_web: + dependency: transitive + description: + name: firebase_auth_web + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.1+4" + firebase_core: + dependency: transitive + description: + name: firebase_core + url: "https://pub.dartlang.org" + source: hosted + version: "0.4.3+2" + firebase_core_platform_interface: + dependency: transitive + description: + name: firebase_core_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" + firebase_core_web: + dependency: transitive + description: + name: firebase_core_web + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.1+2" fixnum: dependency: transitive description: @@ -233,6 +296,27 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.2.0" + google_sign_in: + dependency: "direct main" + description: + name: google_sign_in + url: "https://pub.dartlang.org" + source: hosted + version: "4.1.1" + google_sign_in_platform_interface: + dependency: transitive + description: + name: google_sign_in_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.3" + google_sign_in_web: + dependency: transitive + description: + name: google_sign_in_web + url: "https://pub.dartlang.org" + source: hosted + version: "0.8.3" gql: dependency: transitive description: @@ -673,5 +757,5 @@ packages: source: hosted version: "2.2.0" sdks: - dart: ">=2.6.0 <3.0.0" + dart: ">=2.7.0-dev <3.0.0" flutter: ">=1.12.13+hotfix.5 <2.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index da621bb..fe9e381 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -23,10 +23,14 @@ dependencies: # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. # cupertino_icons: ^0.1.2 + cloud_firestore: ^0.13.0+1 connectivity: ^0.4.6+2 device_info: ^0.4.1+4 dio: ^3.0.8 + firebase_analytics: ^5.0.9 + firebase_auth: ^0.15.3+1 flutter_spinkit: ^4.1.1+1 + google_sign_in: ^4.1.1 graphql: ^3.0.0 hive: ^1.3.0 hive_flutter: ^0.3.0+1 From c245bdfb04ec12802fa7db4f3699c0f5d973cd0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Mart=C3=ADn=20Seery?= Date: Tue, 21 Jan 2020 23:18:14 -0300 Subject: [PATCH 12/15] Anime from/to map --- lib/screens/anime/anime.dart | 19 ++----------------- lib/screens/browse.dart | 19 ++----------------- lib/services/requests.dart | 24 ++++++++++++++++++++---- lib/utils/models.dart | 30 ++++++++++++++++++++++++++++++ 4 files changed, 54 insertions(+), 38 deletions(-) diff --git a/lib/screens/anime/anime.dart b/lib/screens/anime/anime.dart index 43efa37..a2e571e 100644 --- a/lib/screens/anime/anime.dart +++ b/lib/screens/anime/anime.dart @@ -1,5 +1,3 @@ -import 'dart:convert'; - import 'package:animu/screens/anime/episode_list.dart'; import 'package:animu/services/requests.dart'; import 'package:animu/utils/models.dart'; @@ -30,21 +28,8 @@ class _AnimeScreenState extends State { } void getEpisodes() async { - List response = await RequestsService.getEpisodes(anime: anime); - - if (mounted) - setState(() { - episodes = new List.from( - response - .map((map) => Episode( - id: map['id'], - n: map['n'], - thumbnail: base64Decode(map['thumbnail']), - )) - .toList(), - ); - loading = false; - }); + episodes = await RequestsService.getEpisodes(anime); + if (mounted) setState(() => loading = false); } @override diff --git a/lib/screens/browse.dart b/lib/screens/browse.dart index 9ed3252..9f7c8d0 100644 --- a/lib/screens/browse.dart +++ b/lib/screens/browse.dart @@ -1,5 +1,3 @@ -import 'dart:convert'; - import 'package:animu/services/requests.dart'; import 'package:animu/widgets/anime_list.dart'; import 'package:animu/widgets/search_bar.dart'; @@ -19,22 +17,9 @@ class _BrowseState extends State { void getAnimes(String query) async { setState(() => loading = true); - List response = await RequestsService.searchAnimes(query); + animes = await RequestsService.searchAnimes(query); - if (mounted) - setState(() { - animes = new List.from(response - .map( - (map) => Anime( - id: map['id'], - name: map['name'], - slug: map['slug'], - cover: base64Decode(map['cover']), - ), - ) - .toList()); - loading = false; - }); + if (mounted) setState(() => loading = false); } Widget bigContent() { diff --git a/lib/services/requests.dart b/lib/services/requests.dart index 3263f4a..233434c 100644 --- a/lib/services/requests.dart +++ b/lib/services/requests.dart @@ -1,3 +1,5 @@ +import 'dart:convert'; + import 'package:animu/utils/models.dart'; import 'package:dio/dio.dart'; import 'package:graphql/client.dart'; @@ -43,7 +45,7 @@ class RequestsService { } } - static Future getEpisodes({Anime anime}) async { + static Future> getEpisodes(Anime anime) async { try { final response = await _query( query: """ @@ -62,7 +64,17 @@ class RequestsService { 'animeSlug': anime.slug, }, ); - return response.data['anime']['episodes']; + + final episodes = response.data['anime']['episodes']; + return new List.from( + episodes.map( + (map) => Episode( + id: map['id'], + n: map['n'], + thumbnail: base64Decode(map['thumbnail']), + ), + ), + ); } catch (e) { print(e); return null; @@ -81,7 +93,7 @@ class RequestsService { } } - static Future searchAnimes(String query) async { + static Future> searchAnimes(String query) async { try { final response = await _query( query: """ @@ -96,7 +108,11 @@ class RequestsService { """, variables: {'query': query}, ); - return response.data['search']; + + final animes = response.data['search']; + return new List.from( + animes.map((map) => Anime.fromMap(map)), + ); } catch (e) { print(e); return null; diff --git a/lib/utils/models.dart b/lib/utils/models.dart index 6736def..d34e279 100644 --- a/lib/utils/models.dart +++ b/lib/utils/models.dart @@ -1,3 +1,4 @@ +import 'dart:convert'; import 'dart:typed_data'; import 'package:animu/utils/watching_states.dart'; @@ -31,6 +32,35 @@ class Anime { this.watchingState, this.episodesSeen, }); + + factory Anime.fromMap(Map map) { + return Anime( + id: map['id'], + name: map['name'], + slug: map['slug'], + cover: map['cover'] == null ? null : base64Decode(map['cover']), + favorite: map['favorite'] ?? false, + watchingState: map['watchingState'] == null + ? null + : watchingStateString.entries + .firstWhere((x) => x.value == map['watchingState']) + .key, + episodesSeen: map['episodesSeen'] ?? [], + ); + } + + Map toMap([bool limited = false]) { + return { + 'id': id, + 'name': limited ? null : name, + 'slug': slug, + 'cover': limited || cover == null ? null : base64Encode(cover), + 'favorite': favorite, + 'watchingState': + watchingState == null ? null : watchingStateString[watchingState], + 'episodesSeen': episodesSeen, + }; + } } class Episode { From 3cbf851f22f580d13096aef525b87de03e7ec2b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Mart=C3=ADn=20Seery?= Date: Wed, 22 Jan 2020 19:14:46 -0300 Subject: [PATCH 13/15] Backups --- lib/main.dart | 2 +- lib/screens/settings/backups.dart | 4 ++-- lib/services/anime_database.dart | 2 ++ lib/services/backup.dart | 27 ++++++++++++++++++++++----- 4 files changed, 27 insertions(+), 8 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 2ffa582..3c08aca 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -35,7 +35,7 @@ class MyApp extends StatelessWidget { return MultiProvider( providers: [ ChangeNotifierProvider.value(value: VLCNotifier()), - StreamProvider.value(value: BackupService().userStream), + StreamProvider.value(value: BackupService.userStream), ], child: MaterialApp( title: 'Animú', diff --git a/lib/screens/settings/backups.dart b/lib/screens/settings/backups.dart index 411ba0f..83702d0 100644 --- a/lib/screens/settings/backups.dart +++ b/lib/screens/settings/backups.dart @@ -37,9 +37,9 @@ class _BackupsSettingsState extends State { onPressed: () async { setState(() => loading = true); if (signedIn) - await BackupService().signOut(); + await BackupService.signOut(); else - await BackupService().signIn(); + await BackupService.signIn(); setState(() => loading = false); }, ), diff --git a/lib/services/anime_database.dart b/lib/services/anime_database.dart index bf1c6a5..221df01 100644 --- a/lib/services/anime_database.dart +++ b/lib/services/anime_database.dart @@ -1,3 +1,4 @@ +import 'package:animu/services/backup.dart'; import 'package:animu/utils/models.dart'; import 'package:animu/utils/watching_states.dart'; import 'package:hive/hive.dart'; @@ -7,6 +8,7 @@ class AnimeDatabaseService { static Anime updateAnime(Anime anime) { _box.put(anime.id, anime); + BackupService.uploadToDB(_box); return anime; } diff --git a/lib/services/backup.dart b/lib/services/backup.dart index 54edda8..8be968b 100644 --- a/lib/services/backup.dart +++ b/lib/services/backup.dart @@ -1,13 +1,18 @@ +import 'package:animu/utils/models.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:firebase_auth/firebase_auth.dart'; import 'package:google_sign_in/google_sign_in.dart'; +import 'package:hive/hive.dart'; class BackupService { - final _auth = FirebaseAuth.instance; - final _googleSignIn = GoogleSignIn(); + static final _auth = FirebaseAuth.instance; + static final _googleSignIn = GoogleSignIn(); + static final _db = Firestore.instance; - Stream get userStream => _auth.onAuthStateChanged; + static Future get user => _auth.currentUser(); + static Stream get userStream => _auth.onAuthStateChanged; - Future signIn() async { + static Future signIn() async { try { final googleUser = await _googleSignIn.signIn(); final googleAuth = await googleUser.authentication; @@ -24,7 +29,7 @@ class BackupService { } } - Future signOut() async { + static Future signOut() async { try { await _auth.signOut(); return true; @@ -33,4 +38,16 @@ class BackupService { return false; } } + + static void uploadToDB(Box box) async { + final u = await user; + if (u == null) return; + + await _db.document('users/${u.uid}').setData({ + 'animes': box.toMap().map((id, anime) { + return new MapEntry(id.toString(), anime.toMap(true)); + }), + 'updatedAt': FieldValue.serverTimestamp(), + }); + } } From 5c259104d1d1e5341437ea929065aad3fdc8eefb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Mart=C3=ADn=20Seery?= Date: Thu, 23 Jan 2020 01:14:16 -0300 Subject: [PATCH 14/15] Restore backups --- lib/screens/settings/backups.dart | 5 +- lib/services/anime_database.dart | 2 +- lib/services/backup.dart | 135 ++++++++++++++++++++++++++++-- lib/services/requests.dart | 26 ++++++ lib/utils/helpers.dart | 41 +++++++-- lib/utils/models.dart | 24 ++++-- 6 files changed, 208 insertions(+), 25 deletions(-) diff --git a/lib/screens/settings/backups.dart b/lib/screens/settings/backups.dart index 83702d0..217a589 100644 --- a/lib/screens/settings/backups.dart +++ b/lib/screens/settings/backups.dart @@ -33,13 +33,14 @@ class _BackupsSettingsState extends State { ), RaisedButton( child: Text( - signedIn ? 'Desconectarse' : 'Conectarse con Google'), + signedIn ? 'Desconectarse' : 'Conectarse con Google', + ), onPressed: () async { setState(() => loading = true); if (signedIn) await BackupService.signOut(); else - await BackupService.signIn(); + await BackupService.signIn(context); setState(() => loading = false); }, ), diff --git a/lib/services/anime_database.dart b/lib/services/anime_database.dart index 221df01..bd2fe9f 100644 --- a/lib/services/anime_database.dart +++ b/lib/services/anime_database.dart @@ -8,7 +8,7 @@ class AnimeDatabaseService { static Anime updateAnime(Anime anime) { _box.put(anime.id, anime); - BackupService.uploadToDB(_box); + BackupService.uploadToDB(); return anime; } diff --git a/lib/services/backup.dart b/lib/services/backup.dart index 8be968b..f344cc3 100644 --- a/lib/services/backup.dart +++ b/lib/services/backup.dart @@ -1,6 +1,11 @@ +import 'package:animu/services/requests.dart'; +import 'package:animu/utils/helpers.dart'; import 'package:animu/utils/models.dart'; +import 'package:animu/widgets/dialog_button.dart'; +import 'package:animu/widgets/spinner.dart'; import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter/material.dart'; import 'package:google_sign_in/google_sign_in.dart'; import 'package:hive/hive.dart'; @@ -11,8 +16,13 @@ class BackupService { static Future get user => _auth.currentUser(); static Stream get userStream => _auth.onAuthStateChanged; + static Future get userDocument async { + final u = await user; + if (u == null) return null; + return _db.document('users/${u.uid}'); + } - static Future signIn() async { + static Future signIn(BuildContext context) async { try { final googleUser = await _googleSignIn.signIn(); final googleAuth = await googleUser.authentication; @@ -20,9 +30,23 @@ class BackupService { accessToken: googleAuth.accessToken, idToken: googleAuth.idToken, ); - await _auth.signInWithCredential(googleCredential); - return await _auth.currentUser(); + + final docReference = await userDocument; + if (docReference != null) { + final doc = await docReference.get(); + if (doc.exists) { + await showDialog( + context: context, + barrierDismissible: false, + builder: (context) => WillPopScope( + onWillPop: () async => false, + child: _BackupFound(doc), + ), + ); + } + } + return await user; } catch (e) { print(e); return null; @@ -39,15 +63,112 @@ class BackupService { } } - static void uploadToDB(Box box) async { + static void uploadToDB() async { final u = await user; if (u == null) return; + final animes = Hive.box('animes'); + await _db.document('users/${u.uid}').setData({ - 'animes': box.toMap().map((id, anime) { - return new MapEntry(id.toString(), anime.toMap(true)); - }), + 'animes': animes.toMap().map( + (id, anime) { + return new MapEntry(id.toString(), anime.toMap(true)); + }, + ), 'updatedAt': FieldValue.serverTimestamp(), }); } + + static void restore(dynamic data) async { + final u = await user; + if (u == null) return; + + final animes = new List.from( + data['animes'] + .values + .map((map) => Anime.fromMap(Map.from(map))), + ); + + await Future.wait(animes.map(getAnimes)); + } + + static Future getAnimes(Anime anime) async { + final newData = await RequestsService.getAnime(anime); + final mergedAnimeMap = { + ...newData.toMap()..removeWhere((_, value) => value == null), + ...anime.toMap(true)..removeWhere((_, value) => value == null), + }; + final finalAnime = Anime.fromMap(mergedAnimeMap); + + Hive.box('animes').put(finalAnime.id, finalAnime); + } +} + +class _BackupFound extends StatefulWidget { + final DocumentSnapshot doc; + + const _BackupFound(this.doc, {Key key}) : super(key: key); + + @override + __BackupFoundState createState() => __BackupFoundState(); +} + +class __BackupFoundState extends State<_BackupFound> { + bool loading = false; + + void run(Function f) async { + setState(() => loading = true); + await f(); + Navigator.pop(context); + } + + @override + Widget build(BuildContext context) { + final date = formatDay(widget.doc.data['updatedAt'].toDate()); + + if (loading) + return Dialog( + child: SizedBox( + height: 75, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Spinner(size: 25), + SizedBox(width: 15), + Text('Cargando...'), + ], + ), + ), + ); + + return AlertDialog( + title: Text('Copia encontrada'), + content: Text( + 'Se ha encontrado una copia de seguridad vinculada a esta cuenta del $date. Podés: \n\n' + + 'A) Recuperar la copia y borrar los animes que tengas localmente\n' + + 'B) Borrar la copia y subir los animes que tenés localmetne\n' + + 'C) No hacer nada', + ), + actions: [ + DialogButton( + label: 'A - Recuperar', + onPressed: () { + run(() async => BackupService.restore(widget.doc.data)); + }, + ), + DialogButton( + label: 'B - Borrar', + onPressed: () { + run(BackupService.uploadToDB); + }, + ), + DialogButton( + label: 'C - Cancelar', + onPressed: () { + run(BackupService.signOut); + }, + ), + ], + ); + } } diff --git a/lib/services/requests.dart b/lib/services/requests.dart index 233434c..20dc23a 100644 --- a/lib/services/requests.dart +++ b/lib/services/requests.dart @@ -118,4 +118,30 @@ class RequestsService { return null; } } + + static Future getAnime(Anime anime) async { + try { + final response = await _query( + query: """ + query GetEpisodes(\$animeId: Int!, \$animeSlug: String!) { + anime(id: \$animeId, slug: \$animeSlug) { + id + name + slug + cover + } + } + """, + variables: { + 'animeId': anime.id, + 'animeSlug': anime.slug, + }, + ); + + return Anime.fromMap(response.data['anime']); + } catch (e) { + print(e); + return null; + } + } } diff --git a/lib/utils/helpers.dart b/lib/utils/helpers.dart index af8ce0b..a0040cb 100644 --- a/lib/utils/helpers.dart +++ b/lib/utils/helpers.dart @@ -1,5 +1,3 @@ -import 'package:animu/utils/models.dart'; - String formatDuration(Duration duration) { bool hours = duration.inHours > 0; String _twoDigits(int n) { @@ -18,11 +16,36 @@ String formatDuration(Duration duration) { return hours ? '${duration.inHours.toString()}:$mmSs' : mmSs; } -enum ImageURLType { cover, thumbnail } -String getImageURL(ImageURLType type, {Anime anime, Episode episode}) { - if (type == ImageURLType.cover) { - return 'https://animeflv.net/uploads/animes/covers/${anime.id}.jpg'; - } else { - return 'https://cdn.animeflv.net/screenshots/${anime.id}/${episode.n}/th_3.jpg'; - } +String formatDay(DateTime date) { + final weekdays = { + DateTime.sunday: 'domingo', + DateTime.monday: 'lunes', + DateTime.tuesday: 'martes', + DateTime.wednesday: 'miércoles', + DateTime.thursday: 'jueves', + DateTime.friday: 'viernes', + DateTime.saturday: 'sábado', + }; + final months = { + DateTime.january: 'enero', + DateTime.february: 'febrero', + DateTime.march: 'marzo', + DateTime.april: 'abril', + DateTime.may: 'mayo', + DateTime.june: 'junio', + DateTime.july: 'julio', + DateTime.august: 'agosto', + DateTime.september: 'septiembre', + DateTime.october: 'octubre', + DateTime.november: 'noviembre', + DateTime.december: 'diciembre', + }; + + return weekdays[date.weekday] + + ' ' + + date.day.toString() + + ' de ' + + months[date.month] + + ' del ' + + date.year.toString(); } diff --git a/lib/utils/models.dart b/lib/utils/models.dart index d34e279..b55a1ee 100644 --- a/lib/utils/models.dart +++ b/lib/utils/models.dart @@ -11,11 +11,11 @@ class Anime { @HiveField(0) final int id; @HiveField(1) - final String name; - @HiveField(2) final String slug; + @HiveField(2) + String name; @HiveField(3) - final Uint8List cover; + Uint8List cover; @HiveField(4) bool favorite; @HiveField(5) @@ -25,8 +25,8 @@ class Anime { Anime({ this.id, - this.name, this.slug, + this.name, this.cover, this.favorite = false, this.watchingState, @@ -45,12 +45,14 @@ class Anime { : watchingStateString.entries .firstWhere((x) => x.value == map['watchingState']) .key, - episodesSeen: map['episodesSeen'] ?? [], + episodesSeen: map['episodesSeen'] == null + ? [] + : List.from(map['episodesSeen']), ); } Map toMap([bool limited = false]) { - return { + final Map map = { 'id': id, 'name': limited ? null : name, 'slug': slug, @@ -60,6 +62,16 @@ class Anime { watchingState == null ? null : watchingStateString[watchingState], 'episodesSeen': episodesSeen, }; + + if (!limited) { + return { + ...map, + 'name': name, + 'cover': cover == null ? null : base64Encode(cover), + }; + } else { + return map; + } } } From 15344c8777bd1b619be0cf76ac956b1fab12d463 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Mart=C3=ADn=20Seery?= Date: Thu, 23 Jan 2020 01:14:59 -0300 Subject: [PATCH 15/15] Bump-it! --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index fe9e381..8bca524 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,7 +11,7 @@ description: Ver anime sin complicaciones. # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 0.7.1 +version: 0.8.0 environment: sdk: ">=2.6.0 <3.0.0"