diff --git a/analysis_options.yaml b/analysis_options.yaml index 6a1c107..cc27b17 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -30,6 +30,7 @@ linter: # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule public_member_api_docs: false lines_longer_than_80_chars: false + avoid_function_literals_in_foreach_calls: false # Additional information about this file can be found at # https://dart.dev/guides/language/analysis-options diff --git a/assets/images/card_placeholder.webp b/assets/images/card_placeholder.webp index 44a3bb9..9899169 100644 Binary files a/assets/images/card_placeholder.webp and b/assets/images/card_placeholder.webp differ diff --git a/assets/json/open_source_licenses.json b/assets/json/open_source_licenses.json new file mode 100644 index 0000000..7bf4e32 --- /dev/null +++ b/assets/json/open_source_licenses.json @@ -0,0 +1,110 @@ +[ + { + "name": "cached_network_image", + "repos": "https://github.com/Baseflow/flutter_cached_network_image", + "license_path": "https://github.com/google/json_serializable.dart/blob/master/LICENSE", + "version": "^3.2.3" + }, + { + "name": "webview_flutter", + "repos": "https://github.com/flutter/packages/tree/main/packages/webview_flutter/webview_flutter", + "license_path": "https://github.com/flutter/packages/blob/main/packages/webview_flutter/webview_flutter/LICENSE", + "version": "^4.7.0" + }, + { + "name": "get", + "repos": "https://github.com/jonataslaw/getx", + "license_path": "https://github.com/jonataslaw/getx/blob/master/LICENSE", + "version": "^4.6.5" + }, + { + "name": "json_annotation", + "repos": "https://github.com/google/json_serializable.dart/tree/master/json_annotation", + "license_path": "https://github.com/google/json_serializable.dart/blob/master/json_annotation/LICENSE", + "version": "^4.8.1" + }, + { + "name": "intl", + "repos": "https://github.com/dart-lang/i18n/tree/main/pkgs/intl", + "license_path": "https://github.com/dart-lang/i18n/blob/main/pkgs/intl/LICENSE", + "version": "^0.19.0" + }, + { + "name": "flutter_svg", + "repos": "https://github.com/dnfield/flutter_svg", + "license_path": "https://github.com/dnfield/flutter_svg/blob/master/LICENSE", + "version": "^2.0.10+1" + }, + { + "name": "path", + "repos": "https://github.com/dart-lang/path", + "license_path": "https://github.com/dart-lang/path/blob/master/LICENSE", + "version": "^1.9.0" + }, + { + "name": "hive", + "repos": "https://github.com/isar/hive", + "license_path": "https://github.com/isar/hive/blob/main/LICENSE", + "version": "^2.2.3" + }, + { + "name": "hive_flutter", + "repos": "https://github.com/isar/hive", + "license_path": "https://github.com/isar/hive/blob/main/LICENSE", + "version": "^1.1.0" + }, + { + "name": "flutter_smart_dialog", + "repos": "https://github.com/fluttercandies/flutter_smart_dialog", + "license_path": "https://github.com/fluttercandies/flutter_smart_dialog/blob/master/LICENSE", + "version": "^4.9.6+1" + }, + { + "name": "animations", + "repos": "https://github.com/flutter/packages/tree/main/packages/animations", + "license_path": "https://github.com/flutter/packages/blob/main/packages/animations/LICENSE", + "version": "^2.0.11" + }, + { + "name": "build_runner", + "repos": "https://github.com/dart-lang/build/tree/master/build_runner", + "license_path": "https://github.com/dart-lang/build/blob/master/build_runner/LICENSE", + "version": "^2.4.6" + }, + { + "name": "json_serializable", + "repos": "https://github.com/google/json_serializable.dart", + "license_path": "https://github.com/google/json_serializable.dart/blob/master/LICENSE", + "version": "^6.7.1" + }, + { + "name": "flutter_lints", + "repos": "https://github.com/flutter/packages/tree/main/packages/flutter_lints", + "license_path": "https://github.com/flutter/packages/blob/main/packages/flutter_lints/LICENSE", + "version": "^3.0.2" + }, + { + "name": "flutter_gen_runner", + "repos": "https://github.com/FlutterGen/flutter_gen", + "license_path": "https://github.com/FlutterGen/flutter_gen/blob/main/LICENSE", + "version": "^5.3.1" + }, + { + "name": "icons_launcher", + "repos": "https://github.com/mrrhak/icons_launcher", + "license_path": "https://github.com/mrrhak/icons_launcher/blob/master/LICENSE", + "version": "^2.1.7" + }, + { + "name": "very_good_analysis", + "repos": "https://github.com/VeryGoodOpenSource/very_good_analysis", + "license_path": "https://github.com/VeryGoodOpenSource/very_good_analysis/blob/main/LICENSE", + "version": "^5.1.0" + }, + { + "name": "hive_generator", + "repos": "https://github.com/isar/hive", + "license_path": "https://github.com/isar/hive/blob/main/LICENSE", + "version": "^2.0.1" + } +] diff --git a/lib/components/MdCardItemView.dart b/lib/components/MdCardItemView.dart index 973d4a6..53cb81e 100644 --- a/lib/components/MdCardItemView.dart +++ b/lib/components/MdCardItemView.dart @@ -4,7 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; class MdCardItemView extends StatefulWidget { - const MdCardItemView({super.key, required this.mdCard, this.bottomWidget, this.showBanStatus = true, this.trend, this.onTap}); + const MdCardItemView({required this.mdCard, super.key, this.bottomWidget, this.showBanStatus = true, this.trend, this.onTap}); final MdCard mdCard; final Widget? bottomWidget; @@ -51,14 +51,15 @@ class _MdCardItemViewState extends State { ), if (widget.showBanStatus && mdCard.banStatus != null) Positioned( - child: ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(10)), - child: SvgPicture.asset( - 'assets/images/icon_${mdCard.banStatus!.toLowerCase()}.svg', - width: 20, - height: 20, + child: ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(10)), + child: SvgPicture.asset( + 'assets/images/icon_${mdCard.banStatus!.toLowerCase()}.svg', + width: 20, + height: 20, + ), ), - )), + ), if (widget.trend != null && widget.trend != '') Positioned( top: 0, diff --git a/lib/components/MdCardItemView2.dart b/lib/components/MdCardItemView2.dart index 1adce0f..4bd7124 100644 --- a/lib/components/MdCardItemView2.dart +++ b/lib/components/MdCardItemView2.dart @@ -1,13 +1,14 @@ -import 'dart:developer'; - import 'package:cached_network_image/cached_network_image.dart'; import 'package:duel_links_meta/extension/Future.dart'; import 'package:duel_links_meta/gen/assets.gen.dart'; import 'package:duel_links_meta/hive/MyHive.dart'; +import 'package:duel_links_meta/hive/db/CardHiveDb.dart'; import 'package:duel_links_meta/http/CardApi.dart'; +import 'package:duel_links_meta/store/BanCardStore.dart'; import 'package:duel_links_meta/type/MdCard.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:get/get.dart'; class MdCardItemView2 extends StatefulWidget { const MdCardItemView2({super.key, this.mdCard, required this.id, this.bottomWidget, this.showBanStatus = true, this.trend, this.onTap}); @@ -30,18 +31,22 @@ class _MdCardItemViewState extends State { MdCard? _mdCard; + BanCardStore banCardStore = Get.put(BanCardStore()); + + String? get banStatus => banCardStore.idToCardMap[cardId]?.banStatus; + // Future init() async { if (widget.mdCard != null && widget.mdCard?.type != '') return; - var card = await MyHive.box2.get('card:$cardId') as MdCard?; + var card = await CardHiveDb.get(cardId); if (card == null) { final (err, res) = await CardApi().getById(cardId).toCatch; - if (err != null || res!.isEmpty) return; + if (err != null || res == null) return; - card = res[0]; - MyHive.box2.put('card:$cardId', card); + card = res; + await CardHiveDb.setCard(card); } setState(() { @@ -67,7 +72,7 @@ class _MdCardItemViewState extends State { SizedBox( height: 8, child: (mdCard != null && mdCard?.rarity != '') - ? Image.asset('assets/images/rarity_${mdCard?.rarity.toLowerCase()}.webp') + ? Image.asset('assets/images/rarity_${mdCard!.rarity.toLowerCase()}.webp') : null, ), ], @@ -75,12 +80,12 @@ class _MdCardItemViewState extends State { Stack( children: [ CachedNetworkImage( - // placeholder: (context, url) => Image.asset('assets/images/card_placeholder.webp'), placeholder: (context, url) => Assets.images.cardPlaceholder.image(), errorWidget: (context, url, err) => Assets.images.cardPlaceholder.image(), - fadeInDuration: const Duration(milliseconds: 0), + fadeInDuration: Duration.zero, fadeOutDuration: null, imageUrl: 'https://s3.duellinksmeta.com/cards/${cardId}_w100.webp'), + if (widget.bottomWidget != null) Positioned( bottom: 0, @@ -88,17 +93,19 @@ class _MdCardItemViewState extends State { right: 0, child: widget.bottomWidget!, ), - if (widget.showBanStatus && mdCard?.banStatus != null) + + if (banStatus != null) Positioned( child: ClipRRect( borderRadius: const BorderRadius.all(Radius.circular(10)), child: SvgPicture.asset( - 'assets/images/icon_${mdCard?.banStatus?.toLowerCase()}.svg', + 'assets/images/icon_${banStatus!.toLowerCase()}.svg', width: 20, height: 20, ), ), ), + if (widget.trend != null && widget.trend != '') Positioned( top: 0, diff --git a/lib/components/SkillModalView.dart b/lib/components/SkillModalView.dart index a9d053a..05694a1 100644 --- a/lib/components/SkillModalView.dart +++ b/lib/components/SkillModalView.dart @@ -1,16 +1,17 @@ import 'dart:developer'; import 'package:cached_network_image/cached_network_image.dart'; -import 'package:duel_links_meta/constant/colors.dart'; +import 'package:duel_links_meta/extension/Future.dart'; +import 'package:duel_links_meta/gen/assets.gen.dart'; +import 'package:duel_links_meta/hive/db/SkillHiveDb.dart'; import 'package:duel_links_meta/http/SkillApi.dart'; import 'package:duel_links_meta/type/enum/PageStatus.dart'; import 'package:duel_links_meta/type/skill/Skill.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; -import '../gen/assets.gen.dart'; - class SkillModalView extends StatefulWidget { - const SkillModalView({super.key, required this.name, this.skill}); + const SkillModalView({required this.name, super.key, this.skill}); final String name; final Skill? skill; @@ -20,77 +21,110 @@ class SkillModalView extends StatefulWidget { } class _SkillModalViewState extends State { - String get name => widget.name; + String get _name => widget.name; Skill _skill = Skill(); var _pageStatus = PageStatus.loading; - fetchData() async { - if (widget.skill != null) { - _skill = widget.skill!; - _pageStatus = PageStatus.success; - return; - } + // + Future fetchData({bool force = false}) async { + // await Future.delayed(Duration(seconds: 1)); + // return false; + var skill = await SkillHiveDb.get(_name); + final expireTime = await SkillHiveDb.getExpireTime(_name); + Exception? err; + var shouldRefresh = false; - // var _name = 'The Legend of the Heroes'; - // var _name = 'Monster Move'; - var res = await SkillApi().getByName(name); - log('res ${res.body}, status: ${res.status.code}'); + if (skill == null || force) { + (err, skill) = await SkillApi().getByName(_name).toCatch; + if (err != null || skill == null) { + setState(() { + _pageStatus = PageStatus.fail; + }); + return false; + } - var skill = res.body!; + SkillHiveDb.set(skill).ignore(); + SkillHiveDb.setExpireTime(skill.name, DateTime.now().add(const Duration(days: 1))).ignore(); + } else { + shouldRefresh = expireTime == null || expireTime.isBefore(DateTime.now()); + } setState(() { - _skill = skill; + _skill = skill!; _pageStatus = PageStatus.success; }); + + return shouldRefresh; + } + + Future init() async { + if (widget.skill != null) { + setState(() { + _skill = widget.skill!; + _pageStatus = PageStatus.success; + }); + return; + } + + final shouldRefresh = await fetchData(); + if (shouldRefresh) { + await fetchData(force: true); + } } @override void initState() { super.initState(); - fetchData(); + init(); } @override Widget build(BuildContext context) { - return Container( - child: Center( - child: ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(6)), - child: Container( - decoration: const BoxDecoration( - image: DecorationImage(image: AssetImage('assets/images/modal_bg.webp'), fit: BoxFit.fitWidth), - color: Colors.black, - ), - // constraints: BoxConstraints( - // minHeight: 200, maxHeight: 400 - // ), - padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 12), - child: Stack( - children: [ - if (_pageStatus == PageStatus.success) - Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - color: Colors.black12, - child: Column( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - _skill.name, - style: const TextStyle(color: Colors.white, fontSize: 24, fontWeight: FontWeight.w500), - ), - Text(_skill.description, style: const TextStyle(color: Colors.white, fontSize: 12)), - const SizedBox(height: 4), - if (_skill.relatedCards.isNotEmpty) - Container( - height: 60, - child: ListView.builder( + return Center( + child: ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(14)), + child: Container( + // constraints: const BoxConstraints(maxHeight: 400), + width: MediaQuery.of(context).size.width * 0.9, + decoration: BoxDecoration( + // color: Colors.white, + image: DecorationImage(image: Assets.images.modalBg.image().image, fit: BoxFit.fitWidth), + color: Theme.of(context).cardColor, + ), + padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 12), + child: Stack( + children: [ + Container( + constraints: const BoxConstraints(maxHeight: 300), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Center( + child: Text( + widget.name, + style: const TextStyle(fontSize: 24, fontWeight: FontWeight.w500), + ), + ), + ), + Expanded( + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(_skill.description, style: const TextStyle(fontSize: 12)), + const SizedBox(height: 4), + if (_skill.relatedCards.isNotEmpty) + SizedBox( + height: 60, + child: ListView.builder( scrollDirection: Axis.horizontal, itemCount: _skill.relatedCards.length, itemBuilder: (context, index) { @@ -105,77 +139,84 @@ class _SkillModalViewState extends State { imageUrl: 'https://s3.duellinksmeta.com/cards/${_skill.relatedCards[index].oid}_w100.webp', ), ), - const SizedBox(width: 4) - ], - ); - }), - ) - ], - ), - ), - const SizedBox(height: 5), - if (_skill.characters.isNotEmpty) - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text('Characters', style: TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.w500)), - Container( - height: 42, - child: Scrollbar( - child: ListView.builder( - padding: const EdgeInsets.all(0), - scrollDirection: Axis.horizontal, - itemCount: _skill.characters.length, - itemBuilder: (context, index) { - return Row( - children: [ - Container( - width: 36, - height: 42, - child: CachedNetworkImage( - fit: BoxFit.cover, - imageUrl: 'https://s3.duellinksmeta.com${_skill.characters[index].character.thumbnailImage}', - ), - ), const SizedBox(width: 4), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(_skill.characters[index].character.name, - style: const TextStyle(color: Colors.white, fontSize: 13, fontWeight: FontWeight.w500)), - Text(_skill.characters[index].how, style: const TextStyle(color: Colors.grey, fontSize: 12)), - ], - ), - const SizedBox(width: 8) ], ); - }), - ), + }, + ), + ) + ], + ), + const SizedBox(height: 5), + if (_skill.characters.isNotEmpty) + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Characters', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500)), + Container( + height: 42, + child: Scrollbar( + child: ListView.builder( + padding: EdgeInsets.zero, + scrollDirection: Axis.horizontal, + itemCount: _skill.characters.length, + itemBuilder: (context, index) { + return Row( + children: [ + Container( + width: 36, + height: 42, + child: CachedNetworkImage( + fit: BoxFit.cover, + imageUrl: + 'https://s3.duellinksmeta.com${_skill.characters[index].character.thumbnailImage}', + ), + ), + const SizedBox(width: 4), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(_skill.characters[index].character.name, + style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500)), + Text( + _skill.characters[index].how, + style: const TextStyle(fontSize: 12), + ), + ], + ), + const SizedBox(width: 4), + ], + ); + }, + ), + ), + ), + ], ), - ], - ), - if (_skill.source != '') - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text('Source', style: TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.w500)), - Text(_skill.source, style: const TextStyle(color: Colors.white, fontSize: 12)), - ], - ) - ], - ), - if (_pageStatus == PageStatus.loading) - const SizedBox( - height: 200, - child: Center( - child: CircularProgressIndicator( - strokeWidth: 3, - color: Colors.white, + if (_skill.source != '') + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Source', style: TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.w500)), + Text(_skill.source, style: const TextStyle(color: Colors.white, fontSize: 12)), + ], + ) + ], ), ), - ) - ], - )), + ), + ], + ), + ), + if (_pageStatus == PageStatus.loading) + const Center( + child: CircularProgressIndicator( + strokeWidth: 3, + color: Colors.white, + ), + ), + ], + ), ), ), ); diff --git a/lib/constant/exception/HttpCacheException.dart b/lib/constant/exception/HttpCacheException.dart new file mode 100644 index 0000000..04543c1 --- /dev/null +++ b/lib/constant/exception/HttpCacheException.dart @@ -0,0 +1,6 @@ +import 'dart:io'; + +class HttpCacheException extends HttpException { + HttpCacheException(super.message); + +} \ No newline at end of file diff --git a/lib/db/Table_NavTab.dart b/lib/db/Table_NavTab.dart deleted file mode 100644 index e0c8ef8..0000000 --- a/lib/db/Table_NavTab.dart +++ /dev/null @@ -1,31 +0,0 @@ - -import 'package:duel_links_meta/db/index.dart'; -import 'package:sqflite/sqflite.dart'; - -class Table_NavTab { - - static Table_NavTab? _instance; - - static Table_NavTab _getInstance() { - - _instance ??= Table_NavTab(); - - return _instance!; - } - - static Table_NavTab get instance => _getInstance(); - - var table = 'nav_tab'; - - Future insert(Map data) { - return Db.db.insert(table, data, conflictAlgorithm: ConflictAlgorithm.replace); - } - - Future delete() { - return Db.db.delete(table); - } - - Future>> query() { - return Db.db.query(table); - } -} diff --git a/lib/db/index.dart b/lib/db/index.dart deleted file mode 100644 index 5df63c2..0000000 --- a/lib/db/index.dart +++ /dev/null @@ -1,22 +0,0 @@ -import 'package:path/path.dart'; -import 'package:sqflite/sqflite.dart'; - -class Db { - static late Database db; - - static init() async { - final database = openDatabase( - join(await getDatabasesPath(), 'duel_links_meta.db'), - onCreate: (db, version) async{ - await db.execute('CREATE TABLE IF NOT EXISTS nav_tab(id INTEGER PRIMARY KEY, _id TEXT, image TEXT, title TEXT)'); - }, - version: 2, - ); - - db = await database; - } - - static deleteDatabase() { - databaseFactory.deleteDatabase('duel_links_meta.db'); - } -} diff --git a/lib/extension/Future.dart b/lib/extension/Future.dart index 3c874a1..aff2311 100644 --- a/lib/extension/Future.dart +++ b/lib/extension/Future.dart @@ -19,13 +19,9 @@ extension FutureEx on Future> { throw HttpException('status: ${res.statusCode}'); } - if (res.body == null) { - throw const HttpException('response body is null'); - } - return (null, res.body); } catch (err) { - log('[toCatch] err: $err'); + log('[toCatch] err: $err, ${err.runtimeType}'); // toast error err.toString().toast(); diff --git a/lib/hive/MyHive.dart b/lib/hive/MyHive.dart index 370d159..dcd9af2 100644 --- a/lib/hive/MyHive.dart +++ b/lib/hive/MyHive.dart @@ -3,10 +3,10 @@ import 'package:duel_links_meta/type/NavTab.dart'; import 'package:duel_links_meta/type/ban_list_change/BanListChange.dart'; import 'package:duel_links_meta/type/ban_list_change/BanStatusCard.dart'; import 'package:duel_links_meta/type/deck_type/DeckType.dart'; -import 'package:duel_links_meta/type/deck_type/TierList_PowerRanking_Expire.dart'; import 'package:duel_links_meta/type/deck_type/TierList_PowerRanking.dart'; -import 'package:duel_links_meta/type/pack_set/ExpireData.dart'; +import 'package:duel_links_meta/type/deck_type/TierList_PowerRanking_Expire.dart'; import 'package:duel_links_meta/type/pack_set/PackSet.dart'; +import 'package:duel_links_meta/type/skill/Skill.dart'; import 'package:duel_links_meta/type/tier_list_top_tier/TierList_TopTier.dart'; import 'package:duel_links_meta/type/tier_list_top_tier/TierList_TopTier_Expire.dart'; import 'package:duel_links_meta/type/top_deck/TopDeck.dart'; @@ -37,6 +37,10 @@ class MyHive { static const int top_deck_skill = 19; static const int top_deck_ranked_type = 20; static const int top_deck_tournament_type = 21; + static const int skill = 22; + static const int skill_related_card = 23; + static const int skill_related_character = 24; + static const int skill_related_character_character = 25; // static const int expire_data = 10; @@ -46,7 +50,7 @@ class MyHive { MyHive._(); - static init() async { + static Future init() async { await Hive.initFlutter(); Hive @@ -70,7 +74,11 @@ class MyHive { ..registerAdapter(TopDeckDeckTypeAdapter()) ..registerAdapter(TopDeckSkillAdapter()) ..registerAdapter(TopDeckRankedTypeAdapter()) - ..registerAdapter(TopDeckTournamentTypeAdapter()); + ..registerAdapter(TopDeckTournamentTypeAdapter()) + ..registerAdapter(SkillAdapter()) + ..registerAdapter(SkillRelatedCardAdapter()) + ..registerAdapter(SkillCharacterAdapter()) + ..registerAdapter(SkillCharacterCharacterAdapter()); // Hive.registerAdapter(TLoginFormAdapter()); box = await Hive.openBox(boxName); diff --git a/lib/hive/db/BanListChangeHiveDb.dart b/lib/hive/db/BanListChangeHiveDb.dart new file mode 100644 index 0000000..eda055d --- /dev/null +++ b/lib/hive/db/BanListChangeHiveDb.dart @@ -0,0 +1,39 @@ +import 'dart:developer'; + +import 'package:duel_links_meta/hive/MyHive.dart'; +import 'package:duel_links_meta/type/ban_list_change/BanListChange.dart'; + +class BanListChangeHiveDb { + static const String _key = 'ban_list_change:list'; + static const String _expireKey = 'ban_list_change:fetch_date'; + + static Future?> get() async { + List? data; + try { + final list = await MyHive.box2.get(_key) as List?; + data = list?.map((e) => e as BanListChange).toList(); + } catch (e) { + return null; + } + + return data; + } + + static Future getExpireTime() async { + DateTime? expireTime; + try { + expireTime = await MyHive.box2.get(_expireKey) as DateTime?; + } catch (e) { + return null; + } + return expireTime; + } + + static Future set(List data) { + return MyHive.box2.put(_key, data); + } + + static Future setExpireTime(DateTime time) { + return MyHive.box2.put(_expireKey, time); + } +} diff --git a/lib/hive/db/CardHiveDb.dart b/lib/hive/db/CardHiveDb.dart new file mode 100644 index 0000000..016431d --- /dev/null +++ b/lib/hive/db/CardHiveDb.dart @@ -0,0 +1,27 @@ +import 'package:duel_links_meta/hive/MyHive.dart'; +import 'package:duel_links_meta/type/MdCard.dart'; + +class CardHiveDb { + static String _getKey(String id) { + return 'card:$id'; + } + + static Future get(String id) async { + final key = _getKey(id); + + MdCard? card; + try { + card = await MyHive.box2.get(key) as MdCard?; + } catch (e) { + return null; + } + + return card; + } + + static Future setCard(MdCard card) async { + final key = _getKey(card.oid); + + return MyHive.box2.put(key, card); + } +} diff --git a/lib/hive/db/DarkModeHiveDb.dart b/lib/hive/db/DarkModeHiveDb.dart new file mode 100644 index 0000000..607aa68 --- /dev/null +++ b/lib/hive/db/DarkModeHiveDb.dart @@ -0,0 +1,25 @@ +import 'package:flutter/material.dart'; + +import '../MyHive.dart'; + +class DarkModeHiveDb { + static const String _key = 'dark_mode'; + + static void set() { + return MyHive.box2.put(_key, 'light').ignore(); + } + + static Future get() async { + final mode = await MyHive.box2.get('dark_mode'); + + if (mode == 'dark') { + return ThemeMode.dark; + } + + if (mode == 'system') { + return ThemeMode.system; + } + + return ThemeMode.light; + } +} diff --git a/lib/hive/db/DeckTypeDetailHiveDb.dart b/lib/hive/db/DeckTypeDetailHiveDb.dart new file mode 100644 index 0000000..6b5e368 --- /dev/null +++ b/lib/hive/db/DeckTypeDetailHiveDb.dart @@ -0,0 +1,42 @@ +import 'package:duel_links_meta/hive/MyHive.dart'; +import 'package:duel_links_meta/type/deck_type/DeckType.dart'; + +class DeckTypeDetailHiveDb { + static String _getKey(String deckTypeName) { + return 'deck_type:$deckTypeName'; + } + + static String _getExpireTimeKey(String deckTypeName) { + return 'deck_type_fetch_date:$deckTypeName'; + } + + static Future getDetail(String deckTypeName) async { + final key = _getKey(deckTypeName); + + DeckType? deckType; + try { + deckType = await MyHive.box2.get(key) as DeckType?; + } catch (e) { + return null; + } + + return deckType; + } + + static Future getDetailExpireDate(String deckTypeName) async { + final key = _getExpireTimeKey(deckTypeName); + final date = await MyHive.box2.get(key) as DateTime?; + + return date; + } + + static Future setDeckType(DeckType deckType) { + final key = _getKey(deckType.name); + return MyHive.box2.put(key, deckType); + } + + static Future setDeckTypeExpireDate(DeckType deckType, DateTime date) { + final key = _getExpireTimeKey(deckType.name); + return MyHive.box2.put(key, date); + } +} diff --git a/lib/hive/db/NavHiveDb.dart b/lib/hive/db/NavHiveDb.dart new file mode 100644 index 0000000..ad2a75b --- /dev/null +++ b/lib/hive/db/NavHiveDb.dart @@ -0,0 +1,43 @@ +import 'package:duel_links_meta/hive/MyHive.dart'; +import 'package:duel_links_meta/type/NavTab.dart'; + +class HomeHiveDb { + List? navTabList; + + static const _navTabKey = 'nav_tab:list'; + static const _navTabFetchDateKey = 'nav_tab:fetch_date'; + + static Future?> getNavTabList() async { + final hiveData = await MyHive.box2.get(_navTabKey) as List?; + + if (hiveData == null) return null; + + try { + return hiveData.cast(); + } catch (e) { + return null; + } + } + + static Future deleteNavTabList() { + return MyHive.box2.delete(_navTabKey); + } + + static Future deleteNavTabListExpireTime() { + return MyHive.box2.delete(_navTabFetchDateKey); + } + + static Future getNavTabListExpireTime() async { + final expireTime = await MyHive.box2.get(_navTabFetchDateKey) as DateTime?; + + return expireTime; + } + + static Future setNavTabList(List? data) { + return MyHive.box2.put(_navTabKey, data); + } + + static Future setNavTabListExpireTime(DateTime? time) { + return MyHive.box2.put(_navTabFetchDateKey, time); + } +} diff --git a/lib/hive/db/PackHiveDb.dart b/lib/hive/db/PackHiveDb.dart new file mode 100644 index 0000000..81dc601 --- /dev/null +++ b/lib/hive/db/PackHiveDb.dart @@ -0,0 +1,26 @@ +import 'package:duel_links_meta/hive/MyHive.dart'; + +class PackHiveDb { + static String _getKey(String packId) { + return 'pack_card_ids:$packId'; + } + + static Future?>? getIds(String packId) async { + final key = _getKey(packId); + + List? ids; + try { + ids = await MyHive.box2.get(key) as List?; + } catch (e) { + return null; + } + + return ids; + } + + static Future setIds(String packId, List ids) { + final key = _getKey(packId); + + return MyHive.box2.put(key, ids); + } +} diff --git a/lib/hive/db/PacksHiveDb.dart b/lib/hive/db/PacksHiveDb.dart new file mode 100644 index 0000000..5f8433e --- /dev/null +++ b/lib/hive/db/PacksHiveDb.dart @@ -0,0 +1,40 @@ +import 'dart:developer'; + +import 'package:duel_links_meta/hive/MyHive.dart'; +import 'package:duel_links_meta/type/pack_set/PackSet.dart'; + +class PackHiveDb { + static const String _key = 'pack_set:list'; + static const String _expireTimeKey = 'pack_set:list_last_fetch_date'; + + static Future set(List data) { + return MyHive.box2.put(_key, data); + } + + static Future?>? get() async { + List? list; + try { + final data = await MyHive.box2.get(_key) as List?; + list = data?.map((e) => e as PackSet).toList(); + } catch (e) { + log('转换失败 $e'); + } + + return list; + } + + static Future setExpireTime(DateTime date) { + return MyHive.box2.put(_expireTimeKey, date); + } + + static Future? getExpireTime() async { + DateTime? time; + try { + time = await MyHive.box2.get(_expireTimeKey) as DateTime?; + } catch (e) { + log('转换失败 $e'); + } + + return time; + } +} diff --git a/lib/hive/db/SkillHiveDb.dart b/lib/hive/db/SkillHiveDb.dart new file mode 100644 index 0000000..39bcfa4 --- /dev/null +++ b/lib/hive/db/SkillHiveDb.dart @@ -0,0 +1,53 @@ +import 'dart:developer'; + +import 'package:duel_links_meta/hive/MyHive.dart'; +import 'package:duel_links_meta/type/skill/Skill.dart'; +import 'package:get/get.dart'; + +class SkillHiveDb { + static String _getKey(String skillName) { + return 'skill:$skillName'; + } + + static String _getExpireTimeKey(String skillName) { + return 'skill_expire_time:$skillName'; + } + + static Future? get(String skillName) async { + final key = _getKey(skillName); + + Skill? skill; + try { + skill = await MyHive.box2.get(key) as Skill?; + } catch (e) { + log('转换失败 $e'); + return null; + } + + return skill; + } + + static Future? getExpireTime(String skillName) async { + final key = _getExpireTimeKey(skillName); + DateTime? time; + try { + time = await MyHive.box2.get(key) as DateTime?; + } catch (e) { + log('转换失败 $e'); + return null; + } + + return time; + } + + static Future set(Skill skill) { + final key = _getKey(skill.name); + return MyHive.box2.put(key, skill); + } + + static Future setExpireTime(String skillName, DateTime time) { + final key = _getExpireTimeKey(skillName); + + return MyHive.box2.put(key, time); + } +} diff --git a/lib/http/BanListChangeApi.dart b/lib/http/BanListChangeApi.dart index 5a8777a..c4a0a22 100644 --- a/lib/http/BanListChangeApi.dart +++ b/lib/http/BanListChangeApi.dart @@ -1,7 +1,11 @@ - import 'package:duel_links_meta/http/http.dart'; +import 'package:duel_links_meta/type/ban_list_change/BanListChange.dart'; import 'package:get/get_connect/http/src/response/response.dart'; class BanListChangeApi extends Net { - Future> list({Map? params}) => httpClient.get('/api/v1/banlist-changes', query: params); -} \ No newline at end of file + Future>> list({Map? params}) => httpClient.get( + '/api/v1/banlist-changes', + query: params, + decoder: (data) => (data as List).map(BanListChange.fromJson).toList(), + ); +} diff --git a/lib/http/CardApi.dart b/lib/http/CardApi.dart index 74d5b64..5a98047 100644 --- a/lib/http/CardApi.dart +++ b/lib/http/CardApi.dart @@ -3,11 +3,24 @@ import 'package:duel_links_meta/type/MdCard.dart'; import 'package:get/get.dart'; class CardApi extends Net { - Future>> getById(String ids) => httpClient.get('/api/v1/cards?_id[\$in]=$ids', + Future>> getByIds(String ids) => httpClient.get( + '/api/v1/cards?_id[\$in]=$ids', + decoder: (data) => (data as List).map(MdCard.fromJson).toList(), + ); - decoder: (data) => (data as List).map(MdCard.fromJson).toList()); + Future> getById(String id) => httpClient.get( + '/api/v1/cards?_id[\$in]=$id&limit=1', + decoder: MdCard.fromJson, + ); - Future> getObtainSourceId(String sourceId) => httpClient.get('/api/v1/cards?obtain.source=$sourceId&sort=-rarity&limit=0'); + Future>> getByObtainSource(String sourceId) => httpClient.get( + '/api/v1/cards?obtain.source=$sourceId&sort=-rarity&limit=0', + decoder: (data) => (data as List).map(MdCard.fromJson).toList(), + ); - Future> list(Map? params) => httpClient.get('/api/v1/cards', query: params); + Future>> list(Map? params) => httpClient.get( + '/api/v1/cards', + query: params, + decoder: (data) => (data as List).map(MdCard.fromJson).toList(), + ); } diff --git a/lib/http/NavTabApi.dart b/lib/http/NavTabApi.dart index dfd49ac..e8deddd 100644 --- a/lib/http/NavTabApi.dart +++ b/lib/http/NavTabApi.dart @@ -1,8 +1,18 @@ import 'package:duel_links_meta/http/http.dart'; +import 'package:duel_links_meta/type/NavTab.dart'; import 'package:get/get.dart'; class NavTabApi extends Net { - Future> list() => httpClient.get('/api/v1/nav-tabs'); + factory NavTabApi() { + return _instance; + } - // Future getTierListChangesLatestDate() => httpClient.get('/api/v1/tierlist-changes?fields=-_id,date&sort=-date&limit=1'); + NavTabApi._privateConstructor(); + + static final NavTabApi _instance = NavTabApi._privateConstructor(); + + Future>> list() => httpClient.get( + '/api/v1/nav-tabs', + decoder: (data) => (data as List).map(NavTab.fromJson).toList(), + ); } diff --git a/lib/http/PackSetApi.dart b/lib/http/PackSetApi.dart index 41eb14f..b77a4ed 100644 --- a/lib/http/PackSetApi.dart +++ b/lib/http/PackSetApi.dart @@ -1,6 +1,10 @@ import 'package:duel_links_meta/http/http.dart'; +import 'package:duel_links_meta/type/pack_set/PackSet.dart'; import 'package:get/get.dart'; class PackSetApi extends Net { - Future> list() => httpClient.get('/api/v1/sets?sort=-release&limit=0'); -} \ No newline at end of file + Future>> list() => httpClient.get( + '/api/v1/sets?sort=-release&limit=0', + decoder: (data) => (data as List).map(PackSet.fromJson).toList(), + ); +} diff --git a/lib/http/SkillApi.dart b/lib/http/SkillApi.dart index a337a67..b66fc18 100644 --- a/lib/http/SkillApi.dart +++ b/lib/http/SkillApi.dart @@ -3,10 +3,12 @@ import 'package:duel_links_meta/type/skill/Skill.dart'; import 'package:get/get_connect/http/src/response/response.dart'; class SkillApi extends Net { - // 获取技能列表 - Future> getByName(String name) => httpClient.get('/api/v1/skills?name[\$in]=$name&limit=1', decoder: Skill.fromJson); + Future> getByName(String name) => httpClient.get( + '/api/v1/skills?name[\$in]=$name&limit=1', + decoder: Skill.fromJson, + ); Future> getByCharacterId(String characterId) => httpClient.get('/api/v1/skills?characters.character[\$or]=$characterId&archive[\$or]=true&rush[\$ne]=true&sort=name'); -} \ No newline at end of file +} diff --git a/lib/http/http.dart b/lib/http/http.dart index 95f86cf..9facadf 100644 --- a/lib/http/http.dart +++ b/lib/http/http.dart @@ -1,121 +1,32 @@ -import 'dart:async'; -import 'dart:convert'; import 'dart:developer'; -import 'dart:io'; -import 'package:duel_links_meta/http/httpCache.dart'; import 'package:get/get.dart'; -import 'package:get/get_connect/http/src/request/request.dart'; class Net extends GetConnect { - Net() : super(timeout: const Duration(seconds: 30), userAgent: 'Sesame-Client') { + Net() + : super( + timeout: const Duration(seconds: 30), + userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0', + ) { var start = DateTime.now(); - httpClient.addRequestModifier((request) async { - log('[Net] 请求开始'); - _logRequest(request); + httpClient + ..addRequestModifier((request) async { + start = DateTime.now(); - await _setupHeader(request); + final query = Uri.splitQueryString(request.url.query); + log('[Net] 请求开始, query: $query, path1: ${request.url}'); - return request; - }); - - httpClient.addResponseModifier((request, response) { - log('[Net] 请求结束, take: ${DateTime.now().difference(start).inMilliseconds}ms'); - - // var key = "${request.method}:${request.url}:${request.url.queryParameters}"; - // - // if (response.statusCode == 200 && response.body != null) { - // HttpCache.set(key, response.body); - // } - return response; - }); + return request; + }) + ..addResponseModifier((request, response) { + log('[Net] 请求结束, take: ${DateTime.now().difference(start).inMilliseconds}ms, code: ${response.statusCode}, codeText: ${response.statusText}'); + return response; + }); } @override - // String get baseUrl => currentEnvironment.host; String get baseUrl => 'https://www.duellinksmeta.com'; - - // @override void onInit() { - // // TODO: implement onInit - // // super.onInit(); - // httpClient.defaultDecoder = (data) { - // log("---- 响应 ----\n$data"); - // - // try { - // // return AppResponse.fromJson(data); - // return AppResponse(code: 0, msg: "ok", data: null); - // } catch (error) { - // print("请求出错 $error"); - // - // return AppResponse(code: 500, msg: error.toString(), data: null); - // // return NetResponse(NetCode.serverError, null)..message = '服务端未知错误'; - // } - // }; - // } - // @override - // Decoder get defaultDecoder => (data) { - // log("---- 响应 ----\n$data"); - // try { - // // return AppResponse.fromJson(data); - // return AppResponse(code: 0, msg: "ok", data: null); - // } catch (error) { - // print("请求出错 $error"); - // - // return AppResponse(code: 500, msg: error.toString(), data: null); - // // return NetResponse(NetCode.serverError, null)..message = '服务端未知错误'; - // } - // }; - - // String get _platform { - // if (Platform.isIOS) { - // return 'iOS'; - // } else if (Platform.isAndroid) { - // return 'android'; - // } else { - // return 'web'; - // } - // } - - Future _setupHeader(Request request) async { - print("[Net] _setupHeader"); - // request.headers['Sesame-Platform'] = _platform; - // final token = await StoreToken.getToken(); - - // var authStore = Get.put(AuthStoreController()); - // // var token =; - // - // if (authStore.authToken != null) { - // request.headers['Authorization'] = authStore.authToken!; - // } - } - - void _logRequest(Request request) async { - var str = "---- 请求 ----\nmethod: ${request.method}\nurl: ${request.url}\nquery: ${request.url.queryParameters}"; - - if (request.method != 'post' || request.headers['content-type'] != 'application/json') { - log( - """ - method: ${request.method} - url: ${request.url} - """ - .trim(), - zone: Zone.current, - time: DateTime.now()); - return; - } - - const decoder = Utf8Decoder(); - - List> bodyBytes = []; - - request.bodyBytes.asBroadcastStream(onListen: (subscribe) { - subscribe.onData((data) => bodyBytes.add(data)); - - subscribe.onDone(() { - str += '\nbody: ${(bodyBytes.map((e) => decoder.convert(e)).join())}'; - // log('[Net] done: $str'); - }); - }); - } } + +Net http = Net(); diff --git a/lib/http/httpCache.dart b/lib/http/httpCache.dart deleted file mode 100644 index 375c81f..0000000 --- a/lib/http/httpCache.dart +++ /dev/null @@ -1,13 +0,0 @@ -class HttpCache { - - static Map store = {}; - - static set(String key, dynamic value) { - - store[key] = value; - } - - static get(String key) { - return store[key]; - } -} \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index cacff5d..0e0f98c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,7 +1,7 @@ import 'dart:developer'; -import 'package:duel_links_meta/db/index.dart'; -import 'package:duel_links_meta/pages/ban_list_change/index.dart'; +import 'package:duel_links_meta/hive/MyHive.dart'; +import 'package:duel_links_meta/pages/open_source_licenses/index.dart'; import 'package:duel_links_meta/pages/splash/index.dart'; import 'package:duel_links_meta/widget/toast.dart'; import 'package:flutter/material.dart'; @@ -9,24 +9,23 @@ import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; import 'package:intl/date_symbol_data_local.dart'; -import 'hive/MyHive.dart'; - void main() async { + final onError = FlutterError.onError; + + FlutterError.onError = (FlutterErrorDetails err) { + // FlutterError.dumpErrorToConsole(err); + onError?.call(err); + // TODO + log('main catch exception: ${err.exception}', level: 4); + }; + await MyHive.init(); // Avoid errors caused by flutter upgrade. // Importing 'package:flutter/widgets.dart' is required. WidgetsFlutterBinding.ensureInitialized(); - initializeDateFormatting(); - await Db.init(); - var onError = FlutterError.onError; - - FlutterError.onError = (FlutterErrorDetails err) { - // FlutterError.dumpErrorToConsole(err); - onError?.call(err); - log('捕获异常: ${err.exception}', level: 4); - }; + await initializeDateFormatting(); runApp(const MyApp()); } @@ -39,28 +38,43 @@ class MyApp extends StatelessWidget { return GetMaterialApp( title: 'Duel Links Meta', themeMode: ThemeMode.light, - scrollBehavior: CustomScrollBehavior(), // themeMode: ThemeMode.dark, - darkTheme: ThemeData( - colorScheme: ColorScheme.dark( + darkTheme: ThemeData.dark(useMaterial3: true).copyWith( + colorScheme: const ColorScheme.dark( // seedColor: BaColors.main, // onPrimary: Colors.yellow, // onSecondary: Colors.tealAccent, // background: Colors.yellow, - primary: Colors.pinkAccent, - secondary: Colors.white, + primary: Colors.pink, // tab bar indicator / navigation bar indicator + secondary: Colors.pinkAccent, // tertiary: Colors.orange, // brightness: Brightness.dark, // background: BaColors.main ), - navigationBarTheme: NavigationBarTheme.of(context).copyWith(backgroundColor: Colors.white), + navigationBarTheme: NavigationBarTheme.of(context).copyWith( + backgroundColor: Colors.black, + // iconTheme: const MaterialStatePropertyAll( + // IconThemeData( + // size: 20, + // ), + // ), + iconTheme: MaterialStateProperty.resolveWith((Set states) { + // 这里可以根据不同的状态返回不同的 IconThemeData + // 如果没有特别的状态要求,可以返回一个默认的 IconThemeData + // 例如,在所有状态下使用相同的图标颜色和大小 + return const IconThemeData( + // color: Colors.white, // 设置为白色,以便在黑色背景上清晰可见 + // size: 20, // 设置图标大小 + ); + }), + ), primaryColor: Colors.deepOrangeAccent, - primarySwatch: Colors.yellow, + // primarySwatch: Colors.yellow, // scaffoldBackgroundColor: BaColors.theme, // textTheme: TextTheme( // ), - useMaterial3: true, + // useMaterial3: true, // tabBarTheme: const TabBarTheme(labelColor: BaColors.theme, indicatorColor: BaColors.theme), appBarTheme: const AppBarTheme( @@ -72,27 +86,37 @@ class MyApp extends StatelessWidget { ), ), - theme: ThemeData( - colorScheme: ColorScheme.light( + theme: ThemeData.light(useMaterial3: true).copyWith( + colorScheme: const ColorScheme.light( // seedColor: Colors.pink, primary: Colors.blue, secondary: Colors.blueAccent, tertiary: Colors.deepOrange, inversePrimary: Colors.deepPurple, - onSecondary: Colors.green, - onSurface: Colors.black87, + // onSecondary: Colors.green, + onSurface: Colors.black54, // tertiary: Colors.orange, // brightness: Brightness.dark, ), - navigationBarTheme: NavigationBarTheme.of(context).copyWith(backgroundColor: Colors.white), - - primaryColor: Colors.deepOrangeAccent, + navigationBarTheme: NavigationBarTheme.of(context).copyWith( + backgroundColor: Colors.white, + surfaceTintColor: Colors.transparent, + iconTheme: MaterialStateProperty.resolveWith((Set states) { + // 这里可以根据不同的状态返回不同的 IconThemeData + // 如果没有特别的状态要求,可以返回一个默认的 IconThemeData + // 例如,在所有状态下使用相同的图标颜色和大小 + return IconThemeData( + color: states.contains(MaterialState.selected) ? Colors.white : Colors.black54, // 设置为白色,以便在黑色背景上清晰可见 + // size: 20, // 设置图标大小 + ); + }), + ), + primaryColor: Colors.pink, // primaryColorDark: Colors.blue, // scaffoldBackgroundColor: Colors.blueAccent, // textTheme: TextTheme( // ), - useMaterial3: true, - + // useMaterial3: true, // tabBarTheme: const TabBarTheme(labelColor: BaColors.theme, indicatorColor: BaColors.theme), appBarTheme: const AppBarTheme( elevation: 2, @@ -106,21 +130,7 @@ class MyApp extends StatelessWidget { toastBuilder: (msg) => ToastWidget(msg: msg), ), home: const SplashPage(), - // initialRoute: '/splash', - // routes: { - // // '/splash': (context) => const SplashPage(), - // // '/splash': (context) => const DeckTypeDetailPage(), - // // '/splash': (context) => const SkillStatsPage(name: 'The Legend of the Heroes'), - // // '/splash': (context) => const CharactersPage(), - // // '/splash': (context) => const MainPage(), - // '/splash': (context) => const BanListChangePage(), - // // '/splash': (context) => const SkillStatsPage(name: 'Monster Move'), - // // '/splash': (context) => const SkillModalView(name: 'Photon Dragon Advent'), - // // '/splash': (context) => const CardsViewpagerPage(mdCards: [], index: 0,), - // // '/home': (context) => const HomePage() // TODO: routes声明与不声明有什么区别 - // }, + // home: const OpenSourceLicensePage(), ); } } - -class CustomScrollBehavior extends ScrollBehavior {} diff --git a/lib/pages/about/index.dart b/lib/pages/about/index.dart new file mode 100644 index 0000000..a2406c6 --- /dev/null +++ b/lib/pages/about/index.dart @@ -0,0 +1,28 @@ +import 'package:flutter/material.dart'; + +class AboutPage extends StatefulWidget { + const AboutPage({super.key}); + + @override + State createState() => _AboutPageState(); +} + +class _AboutPageState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text('About'), + ), + body: SingleChildScrollView( + padding: EdgeInsets.all(12), + physics: AlwaysScrollableScrollPhysics(), + child: Column( + children: [ + Text('关于此项目') + ], + ), + ), + ); + } +} diff --git a/lib/pages/articles/components/ArticleItem.dart b/lib/pages/articles/components/ArticleItem.dart index bf8412a..db2f7d3 100644 --- a/lib/pages/articles/components/ArticleItem.dart +++ b/lib/pages/articles/components/ArticleItem.dart @@ -18,18 +18,19 @@ class ArticleItem extends StatefulWidget { class _ArticleItemState extends State { Article get article => super.widget.article; - String get coverUrl => "https://wsrv.nl/?url=https://s3.duellinksmeta.com${article.image}&w=520&output=webp&we&n=-1&maxage=7d"; + String get coverUrl => 'https://wsrv.nl/?url=https://s3.duellinksmeta.com${article.image}&w=520&output=webp&we&n=-1&maxage=7d'; @override Widget build(BuildContext context) { return GestureDetector( - onTap: () => {widget.onTap?.call(widget.article)}, + onTap: () => widget.onTap?.call(widget.article), child: Stack( children: [ Container( padding: const EdgeInsets.only(left: 5, right: 5), child: ClipRRect( borderRadius: const BorderRadius.all(Radius.circular(6)), + // clipBehavior: Clip.hardEdge, child: Stack( children: [ SizedBox( @@ -37,8 +38,7 @@ class _ArticleItemState extends State { height: 120, child: CachedNetworkImage( fit: BoxFit.cover, - fadeInDuration: const Duration(milliseconds: 0), - placeholder: (context, url) => Container(color: articleCategoryColorMap[widget.article.category] ?? Colors.grey), + placeholder: (context, url) => Container(color: articleCategoryColorMap[widget.article.category]?.withOpacity(0.3) ?? Colors.grey), imageUrl: coverUrl, ), ), diff --git a/lib/pages/articles/index.dart b/lib/pages/articles/index.dart index 8de8ea9..a79dc3c 100644 --- a/lib/pages/articles/index.dart +++ b/lib/pages/articles/index.dart @@ -1,23 +1,15 @@ import 'dart:developer'; -import 'package:cached_network_image/cached_network_image.dart'; import 'package:duel_links_meta/components/ListFooter.dart'; import 'package:duel_links_meta/components/Loading.dart'; -import 'package:duel_links_meta/constant/colors.dart'; -import 'package:duel_links_meta/extension/Function.dart'; import 'package:duel_links_meta/extension/Future.dart'; import 'package:duel_links_meta/http/ArticleApi.dart'; import 'package:duel_links_meta/pages/articles/components/ArticleItem.dart'; import 'package:duel_links_meta/pages/webview/index.dart'; import 'package:duel_links_meta/type/Article.dart'; import 'package:duel_links_meta/type/enum/PageStatus.dart'; -import 'package:duel_links_meta/util/index.dart'; -import 'package:flutter/cupertino.dart'; +import 'package:duel_links_meta/type/listViewData.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; -import 'package:intl/intl.dart'; - -import '../../type/listViewData.dart'; class ArticlesPage extends StatefulWidget { const ArticlesPage({super.key}); @@ -27,34 +19,27 @@ class ArticlesPage extends StatefulWidget { } class _ArticlesPageState extends State with AutomaticKeepAliveClientMixin { - // List
_articles = []; final ScrollController _scrollController = ScrollController(); - // var _page = 1; - // final _size = 8; - // var _pageStatus = PageStatus.loading; - // var _loadMoreStatus = PageStatus.success; - // var _hasMore = true; - final _listViewData = ListViewData
(); List
get _articles => _listViewData.data; - handleTapArticleItem(Article article) { - var title = article.title; - var url = 'https://www.duellinksmeta.com/articles${article.url}'; + void handleTapArticleItem(Article article) { + final title = article.title; + final url = 'https://www.duellinksmeta.com/articles${article.url}'; - Navigator.push(context, MaterialPageRoute(builder: (context) => WebviewPage(title: title, url: url))); + Navigator.push(context, MaterialPageRoute(builder: (context) => WebviewPage(title: title, url: url))); } // - fetchData({bool isLoadMore = false}) async { - var params = {}; + Future fetchData({bool isLoadMore = false}) async { + final params = {}; params['limit'] = _listViewData.size.toString(); params['page'] = _listViewData.page.toString(); - var (err, res) = await ArticleApi().articleList(params).toCatch; + final (err, res) = await ArticleApi().articleList(params).toCatch; if (err != null) { if (isLoadMore) { setState(() { @@ -123,7 +108,7 @@ class _ArticlesPageState extends State with AutomaticKeepAliveClie return Scaffold( appBar: AppBar( - title: const Text("Articles"), + title: const Text('Articles'), ), body: Stack( children: [ diff --git a/lib/pages/articles/type/ArticleCategoryColorMap.dart b/lib/pages/articles/type/ArticleCategoryColorMap.dart index e5935eb..a353797 100644 --- a/lib/pages/articles/type/ArticleCategoryColorMap.dart +++ b/lib/pages/articles/type/ArticleCategoryColorMap.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; Map articleCategoryColorMap = { - 'set-release': const Color(0xffA72400), 'news': const Color(0xff3B98F0), 'memes': const Color(0xffd60bf8), diff --git a/lib/pages/ban_list_change/components/BanListChangeCardView.dart b/lib/pages/ban_list_change/components/BanListChangeCardView.dart index 58c4e83..82f7eea 100644 --- a/lib/pages/ban_list_change/components/BanListChangeCardView.dart +++ b/lib/pages/ban_list_change/components/BanListChangeCardView.dart @@ -1,12 +1,9 @@ import 'package:cached_network_image/cached_network_image.dart'; -import 'package:flutter/cupertino.dart'; +import 'package:duel_links_meta/type/ban_list_change/BanListChange.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; - -import '../../../type/ban_list_change/BanListChange.dart'; class BanListChangeCardView extends StatelessWidget { - const BanListChangeCardView({super.key, required this.change, required this.onTap}); + const BanListChangeCardView({required this.change, required this.onTap, super.key}); final BanListChange_Change change; @@ -18,7 +15,6 @@ class BanListChangeCardView extends StatelessWidget { onTap: onTap, child: Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - // height: 60, child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -30,35 +26,31 @@ class BanListChangeCardView extends StatelessWidget { ), const SizedBox(width: 8), Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - change.card?.name ?? '', - style: TextStyle( - overflow: TextOverflow.ellipsis, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + change.card?.name ?? '', + textAlign: TextAlign.left, + style: const TextStyle( + overflow: TextOverflow.ellipsis, + ), + ), + Row( + children: [ + const Text('From: ', style: TextStyle(fontSize: 11)), + Text(change.from ?? '—'), + ], ), - // maxLines: 1, - textAlign: TextAlign.left, - ), - Row( - children: [ - const Text('From: ', style: TextStyle(fontSize: 11)), - Text( - change.from ?? '—', - ) - ], - ), - Row( - children: [ - const Text('To: ', style: TextStyle(fontSize: 11)), - Text( - change.to ?? '—', - ) - ], - ), - ], - )) + Row( + children: [ + const Text('To: ', style: TextStyle(fontSize: 11)), + Text(change.to ?? '—'), + ], + ), + ], + ), + ), ], ), ), diff --git a/lib/pages/ban_list_change/components/BanListChangePickerView.dart b/lib/pages/ban_list_change/components/BanListChangePickerView.dart index 1d83559..2392593 100644 --- a/lib/pages/ban_list_change/components/BanListChangePickerView.dart +++ b/lib/pages/ban_list_change/components/BanListChangePickerView.dart @@ -1,28 +1,29 @@ import 'dart:async'; import 'dart:developer'; -import 'package:duel_links_meta/extension/Function.dart'; +import 'package:duel_links_meta/pages/ban_list_change/type/DataGroup.dart'; +import 'package:duel_links_meta/type/ban_list_change/BanListChange.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; -import '../../../type/ban_list_change/BanListChange.dart'; -import '../type/DataGroup.dart'; - - int _defaultYearIndex = 0; - int _defaultItemIndex = 0; +int _defaultYearIndex = 0; +int _defaultItemIndex = 0; class BanListChangePicker extends StatefulWidget { - const BanListChangePicker({super.key, required this.data, required this.onConfirm,}); + const BanListChangePicker({ + required this.data, + required this.onConfirm, + super.key, + }); final List> data; - final Function onConfirm; - + final void Function(int index1, int index2) onConfirm; @override State createState() => _BanListChangePickerState(); } -class _BanListChangePickerState extends State{ +class _BanListChangePickerState extends State { List get years => widget.data.map((e) => e.name).toList(); Timer? yearPickerTimer; @@ -50,7 +51,7 @@ class _BanListChangePickerState extends State{ borderRadius: const BorderRadius.only(topLeft: Radius.circular(18), topRight: Radius.circular(18)), child: Container( height: 240, - padding: const EdgeInsets.only(top: 8.0), + padding: const EdgeInsets.only(top: 8), color: Theme.of(context).colorScheme.onPrimary, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, @@ -70,13 +71,9 @@ class _BanListChangePickerState extends State{ // log('change, int: ${selectedItem}'); // }.debounce(1000), onSelectedItemChanged: (int selectedItem) { - log('change $int'); - if (yearPickerTimer?.isActive ?? false) yearPickerTimer!.cancel(); yearPickerTimer = Timer(const Duration(milliseconds: 500), () { - log('execute'); - setState(() { selectedYearKey = selectedItem; @@ -92,28 +89,26 @@ class _BanListChangePickerState extends State{ ), ), Expanded( - child: Container( - child: CupertinoPicker( - magnification: 1.22, - squeeze: 1.2, - useMagnifier: true, - itemExtent: 32, - scrollController: _controller, - onSelectedItemChanged: (int selectedItem) { - if (itemPickerTimer?.isActive ?? false) itemPickerTimer!.cancel(); - - itemPickerTimer = Timer(const Duration(milliseconds: 500), () { - setState(() { - selectedItemKey = selectedItem; - }); + child: CupertinoPicker( + magnification: 1.22, + squeeze: 1.2, + useMagnifier: true, + itemExtent: 32, + scrollController: _controller, + onSelectedItemChanged: (int selectedItem) { + if (itemPickerTimer?.isActive ?? false) itemPickerTimer!.cancel(); + + itemPickerTimer = Timer(const Duration(milliseconds: 500), () { + setState(() { + selectedItemKey = selectedItem; }); - }, - children: widget.data[selectedYearKey].items.map((item) { - return Center( - child: Text(item.formattedMonthDay), - ); - }).toList(), - ), + }); + }, + children: widget.data[selectedYearKey].items.map((item) { + return Center( + child: Text(item.formattedMonthDay), + ); + }).toList(), ), ), ], @@ -122,7 +117,7 @@ class _BanListChangePickerState extends State{ Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), child: ElevatedButton( - onPressed: () { + onPressed: () { log('confirm: $selectedYearKey, $selectedItemKey'); _defaultItemIndex = selectedItemKey; _defaultYearIndex = selectedYearKey; @@ -136,5 +131,4 @@ class _BanListChangePickerState extends State{ ), ); } - } diff --git a/lib/pages/ban_list_change/components/BanListChangeView.dart b/lib/pages/ban_list_change/components/BanListChangeView.dart index 1594924..b01fd76 100644 --- a/lib/pages/ban_list_change/components/BanListChangeView.dart +++ b/lib/pages/ban_list_change/components/BanListChangeView.dart @@ -1,10 +1,9 @@ import 'dart:developer'; -import 'package:duel_links_meta/components/Loading.dart'; import 'package:duel_links_meta/extension/Future.dart'; -import 'package:duel_links_meta/hive/MyHive.dart'; +import 'package:duel_links_meta/hive/db/BanListChangeHiveDb.dart'; +import 'package:duel_links_meta/hive/db/CardHiveDb.dart'; import 'package:duel_links_meta/http/BanListChangeApi.dart'; -import 'package:duel_links_meta/http/CardApi.dart'; import 'package:duel_links_meta/pages/ban_list_change/components/BanListChangeCardView.dart'; import 'package:duel_links_meta/pages/ban_list_change/components/BanListChangePickerView.dart'; import 'package:duel_links_meta/pages/ban_list_change/type/DataGroup.dart'; @@ -14,6 +13,7 @@ import 'package:duel_links_meta/type/ban_list_change/BanListChange.dart'; import 'package:duel_links_meta/type/enum/PageStatus.dart'; import 'package:duel_links_meta/util/time_util.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; import 'package:intl/intl.dart'; class BanListChangeView extends StatefulWidget { @@ -26,63 +26,53 @@ class BanListChangeView extends StatefulWidget { class _BanListChangeViewState extends State with AutomaticKeepAliveClientMixin { var _pageStatus = PageStatus.loading; + final _refreshIndicatorKey = GlobalKey(); + List> banListChangeGroup = []; BanListChange? currentBanListChange; // Future fetchBanListChanges({bool force = false}) async { - var banListChangeKey = 'ban_list_change:list'; - var banListChangeFetchDateKey = 'ban_list_change:fetch_date'; var reRefreshFlag = false; - List list = []; - - var hiveData = await MyHive.box2.get(banListChangeKey); - final lastFetchDate = await MyHive.box2.get(banListChangeFetchDateKey); + var list = await BanListChangeHiveDb.get(); + final expireTime = await BanListChangeHiveDb.getExpireTime(); - if (hiveData == null || force) { - final params = {r'rush[$ne]': 'true', 'sort': '-date,-announced', 'fields': '-linkedArticle'}; + if (list == null || force) { + if (list == null) { + log('本地没数据'); + } else { + log('强制刷新'); + } + final params = { + r'rush[$ne]': 'true', + 'sort': '-date,-announced', + 'fields': '-linkedArticle', + }; final (err, res) = await BanListChangeApi().list(params: params).toCatch; - if (err != null) { + if (err != null || res == null) { setState(() { _pageStatus = PageStatus.fail; }); - return true; + return false; } - list = res!.map(BanListChange.fromJson).toList(); - MyHive.box2.put(banListChangeKey, list); - MyHive.box2.put(banListChangeFetchDateKey, DateTime.now()); - log('本地保存数据'); + list = res; + BanListChangeHiveDb.set(list).ignore(); + BanListChangeHiveDb.setExpireTime(DateTime.now().add(const Duration(days: 1))).ignore(); + log('本地保存数据'); } else { log('本地获取到数据'); - - try { - - list = (hiveData as List).map((e) => e as BanListChange).toList(); - if (lastFetchDate != null) { - // 超过刷新时间 - if ((lastFetchDate as DateTime).add(const Duration(seconds: 10)).isBefore(DateTime.now())) { - log('超过默认的数据有效时间,需要刷新'); - - reRefreshFlag = true; - } - } - log('转换成功'); - }catch (e) { - await MyHive.box2.delete(banListChangeKey); - await MyHive.box2.delete(banListChangeFetchDateKey); - return true; + reRefreshFlag = expireTime == null || expireTime.isBefore(DateTime.now()); + if (reRefreshFlag) { + log('本地数据已过期'); } - } - final cardIds = list.map((e) => e.changes.map((e) => e.card!.oid).toList()).expand((element) => element).toSet().toList(); - final formatter = DateFormat('MM-dd'); final dataGroupList = >[]; @@ -110,65 +100,24 @@ class _BanListChangeViewState extends State with AutomaticKee _pageStatus = PageStatus.success; }); - // fetchCards(cardIds); - return reRefreshFlag; } - // Future fetchCards(List cardIds) async { - // final cards = []; - // - // var size = 0; - // while (size < cardIds.length) { - // final ids = cardIds.sublist(size, size + 100 > cardIds.length ? cardIds.length : size + 100); - // size += 100; - // - // final (cardsErr, cardsRes) = await CardApi().getById(ids.join(',')).toCatch; - // if (cardsErr != null) { - // return; - // } - // cards.addAll(cardsRes!.map(MdCard.fromJson).toList()); - // - // log('cards: ${cards.length}'); - // } // - // final cardId2CardMap = {}; - // - // cards.forEach((card) { - // cardId2CardMap[card.oid] = card; - // }); - // log('cardId2CardMap: ${cardId2CardMap.length}'); - // - // banListChangeGroup.forEach((group) { - // group.items.forEach((item) { - // item.changes.forEach((change) { - // if (cardId2CardMap[change.card?.oid] != null) { - // change.card2 = cardId2CardMap[change.card?.oid]!; - // } - // }); - // }); - // }); - // } - - void handleTapBanListCard(int index) async{ - // final cards = currentBanListChange!.changes.map((e) => e.).toList(); - List cards = []; - - for (var i =0; i handleTapBanListCard(int index) async { + final cards = []; + + for (var i = 0; i < currentBanListChange!.changes.length; i++) { + final item = currentBanListChange!.changes[i]; + var card = await CardHiveDb.get(item.card!.oid); card ??= MdCard() ..oid = item.card!.oid ..name = item.card!.name; - log('本地获取到card'); - // return card as MdCard; cards.add(card); } - // var cards = currentBanListChange!.changes.map((e) async{ - // - // }).toList(); + // TODO await showDialog( context: context, builder: (context) => Dialog.fullscreen( @@ -188,32 +137,57 @@ class _BanListChangeViewState extends State with AutomaticKee Navigator.pop(context); } + // void showUpdatesDatePicker() { showModalBottomSheet( context: context, - backgroundColor: Colors.black12, - builder: (context) => BanListChangePicker(data: banListChangeGroup, onConfirm: handlePickerConfirm), + // backgroundColor: Colors.black12, + builder: (context) => BanListChangePicker( + data: banListChangeGroup, + onConfirm: handlePickerConfirm, + ), ); } + bool isInit = false; + + Future _handleRefresh() async { + final shouldRefresh = await fetchBanListChanges(); + isInit = true; + if (shouldRefresh) { + await fetchBanListChanges(force: true); + } + log('_handleRefresh'); + } + @override void initState() { super.initState(); - fetchBanListChanges(); + + SchedulerBinding.instance.addPostFrameCallback((_) { + _refreshIndicatorKey.currentState?.show(); + }); } + @override + bool get wantKeepAlive => true; + @override Widget build(BuildContext context) { - return Stack( - children: [ - AnimatedOpacity( - opacity: _pageStatus == PageStatus.success ? 1 : 0, - duration: const Duration(milliseconds: 400), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - const SizedBox(height: 10), - Padding( + super.build(context); + return RefreshIndicator( + key: _refreshIndicatorKey, + onRefresh: _handleRefresh, + child: Stack( + children: [ + AnimatedOpacity( + opacity: _pageStatus == PageStatus.success ? 1 : 0, + duration: const Duration(milliseconds: 300), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const SizedBox(height: 10), + Padding( padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 8), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -229,32 +203,25 @@ class _BanListChangeViewState extends State with AutomaticKee ), ) ], - )), - const SizedBox(height: 8), - Expanded( - child: ListView.builder( - itemCount: currentBanListChange?.changes.length ?? 0, - itemBuilder: (context, index) { - return BanListChangeCardView( - change: currentBanListChange!.changes[index], - onTap: () => handleTapBanListCard(index), - ); - }, + ), ), - ), - ], - ), - ), - if (_pageStatus == PageStatus.loading) - const Positioned.fill( - child: Center( - child: Loading(), + const SizedBox(height: 8), + Expanded( + child: ListView.builder( + itemCount: currentBanListChange?.changes.length ?? 0, + itemBuilder: (context, index) { + return BanListChangeCardView( + change: currentBanListChange!.changes[index], + onTap: () => handleTapBanListCard(index), + ); + }, + ), + ), + ], ), - ) - ], + ), + ], + ), ); } - - @override - bool get wantKeepAlive => true; } diff --git a/lib/pages/ban_list_change/components/BanStatusCardView.dart b/lib/pages/ban_list_change/components/BanStatusCardView.dart index 92d7214..3342a42 100644 --- a/lib/pages/ban_list_change/components/BanStatusCardView.dart +++ b/lib/pages/ban_list_change/components/BanStatusCardView.dart @@ -1,17 +1,14 @@ -import 'dart:developer'; - import 'package:duel_links_meta/components/Loading.dart'; -import 'package:duel_links_meta/components/MdCardItemView.dart'; import 'package:duel_links_meta/components/MdCardItemView2.dart'; -import 'package:duel_links_meta/extension/Future.dart'; -import 'package:duel_links_meta/hive/MyHive.dart'; -import 'package:duel_links_meta/http/CardApi.dart'; import 'package:duel_links_meta/pages/cards_viewpager/index.dart'; +import 'package:duel_links_meta/pages/top_decks/type/Group.dart'; +import 'package:duel_links_meta/store/BanCardStore.dart'; import 'package:duel_links_meta/type/MdCard.dart'; -import 'package:duel_links_meta/type/ban_list_change/BanStatusCard.dart'; import 'package:duel_links_meta/type/enum/PageStatus.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:get/get.dart'; + class BanStatusCardView extends StatefulWidget { const BanStatusCardView({super.key}); @@ -21,10 +18,25 @@ class BanStatusCardView extends StatefulWidget { } class _BanStatusCardViewState extends State with AutomaticKeepAliveClientMixin { - Map> banStatus2CardsGroup = {}; - var _pageStatus = PageStatus.loading; + BanCardStore banCardsStore = Get.put(BanCardStore()); + + List> get groups1 => [ + Group(key: 'Forbidden', data: banCardsStore.group.value['Forbidden'] ?? []), + Group(key: 'Limited 1', data: banCardsStore.group.value['Limited 1'] ?? []), + Group(key: 'Limited 2', data: banCardsStore.group.value['Limited 2'] ?? []), + Group(key: 'Limited 3', data: banCardsStore.group.value['Limited 3'] ?? []) + ]; + + // List> get groups2 => [ + // Group(key: 'Limited 2', data: banCardsStore.group.value['Limited 2'] ?? []), + // Group(key: 'Limited 3', data: banCardsStore.group.value['Limited 3'] ?? []) + // ]; + var _initFlag = false; + + // void handleTapCardItem(List cards, int index) { + // TODO showDialog( context: context, builder: (context) => Dialog.fullscreen( @@ -34,88 +46,17 @@ class _BanStatusCardViewState extends State with AutomaticKee ); } - // - Future fetchCards({bool force = false}) async { - var banStatusCardIdsKey = 'ban_status:card_ids'; - var banStatusCardIdsFetchDateKey = 'ban_status:card_ids'; - - var cardIds = await MyHive.box2.get(banStatusCardIdsKey) as List?; - - var refreshFlag = false; - - var list = []; - - if (cardIds == null) { - final params = { - 'limit': '0', - r'banStatus[$exists]': 'true', - r'alternateArt[$ne]': 'true', - r'rush[$ne]': 'true', - // 'fields': 'oid,banStatus' - }; - - final (err, res) = await CardApi().list(params).toCatch; - if (err != null) { - setState(() { - _pageStatus = PageStatus.fail; - }); - return false; - } - list = res!.map(MdCard.fromJson).toList(); - - // 保存ids - MyHive.box2.put(banStatusCardIdsKey, list.map((e) => e.oid).toList()); - // 保存card - list.forEach((element) { - MyHive.box2.put('card:${element.oid}', element); - }); - } else { - log('本地有数据'); - // await Future.delayed(Duration(milliseconds: 300)); - - try { - // list = (cardIds as List).map((e) => (MyHive.box2.get('card:$e') ?? MdCard()..oid = e) as MdCard).toList(); - // list = cardIds.map((e) => (MyHive.box2.get('card:$e') ?? MdCard()..oid = e) as MdCard).toList(); - - var start = DateTime.now(); - for (var i=0; i < cardIds.length;i++) { - final card = await MyHive.box2.get('card:${cardIds[i]}') as MdCard?; - - list.add(card??MdCard()..oid = cardIds[i]); - } - log('读取本地数据消耗时间: ${DateTime.now().difference(start).inMilliseconds}'); - } catch (e) { - MyHive.box2.delete(banStatusCardIdsKey); - - log('转换失败 $e'); - - return true; - } - } - - final group = >{ - 'Forbidden': [], - 'Limited 1': [], - 'Limited 2': [], - 'Limited 3': [], - }; - - list.forEach((item) { - group[item.banStatus]?.add(item); - }); - + Future init() async { + await Future.delayed(const Duration(milliseconds: 300)); setState(() { - banStatus2CardsGroup = group; - _pageStatus = PageStatus.success; + _initFlag = true; }); - - return refreshFlag; } @override void initState() { super.initState(); - fetchCards(); + init(); } @override @@ -124,57 +65,82 @@ class _BanStatusCardViewState extends State with AutomaticKee return Stack( children: [ - AnimatedOpacity( - opacity: _pageStatus == PageStatus.success ? 1 : 0, - duration: const Duration(milliseconds: 300), - child: SingleChildScrollView( - padding: const EdgeInsets.symmetric(horizontal: 4), - child: Column( - children: banStatus2CardsGroup.keys.map((key) => Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 4), - child: Row( - children: [ - ClipRRect( - borderRadius: BorderRadius.circular(20), - child: SvgPicture.asset('assets/images/icon_${key.toLowerCase()}.svg', - width: 20, height: 20), - ), - const SizedBox(width: 4), - Text(key, style: const TextStyle(fontSize: 20)), - ], - ), - ), - Card( - // margin: EdgeInsets.all(0), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(6), - ), - child: Container( - padding: const EdgeInsets.only(left: 8, right: 8, top: 8), - child: GridView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: banStatus2CardsGroup[key]!.length, - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 5, childAspectRatio: 0.57, crossAxisSpacing: 6), - itemBuilder: (context, _index) { - return MdCardItemView2( - id: banStatus2CardsGroup[key]![_index].oid, - mdCard: banStatus2CardsGroup[key]![_index], - onTap: (card) => handleTapCardItem(banStatus2CardsGroup[key]!, _index), - ); - }), - ), - ), - ], - )).toList(), + Obx( + () => AnimatedOpacity( + opacity: (banCardsStore.pageStatus.value == PageStatus.success && _initFlag) ? 1 : 0, + duration: const Duration(milliseconds: 300), + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 4), + child: Column( + children: groups1 + .map( + (group) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 4), + child: Row( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(20), + child: SvgPicture.asset( + 'assets/images/icon_${group.key.toLowerCase()}.svg', + width: 20, + height: 20, + ), + ), + const SizedBox(width: 4), + Text( + group.key, + style: const TextStyle(fontSize: 20), + ), + ], + ), + ), + Card( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(6), + ), + child: Container( + padding: const EdgeInsets.only(left: 8, right: 8, top: 8), + child: GridView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: group.data.length, + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 5, + childAspectRatio: 0.57, + crossAxisSpacing: 6, + ), + itemBuilder: (context, _index) { + return MdCardItemView2( + id: group.data[_index].oid, + mdCard: group.data[_index], + onTap: (card) => handleTapCardItem(group.data, _index), + ); + }, + ), + ), + ), + ], + ), + ) + .toList(), + ), ), ), ), - if (_pageStatus == PageStatus.loading) const Positioned.fill(child: Center(child: Loading())) + Obx( + () { + return banCardsStore.pageStatus.value == PageStatus.loading + ? const Positioned.fill( + child: Center( + child: Loading(), + ), + ) + : const SizedBox(); + }, + ) ], ); } diff --git a/lib/pages/ban_list_change/components/BanStatusCardView_3.dart b/lib/pages/ban_list_change/components/BanStatusCardView_3.dart new file mode 100644 index 0000000..1afddde --- /dev/null +++ b/lib/pages/ban_list_change/components/BanStatusCardView_3.dart @@ -0,0 +1,196 @@ +import 'dart:developer'; + +import 'package:duel_links_meta/components/Loading.dart'; +import 'package:duel_links_meta/components/MdCardItemView.dart'; +import 'package:duel_links_meta/components/MdCardItemView2.dart'; +import 'package:duel_links_meta/extension/Future.dart'; +import 'package:duel_links_meta/hive/MyHive.dart'; +import 'package:duel_links_meta/http/CardApi.dart'; +import 'package:duel_links_meta/pages/cards_viewpager/index.dart'; +import 'package:duel_links_meta/store/BanCardStore.dart'; +import 'package:duel_links_meta/type/MdCard.dart'; +import 'package:duel_links_meta/type/ban_list_change/BanStatusCard.dart'; +import 'package:duel_links_meta/type/enum/PageStatus.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:get/get.dart'; + +class BanStatusCardView extends StatefulWidget { + const BanStatusCardView({super.key}); + + @override + State createState() => _BanStatusCardViewState(); +} + +class _BanStatusCardViewState extends State with AutomaticKeepAliveClientMixin { + // Map> banStatus2CardsGroup = {}; + + // var _pageStatus = PageStatus.loading; + + var banCardsStore = Get.put(BanCardStore()); + + // + void handleTapCardItem(List cards, int index) { + // TODO + showDialog( + context: context, + builder: (context) => Dialog.fullscreen( + backgroundColor: Colors.black87.withOpacity(0.3), + child: CardsViewpagerPage(cards: cards, index: index), + ), + ); + } + + // + // Future fetchCards({bool force = false}) async { + // const banStatusCardIdsKey = 'ban_status:card_ids'; + // var banStatusCardIdsFetchDateKey = 'ban_status:card_ids'; + // + // var cardIds = await MyHive.box2.get(banStatusCardIdsKey) as List?; + // + // var refreshFlag = false; + // + // var list = []; + // + // if (cardIds == null || force) { + // final params = { + // 'limit': '0', + // r'banStatus[$exists]': 'true', + // r'alternateArt[$ne]': 'true', + // r'rush[$ne]': 'true', + // // 'fields': 'oid,banStatus' + // }; + // + // final (err, res) = await CardApi().list(params).toCatch; + // if (err != null) { + // setState(() { + // _pageStatus = PageStatus.fail; + // }); + // return false; + // } + // list = res!.map(MdCard.fromJson).toList(); + // + // // 保存ids + // MyHive.box2.put(banStatusCardIdsKey, list.map((e) => e.oid).toList()); + // // 保存card + // list.forEach((element) { + // MyHive.box2.put('card:${element.oid}', element); + // }); + // } else { + // log('本地有数据'); + // // await Future.delayed(Duration(milliseconds: 300)); + // + // try { + // // list = (cardIds as List).map((e) => (MyHive.box2.get('card:$e') ?? MdCard()..oid = e) as MdCard).toList(); + // // list = cardIds.map((e) => (MyHive.box2.get('card:$e') ?? MdCard()..oid = e) as MdCard).toList(); + // + // var start = DateTime.now(); + // for (var i=0; i < cardIds.length;i++) { + // final card = await MyHive.box2.get('card:${cardIds[i]}') as MdCard?; + // + // list.add(card??MdCard()..oid = cardIds[i]); + // } + // log('读取本地数据消耗时间: ${DateTime.now().difference(start).inMilliseconds}'); + // } catch (e) { + // MyHive.box2.delete(banStatusCardIdsKey); + // + // log('转换失败 $e'); + // + // return true; + // } + // } + // + // final group = >{ + // 'Forbidden': [], + // 'Limited 1': [], + // 'Limited 2': [], + // 'Limited 3': [], + // }; + // + // list.forEach((item) { + // group[item.banStatus]?.add(item); + // }); + // + // setState(() { + // banStatus2CardsGroup = group; + // _pageStatus = PageStatus.success; + // }); + // + // return refreshFlag; + // } + + @override + void initState() { + super.initState(); + // fetchCards(); + } + + @override + Widget build(BuildContext context) { + super.build(context); + + return Stack( + children: [ + Obx(() => + AnimatedOpacity( + opacity: banCardsStore.pageStatus.value == PageStatus.success ? 1 : 0, + duration: const Duration(milliseconds: 300), + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: Column( + children: banCardsStore.group.value.keys + .map((key) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 4), + child: Row( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(20), + child: SvgPicture.asset('assets/images/icon_${key.toLowerCase()}.svg', width: 20, height: 20), + ), + const SizedBox(width: 4), + Text(key, style: const TextStyle(fontSize: 20)), + ], + ), + ), + Card( + // margin: EdgeInsets.all(0), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(6), + ), + child: Container( + padding: const EdgeInsets.only(left: 8, right: 8, top: 8), + child: GridView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: banCardsStore.group?.value[key]!.length, + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 5, childAspectRatio: 0.57, crossAxisSpacing: 6), + itemBuilder: (context, _index) { + return MdCardItemView2( + id: banCardsStore.group.value[key]![_index].oid, + mdCard: banCardsStore.group.value[key]![_index], + onTap: (card) => handleTapCardItem(banCardsStore.group.value[key]!, _index), + ); + }), + ), + ), + ], + )) + .toList(), + ), + ), + ) + ), + Obx( + () => banCardsStore.pageStatus.value == PageStatus.loading ? const Positioned.fill(child: Center(child: Loading())) : SizedBox(), + ) + ], + ); + } + + @override + bool get wantKeepAlive => true; +} diff --git a/lib/pages/ban_list_change/index.dart b/lib/pages/ban_list_change/index.dart index f75e5ee..62c6bbe 100644 --- a/lib/pages/ban_list_change/index.dart +++ b/lib/pages/ban_list_change/index.dart @@ -12,26 +12,19 @@ class BanListChangePage extends StatefulWidget { class _BanListChangePageState extends State with SingleTickerProviderStateMixin, AutomaticKeepAliveClientMixin { - @override - void initState() { - super.initState(); - } - - @override - void dispose() { - super.dispose(); - } - @override bool get wantKeepAlive => true; @override Widget build(BuildContext context) { + super.build(context); + return DefaultTabController( length: 2, child: Scaffold( appBar: AppBar( title: const TabBar( + // center tab tabAlignment: TabAlignment.start, isScrollable: true, dividerHeight: 0, diff --git a/lib/pages/cards_viewpager/CardView.dart b/lib/pages/cards_viewpager/CardView.dart index e5be6e5..62e48a5 100644 --- a/lib/pages/cards_viewpager/CardView.dart +++ b/lib/pages/cards_viewpager/CardView.dart @@ -3,17 +3,25 @@ import 'dart:developer'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:duel_links_meta/constant/colors.dart'; import 'package:duel_links_meta/extension/Future.dart'; +import 'package:duel_links_meta/gen/assets.gen.dart'; import 'package:duel_links_meta/hive/MyHive.dart'; +import 'package:duel_links_meta/hive/db/CardHiveDb.dart'; import 'package:duel_links_meta/http/CardApi.dart'; +import 'package:duel_links_meta/store/BanCardStore.dart'; import 'package:duel_links_meta/type/MdCard.dart'; import 'package:duel_links_meta/util/time_util.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:get/get.dart'; import 'package:intl/intl.dart'; import '../../components/IfElseBox.dart'; class CardView extends StatefulWidget { - const CardView({super.key, required this.card}); + const CardView({required this.card, super.key}); final MdCard card; @@ -21,47 +29,57 @@ class CardView extends StatefulWidget { State createState() => _CardViewState(); } +// 如果有传入card,使用传入的card, +// 如果传入的card是只有基础信息(id,name)的card,需要从本地获取完整信息的card +// 如果从本地获取不到card,则请求获取一次再存到本地 class _CardViewState extends State { - MdCard? _card = null; + // MdCard? _card = MdCard()..oid="60c2b3a9a0e24f2d54a517cf"; + MdCard? _card; + + BanCardStore banCardStore = Get.put(BanCardStore()); + + String? get banStatus => banCardStore.idToCardMap[widget.card.oid]?.banStatus; @override void dispose() { super.dispose(); - - log('destroy'); } - init() async { - log('_init: ${_card == null}'); + var banStatusStore = Get.put(BanCardStore()); + + Future init() async { + log('_init, _card == null: ${_card == null}, widget.card: ${widget.card}'); + // _card已经有值 if (_card != null) return; + setState(() { _card = widget.card; }); + // 传入的card是完整信息的card if (widget.card.rarity != '') { + return; + } + + final card = await CardHiveDb.get(widget.card.oid); + + if (card != null) { + log('本地获取到card111'); + setState(() { + _card = card; + }); } else { - var hiveData = await MyHive.box2.get('card:${widget.card.oid}'); - - if (hiveData != null) { - log('本地获取到card111'); - - setState(() { - _card = hiveData as MdCard; - }); - } else { - var (err, res) = await CardApi().getById(_card!.oid).toCatch; - if (err != null) { - return; - } - - // var card = MdCard.fromJson(res![0]); - var card = res![0]; - setState(() { - _card = card; - }); - MyHive.box2.put('card:${card.oid}', card); - } + log('请求获取card111'); + final (err, res) = await CardApi().getById(_card!.oid).toCatch; + if (err != null || res == null) return; + + final card = res; + setState(() { + _card = card; + }); + + CardHiveDb.setCard(card).ignore(); } } @@ -81,9 +99,8 @@ class _CardViewState extends State { child: Container( padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 8), decoration: BoxDecoration( - image: const DecorationImage(image: AssetImage('assets/images/modal_bg.webp'), fit: BoxFit.fitWidth), - color: Theme.of(context).colorScheme.background, - // color: BaColors.main, + image: DecorationImage(image: Assets.images.modalBg.image().image, fit: BoxFit.fitWidth), + color: Theme.of(context).dialogBackgroundColor, ), child: Column( mainAxisSize: MainAxisSize.min, @@ -103,134 +120,164 @@ class _CardViewState extends State { ) : null, ), - Container( - width: 200, - height: 200 * 1.46, + AspectRatio( + aspectRatio: 0.685, child: Stack( + fit: StackFit.expand, children: [ CachedNetworkImage( fit: BoxFit.cover, width: double.infinity, - imageUrl: 'https://s3.duellinksmeta.com/cards/${_card?.oid}_w100.webp', + imageUrl: 'https://s3.duellinksmeta.com/cards/${_card!.oid}_w100.webp', ), Positioned( - top: 0, + top: 0, + left: 0, + right: 0, + bottom: 0, + child: CachedNetworkImage( + width: double.infinity, + fit: BoxFit.cover, + imageUrl: 'https://s3.duellinksmeta.com/cards/${_card!.oid}_w420.webp', + ), + ), + if (banStatus != null) + Positioned( left: 0, - right: 0, - bottom: 0, - child: CachedNetworkImage( - fit: BoxFit.cover, - imageUrl: 'https://s3.duellinksmeta.com/cards/${_card?.oid}_w420.webp', - )) + top: 0, + child: ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(15)), + child: SvgPicture.asset( + 'assets/images/icon_${banStatus!.toLowerCase()}.svg', + width: 30, + height: 30, + ), + ), + ), ], ), - ) + ), ], ), ), Container( - padding: const EdgeInsets.only(top: 12), - height: 160, - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Text( - _card!.name, - overflow: TextOverflow.ellipsis, - maxLines: 2, - style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600), + padding: const EdgeInsets.only(top: 12), + height: 160, + child: Stack( + children: [ + SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + _card?.name ?? '', + overflow: TextOverflow.ellipsis, + maxLines: 2, + style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600), + ), + ), + Row( + children: [ + if (_card!.type == 'Monster') + Row( + children: [ + Image.asset( + 'assets/images/icon_attribute_${_card!.attribute!.toLowerCase()}.png', + width: 14, + height: 14, + ), + const SizedBox(width: 2), + Text(_card!.attribute!, style: const TextStyle(fontSize: 12)), + if (_card!.level != null) + Row( + children: [ + const SizedBox(width: 4), + IfElseBox( + condition: _card!.monsterType.contains('Xyz'), + ifTure: Assets.images.iconNormalRank.image(width: 14, height: 14), + elseTrue: Assets.images.iconNormalLevel.image(width: 14, height: 14), + ), + const SizedBox(width: 2), + Text(_card!.level?.toString() ?? '', style: const TextStyle(fontSize: 12)), + ], + ), + ], + ), + if (_card!.type == 'Spell' || _card!.type == 'Trap') + Row( + children: [ + Image.asset('assets/images/icon_card_type_${_card!.type.toLowerCase()}.png', width: 14, height: 14), + const SizedBox(width: 2), + Text(_card!.type, style: const TextStyle(fontSize: 12)), + const SizedBox(width: 4), + Image.asset('assets/images/icon_card_race_${_card!.race.toLowerCase()}.png', width: 14, height: 14), + const SizedBox(width: 2), + Text(_card!.race.toLowerCase(), style: const TextStyle(fontSize: 12)), + ], + ), + ], ), + ], + ), + if (_card!.monsterType.isNotEmpty) + Row( + children: [ + const SizedBox(height: 4), + Text( + '[${_card!.race}/${_card!.monsterType.join('/')}]', + style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w500), + ), + ], ), + const SizedBox(height: 4), + Text(_card!.description, style: const TextStyle(fontSize: 12)), + const SizedBox(height: 4), + if (_card?.atk != null) Row( children: [ - if (_card!.type == 'Monster') - Row( + const Text('ATK', style: TextStyle(fontSize: 12, fontWeight: FontWeight.w600)), + Text('/${_card!.atk}', style: const TextStyle(fontSize: 12)), + const SizedBox(width: 4), + IfElseBox( + condition: _card!.monsterType.contains('Link'), + ifTure: Row( children: [ - Image.asset('assets/images/icon_attribute_${_card!.attribute!.toLowerCase()}.png', - width: 12, height: 12), - const SizedBox(width: 2), - Text(_card!.attribute!, style: const TextStyle(fontSize: 10)), - if (_card!.level != null) - Row( - children: [ - const SizedBox(width: 4), - if (_card!.monsterType.contains('Xyz')) - Image.asset('assets/images/icon_normal_rank.png', width: 12, height: 12) - else - Image.asset('assets/images/icon_normal_level.png', width: 12, height: 12), - const SizedBox(width: 2), - Text(_card!.level?.toString() ?? '', style: const TextStyle(fontSize: 10)) - ], - ) + const Text('LINK', style: TextStyle(fontSize: 12, fontWeight: FontWeight.w600)), + Text('-${_card!.linkRating ?? 0}', style: const TextStyle(fontSize: 12)), ], ), - if (_card!.type == 'Spell' || _card!.type == 'Trap') - Row( + elseTrue: Row( children: [ - Image.asset('assets/images/icon_card_type_${_card!.type.toLowerCase()}.png', width: 12, height: 12), - const SizedBox(width: 2), - Text(_card!.type, style: const TextStyle(fontSize: 10)), - const SizedBox(width: 4), - Image.asset('assets/images/icon_card_race_${_card!.race.toLowerCase()}.png', width: 12, height: 12), - // Image.asset('assets/images/icon_card_race_ritual.png', width: 12, height: 12), - const SizedBox(width: 2), - - Text(_card!.race.toLowerCase(), style: const TextStyle(fontSize: 10)) + const Text('DEF', style: TextStyle(fontSize: 12, fontWeight: FontWeight.w600)), + Text('/${_card!.def ?? 0}', style: const TextStyle(fontSize: 12)), ], ), - ], - ) - ], - ), - if (_card!.monsterType.isNotEmpty) - Row( - children: [ - const SizedBox(height: 4), - Text('[${_card!.monsterType.join('/')}]', style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w500)), - ], - ), - const SizedBox(height: 4), - Text(_card!.description, style: const TextStyle(fontSize: 12)), - const SizedBox(height: 4), - if (_card!.atk != null) - Row( - children: [ - const Text('ATK', style: TextStyle(fontSize: 12, fontWeight: FontWeight.w600)), - Text('/${_card!.atk}', style: const TextStyle(fontSize: 12)), - const SizedBox(width: 4), - IfElseBox( - condition: _card!.monsterType.contains('Link'), - ifTure: Row( - children: [ - const Text('LINK', style: TextStyle(fontSize: 12, fontWeight: FontWeight.w600)), - Text('/${_card!.linkRating ?? 0}', style: const TextStyle(fontSize: 12)), - ], ), - elseTrue: Row(children: [ - const Text('DEF', style: TextStyle(fontSize: 12, fontWeight: FontWeight.w600)), - Text('/${_card!.def ?? 0}', style: const TextStyle(fontSize: 12)), - ]), - ), - ], - ), - const Text( - 'How to Obtain', - style: TextStyle(fontSize: 12, decoration: TextDecoration.underline, decorationStyle: TextDecorationStyle.solid), - ), - const SizedBox(height: 10), - if (_card!.release != null) - Text( - 'Released on ${TimeUtil.format(_card!.release)}', - style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 12), - ) - ], + ], + ), + if (_card?.obtain.isNotEmpty ?? false) + const Text( + 'How to Obtain', + style: + TextStyle(fontSize: 12, decoration: TextDecoration.underline, decorationStyle: TextDecorationStyle.solid), + ), + const SizedBox(height: 10), + if (_card!.release != null) + Text( + 'Released on ${TimeUtil.format(_card!.release)}', + style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 12), + ), + ], + ), ), - )) + if (_card?.rarity == '') const Center(child: CircularProgressIndicator()), + ], + ), + ), ], ), ), diff --git a/lib/pages/cards_viewpager/index.dart b/lib/pages/cards_viewpager/index.dart index 244e719..f67de1f 100644 --- a/lib/pages/cards_viewpager/index.dart +++ b/lib/pages/cards_viewpager/index.dart @@ -1,15 +1,12 @@ -import 'package:cached_network_image/cached_network_image.dart'; -import 'package:duel_links_meta/components/IfElseBox.dart'; -import 'package:duel_links_meta/constant/colors.dart'; import 'package:duel_links_meta/pages/cards_viewpager/CardView.dart'; import 'package:duel_links_meta/type/MdCard.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; -import 'package:intl/intl.dart'; +import 'package:flutter/widgets.dart'; class CardsViewpagerPage extends StatefulWidget { - const CardsViewpagerPage({super.key, required this.cards, required this.index}); + const CardsViewpagerPage({required this.cards, required this.index, super.key}); final List cards; final int index; @@ -19,26 +16,27 @@ class CardsViewpagerPage extends StatefulWidget { } class _CardsViewpagerPageState extends State { - List get mdCards => widget.cards; + List get _mdCards => widget.cards; - int get index => widget.index; - late PageController _controller; - var animationFlag = false; + int get initialIndex => widget.index; + + late PageController _pageController; + bool _animationFlag = false; @override void dispose() { super.dispose(); - _controller.dispose(); + _pageController.dispose(); } @override void initState() { super.initState(); - _controller = PageController(initialPage: index); + _pageController = PageController(initialPage: initialIndex); SchedulerBinding.instance.addPostFrameCallback((_) { setState(() { - animationFlag = true; + _animationFlag = true; }); }); } @@ -46,22 +44,39 @@ class _CardsViewpagerPageState extends State { @override Widget build(BuildContext context) { return AnimatedScale( - scale: animationFlag ? 1 : 1.05, + scale: _animationFlag ? 1 : 1.05, duration: const Duration(milliseconds: 100), child: AnimatedOpacity( - opacity: animationFlag ? 1 : 0, + opacity: _animationFlag ? 1 : 0, duration: const Duration(milliseconds: 100), child: PageView( - controller: _controller, - children: mdCards - .map((card) => Container( - // color: Colors.transparent, - padding: const EdgeInsets.symmetric(horizontal: 30), - child: Center( - child: CardView(card: card) + controller: _pageController, + children: _mdCards + .map( + (card) => Container( + padding: const EdgeInsets.symmetric(horizontal: 30), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CardView(card: card), + SizedBox( + height: 50, + child: Center( + child: Text( + card.name, + style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.white), + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + maxLines: 2, + ), + ), ), - )) - .toList()), + ], + ), + ), + ) + .toList(), + ), ), ); } diff --git a/lib/pages/character/index.dart b/lib/pages/character/index.dart index 3de2231..f994baa 100644 --- a/lib/pages/character/index.dart +++ b/lib/pages/character/index.dart @@ -81,7 +81,7 @@ class _CharacterPageState extends State { var [skillRes, cardsRes] = await Future.wait([ SkillApi().getByCharacterId(character.oid), - CardApi().getObtainSourceId(character.oid) + CardApi().getByObtainSource(character.oid) ]); if (skillRes.statusCode != 200 || cardsRes.statusCode != 200) { setState(() { diff --git a/lib/pages/deck_detail/components/DeckInfo.dart b/lib/pages/deck_detail/components/DeckInfo.dart index 46b7cc3..c3b1cb6 100644 --- a/lib/pages/deck_detail/components/DeckInfo.dart +++ b/lib/pages/deck_detail/components/DeckInfo.dart @@ -3,23 +3,24 @@ import 'dart:developer'; import 'package:duel_links_meta/components/MdCardItemView2.dart'; import 'package:duel_links_meta/extension/Future.dart'; import 'package:duel_links_meta/gen/assets.gen.dart'; -import 'package:duel_links_meta/hive/MyHive.dart'; +import 'package:duel_links_meta/hive/db/CardHiveDb.dart'; +import 'package:duel_links_meta/http/CardApi.dart'; import 'package:duel_links_meta/http/TopDeckApi.dart'; import 'package:duel_links_meta/pages/cards_viewpager/index.dart'; import 'package:duel_links_meta/type/MdCard.dart'; import 'package:duel_links_meta/type/enum/PageStatus.dart'; import 'package:duel_links_meta/type/top_deck/TopDeck.dart'; import 'package:duel_links_meta/util/time_util.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; -import '../../../http/CardApi.dart'; - class DeckInfo extends StatefulWidget { const DeckInfo({super.key, this.deckTypeId, this.topDeck, this.loadingVisible}); final TopDeck? topDeck; final String? deckTypeId; final bool? loadingVisible; + // final void Function(PageStatus pageStatus)? onPageStatusUpdate; @override State createState() => _DeckInfoState(); @@ -85,20 +86,20 @@ class _DeckInfoState extends State { } // - Future init() async { - final params = {'limit': '1'}; - if (deckTypeId != null) { - params['deckType'] = deckTypeId!; - params['sort'] = '-tournamentType,-created'; - params[r'created[$gte'] = '(days-60)'; - } - - if (widget.topDeck != null) { - params['url'] = widget.topDeck!.url ?? ''; + Future fetchData() async { + var topDeck = widget.topDeck; + Exception? topDeckErr; + if (topDeck?.url == null) { + final params = {'limit': '1'}; + + if (deckTypeId != null) { + params['deckType'] = deckTypeId!; + params['sort'] = '-tournamentType,-created'; + params[r'created[$gte'] = '(days-60)'; + } + (topDeckErr, topDeck) = await TopDeckApi().getBreakdownSample(params).toCatch; } - final (topDeckErr, topDeck) = await TopDeckApi().getBreakdownSample(params).toCatch; - if (topDeckErr != null || topDeck == null) { setState(() { _pageStatus = PageStatus.fail; @@ -111,19 +112,13 @@ class _DeckInfoState extends State { final extraCardIds = topDeck.extra.map((e) => e.card.oid).toList(); mainCardIds.addAll(extraCardIds); - // TODO: 第一次时可以批量获取card再存到本地,第二次时可以减少单独请求card次数 - // final idStrings = mainCardIds.join(','); - - // 从本地获取card - - // final cardsRes = await CardApi().getById(idStrings); - // final cards = cardsRes.body!.map(MdCard.fromJson); - log('mainCardIds length: ${mainCardIds.length}'); + final needFetchCardIds = []; final cards = []; + for (var i = 0; i < mainCardIds.length; i++) { - final hiveData = await MyHive.box2.get('card:${mainCardIds[i]}') as MdCard?; + final hiveData = await CardHiveDb.get(mainCardIds[i]); if (hiveData == null) { log('hiveData为null, ${mainCardIds[i]}'); needFetchCardIds.add(mainCardIds[i]); @@ -131,13 +126,14 @@ class _DeckInfoState extends State { cards.add(hiveData); } } - if (needFetchCardIds.length > 0) { - log('需要请求获取card ${needFetchCardIds}, length: ${needFetchCardIds.length}'); - final (cardsErr, cardsRes) = await CardApi().getById(needFetchCardIds.join(',')).toCatch; + + if (needFetchCardIds.isNotEmpty) { + log('需要请求获取card $needFetchCardIds, length: ${needFetchCardIds.length}'); + final (cardsErr, cardsRes) = await CardApi().getByIds(needFetchCardIds.join(',')).toCatch; if (cardsRes != null) { cards.addAll(cardsRes); cardsRes.forEach((element) { - MyHive.box2.put('card:${element.oid}', element); + CardHiveDb.setCard(element).ignore(); }); } } @@ -164,12 +160,14 @@ class _DeckInfoState extends State { }); topDeck.extra.forEach((item) { - final card = id2CardMap[item.card.oid] ?? MdCard()..oid = item.card.oid; + final card = id2CardMap[item.card.oid] ?? MdCard() + ..oid = item.card.oid; extraSingularCards.add(card); var amount = item.amount; while (amount > 0) { - extraCards.add(id2CardMap[item.card.oid] ?? MdCard()..oid = item.card.oid); + extraCards.add(id2CardMap[item.card.oid] ?? MdCard() + ..oid = item.card.oid); amount--; } }); @@ -183,13 +181,16 @@ class _DeckInfoState extends State { _extraSingularCards = extraSingularCards; }); + // widget.onPageStatusUpdate?.call(PageStatus.success); + return false; } @override void initState() { super.initState(); - init(); + + fetchData(); } @override @@ -210,7 +211,7 @@ class _DeckInfoState extends State { if (_topDeck != null) Text(TimeUtil.format(_topDeck?.created), style: const TextStyle(fontSize: 12)), const Padding(padding: EdgeInsets.symmetric(horizontal: 4), child: Text('—', style: TextStyle(fontSize: 12))), if (_topDeck != null) - Text(_topDeck!.author is String ? _topDeck!.author.toString() : '', style: const TextStyle(fontSize: 12)) + Text(_topDeck!.author is String ? _topDeck!.author.toString() : '', style: const TextStyle(fontSize: 12)), ], ), const SizedBox(height: 10), @@ -220,38 +221,49 @@ class _DeckInfoState extends State { width: 80, child: Row( children: [ - Assets.images.iconGem.image(width: 13, height: 13), + Assets.images.iconGem.image(width: 15, height: 15), const SizedBox(width: 4), Text(formatToK(_topDeck?.gemsPrice ?? 0), style: const TextStyle(fontSize: 11)), - const Padding(padding: EdgeInsets.symmetric(horizontal: 4), child: Text('+', style: TextStyle(fontSize: 11))), - Text('\$${_topDeck?.dollarsPrice.toString() ?? '0'}', style: const TextStyle(fontSize: 11)), + if ((_topDeck?.dollarsPrice ?? 0) > 0) const Padding(padding: EdgeInsets.symmetric(horizontal: 4), child: Text('+', style: TextStyle(fontSize: 11))), + if ((_topDeck?.dollarsPrice ?? 0) > 0) Text('\$${_topDeck?.dollarsPrice.toString() ?? '0'}', style: const TextStyle(fontSize: 11)), ], ), ), Expanded( + child: Center( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Assets.images.iconSkill2.image(width: 15, height: 15), + const SizedBox(width: 2), + Expanded( + child: Text( + _topDeck?.skill?.name ?? '', + style: const TextStyle(color: Color(0xff0a87bb), fontSize: 12), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ), + ), + SizedBox( + width: 80, child: Row( - mainAxisAlignment: MainAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.end, children: [ - Assets.images.iconSkill2.image(width: 15, height: 15), - const SizedBox(width: 2), - Text(_topDeck?.skill?.name ?? '', style: const TextStyle(color: Color(0xff0a87bb), fontSize: 12)), + Text( + _topDeck?.main.map((e) => e.amount).reduce((a, b) => a + b).toString() ?? '', + style: const TextStyle(fontSize: 11), + ), ], ), ), - SizedBox( - width: 80, - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Text( - _topDeck?.main.map((e) => e.amount).reduce((a, b) => a + b).toString() ?? '', - style: const TextStyle(fontSize: 11), - ), - ], - )) ], ), + const SizedBox(height: 10), + Card( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)), margin: EdgeInsets.zero, @@ -260,7 +272,7 @@ class _DeckInfoState extends State { child: GridView.builder( gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 5, mainAxisSpacing: 0, crossAxisSpacing: 8, childAspectRatio: 0.57), - padding: const EdgeInsets.all(0), + padding: EdgeInsets.zero, shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), itemCount: _mainCards.length, @@ -274,37 +286,34 @@ class _DeckInfoState extends State { ), ), ), + const SizedBox(height: 10), if (_extraCards.isNotEmpty) - Column( - children: [ - const SizedBox(height: 10), - Card( - margin: EdgeInsets.zero, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)), - child: Padding( - padding: const EdgeInsets.only(left: 8, right: 8, top: 8), - child: GridView.builder( - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 8, mainAxisSpacing: 0, crossAxisSpacing: 8, childAspectRatio: 0.53), - padding: const EdgeInsets.all(0), - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: _extraCards.length, - itemBuilder: (context, index) { - return MdCardItemView2( - onTap: (card) => handleTapSampleDeckExtraCard(index), - mdCard: _extraCards[index], - id: _extraCards[index].oid, - ); - }, - ), - ), + Card( + margin: EdgeInsets.zero, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)), + child: Padding( + padding: const EdgeInsets.only(left: 8, right: 8, top: 8), + child: GridView.builder( + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 8, mainAxisSpacing: 0, crossAxisSpacing: 8, childAspectRatio: 0.53), + padding: EdgeInsets.zero, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: _extraCards.length, + itemBuilder: (context, index) { + return MdCardItemView2( + onTap: (card) => handleTapSampleDeckExtraCard(index), + mdCard: _extraCards[index], + id: _extraCards[index].oid, + ); + }, ), - ], + ), ) ], ), ), + if (_loadingVisible && _pageStatus != PageStatus.success) const SizedBox( height: 200, diff --git a/lib/pages/deck_detail/index.dart b/lib/pages/deck_detail/index.dart index d3d0bb1..a18a94a 100644 --- a/lib/pages/deck_detail/index.dart +++ b/lib/pages/deck_detail/index.dart @@ -1,11 +1,16 @@ import 'dart:developer'; +import 'package:duel_links_meta/extension/Future.dart'; import 'package:duel_links_meta/pages/deck_detail/components/DeckInfo.dart'; +import 'package:duel_links_meta/type/enum/PageStatus.dart'; import 'package:duel_links_meta/type/top_deck/TopDeck.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; + +import '../../http/TopDeckApi.dart'; class DeckDetailPage extends StatefulWidget { - const DeckDetailPage({super.key, required this.topDeck}); + const DeckDetailPage({required this.topDeck, super.key}); final TopDeck topDeck; @@ -14,21 +19,72 @@ class DeckDetailPage extends StatefulWidget { } class _DeckDetailPageState extends State { - TopDeck get _topDeck => widget.topDeck; + final _refreshIndicatorKey = GlobalKey(); + PageStatus _pageStatus = PageStatus.loading; + + TopDeck? __topDeck; + + // + Future fetchData({bool force = false}) async { + final params = { + 'limit': '1', + 'url': _topDeck.url!, + }; + + final (topDeckErr, topDeck) = await TopDeckApi().getBreakdownSample(params).toCatch; + + if (topDeckErr != null || topDeck == null) { + setState(() { + _pageStatus = PageStatus.fail; + }); + return; + } + + setState(() { + _pageStatus = PageStatus.success; + __topDeck = topDeck; + }); + } + + Future _handleRefresh() async { + await fetchData(); + } @override - Widget build(BuildContext context) { + void initState() { + super.initState(); + SchedulerBinding.instance.addPostFrameCallback((timeStamp) { + _refreshIndicatorKey.currentState?.show(); + }); + } + @override + Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(_topDeck.deckType.name), ), - body: SingleChildScrollView( - padding: const EdgeInsets.all(8), - child: Column( + body: RefreshIndicator( + key: _refreshIndicatorKey, + onRefresh: _handleRefresh, + child: Stack( + fit: StackFit.expand, children: [ - DeckInfo(topDeck: _topDeck, loadingVisible: false,), + SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + padding: const EdgeInsets.all(8), + child: _pageStatus == PageStatus.success + ? DeckInfo( + topDeck: __topDeck, + loadingVisible: false, + ) + : null, + ), + if (_pageStatus == PageStatus.fail) + const Center( + child: Text('Loading failed'), + ), ], ), ), diff --git a/lib/pages/deck_type_detail/components/DeckTypeBreakdownGridView.dart b/lib/pages/deck_type_detail/components/DeckTypeBreakdownGridView.dart index fcfeb07..ef1f5e9 100644 --- a/lib/pages/deck_type_detail/components/DeckTypeBreakdownGridView.dart +++ b/lib/pages/deck_type_detail/components/DeckTypeBreakdownGridView.dart @@ -27,8 +27,8 @@ class _DeckTypeBreakdownGridViewState extends State { return cards.map((e) => e.card).toList(); } - handleTapCardItem(int index) { - showDialog( + void handleTapCardItem(int index) { + showDialog( context: context, builder: (context) => Dialog.fullscreen( backgroundColor: Colors.black87.withOpacity(0.3), @@ -43,11 +43,11 @@ class _DeckTypeBreakdownGridViewState extends State { shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(6) ), - margin: const EdgeInsets.all(0), + margin: EdgeInsets.zero, child: Padding( padding: const EdgeInsets.only(left: 8, right: 8, top: 8), child: GridView.builder( - padding: const EdgeInsets.all(0), + padding: EdgeInsets.zero, shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), itemCount: cards.length, diff --git a/lib/pages/deck_type_detail/index.dart b/lib/pages/deck_type_detail/index.dart index bbff8fe..78c97f7 100644 --- a/lib/pages/deck_type_detail/index.dart +++ b/lib/pages/deck_type_detail/index.dart @@ -1,32 +1,24 @@ import 'dart:developer'; import 'dart:ui'; +import 'package:animations/animations.dart'; import 'package:cached_network_image/cached_network_image.dart'; -import 'package:duel_links_meta/components/Loading.dart'; -import 'package:duel_links_meta/components/MdCardItemView.dart'; -import 'package:duel_links_meta/components/MdCardItemView2.dart'; import 'package:duel_links_meta/components/SkillModalView.dart'; -import 'package:duel_links_meta/constant/colors.dart'; import 'package:duel_links_meta/extension/Future.dart'; -import 'package:duel_links_meta/gen/assets.gen.dart'; -import 'package:duel_links_meta/hive/MyHive.dart'; -import 'package:duel_links_meta/http/CardApi.dart'; +import 'package:duel_links_meta/hive/db/DeckTypeDetailHiveDb.dart'; import 'package:duel_links_meta/http/DeckTypeApi.dart'; -import 'package:duel_links_meta/http/TopDeckApi.dart'; -import 'package:duel_links_meta/pages/cards_viewpager/index.dart'; import 'package:duel_links_meta/pages/deck_detail/components/DeckInfo.dart'; import 'package:duel_links_meta/pages/deck_type_detail/components/DeckTypeBreakdownGridView.dart'; -import 'package:duel_links_meta/type/MdCard.dart'; +import 'package:duel_links_meta/store/AppStore.dart'; import 'package:duel_links_meta/type/deck_type/DeckType.dart'; import 'package:duel_links_meta/type/enum/PageStatus.dart'; -import 'package:duel_links_meta/type/top_deck/TopDeck.dart'; -import 'package:duel_links_meta/util/time_util.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; -import 'package:get/utils.dart'; +import 'package:flutter/scheduler.dart'; +import 'package:get/get.dart'; class DeckTypeDetailPage extends StatefulWidget { - const DeckTypeDetailPage({super.key, required this.name}); + const DeckTypeDetailPage({required this.name, super.key}); final String name; @@ -36,6 +28,9 @@ class DeckTypeDetailPage extends StatefulWidget { class _DeckTypeDetailPageState extends State { String get _deckTypeName => widget.name; + final _refreshIndicator = GlobalKey(); + + final AppStore appStore = Get.put(AppStore()); bool isExtraCard(List monsterType) { if (monsterType.contains('Link')) return true; @@ -50,7 +45,7 @@ class _DeckTypeDetailPageState extends State { var _pageStatus = PageStatus.loading; - List get breakdownCards { + List get _breakdownCards { if (_deckType == null) { return []; } @@ -58,7 +53,7 @@ class _DeckTypeDetailPageState extends State { return _deckType!.deckBreakdown.cards.where((item) => !isExtraCard(item.card.monsterType)).toList(); } - List get breakdownExtraCards { + List get _breakdownExtraCards { if (_deckType == null) { return []; } @@ -66,53 +61,72 @@ class _DeckTypeDetailPageState extends State { return _deckType!.deckBreakdown.cards.where((item) => isExtraCard(item.card.monsterType)).toList(); } - void handleTapSkill(DeckType_DeckBreakdown_Skill skill) { - showDialog( + Future handleTapSkill(DeckType_DeckBreakdown_Skill skill) async { + // await showModal( + // context: context, + // builder: (context) { + // return Padding(padding: EdgeInsets.symmetric(horizontal: 12), child: SkillModalView(name: skill.name)); + // }, + // // configuration: FadeScaleTransitionConfiguration(), + // ); + await showDialog( context: context, - builder: (context) => Dialog.fullscreen( - backgroundColor: Colors.black.withOpacity(0.2), - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 30), - child: SkillModalView(name: skill.name), - ), - ), + builder: (context) => SkillModalView(name: skill.name), + // builder: (context) => Dialog.fullscreen( + // backgroundColor: Colors.black.withOpacity(0.3), + // child: Container( + // padding: const EdgeInsets.symmetric(horizontal: 30), + // child: SkillModalView(name: skill.name), + // ), + // ), ); + + // await showGeneralDialog( + // context: context, + // barrierDismissible: true, + // barrierLabel: '', + // pageBuilder: (context, anim1, anim2) { + // // return SkillModalView(name: skill.name); + // return SkillModalView(name: skill.name); + // }, + // transitionDuration: Duration(milliseconds: 400), + // transitionBuilder: (context, anim1, anim2, child) { + // return Transform.scale( + // scale: anim1.value, + // child: Opacity( + // opacity: anim1.value, + // child: child, + // ), + // ); + // }, + // ); } // Future fetchDeckType({bool force = false}) async { - final hiveDeckTypeKey = 'deck_type:$_deckTypeName'; - final hiveDeckTypeFetchDateKey = 'deck_type_fetch_date:$_deckTypeName'; - var deckType = await MyHive.box2.get(hiveDeckTypeKey) as DeckType?; - final hiveDataExpire = await MyHive.box2.get(hiveDeckTypeFetchDateKey) as DateTime?; + var deckType = await DeckTypeDetailHiveDb.getDetail(_deckTypeName); + final hiveDataExpire = await DeckTypeDetailHiveDb.getDetailExpireDate(_deckTypeName); Exception? err; var refreshFlag = false; if (deckType == null || force) { + log('需要请求获取数据, 本地数据为空: ${deckType == null}, 是否强制刷新: $force'); + (err, deckType) = await DeckTypeApi().getDetailByName(_deckTypeName).toCatch; - if (err != null) { + if (err != null || deckType == null) { setState(() { _pageStatus = PageStatus.fail; }); return false; } - MyHive.box2.put(hiveDeckTypeKey, deckType); - MyHive.box2.put(hiveDeckTypeFetchDateKey, DateTime.now()); + await DeckTypeDetailHiveDb.setDeckType(deckType); + await DeckTypeDetailHiveDb.setDeckTypeExpireDate(deckType, DateTime.now().add(const Duration(days: 1))); } else { - log('从本地获取数据'); - try { - if (hiveDataExpire != null && hiveDataExpire.add(const Duration(hours: 12)).isBefore(DateTime.now())) { - refreshFlag = true; - } - } catch (e) { - log('转换失败'); - MyHive.box2.delete(hiveDeckTypeKey); - MyHive.box2.delete(hiveDeckTypeFetchDateKey); - return true; - } + log('从本地获取数据 $hiveDataExpire'); + refreshFlag = hiveDataExpire == null || hiveDataExpire.isBefore(DateTime.now()); } setState(() { @@ -123,148 +137,195 @@ class _DeckTypeDetailPageState extends State { return refreshFlag; } + Future init() async { + final shouldRefresh = await fetchDeckType(); + if (shouldRefresh) { + await fetchDeckType(force: true); + } + } + + var _isInit = false; + + Future _handleRefresh() async { + final shouldRefresh = await fetchDeckType(force: _isInit); + _isInit = true; + if (shouldRefresh) { + await fetchDeckType(force: true); + } + } + @override void initState() { super.initState(); - fetchDeckType(); + + SchedulerBinding.instance.addPostFrameCallback((timeStamp) { + _refreshIndicator.currentState?.show(); + }); } @override Widget build(BuildContext context) { return Scaffold( - body: Stack( - children: [ - SingleChildScrollView( - child: Column( - children: [ - Stack( - children: [ - Stack( - children: [ - AspectRatio( - aspectRatio: 1, - child: ClipRRect( - child: ImageFiltered( - imageFilter: ImageFilter.blur(sigmaX: 10, sigmaY: 10), - child: CachedNetworkImage( - width: double.infinity, - imageUrl: 'https://imgserv.duellinksmeta.com/v2/dlm/deck-type/$_deckTypeName?portrait=true&width=50', - fit: BoxFit.cover, + body: RefreshIndicator( + onRefresh: _handleRefresh, + key: _refreshIndicator, + child: Stack( + fit: StackFit.expand, + children: [ + SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + child: Column( + children: [ + Stack( + children: [ + Stack( + children: [ + AspectRatio( + aspectRatio: 1, + child: ClipRRect( + child: ImageFiltered( + imageFilter: ImageFilter.blur(sigmaX: 10, sigmaY: 10), + child: CachedNetworkImage( + width: double.infinity, + imageUrl: 'https://imgserv.duellinksmeta.com/v2/dlm/deck-type/$_deckTypeName?portrait=true&width=50', + fit: BoxFit.cover, + ), ), ), ), - ), - Positioned.fill( - child: Container( - decoration: BoxDecoration( - gradient: LinearGradient( + Positioned.fill( + bottom: -1, + child: Container( + decoration: BoxDecoration( + gradient: LinearGradient( begin: Alignment.bottomCenter, end: Alignment.topCenter, // colors: [Theme.of(context).colorScheme.background, BaColors.theme.withOpacity(0)]), - colors: [Colors.white, Colors.white.withOpacity(0)]), - ), - ), - ), - Positioned( - bottom: 200, - left: 0, - right: 0, - child: Padding( - padding: const EdgeInsets.only(left: 8, bottom: 18), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(_deckTypeName, style: const TextStyle(fontSize: 24, fontWeight: FontWeight.w500)), - AnimatedOpacity( - opacity: _pageStatus == PageStatus.success ? 1 : 0, - duration: const Duration(milliseconds: 300), - child: Row( - children: [ - const Text('Average size: ', style: TextStyle(fontSize: 12)), - Text(_deckType?.deckBreakdown.avgMainSize.toString() ?? '', - style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w500)), - const SizedBox(width: 3), - const Text('cards', style: TextStyle(fontSize: 12)), - ], - ), + colors: [ + Theme.of(context).scaffoldBackgroundColor, + Theme.of(context).scaffoldBackgroundColor.withOpacity(0) + ], ), - ], + ), ), ), - ) - ], - ), - AnimatedOpacity( - opacity: _pageStatus == PageStatus.success ? 1 : 0, - duration: const Duration(milliseconds: 400), - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), - child: _pageStatus == PageStatus.success - ? Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox( - height: 140, - ), - const Padding( - padding: EdgeInsets.symmetric(vertical: 12), - child: Text('Top Main Deck', style: TextStyle(fontSize: 20, fontWeight: FontWeight.w500)), - ), - const SizedBox(height: 10), - DeckTypeBreakdownGridView(cards: breakdownCards, crossAxisCount: 6), - if (breakdownExtraCards.isNotEmpty) - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: 20), - const Text('Top Extra Deck', style: TextStyle(fontSize: 20, fontWeight: FontWeight.w500)), - const SizedBox(height: 10), - DeckTypeBreakdownGridView(cards: breakdownExtraCards, crossAxisCount: 6), - ], + // Positioned( + // bottom: 200, + // left: 0, + // right: 0, + // child: Padding( + // padding: const EdgeInsets.only(left: 8, bottom: 18), + // child: Column( + // crossAxisAlignment: CrossAxisAlignment.start, + // children: [ + // Text(_deckTypeName, style: const TextStyle(fontSize: 24, fontWeight: FontWeight.w500)), + // AnimatedOpacity( + // opacity: _pageStatus == PageStatus.success ? 1 : 0, + // duration: const Duration(milliseconds: 300), + // child: Row( + // children: [ + // const Text('Average size: ', style: TextStyle(fontSize: 12)), + // Text(_deckType?.deckBreakdown.avgMainSize.toString() ?? '', + // style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w500)), + // const SizedBox(width: 3), + // const Text('cards', style: TextStyle(fontSize: 12)), + // ], + // ), + // ), + // ], + // ), + // ), + // ) + ], + ), + AnimatedOpacity( + opacity: _pageStatus == PageStatus.success ? 1 : 0, + duration: const Duration(milliseconds: 400), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), + child: _pageStatus == PageStatus.success + ? Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 140), + Text(_deckTypeName, style: const TextStyle(fontSize: 30, fontWeight: FontWeight.w500)), + AnimatedOpacity( + opacity: _pageStatus == PageStatus.success ? 1 : 0, + duration: const Duration(milliseconds: 300), + child: Row( + children: [ + const Text('Average size: ', style: TextStyle(fontSize: 12)), + Text(_deckType?.deckBreakdown.avgMainSize.toString() ?? '', + style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w500)), + const SizedBox(width: 3), + const Text('cards', style: TextStyle(fontSize: 12)), + ], + ), ), - const SizedBox(height: 10), - const Text('Popular Skills', style: TextStyle(fontSize: 20, fontWeight: FontWeight.w500)), - Column( - children: _deckType?.deckBreakdown.skills - .where((item) => ((item.count) / _deckType!.deckBreakdown.total).round() > 0) - .map((skill) => InkWell( - onTap: () => handleTapSkill(skill), - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 2), - child: Row( - children: [ - Text(skill.name, style: const TextStyle(color: Color(0xff0a87bb))), - Text( - ': ${(skill.count * 100 / _deckType!.deckBreakdown.total).toStringAsFixed(0)}%', - style: const TextStyle(fontSize: 12)) - ], + const SizedBox(height: 20), + const Text('Top Main Deck', style: TextStyle(fontSize: 20, fontWeight: FontWeight.w500)), + const SizedBox(height: 10), + DeckTypeBreakdownGridView(cards: _breakdownCards, crossAxisCount: 6), + if (_breakdownExtraCards.isNotEmpty) + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 20), + const Text('Top Extra Deck', style: TextStyle(fontSize: 20, fontWeight: FontWeight.w500)), + const SizedBox(height: 10), + DeckTypeBreakdownGridView(cards: _breakdownExtraCards, crossAxisCount: 6), + ], + ), + const SizedBox(height: 10), + const Text('Popular Skills', style: TextStyle(fontSize: 20, fontWeight: FontWeight.w500)), + Column( + children: _deckType?.deckBreakdown.skills + .where((item) => ((item.count) / _deckType!.deckBreakdown.total).round() > 0) + .map((skill) => InkWell( + onTap: () => handleTapSkill(skill), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 2, horizontal: 2), + child: Row( + children: [ + Expanded( + child: Text( + skill.name + skill.name, + style: const TextStyle(color: Color(0xff0a87bb)), + overflow: TextOverflow.ellipsis, + ), + ), + const SizedBox(width: 4), + Text( + ': ${(skill.count * 100 / _deckType!.deckBreakdown.total).toStringAsFixed(0)}%', + style: const TextStyle(fontSize: 12)) + ], + ), ), - ), - )) - .toList() ?? - [], - ), - const SizedBox(height: 20), - const Text('Sample Deck', style: TextStyle(fontSize: 20, fontWeight: FontWeight.w500)), - - DeckInfo(deckTypeId: _deckType?.oid,) - ], - ) - : null, + )) + .toList() ?? + [], + ), + const SizedBox(height: 20), + const Text('Sample Deck', style: TextStyle(fontSize: 20, fontWeight: FontWeight.w500)), + DeckInfo( + deckTypeId: _deckType?.oid, + ) + ], + ) + : null, + ), ), - ), - ], - ), - ], - ), - ), - if (_pageStatus == PageStatus.loading) - const Positioned.fill( - child: Center( - child: Loading(), + ], + ), + ], ), - ) - ], + ), + if (_pageStatus == PageStatus.fail) + const Center( + child: Text('Loading failed'), + ) + ], + ), ), ); } diff --git a/lib/pages/home/components/NavItemCard.dart b/lib/pages/home/components/NavItemCard.dart index 1f58dd8..b726b76 100644 --- a/lib/pages/home/components/NavItemCard.dart +++ b/lib/pages/home/components/NavItemCard.dart @@ -7,55 +7,54 @@ class NavItemCard extends StatelessWidget { final NavTab navTab; + String get coverUrl => 'https://wsrv.nl/?url=https://s3.duellinksmeta.com${navTab.image}&w=360&output=webp&we&n=-1&maxage=7d'; + @override Widget build(BuildContext context) { - final imageUrl = 'https://wsrv.nl/?url=https://s3.duellinksmeta.com${navTab.image}&w=360&output=webp&we&n=-1&maxage=7d'; - return Card( margin: EdgeInsets.zero, - child: ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(6)), - child: Stack( - fit: StackFit.expand, - children: [ - if (navTab.image != '') - CachedNetworkImage( - fit: BoxFit.cover, - fadeOutDuration: null, - fadeInDuration: Duration.zero, - imageUrl: imageUrl, - ) - else - Container( - color: Colors.black12, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(6), + ), + clipBehavior: Clip.hardEdge, + child: Stack( + fit: StackFit.expand, + children: [ + Container( + child: navTab.image != '' + ? CachedNetworkImage( + fit: BoxFit.cover, + fadeOutDuration: null, + imageUrl: coverUrl, + ) + : null, + ), + Positioned( + bottom: 0, + left: 0, + right: 0, + child: Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.centerRight, + end: Alignment.centerLeft, + colors: [Colors.black12, Colors.black87], + ), ), - Positioned( - bottom: 0, - left: 0, - right: 0, - child: Container( - decoration: const BoxDecoration( - gradient: LinearGradient( - begin: Alignment.centerRight, - end: Alignment.centerLeft, - colors: [Colors.black12, Colors.black87], + height: 30, + padding: const EdgeInsets.symmetric(vertical: 2, horizontal: 4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + navTab.title ?? '', + style: const TextStyle(color: Colors.white, fontSize: 12), ), - ), - height: 30, - padding: const EdgeInsets.symmetric(vertical: 2, horizontal: 4), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - navTab.title ?? '', - style: const TextStyle(color: Colors.white, fontSize: 12), - ) - ], - ), + ], ), - ) - ], - ), + ), + ), + ], ), ); } diff --git a/lib/pages/home/index.dart b/lib/pages/home/index.dart index 71a30b4..de19167 100644 --- a/lib/pages/home/index.dart +++ b/lib/pages/home/index.dart @@ -3,17 +3,23 @@ import 'dart:developer'; import 'package:duel_links_meta/extension/Future.dart'; import 'package:duel_links_meta/hive/MyHive.dart'; import 'package:duel_links_meta/http/NavTabApi.dart'; +import 'package:duel_links_meta/pages/about/index.dart'; import 'package:duel_links_meta/pages/farming_and_event/index.dart'; +import 'package:duel_links_meta/hive/db/NavHiveDb.dart'; import 'package:duel_links_meta/pages/home/components/NavItemCard.dart'; import 'package:duel_links_meta/pages/home/type/NavTabType.dart'; +import 'package:duel_links_meta/pages/open_source_licenses/index.dart'; import 'package:duel_links_meta/pages/tier_list/index.dart'; import 'package:duel_links_meta/pages/top_decks/index.dart'; import 'package:duel_links_meta/pages/webview/index.dart'; import 'package:duel_links_meta/store/AppStore.dart'; import 'package:duel_links_meta/type/NavTab.dart'; -import 'package:duel_links_meta/type/enum/PageStatus.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; +import 'package:flutter/widgets.dart'; import 'package:get/get.dart'; +import 'package:package_info_plus/package_info_plus.dart'; class HomePage extends StatefulWidget { const HomePage({super.key}); @@ -27,6 +33,8 @@ class _HomePageState extends State with AutomaticKeepAliveClientMixin final AppStore appStore = Get.put(AppStore()); + var _isInit = false; + List _navTabs = [ NavTab(id: NavTabType.tierList.value, title: 'TIER LIST'), NavTab(id: NavTabType.topDecksSpeed.value, title: 'TOP DECK: SPEED'), @@ -77,95 +85,222 @@ class _HomePageState extends State with AutomaticKeepAliveClientMixin // Future toggleDarkMode() async { - MyHive.box.clear(); - MyHive.box2.clear(); - + // TODO if (Get.isDarkMode) { Get.changeThemeMode(ThemeMode.light); - appStore.changeThemeMode(ThemeMode.light); - MyHive.box2.put('dark_mode', 'light'); + // appStore.changeThemeMode(ThemeMode.light); + MyHive.box2.put('dark_mode', 'light').ignore(); } else { Get.changeThemeMode(ThemeMode.dark); - appStore.changeThemeMode(ThemeMode.dark); - MyHive.box2.put('dark_mode', 'dark'); + // appStore.changeThemeMode(ThemeMode.dark); + MyHive.box2.put('dark_mode', 'dark').ignore(); } } + // Future fetchData({bool force = false}) async { - const navTabKey = 'nav_tab:list'; - const navTabFetchDateKey = 'nav_tab:fetch_date'; - final hiveData = await MyHive.box2.get(navTabKey) as List?; - final expireTime = await MyHive.box2.get(navTabFetchDateKey) as DateTime?; - var list = []; - - var refreshFlag = false; - if (hiveData == null || force) { - log('需要强制刷新'); + var navTabList = await HomeHiveDb.getNavTabList(); + final navTabListExpireTime = await HomeHiveDb.getNavTabListExpireTime(); + var shouldRefresh = false; + + if (navTabList == null || force) { final (err, res) = await NavTabApi().list().toCatch; - if (err != null) { - setState(() { - // _pageStatus = PageStatus.fail; - }); - return false; - } - list = res!.map(NavTab.fromJson).toList(); - MyHive.box2.put(navTabKey, list); - MyHive.box2.put(navTabFetchDateKey, DateTime.now()); + if (err != null || res == null) return false; + + navTabList = res; + await HomeHiveDb.setNavTabList(navTabList); + // set 1 day expire + await HomeHiveDb.setNavTabListExpireTime(DateTime.now().add(const Duration(days: 1))); } else { - log('本地获取到数据'); - - try { - list = hiveData.cast(); - if (expireTime != null && expireTime.add(const Duration(hours: 12)).isBefore(DateTime.now())) { - log('已过期'); - refreshFlag = true; - } - } catch (e) { - log('解析失败'); - - MyHive.box2.delete(navTabKey); - MyHive.box2.delete(navTabFetchDateKey); - return true; + if (navTabListExpireTime == null || navTabListExpireTime.isBefore(DateTime.now())) { + shouldRefresh = true; } } final id2NavTabMap = {}; - for (final element in list) { + navTabList.forEach((element) { id2NavTabMap[element.id] = element; - } + }); - _navTabs.forEach((item) { - item.image = id2NavTabMap[item.id]?.image ?? ''; + _navTabs.forEach((element) { + element.image = id2NavTabMap[element.id]?.image ?? ''; }); setState(() { _navTabs = _navTabs; - // _pageStatus = PageStatus.success; }); - return refreshFlag; + return shouldRefresh; } + // Future handleRefresh() async { - await fetchData(force: true); - } + final shouldRefresh = await fetchData(force: _isInit); + _isInit = true; - Future init() async { - final flag = await fetchData(); - if (flag) { - _refreshIndicatorKey.currentState?.show(atTop: true); + if (shouldRefresh) { + await fetchData(force: true); } } + void init() { + // trigger refresh indicator when page initialized + SchedulerBinding.instance.addPostFrameCallback((_) async { + await _refreshIndicatorKey.currentState?.show(); + }); + } + + void _handleOpenSettingDialog() async { + PackageInfo packageInfo = await PackageInfo.fromPlatform(); + log("packageInfo ${packageInfo.toString()}"); + + await showModalBottomSheet( + context: context, + // backgroundColor: Theme.of(context).colorScheme.surface, + // backgroundColor: Colors.orange, + builder: (context) { + return ClipRRect( + borderRadius: const BorderRadius.only(topLeft: Radius.circular(24), topRight: Radius.circular(24)), + child: Container( + // height: 300, + padding: const EdgeInsets.only(top: 30), + color: Theme.of(context).colorScheme.onPrimary, + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // const SizedBox(height: 40), + const Padding( + padding: EdgeInsets.symmetric(horizontal: 12), + child: Text( + 'Setting', + style: TextStyle(fontSize: 24), + ), + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 6), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Padding( + padding: EdgeInsets.only(left: 6), + child: Text('Theme mode'), + ), + Row( + children: [ + IconButton( + isSelected: !Get.isDarkMode, + onPressed: () { + Get.changeThemeMode(ThemeMode.light); + // TODO + MyHive.box2.put('dark_mode', 'light').ignore(); + }, + icon: const Icon(Icons.sunny), + ), + IconButton( + isSelected: Get.isDarkMode, + onPressed: () { + Get.changeThemeMode(ThemeMode.dark); + MyHive.box2.put('dark_mode', 'dark').ignore(); + }, + icon: const Icon(Icons.nightlight_rounded), + ), + ], + ), + ], + ), + ), + const Padding( + padding: EdgeInsets.symmetric(vertical: 12, horizontal: 12), + child: Text( + 'About', + style: TextStyle(fontSize: 24), + ), + ), + // Material( + // color: Colors.transparent, + // child: InkWell( + // onTap: () { + // Navigator.push(context, MaterialPageRoute(builder: (context) => const AboutPage())); + // }, + // child: Container( + // height: 50, + // padding: const EdgeInsets.symmetric(horizontal: 12), + // child: const Row( + // mainAxisAlignment: MainAxisAlignment.spaceBetween, + // children: [ + // Text( + // 'About this project', + // overflow: TextOverflow.ellipsis, + // ), + // Icon(Icons.arrow_forward), + // ], + // ), + // ), + // ), + // ), + Container( + height: 50, + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [const Text('App version'), Text(packageInfo.version)], + ), + ), + Container( + height: 50, + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [const Text('Build signature'), Text(packageInfo.buildSignature.substring(0, 6))], + ), + ), + Material( + color: Colors.transparent, + child: InkWell( + onTap: () { + Navigator.push(context, MaterialPageRoute(builder: (context) => const OpenSourceLicensePage())); + }, + child: Container( + height: 50, + padding: const EdgeInsets.symmetric(horizontal: 12), + child: const Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [Text('Github'), Icon(Icons.arrow_forward)], + ), + ), + ), + ), + Material( + color: Colors.transparent, + child: InkWell( + onTap: () { + Navigator.push(context, MaterialPageRoute(builder: (context) => const OpenSourceLicensePage())); + }, + child: Container( + height: 50, + padding: const EdgeInsets.symmetric(horizontal: 12), + child: const Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [Text('Open source licenses'), Icon(Icons.arrow_forward)], + ), + ), + ), + ), + ], + ), + ), + ), + ); + }); + } + @override void initState() { super.initState(); init(); - // SchedulerBinding.instance.addPostFrameCallback((_) { - // _refreshIndicatorKey.currentState?.show(atTop: true); - // }); } @override @@ -173,42 +308,38 @@ class _HomePageState extends State with AutomaticKeepAliveClientMixin @override Widget build(BuildContext context) { + super.build(context); + return Scaffold( appBar: AppBar( automaticallyImplyLeading: false, - title: const Text('Duel Link Meta'), + title: const Text('Duel Links Meta'), actions: [ - IconButton( - onPressed: toggleDarkMode, - icon: Obx(() => Icon(appStore.themeMode.value == ThemeMode.dark ? Icons.nightlight : Icons.sunny)), - ) + // IconButton( + // onPressed: toggleDarkMode, + // icon: Obx(() => Icon(appStore.themeMode.value == ThemeMode.dark ? Icons.nightlight : Icons.sunny)), + // ), + IconButton(onPressed: _handleOpenSettingDialog, icon: const Icon(Icons.settings)) ], ), body: RefreshIndicator( onRefresh: handleRefresh, key: _refreshIndicatorKey, - child: AnimatedOpacity( - // opacity: _pageStatus == PageStatus.success ? 1 : 0, - opacity: 1, - duration: const Duration(milliseconds: 400), - child: GridView.builder( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 2, - mainAxisSpacing: 8, - crossAxisSpacing: 8, - childAspectRatio: 2, - ), - itemCount: _navTabs.length, - itemBuilder: (BuildContext context, int index) { - return GestureDetector( - onTap: () { - handleTapNav(_navTabs[index]); - }, - child: NavItemCard(navTab: _navTabs[index]), - ); - }, + child: GridView.builder( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + mainAxisSpacing: 8, + crossAxisSpacing: 8, + childAspectRatio: 2, ), + itemCount: _navTabs.length, + itemBuilder: (BuildContext context, int index) { + return GestureDetector( + onTap: () => handleTapNav(_navTabs[index]), + child: NavItemCard(navTab: _navTabs[index]), + ); + }, ), ), ); diff --git a/lib/pages/main/index.dart b/lib/pages/main/index.dart index 512a529..c370222 100644 --- a/lib/pages/main/index.dart +++ b/lib/pages/main/index.dart @@ -37,7 +37,7 @@ class _MainPageState extends State { HomePage(), PacksPage(), ArticlesPage(), - BanListChangePage() + BanListChangePage(), ], ), bottomNavigationBar: NavigationBar( @@ -51,6 +51,9 @@ class _MainPageState extends State { selectedIndex: _selectedIndex, onDestinationSelected: _onItemTapped, height: 70, + // overlayColor: Colors.white, + // indicatorColor: Colors.teal, + // surfaceTintColor: Colors.tealAccent, // 背景 // backgroundColor: Theme.of(context).colorScheme.primary, ), // bottomNavigationBar: BottomNavigationBar( diff --git a/lib/pages/open_source_licenses/index.dart b/lib/pages/open_source_licenses/index.dart new file mode 100644 index 0000000..68e81ef --- /dev/null +++ b/lib/pages/open_source_licenses/index.dart @@ -0,0 +1,141 @@ +import 'dart:convert'; + +import 'package:animations/animations.dart'; +import 'package:duel_links_meta/pages/webview/index.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class OpenSourceLicense { + OpenSourceLicense({required this.name, required this.repos, required this.licensesPath, required this.version}); + + String name; + String repos; + String licensesPath; + String version; +} + +class OpenSourceLicensePage extends StatefulWidget { + const OpenSourceLicensePage({super.key}); + + @override + State createState() => _OpenSourceLicensePageState(); +} + +class _OpenSourceLicensePageState extends State { + List licenses = []; + + // + Future _openBrowser(OpenSourceLicense license) async { + var url = license.licensesPath; + if (url.isEmpty) { + url = license.repos; + } + final uri = Uri.parse(url); + + await showModal( + context: context, + builder: (context) { + return AlertDialog( + title: const Text('Open by browser'), + content: Text(url), + actions: [ + Row( + children: [ + Expanded( + child: TextButton( + onPressed: () async { + launchUrl(uri).ignore(); + Navigator.pop(context); + }, + child: const Text('Confirm'), + ), + ), + ], + ), + ], + ); + }, + ); + } + + // + Future init() async { + final jsonData = await rootBundle.loadString('assets/json/open_source_licenses.json'); + final data = json.decode(jsonData) as List; + + final list = data + .map( + (e) => OpenSourceLicense( + name: (e['name'] ?? '') as String, + repos: (e['repos'] ?? '') as String, + licensesPath: (e['license_path'] ?? '') as String, + version: (e['version'] ?? '') as String), + ) + .toList(); + + setState(() { + licenses = list; + }); + } + + @override + void initState() { + super.initState(); + + init(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Open source licenses'), + ), + body: ListView.builder( + padding: const EdgeInsets.all(10), + itemCount: licenses.length, + itemBuilder: (context, index) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + GestureDetector( + onTap: () => _openBrowser(licenses[index]), + child: Card( + margin: EdgeInsets.zero, + shadowColor: Colors.transparent, + child: Padding( + padding: const EdgeInsets.all(10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + licenses[index].name, + style: const TextStyle(fontSize: 22), + ), + ], + ), + Text( + licenses[index].repos, + style: const TextStyle(fontSize: 12), + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ), + ), + const SizedBox( + height: 10, + ) + ], + ); + }, + ), + ); + } +} diff --git a/lib/pages/pack_detail/index.dart b/lib/pages/pack_detail/index.dart index 7c93d2b..5c38f20 100644 --- a/lib/pages/pack_detail/index.dart +++ b/lib/pages/pack_detail/index.dart @@ -3,7 +3,9 @@ import 'dart:developer'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:duel_links_meta/components/Loading.dart'; import 'package:duel_links_meta/components/MdCardItemView.dart'; -import 'package:duel_links_meta/hive/MyHive.dart'; +import 'package:duel_links_meta/extension/Future.dart'; +import 'package:duel_links_meta/hive/db/CardHiveDb.dart'; +import 'package:duel_links_meta/hive/db/PackHiveDb.dart'; import 'package:duel_links_meta/http/CardApi.dart'; import 'package:duel_links_meta/pages/cards_viewpager/index.dart'; import 'package:duel_links_meta/type/MdCard.dart'; @@ -11,9 +13,10 @@ import 'package:duel_links_meta/type/enum/PageStatus.dart'; import 'package:duel_links_meta/type/pack_set/PackSet.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; class PackDetailPage extends StatefulWidget { - const PackDetailPage({super.key, required this.pack}); + const PackDetailPage({required this.pack, super.key}); final PackSet pack; @@ -23,12 +26,12 @@ class PackDetailPage extends StatefulWidget { class _PackDetailPageState extends State { PackSet get pack => widget.pack; - List _cards = []; Map> rarity2CardsGroup = {}; var _pageStatus = PageStatus.loading; + final _refreshIndicatorKey = GlobalKey(); - handleTapCardItem(List cards, int index) { - showDialog( + void handleTapCardItem(List cards, int index) { + showDialog( context: context, builder: (context) => Dialog.fullscreen( backgroundColor: Colors.black87.withOpacity(0.3), @@ -38,91 +41,84 @@ class _PackDetailPageState extends State { } // - Future fetchData() async { - final sourceKey = 'pack_card_ids:${pack.oid}'; - final cardsIds = await MyHive.box2.get(sourceKey) as List?; + Future fetchData({bool force = false}) async { + final cardsIds = await PackHiveDb.getIds(pack.oid); + var cards = []; if (cardsIds != null) { - // await Future.delayed(Duration(milliseconds: 300)); - log('从本地获取到card, sourceKey: ${sourceKey}'); + for (var i = 0; i < cardsIds.length; i++) { + final card = await CardHiveDb.get(cardsIds[i]); - final cards = []; - - for (var i=0; i>{}; - - cards.forEach((item) { - // MyHive.box2.put('card:${item.oid}', item); - - if (rarityGroup[item.rarity] == null) { - rarityGroup[item.rarity] = [item]; - } else { - rarityGroup[item.rarity]!.add(item); - } - }); - - setState(() { - rarity2CardsGroup = rarityGroup; - _cards = cards; - _pageStatus = PageStatus.success; - }); - - return; - } - final res = await CardApi().getObtainSourceId(pack.oid); + cards = list; + PackHiveDb.setIds(pack.oid, list.map((e) => e.oid).toList()).ignore(); + } - final list = res.body!.map(MdCard.fromJson).toList(); - MyHive.box2.put(sourceKey, list.map((e) => e.oid).toList()); final rarityGroup = >{}; - list.forEach((item) { - MyHive.box2.put('card:${item.oid}', item); + cards.forEach((element) { + if (cardsIds == null && element.rarity != '') { + CardHiveDb.setCard(element); + } - if (rarityGroup[item.rarity] == null) { - rarityGroup[item.rarity] = [item]; + if (rarityGroup[element.rarity] == null) { + rarityGroup[element.rarity] = [element]; } else { - rarityGroup[item.rarity]!.add(item); + rarityGroup[element.rarity]!.add(element); } }); setState(() { rarity2CardsGroup = rarityGroup; - _cards = list; _pageStatus = PageStatus.success; }); } + var _isInit = false; + + Future _handleRefresh() async { + await fetchData(force: _isInit); + _isInit = true; + } + + void _triggerRefresh() { + _refreshIndicatorKey.currentState?.show(); + } + @override void initState() { super.initState(); - fetchData(); + + SchedulerBinding.instance.addPostFrameCallback((_) { + _triggerRefresh(); + }); } @override Widget build(BuildContext context) { return Scaffold( - body: Stack( - children: [ - SingleChildScrollView( - child: Column( - children: [ - Hero( - tag: pack.name, - child: SizedBox( + body: RefreshIndicator( + key: _refreshIndicatorKey, + onRefresh: _handleRefresh, + child: Stack( + fit: StackFit.expand, + children: [ + SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + child: Column( + children: [ + SizedBox( height: 200, child: Stack( children: [ @@ -138,63 +134,78 @@ class _PackDetailPageState extends State { decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.bottomCenter, - end: Alignment.topCenter, - colors: [Theme.of(context).colorScheme.background, Colors.transparent], - // colors: [Colors.white, Colors.transparent], + end: Alignment.center, + colors: [ + Theme.of(context).scaffoldBackgroundColor, + Theme.of(context).scaffoldBackgroundColor.withOpacity(0) + ], ), ), ), - ) + ), ], ), ), - ), - AnimatedOpacity( - opacity: _pageStatus == PageStatus.success ? 1 : 0, - duration: const Duration(milliseconds: 500), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), - child: Column( - children: rarity2CardsGroup.keys - .map((key) => Column( + AnimatedOpacity( + opacity: _pageStatus == PageStatus.success ? 1 : 0, + duration: const Duration(milliseconds: 300), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), + child: Column( + children: rarity2CardsGroup.keys + .map( + (key) => Column( children: [ const SizedBox(height: 20), Row( mainAxisAlignment: MainAxisAlignment.end, - children: [Image.asset('assets/images/rarity_${key.toLowerCase()}.webp', height: 20)], + children: [ + Image.asset('assets/images/rarity_${key.toLowerCase()}.webp', height: 20), + ], ), Card( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)), - margin: const EdgeInsets.all(0), + margin: EdgeInsets.zero, child: Padding( padding: const EdgeInsets.only(left: 6, right: 6, top: 6), child: GridView.builder( - padding: const EdgeInsets.all(0), - shrinkWrap: true, - itemCount: rarity2CardsGroup[key]!.length, - physics: const NeverScrollableScrollPhysics(), - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 5, childAspectRatio: 0.58, crossAxisSpacing: 6), - itemBuilder: (context, index) { - return MdCardItemView( - mdCard: rarity2CardsGroup[key]![index], - onTap: (card) => handleTapCardItem(rarity2CardsGroup[key]!, index), - ); - // return Container(color: Colors.white,); - }), + padding: EdgeInsets.zero, + shrinkWrap: true, + itemCount: rarity2CardsGroup[key]!.length, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 5, + childAspectRatio: 0.58, + crossAxisSpacing: 6, + ), + itemBuilder: (context, index) { + return MdCardItemView( + mdCard: rarity2CardsGroup[key]![index], + onTap: (card) => handleTapCardItem(rarity2CardsGroup[key]!, index), + ); + // return Container(color: Colors.white,); + }, + ), ), - ) + ), ], - )) - .toList(), + ), + ) + .toList(), + ), ), ), - ) - ], + ], + ), ), - ), - if (_pageStatus == PageStatus.loading) const Positioned(child: Center(child: Loading())) - ], + if (_pageStatus == PageStatus.fail) + const Positioned( + child: Center( + child: Text('Loading failed'), + ), + ), + ], + ), ), ); } diff --git a/lib/pages/packs/components/PackListView.dart b/lib/pages/packs/components/PackListView.dart index 1acd503..c84202c 100644 --- a/lib/pages/packs/components/PackListView.dart +++ b/lib/pages/packs/components/PackListView.dart @@ -1,14 +1,12 @@ +import 'package:animations/animations.dart'; import 'package:cached_network_image/cached_network_image.dart'; +import 'package:duel_links_meta/extension/DateTime.dart'; import 'package:duel_links_meta/pages/pack_detail/index.dart'; import 'package:duel_links_meta/type/pack_set/PackSet.dart'; -import 'package:duel_links_meta/util/time_util.dart'; -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; -import 'package:intl/intl.dart'; class PackListView extends StatefulWidget { - const PackListView({super.key, required this.packs}); + const PackListView({required this.packs, super.key}); final List packs; @@ -19,8 +17,11 @@ class PackListView extends StatefulWidget { class _PackListViewState extends State with AutomaticKeepAliveClientMixin { List get _packs => widget.packs; - handleTapPackItem(PackSet pack) { - Navigator.push(context, MaterialPageRoute(builder: (context) => PackDetailPage(pack: pack))); + Future handleTapPackItem(PackSet pack) async { + await Navigator.push( + context, + MaterialPageRoute(builder: (context) => PackDetailPage(pack: pack)), + ); } @override @@ -31,76 +32,69 @@ class _PackListViewState extends State with AutomaticKeepAliveClie super.build(context); return ListView.builder( - padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 8), - itemCount: _packs.length, - itemExtent: 118, - itemBuilder: (context, index) { - return GestureDetector( - onTap: () => handleTapPackItem(_packs[index]), - child: Column( - children: [ - Container( - height: 110, - child: Card( - margin: EdgeInsets.all(0), + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 8), + itemCount: _packs.length, + itemExtent: 118, + itemBuilder: (context, index) { + return Column( + children: [ + SizedBox( + height: 110, + child: OpenContainer( + closedColor: Colors.transparent, + closedBuilder: (BuildContext context, void Function() action) { + return Card( + margin: EdgeInsets.zero, shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(6))), - child: ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(6)), - child: Container( - color: Colors.pink, - child: Column( - children: [ - Stack( + clipBehavior: Clip.hardEdge, + child: Stack( + fit: StackFit.expand, + children: [ + CachedNetworkImage( + imageUrl: 'https://s3.duellinksmeta.com${_packs[index].bannerImage}', + fit: BoxFit.cover, + height: 110, + width: double.infinity, + ), + Positioned( + left: 0, + right: 0, + bottom: 0, + child: Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.centerRight, + end: Alignment.centerLeft, + colors: [Colors.black12, Colors.black87], + ), + ), + padding: const EdgeInsets.symmetric(vertical: 2, horizontal: 10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, children: [ - Hero( - tag: _packs[index].name, - child: CachedNetworkImage( - placeholder: (context, url) => Container(color: Colors.white70), - fadeInDuration: const Duration(milliseconds: 0), - imageUrl: 'https://s3.duellinksmeta.com${_packs[index].bannerImage}', - fit: BoxFit.cover, - height: 110, - fadeOutDuration: const Duration(milliseconds: 0), - width: double.infinity, + Text(_packs[index].name, style: const TextStyle(color: Colors.white, fontSize: 18)), + if (_packs[index].release != null) + Text( + 'Released on ${_packs[index].release?.format}', + style: const TextStyle(color: Colors.white), ), - ), - Positioned( - left: 0, - right: 0, - bottom: 0, - child: Container( - decoration: const BoxDecoration( - gradient: LinearGradient( - begin: Alignment.centerRight, - end: Alignment.centerLeft, - colors: [Colors.black12, Colors.black87], - ), - ), - padding: const EdgeInsets.symmetric(vertical: 2, horizontal: 12), - child: Container( - // color: Colors.white, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Text(_packs[index].name, style: const TextStyle(color: Colors.white, fontSize: 18)), - if (_packs[index].release != null) - Text('Released on ${TimeUtil.format(_packs[index].release)}', style: const TextStyle(color: Colors.white)) - ], - ), - ), - )) ], ), - ], + ), ), - ), + ], ), - ), - ), - ], + ); + }, + openBuilder: (BuildContext context, void Function({Object? returnValue}) action) { + return PackDetailPage(pack: _packs[index]); + }, + ), ), - ); - }); + ], + ); + }, + ); } } diff --git a/lib/pages/packs/index.dart b/lib/pages/packs/index.dart index a6765de..af7a2ca 100644 --- a/lib/pages/packs/index.dart +++ b/lib/pages/packs/index.dart @@ -1,15 +1,13 @@ import 'dart:developer'; -import 'package:duel_links_meta/components/IfElseBox.dart'; -import 'package:duel_links_meta/components/Loading.dart'; import 'package:duel_links_meta/extension/Future.dart'; -import 'package:duel_links_meta/hive/MyHive.dart'; +import 'package:duel_links_meta/hive/db/PacksHiveDb.dart'; import 'package:duel_links_meta/http/PackSetApi.dart'; import 'package:duel_links_meta/pages/packs/components/PackListView.dart'; import 'package:duel_links_meta/type/enum/PageStatus.dart'; -import 'package:duel_links_meta/type/pack_set/ExpireData.dart'; import 'package:duel_links_meta/type/pack_set/PackSet.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; class PacksPage extends StatefulWidget { const PacksPage({super.key}); @@ -20,61 +18,44 @@ class PacksPage extends StatefulWidget { class _PacksPageState extends State with SingleTickerProviderStateMixin, AutomaticKeepAliveClientMixin { TabController? _tabController; - String hiveBoxKey = 'pack_set:list'; - String packSetListLastFetchDateKey = 'pack_set:list_last_fetch_date'; - final _refreshIndicatorKey = GlobalKey(); - var _pageStatus = PageStatus.loading; + var _isInit = false; Map> _tabsMap = {}; // Future fetchData({bool force = false}) async { var reRefreshFlag = false; - final hiveValue = await MyHive.box2.get(hiveBoxKey) as List?; - final lastFetchDate = await MyHive.box2.get(packSetListLastFetchDateKey); - late List list; + var packs = await PackHiveDb.get(); + final expireTime = await PackHiveDb.getExpireTime(); - if (hiveValue == null || force) { - log('无本地数据或者强制刷新'); + if (packs == null || force) { + if (packs == null) { + log('无本地数据'); + } + if (force) { + log('强制刷新'); + } final (err, res) = await PackSetApi().list().toCatch; - if (err != null) { + if (err != null || res == null) { setState(() { _pageStatus = PageStatus.fail; }); return false; } - list = res!.map(PackSet.fromJson).toList(); - - MyHive.box2.put(hiveBoxKey, list); - MyHive.box2.put(packSetListLastFetchDateKey, DateTime.now()); + packs = res; + PackHiveDb.set(packs).ignore(); + PackHiveDb.setExpireTime(DateTime.now().add(const Duration(days: 1))).ignore(); } else { log('本地获取到数据'); - try { - list = hiveValue.map((e) => e as PackSet).toList(); - - if (lastFetchDate != null) { - // 超过刷新时间 - if ((lastFetchDate as DateTime).add(const Duration(seconds: 10)).isBefore(DateTime.now())) { - log('超过默认的数据有效时间,需要刷新'); - - reRefreshFlag = true; - } - } - log('转换成功'); - } catch (e) { - log('[fetchTopTiers] 转换失败: $e'); - await MyHive.box2.delete(hiveBoxKey); - await MyHive.box2.delete(packSetListLastFetchDateKey); - return true; - } + reRefreshFlag = expireTime == null || expireTime.isBefore(DateTime.now()); } final tabsMap = >{}; - for (final item in list) { + for (final item in packs) { if (tabsMap[item.type] == null) { tabsMap[item.type] = [item]; } else { @@ -101,21 +82,23 @@ class _PacksPageState extends State with SingleTickerProviderStateMix _tabController?.dispose(); } - Future init() async { - final needRefresh = await fetchData(); + Future _handleRefresh() async { + final needRefresh = await fetchData(force: _isInit); + + _isInit = true; + if (needRefresh) { - await _refreshIndicatorKey.currentState?.show(atTop: true); + await fetchData(force: true); } } @override void initState() { super.initState(); - init(); - } - Future _handleRefresh() async { - await fetchData(force: true); + SchedulerBinding.instance.addPostFrameCallback((_) { + _refreshIndicatorKey.currentState?.show(); + }); } @override @@ -123,48 +106,44 @@ class _PacksPageState extends State with SingleTickerProviderStateMix @override Widget build(BuildContext context) { + super.build(context); + return RefreshIndicator( key: _refreshIndicatorKey, + // 如果子控件是可滚动的, 需要声明notificationPredicate notificationPredicate: (_) => _pageStatus != PageStatus.loading, onRefresh: _handleRefresh, - child: Stack( - children: [ - if (_pageStatus == PageStatus.success) - Scaffold( - appBar: AppBar( - title: _tabController != null - ? TabBar( - isScrollable: true, - tabAlignment: TabAlignment.start, - controller: _tabController, - dividerHeight: 0, - indicatorSize: TabBarIndicatorSize.label, - tabs: _tabsMap.keys.map((key) => Tab(text: key)).toList(), - ) - : null, - ), - body: IfElseBox( - condition: _tabController != null, - ifTure: TabBarView( + child: Scaffold( + appBar: AppBar( + title: _tabController != null + ? TabBar( + isScrollable: true, + tabAlignment: TabAlignment.start, controller: _tabController, - children: _tabsMap.values.map((packs) => PackListView(packs: packs)).toList(), - ), + dividerHeight: 0, + indicatorSize: TabBarIndicatorSize.label, + tabs: _tabsMap.keys.map((key) => Tab(text: key)).toList(), + ) + : null, + ), + body: Stack( + fit: StackFit.expand, + children: [ + if (_tabController != null) + TabBarView( + controller: _tabController, + children: _tabsMap.values.map((packs) => PackListView(packs: packs)).toList(), + ), + if (_pageStatus == PageStatus.fail) + const SingleChildScrollView( + physics: AlwaysScrollableScrollPhysics(), ), - ), - if (_pageStatus == PageStatus.loading) const Positioned.fill(child: Center(child: Loading())), - if (_pageStatus == PageStatus.fail) - Positioned.fill( - child: Column( - // mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text('加载失败'), - ElevatedButton(onPressed: _refreshIndicatorKey.currentState?.show, child: const Text('刷新')), - ], + if (_pageStatus == PageStatus.fail) + const Center( + child: Text('Loading failed'), ), - ) - // Positioned.fill(child: Container(color: Colors.orange, child: Center(child: Text('失败')))) - ], + ], + ), ), ); } diff --git a/lib/pages/skill_stats/components/SampleBarChart.dart b/lib/pages/skill_stats/components/SampleBarChart.dart deleted file mode 100644 index 7c9f6bb..0000000 --- a/lib/pages/skill_stats/components/SampleBarChart.dart +++ /dev/null @@ -1,133 +0,0 @@ -import 'package:cached_network_image/cached_network_image.dart'; -import 'package:duel_links_meta/type/skill_stats/SkillStats.dart'; -import 'package:fl_chart/fl_chart.dart'; -import 'package:flutter/material.dart'; - -// TODO: https://github.com/imaNNeo/fl_chart/issues/406 -class SampleBarChart extends StatelessWidget { - const SampleBarChart(this.skillStats); - - final List skillStats; - - // var touchedIndex = -1; - @override - Widget build(BuildContext context) { - return BarChart( - BarChartData( - barTouchData: barTouchData, - titlesData: titlesData, - borderData: borderData, - barGroups: barGroups, - gridData: const FlGridData(show: false), - alignment: BarChartAlignment.spaceAround, - maxY: 100, - ), - swapAnimationDuration: const Duration(milliseconds: 500), - // swapAnimationDuration: Duration(milliseconds: 350), // Optional - // swapAnimationCurve: Curves.linear, // Optional - ); - } - - BarTouchData get barTouchData => BarTouchData( - enabled: false, - touchTooltipData: BarTouchTooltipData( - tooltipBgColor: Colors.transparent, - tooltipPadding: EdgeInsets.zero, - tooltipMargin: 4, - getTooltipItem: ( - BarChartGroupData group, - int groupIndex, - BarChartRodData rod, - int rodIndex, - ) { - return BarTooltipItem( - '${rod.toY.toStringAsFixed(0)}%', - const TextStyle(color: Colors.white, fontSize: 10, fontWeight: FontWeight.w500), - ); - }, - ), - // touchCallback: (FlTouchEvent event, barTouchResponse) { - // setState(() { - // if (!event.isInterestedForInteractions || barTouchResponse == null || barTouchResponse.spot == null) { - // touchedIndex = -1; - // return; - // } - // touchedIndex = barTouchResponse.spot!.touchedBarGroupIndex; - // }); - // }, - ); - - Widget getTitles(double value, TitleMeta meta) { - const style = TextStyle(color: Colors.white, fontSize: 6, overflow: TextOverflow.clip); - - return SideTitleWidget( - axisSide: meta.axisSide, - space: 4, - child: Container( - child: Column( - // mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - // SizedBox(height: value.toInt() %2 == 0 ? 10: 0,), - Container( - child: ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(15)), - child: Container( - width: 26, - height: 26, - child: CachedNetworkImage( - fit: BoxFit.cover, - imageUrl: 'https://imgserv.duellinksmeta.com/v2/dlm/deck-type/${skillStats[value.toInt()].name}?portrait=true&width=100', - ), - ), - ), - ), - Container(child: Text(skillStats[value.toInt()].name, style: style)), - // if (skillStats[value.toInt()].tier!= null) Container(child: Text(skillStats[value.toInt()].tier.toString(), style: style)), - ], - ), - ), - ); - } - - FlTitlesData get titlesData => FlTitlesData( - show: true, - bottomTitles: AxisTitles( - sideTitles: SideTitles( - showTitles: true, - reservedSize: 50, - getTitlesWidget: getTitles, - ), - ), - leftTitles: const AxisTitles( - sideTitles: SideTitles(showTitles: false), - ), - topTitles: const AxisTitles( - sideTitles: SideTitles(showTitles: false), - ), - rightTitles: const AxisTitles( - sideTitles: SideTitles(showTitles: false), - ), - ); - - FlBorderData get borderData => FlBorderData(show: false); - - // LinearGradient get _barsGradient => const LinearGradient( - List get barGroups => skillStats - .asMap() - .entries - .map( - (e) => BarChartGroupData( - x: e.key, - barRods: [ - BarChartRodData( - toY: e.value.percentage, - width: 14, - borderRadius: const BorderRadius.all(Radius.circular(4)), - color: skillStats[e.key].tier != null ? Colors.orange : Colors.blueAccent, - ), - ], - showingTooltipIndicators: [0], - ), - ) - .toList(); -} diff --git a/lib/pages/splash/BanStatusCardHiveDb.dart b/lib/pages/splash/BanStatusCardHiveDb.dart new file mode 100644 index 0000000..1c69346 --- /dev/null +++ b/lib/pages/splash/BanStatusCardHiveDb.dart @@ -0,0 +1,36 @@ +import 'package:duel_links_meta/hive/MyHive.dart'; + +class BanStatusCardHiveDb { + static const _banStatusCardIdsKey = 'ban_status:card_ids'; + static const _banStatusCardIdsFetchDateKey = 'ban_status:card_ids_refresh_date'; + + static Future?> getCardIds() async { + List? cardIds; + try { + cardIds = await MyHive.box2.get(_banStatusCardIdsKey) as List?; + } catch (e) { + return null; + } + + return cardIds; + } + + static Future getExpireTime() async { + DateTime? expireTime; + try { + expireTime = await MyHive.box2.get(_banStatusCardIdsFetchDateKey) as DateTime?; + } catch (e) { + return null; + } + + return expireTime; + } + + static Future setCardIds(List ids) { + return MyHive.box2.put(_banStatusCardIdsKey, ids); + } + + static Future setExpireTime(DateTime expireTime) { + return MyHive.box2.put(_banStatusCardIdsFetchDateKey, expireTime); + } +} diff --git a/lib/pages/splash/index.dart b/lib/pages/splash/index.dart index b3298bb..c5b95cb 100644 --- a/lib/pages/splash/index.dart +++ b/lib/pages/splash/index.dart @@ -1,9 +1,15 @@ import 'dart:async'; +import 'dart:developer'; -import 'package:duel_links_meta/hive/MyHive.dart'; +import 'package:duel_links_meta/extension/Future.dart'; +import 'package:duel_links_meta/hive/db/CardHiveDb.dart'; +import 'package:duel_links_meta/hive/db/DarkModeHiveDb.dart'; +import 'package:duel_links_meta/http/CardApi.dart'; import 'package:duel_links_meta/pages/main/index.dart'; -import 'package:duel_links_meta/pages/top_decks/index.dart'; -import 'package:duel_links_meta/util/storage/LocalStorage.dart'; +import 'package:duel_links_meta/pages/splash/BanStatusCardHiveDb.dart'; +import 'package:duel_links_meta/store/BanCardStore.dart'; +import 'package:duel_links_meta/type/MdCard.dart'; +import 'package:duel_links_meta/type/enum/PageStatus.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; @@ -19,6 +25,75 @@ class _SplashPageState extends State { late Timer _timer; + @override + void dispose() { + super.dispose(); + _timer.cancel(); + } + + Future fetchBanCards({bool force = false}) async { + final banCardStore = Get.put(BanCardStore()); + final cardIds = await BanStatusCardHiveDb.getCardIds(); + final expireDate = await BanStatusCardHiveDb.getExpireTime(); + var refreshFlag = false; + + var list = []; + + if (cardIds == null || force) { + if (cardIds == null) { + log('splash 获取本地ban cards为空'); + } + + final params = { + 'limit': '0', + r'banStatus[$exists]': 'true', + r'alternateArt[$ne]': 'true', + r'rush[$ne]': 'true', + // 'fields': 'oid,banStatus' + }; + + final (err, res) = await CardApi().list(params).toCatch; + if (err != null || res == null) { + banCardStore.setPageStatus(PageStatus.fail); + return false; + } + list = res; + + // 保存ids + await BanStatusCardHiveDb.setCardIds(list.map((e) => e.oid).toList()); + await BanStatusCardHiveDb.setExpireTime(DateTime.now().add(const Duration(days: 1))); + + list.forEach(CardHiveDb.setCard); + } else { + log('本地有ban cards数据'); + + final start = DateTime.now(); + for (var i = 0; i < cardIds.length; i++) { + final card = await CardHiveDb.get(cardIds[i]); + + list.add( + card ?? MdCard() + ..oid = cardIds[i], + ); + } + + log('读取本地数据消耗时间: ${DateTime.now().difference(start).inMilliseconds}'); + + // 过期 + if (expireDate == null || expireDate.isBefore(DateTime.now())) { + refreshFlag = true; + } + } + + log('获取 ban cards 成功'); + banCardStore + ..setCards(list) + ..setPageStatus(PageStatus.success); + + return refreshFlag; + } + + // void startCounterDown() { _timer = Timer.periodic(const Duration(seconds: 1), (timer) async { if (_count <= 0) { @@ -26,12 +101,8 @@ class _SplashPageState extends State { await Navigator.pushAndRemoveUntil( context, - MaterialPageRoute( - builder: (context) => const MainPage(), - // builder: (context) => const TopDecksPage(isRush: false,), - ), + MaterialPageRoute(builder: (context) => const MainPage()), (route) => false, //if you want to disable back feature set to false - // ModalRoute.withName('/') ); return; @@ -44,19 +115,23 @@ class _SplashPageState extends State { } Future initDarkMode() async { - // final mode = await LocalStorage_DarkMode.get(); - final mode = await MyHive.box2.get('dark_mode'); + final mode = await DarkModeHiveDb.get(); - if (mode == 'dark') { - Get.changeThemeMode(ThemeMode.dark); + if (mode != ThemeMode.light) { + Get.changeThemeMode(mode); } } - void init() { - initDarkMode(); + Future init() async { + await initDarkMode(); + startCounterDown(); - // - // Db.init(); + + final shouldFetchBanCards = await fetchBanCards(); + + if (shouldFetchBanCards) { + await fetchBanCards(force: true); + } } @override diff --git a/lib/pages/tier_list/index.dart b/lib/pages/tier_list/index.dart index fb2d3e8..117f39d 100644 --- a/lib/pages/tier_list/index.dart +++ b/lib/pages/tier_list/index.dart @@ -1,7 +1,8 @@ -import 'package:duel_links_meta/constant/colors.dart'; import 'package:duel_links_meta/pages/tier_list/components/TierListView.dart'; import 'package:duel_links_meta/pages/tier_list/type/TierListType.dart'; +import 'package:duel_links_meta/store/AppStore.dart'; import 'package:flutter/material.dart'; +import 'package:get/get.dart'; class TierListPage extends StatefulWidget { const TierListPage({super.key}); @@ -13,6 +14,8 @@ class TierListPage extends StatefulWidget { class _TierListPageState extends State with SingleTickerProviderStateMixin { late final TabController _tabController = TabController(length: 3, vsync: this); + AppStore appStore = Get.put(AppStore()); + @override void initState() { super.initState(); @@ -22,7 +25,7 @@ class _TierListPageState extends State with SingleTickerProviderSt Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: const Text('Tier List'), + title: const Text('Tier List') , bottom: TabBar( // indicatorColor: Colors.transparent, controller: _tabController, diff --git a/lib/pages/top_decks/components/TopDeckListView.dart b/lib/pages/top_decks/components/TopDeckListView.dart index 211ab82..d3d712e 100644 --- a/lib/pages/top_decks/components/TopDeckListView.dart +++ b/lib/pages/top_decks/components/TopDeckListView.dart @@ -30,7 +30,7 @@ class _TopDeckListViewState extends State { Expanded( child: Container( padding: const EdgeInsets.only(left: 42), - child: const Text('Skill', style: TextStyle(fontSize: 13),), + child: const Text('Skill', style: TextStyle(fontSize: 13)), ), ), Container( @@ -46,86 +46,91 @@ class _TopDeckListViewState extends State { Container( width: 80, padding: const EdgeInsets.symmetric(horizontal: 4), - child: const Text('Date', style: TextStyle(fontSize: 13),), + child: const Text('Date', style: TextStyle(fontSize: 13)), ) ], ), ), Expanded( - child: ListView.builder( - itemCount: _topDecks.length, - itemBuilder: (context, index) { - return InkWell( - onTap: ()=> widget.onTap?.call(_topDecks[index]), - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 4), - child: Row( - children: [ - ClipRRect( - borderRadius: BorderRadius.circular(3), - child: SizedBox( - height: 32, - width: 32, - child: CachedNetworkImage( + child: Container( + color: Theme.of(context).colorScheme.surfaceVariant, + child: ListView.builder( + itemCount: _topDecks.length, + itemBuilder: (context, index) { + return InkWell( + onTap: ()=> widget.onTap?.call(_topDecks[index]), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 4), + child: Row( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(3), + child: SizedBox( + height: 32, width: 32, - fit: BoxFit.cover, - imageUrl: - 'https://imgserv.duellinksmeta.com/v2/dlm/deck-type/${Uri.encodeComponent(_topDecks[index].deckType.name)}?portrait=true&width=50', + child: CachedNetworkImage( + width: 32, + fit: BoxFit.cover, + imageUrl: + 'https://imgserv.duellinksmeta.com/v2/dlm/deck-type/${Uri.encodeComponent(_topDecks[index].deckType.name)}?portrait=true&width=50', + ), ), ), - ), - Expanded( - child: Container( - padding: const EdgeInsets.only(left: 4), - child: Text( - _topDecks[index].skill?.name ?? '', - style: const TextStyle(fontSize: 12), + Expanded( + child: Container( + padding: const EdgeInsets.only(left: 4), + child: Text( + _topDecks[index].skill?.name ?? '', + style: const TextStyle(fontSize: 12), + overflow: TextOverflow.ellipsis, + maxLines: 2, + ), ), ), - ), - SizedBox( - width: 40, - child: Center( - child: CachedNetworkImage( - width: 28, - fit: BoxFit.cover, - imageUrl: - 'https://wsrv.nl/?url=https://s3.duellinksmeta.com${_topDecks[index].tournamentType?.icon ?? _topDecks[index].rankedType?.icon}&w=100&output=webp&we&n=-1&maxage=7d', + SizedBox( + width: 45, + child: Center( + child: CachedNetworkImage( + width: 32, + fit: BoxFit.cover, + imageUrl: + 'https://wsrv.nl/?url=https://s3.duellinksmeta.com${_topDecks[index].tournamentType?.icon ?? _topDecks[index].rankedType?.icon}&w=100&output=webp&we&n=-1&maxage=7d', + ), ), ), - ), - SizedBox( - width: 90, - child: Row( - children: [ - Assets.images.iconGem.image(width: 16), - SizedBox(width: 2,), - Text( - '${(_topDecks[index].gemsPrice / 1000).toStringAsFixed(0)}k', - style: const TextStyle(fontSize: 12), - ), - if (_topDecks[index].dollarsPrice > 0) + SizedBox( + width: 90, + child: Row( + children: [ + Assets.images.iconGem.image(width: 16), + SizedBox(width: 2,), Text( - '+ \$${_topDecks[index].dollarsPrice}', + '${(_topDecks[index].gemsPrice / 1000).toStringAsFixed(0)}k', style: const TextStyle(fontSize: 12), ), - ], - ), - ), - Container( - padding: const EdgeInsets.only(right: 2), - width: 80, - child: Text( - _topDecks[index].created?.toLocal().format ?? '', - style: const TextStyle(fontSize: 12), - textAlign: TextAlign.right, + if (_topDecks[index].dollarsPrice > 0) + Text( + '+ \$${_topDecks[index].dollarsPrice}', + style: const TextStyle(fontSize: 12), + ), + ], + ), ), - ) - ], + Container( + padding: const EdgeInsets.only(right: 2), + width: 80, + child: Text( + _topDecks[index].created?.toLocal().format ?? '', + style: const TextStyle(fontSize: 12), + textAlign: TextAlign.right, + ), + ) + ], + ), ), - ), - ); - }, + ); + }, + ), ), ), ], diff --git a/lib/pages/top_decks/index.dart b/lib/pages/top_decks/index.dart index 2dcdccf..170788e 100644 --- a/lib/pages/top_decks/index.dart +++ b/lib/pages/top_decks/index.dart @@ -1,7 +1,5 @@ import 'dart:developer'; -import 'dart:ffi'; -import 'package:cached_network_image/cached_network_image.dart'; import 'package:duel_links_meta/components/TopDeckItem.dart'; import 'package:duel_links_meta/extension/Future.dart'; import 'package:duel_links_meta/hive/MyHive.dart'; @@ -11,12 +9,11 @@ import 'package:duel_links_meta/pages/top_decks/components/TopDeckListView.dart' import 'package:duel_links_meta/pages/top_decks/type/Group.dart'; import 'package:duel_links_meta/type/enum/PageStatus.dart'; import 'package:duel_links_meta/type/top_deck/TopDeck.dart'; -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; +import 'package:flutter/scheduler.dart'; class TopDecksPage extends StatefulWidget { - const TopDecksPage({super.key, required this.isRush}); + const TopDecksPage({required this.isRush, super.key}); final bool isRush; @@ -31,11 +28,11 @@ class _TopDecksPageState extends State { List> _tournamentTypeTopDeckGroups = []; final _refreshIndicatorKey = GlobalKey(); - var _init = false; + var _isInit = false; int? selectedTournamentType; - DateTime now = DateTime.now(); + final DateTime _now = DateTime.now(); bool get _isRush => widget.isRush; @@ -53,7 +50,6 @@ class _TopDecksPageState extends State { shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(2)), ), - // backgroundColor: Colors.transparent, context: context, builder: (context) => TopDeckListView( topDecks: group.data, @@ -64,6 +60,7 @@ class _TopDecksPageState extends State { ); } + // Future fetchData({bool force = false}) async { final hiveDataKey = 'top_deck:list:${_isRush ? 'rush' : 'speed'}'; final hiveRefreshKey = 'top_deck:list:${_isRush ? 'rush' : 'speed'}:refresh'; @@ -78,10 +75,10 @@ class _TopDecksPageState extends State { final params = { r'created[$gte]': '(days-28)', 'fields': 'rankedType,deckType,created,tournamentType,gemsPrice,dollarsPrice,url,skill', - // 'fields': '-', 'limit': '0', - // r'rush[$ne]': _isRush ? 'true' : 'false', 'rush': _isRush ? 'true' : 'false', + // 'fields': '-', + // r'rush[$ne]': _isRush ? 'true' : 'false', }; final (err, res) = await TopDeckApi().list(params).toCatch; @@ -94,18 +91,19 @@ class _TopDecksPageState extends State { } topDecks = res!; - MyHive.box2.put(hiveDataKey, topDecks); - MyHive.box2.put(hiveRefreshKey, DateTime.now()); + await MyHive.box2.put(hiveDataKey, topDecks); + await MyHive.box2.put(hiveRefreshKey, DateTime.now()); } else { - await Future.delayed(const Duration(milliseconds: 200)); try { topDecks = hiveData.map((e) => e as TopDeck).toList(); if (hiveRefreshDate != null && hiveRefreshDate.add(const Duration(hours: 12)).isBefore(DateTime.now())) { refreshFlag = true; } } catch (e) { - MyHive.box2.delete(hiveDataKey); - MyHive.box2.delete(hiveRefreshKey); + await MyHive.box2.delete(hiveDataKey); + await MyHive.box2.delete(hiveRefreshKey); + + return true; } } @@ -210,10 +208,9 @@ class _TopDecksPageState extends State { } Future init() async { - log('init ${_refreshIndicatorKey.currentState}'); - await Future.delayed(Duration(milliseconds: 10)); - - await _refreshIndicatorKey.currentState?.show(); + SchedulerBinding.instance.addPostFrameCallback((timeStamp) { + _refreshIndicatorKey.currentState?.show(); + }); } @override @@ -224,10 +221,11 @@ class _TopDecksPageState extends State { } Future _handleRefresh() async { - log('触发下拉刷新'); - - await fetchData(force: _init); - _init = true; + final shouldRefresh = await fetchData(force: _isInit); + _isInit = true; + if (shouldRefresh) { + await fetchData(force: true); + } } @override @@ -235,71 +233,33 @@ class _TopDecksPageState extends State { return Scaffold( appBar: AppBar( title: const Text('Top Decks'), - actions: [ - IconButton(onPressed: fetchData, icon: const Icon(Icons.refresh)), - // IconButton(onPressed: _showFilterPopup, icon: const Icon(Icons.filter_list)) - ], ), - body: Builder(builder: (context) { - return RefreshIndicator( - onRefresh: _handleRefresh, - key: _refreshIndicatorKey, - // notificationPredicate: (_) => _pageStatus != PageStatus.loading, - child: Stack( - children: [ - SingleChildScrollView( - physics: const AlwaysScrollableScrollPhysics(), - child: Stack( - children: [ - Column( - children: [ - GridView.builder( - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2, childAspectRatio: 4), - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - padding: const EdgeInsets.only(top: 8, left: 4, right: 4, bottom: 4), - itemCount: _tournamentTypeTopDeckGroups.length, - itemBuilder: (BuildContext context, int index) { - return TopDeckItem( - topDeck: TopDeck()..deckType.name = _tournamentTypeTopDeckGroups[index].key, - isActive: index == selectedTournamentType, - bottomRight: Container( - padding: const EdgeInsets.symmetric(horizontal: 2), - decoration: const BoxDecoration( - color: Color(0xc5dbe2dc), - borderRadius: BorderRadius.only( - topLeft: Radius.circular(6), - ), - border: BorderDirectional( - top: BorderSide(color: Colors.white, width: 0.5), - start: BorderSide(color: Colors.white, width: 0.5), - ), - ), - child: Text( - _tournamentTypeTopDeckGroups[index].data.length.toString(), - // _topDeckGroups[index].data[0].created!.toLocal().format, - style: const TextStyle(fontSize: 9), - ), - ), - coverUrl: - 'https://wsrv.nl/?url=https://s3.duellinksmeta.com${_tournamentTypeTopDeckGroups[index].data[0].tournamentType?.icon ?? _tournamentTypeTopDeckGroups[index].data[0].rankedType?.icon}&w=100&output=webp&we&n=-1&maxage=7d', - onTap: () => _handleFilter(index), - ); - }, - ), - GridView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - padding: const EdgeInsets.only(top: 8, left: 4, right: 4, bottom: 4), - itemCount: _showTopDecks.length, - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 2, childAspectRatio: 4, crossAxisSpacing: 0, mainAxisSpacing: 0), - itemBuilder: (context, index) { - return Column( - children: [ - TopDeckItem( - onTap: () => _handleTapTopDeckItem(_showTopDecks[index]), - topDeck: _showTopDecks[index].data[0], + body: Builder( + builder: (context) { + return RefreshIndicator( + onRefresh: _handleRefresh, + key: _refreshIndicatorKey, + child: Stack( + children: [ + SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + child: Stack( + children: [ + AnimatedOpacity( + opacity: _pageStatus == PageStatus.success ? 1 : 0, + duration: const Duration(milliseconds: 300), + child: Column( + children: [ + GridView.builder( + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2, childAspectRatio: 4), + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + padding: const EdgeInsets.only(top: 8, left: 4, right: 4, bottom: 4), + itemCount: _tournamentTypeTopDeckGroups.length, + itemBuilder: (BuildContext context, int index) { + return TopDeckItem( + topDeck: TopDeck()..deckType.name = _tournamentTypeTopDeckGroups[index].key, + isActive: index == selectedTournamentType, bottomRight: Container( padding: const EdgeInsets.symmetric(horizontal: 2), decoration: const BoxDecoration( @@ -313,51 +273,93 @@ class _TopDecksPageState extends State { ), ), child: Text( - _showTopDecks[index].data.length.toString(), - style: const TextStyle(fontSize: 9), + _tournamentTypeTopDeckGroups[index].data.length.toString(), + style: const TextStyle(fontSize: 9, color: Colors.black54), ), ), - isNew: now.difference(_showTopDecks[index].data[0].created!.toLocal()).inHours < 72, - topLeft: _showTopDecks[index].data[0].deckType.tier != null - ? Container( - width: 20, - height: 19, - decoration: BoxDecoration( - color: _tier2colorMap[_showTopDecks[index].data[0].deckType.tier!] ?? Colors.white, - borderRadius: const BorderRadius.only(bottomRight: Radius.circular(4)), + coverUrl: + 'https://wsrv.nl/?url=https://s3.duellinksmeta.com${_tournamentTypeTopDeckGroups[index].data[0].tournamentType?.icon ?? _tournamentTypeTopDeckGroups[index].data[0].rankedType?.icon}&w=100&output=webp&we&n=-1&maxage=7d', + onTap: () => _handleFilter(index), + ); + }, + ), + GridView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + padding: const EdgeInsets.only(top: 8, left: 4, right: 4, bottom: 4), + itemCount: _showTopDecks.length, + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + childAspectRatio: 4, + crossAxisSpacing: 0, + mainAxisSpacing: 0, + ), + itemBuilder: (context, index) { + return Column( + children: [ + TopDeckItem( + onTap: () => _handleTapTopDeckItem(_showTopDecks[index]), + topDeck: _showTopDecks[index].data[0], + bottomRight: Container( + padding: const EdgeInsets.symmetric(horizontal: 2), + decoration: const BoxDecoration( + color: Color(0xc5dbe2dc), + borderRadius: BorderRadius.only( + topLeft: Radius.circular(6), ), - child: Center( - child: SizedBox( - width: 13, - child: Image.asset( - 'assets/images/tier_m_${_showTopDecks[index].data[0].deckType.tier}.webp', - fit: BoxFit.contain, - ), - ), + border: BorderDirectional( + top: BorderSide(color: Colors.white, width: 0.5), + start: BorderSide(color: Colors.white, width: 0.5), ), - ) - : null, - ), - ], - ); - }, + ), + child: Text( + _showTopDecks[index].data.length.toString(), + style: const TextStyle(fontSize: 9, color: Colors.black54), + ), + ), + isNew: _now.difference(_showTopDecks[index].data[0].created!.toLocal()).inHours < 72, + topLeft: _showTopDecks[index].data[0].deckType.tier != null + ? Container( + width: 20, + height: 19, + decoration: BoxDecoration( + color: _tier2colorMap[_showTopDecks[index].data[0].deckType.tier!] ?? Colors.white, + borderRadius: const BorderRadius.only(bottomRight: Radius.circular(4)), + ), + child: Center( + child: SizedBox( + width: 13, + child: Image.asset( + 'assets/images/tier_m_${_showTopDecks[index].data[0].deckType.tier}.webp', + fit: BoxFit.contain, + ), + ), + ), + ) + : null, + ), + ], + ); + }, + ), + ], ), - ], - ), - Opacity( - opacity: _pageStatus == PageStatus.fail ? 1 : 0, - child: SizedBox( - height: MediaQuery.of(context).size.height - (Scaffold.of(context).appBarMaxHeight ?? 0), - child: const Center(child: Text('加载失败')), ), - ) - ], + Opacity( + opacity: _pageStatus == PageStatus.fail ? 1 : 0, + child: SizedBox( + height: MediaQuery.of(context).size.height - (Scaffold.of(context).appBarMaxHeight ?? 0), + child: const Center(child: Text('Loading failed')), + ), + ) + ], + ), ), - ), - ], - ), - ); - }), + ], + ), + ); + }, + ), ); } } diff --git a/lib/repository/TierListTopTierRepository.dart b/lib/repository/TierListTopTierRepository.dart deleted file mode 100644 index baf21cf..0000000 --- a/lib/repository/TierListTopTierRepository.dart +++ /dev/null @@ -1,25 +0,0 @@ -import '../hive/MyHive.dart'; -import '../type/tier_list_top_tier/TierList_TopTier.dart'; - -class TierListTopTierRepository { - - - static Future?> get() async { - var list = []; - print('[fetchTopTiers] 从box取值开始'); - - const hiveBoxKey = 'tier_list:top_tier'; - // if (!force) { - var hiveValue = await MyHive.box2.get(hiveBoxKey); - print('[fetchTopTiers] box取值,value: $hiveValue, value == null ${hiveValue == null}, value type: ${hiveValue.runtimeType}'); - // } - - if (hiveValue == null){ - return null; - } - - - - } - -} \ No newline at end of file diff --git a/lib/store/AppStore.dart b/lib/store/AppStore.dart index 9f1b8e7..d3ed893 100644 --- a/lib/store/AppStore.dart +++ b/lib/store/AppStore.dart @@ -2,7 +2,9 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; class AppStore extends GetxController{ - Rx themeMode = ThemeMode.light.obs; - - void changeThemeMode(ThemeMode mode) => themeMode.value = mode; + // Rx themeMode = ThemeMode.light.obs; + // + // void changeThemeMode(ThemeMode mode) { + // themeMode.value = mode; + // } } \ No newline at end of file diff --git a/lib/store/BanCardStore.dart b/lib/store/BanCardStore.dart new file mode 100644 index 0000000..72ff9d6 --- /dev/null +++ b/lib/store/BanCardStore.dart @@ -0,0 +1,40 @@ +import 'package:duel_links_meta/type/MdCard.dart'; +import 'package:duel_links_meta/type/enum/PageStatus.dart'; +import 'package:get/get.dart'; + +class BanCardStore extends GetxController { + + RxList cards = [].obs; + + Rx pageStatus = PageStatus.loading.obs; + + Map idToCardMap = {}; + + Rx>> group = Rx>>({ + 'Forbidden': [], + 'Limited 1': [], + 'Limited 2': [], + 'Limited 3': [], + }); + + void setCards(List data) { + final _group = >{ + 'Forbidden': [], + 'Limited 1': [], + 'Limited 2': [], + 'Limited 3': [], + }; + data.forEach((item) { + _group[item.banStatus]?.add(item); + + idToCardMap[item.oid] = item; + }); + + group.value = _group; + cards.value = data; + } + + void setPageStatus(PageStatus value) { + pageStatus.value = value; + } +} \ No newline at end of file diff --git a/lib/type/Article.dart b/lib/type/Article.dart index 83c5262..a27c0a1 100644 --- a/lib/type/Article.dart +++ b/lib/type/Article.dart @@ -28,7 +28,7 @@ class Article { Article(); // factory Article.fromJson(Map json) => _$ArticleFromJson(json); - factory Article.fromJson(dynamic json) => _$ArticleFromJson(json); + factory Article.fromJson(dynamic json) => _$ArticleFromJson(json as Map); dynamic toJson() => _$ArticleToJson(this); } diff --git a/lib/type/MdCard.dart b/lib/type/MdCard.dart index 1241579..c19fcc3 100644 --- a/lib/type/MdCard.dart +++ b/lib/type/MdCard.dart @@ -8,13 +8,13 @@ part 'MdCard.g.dart'; @HiveType(typeId: MyHive.md_card) class MdCard { @HiveField(0) - int? atk = 0; + int? atk; @HiveField(1) String? attribute = ''; @HiveField(2) - int? def = 0; + int? def; @HiveField(3) String description = ''; @@ -56,7 +56,7 @@ class MdCard { MdCard(); - factory MdCard.fromJson(dynamic json) => _$MdCardFromJson(json); + factory MdCard.fromJson(dynamic json) => _$MdCardFromJson(json as Map); dynamic toJson() => _$MdCardToJson(this); } @@ -76,7 +76,7 @@ class MdCard_Obtain { MdCard_Obtain(); - factory MdCard_Obtain.fromJson(dynamic json) => _$MdCard_ObtainFromJson(json); + factory MdCard_Obtain.fromJson(Map json) => _$MdCard_ObtainFromJson(json); dynamic toJson() => _$MdCard_ObtainToJson(this); } @@ -94,7 +94,7 @@ class MdCard_Obtain_Source { MdCard_Obtain_Source(); - factory MdCard_Obtain_Source.fromJson(dynamic json) => _$MdCard_Obtain_SourceFromJson(json); + factory MdCard_Obtain_Source.fromJson(dynamic json) => _$MdCard_Obtain_SourceFromJson(json as Map); dynamic toJson() => _$MdCard_Obtain_SourceToJson(this); } diff --git a/lib/type/MdCard.g.dart b/lib/type/MdCard.g.dart index 0c38982..17f5c95 100644 --- a/lib/type/MdCard.g.dart +++ b/lib/type/MdCard.g.dart @@ -91,8 +91,9 @@ MdCard _$MdCardFromJson(Map json) => MdCard() ..monsterType = (json['monsterType'] as List).map((e) => e as String).toList() ..name = json['name'] as String - ..obtain = - (json['obtain'] as List).map(MdCard_Obtain.fromJson).toList() + ..obtain = (json['obtain'] as List) + .map((e) => MdCard_Obtain.fromJson(e as Map)) + .toList() ..race = json['race'] as String ..rarity = json['rarity'] as String? ?? '' ..release = diff --git a/lib/type/NavTab.dart b/lib/type/NavTab.dart index 3679667..cd0a099 100644 --- a/lib/type/NavTab.dart +++ b/lib/type/NavTab.dart @@ -22,7 +22,7 @@ class NavTab { NavTab({required this.id, this.title = ''}); - factory NavTab.fromJson(dynamic json) => _$NavTabFromJson(json); + factory NavTab.fromJson(dynamic json) => _$NavTabFromJson(json as Map); Map toJson() => _$NavTabToJson(this); diff --git a/lib/type/ban_list_change/BanListChange.dart b/lib/type/ban_list_change/BanListChange.dart index b66bbda..f17771d 100644 --- a/lib/type/ban_list_change/BanListChange.dart +++ b/lib/type/ban_list_change/BanListChange.dart @@ -1,16 +1,23 @@ import 'package:duel_links_meta/hive/MyHive.dart'; +import 'package:duel_links_meta/type/MdCard.dart'; import 'package:hive/hive.dart'; import 'package:json_annotation/json_annotation.dart'; -import 'package:duel_links_meta/type/MdCard.dart'; - part 'BanListChange.g.dart'; @JsonSerializable() @HiveType(typeId: MyHive.ban_list_chnage) class BanListChange { + + BanListChange(); + + factory BanListChange.fromJson(dynamic json) => _$BanListChangeFromJson(json as Map); + + dynamic toJson() => _$BanListChangeToJson(this); + @HiveField(0) DateTime? announced; + @HiveField(1) DateTime? date; @@ -25,22 +32,18 @@ class BanListChange { @JsonKey(defaultValue: '') String oid = ''; - // BanListChange_LinkedArticle? linkedArticle; - @HiveField(4) @JsonKey(defaultValue: []) List changes = [] ; - - BanListChange(); - - factory BanListChange.fromJson(dynamic json) => _$BanListChangeFromJson(json); - - dynamic toJson() => _$BanListChangeToJson(this); } @JsonSerializable() @HiveType(typeId: MyHive.ban_list_chnage_changes) class BanListChange_Change { + + BanListChange_Change(); + + factory BanListChange_Change.fromJson(dynamic json) => _$BanListChange_ChangeFromJson(json as Map); @HiveField(0) String? to = ''; @@ -53,16 +56,16 @@ class BanListChange_Change { @JsonKey(includeFromJson: false) MdCard card2 = MdCard(); - BanListChange_Change(); - - factory BanListChange_Change.fromJson(dynamic json) => _$BanListChange_ChangeFromJson(json); - dynamic toJson() => _$BanListChange_ChangeToJson(this); } @JsonSerializable() @HiveType(typeId: MyHive.ban_list_chnage_changes_card) class BanListChange_Change_Card { + + BanListChange_Change_Card(); + + factory BanListChange_Change_Card.fromJson(dynamic json) => _$BanListChange_Change_CardFromJson(json as Map); @JsonKey(name: '_id', defaultValue: '') @HiveField(0) String oid = ''; @@ -71,15 +74,15 @@ class BanListChange_Change_Card { @JsonKey(defaultValue: '') String name = ''; - BanListChange_Change_Card(); - - factory BanListChange_Change_Card.fromJson(dynamic json) => _$BanListChange_Change_CardFromJson(json); - dynamic toJson() => _$BanListChange_Change_CardToJson(this); } @JsonSerializable() class BanListChange_LinkedArticle { + + BanListChange_LinkedArticle(); + + factory BanListChange_LinkedArticle.fromJson(dynamic json) => _$BanListChange_LinkedArticleFromJson(json as Map); @JsonKey(defaultValue: '') String oid = ''; @@ -89,9 +92,5 @@ class BanListChange_LinkedArticle { @JsonKey(defaultValue: '') String title = ''; - BanListChange_LinkedArticle(); - - factory BanListChange_LinkedArticle.fromJson(dynamic json) => _$BanListChange_LinkedArticleFromJson(json); - dynamic toJson() => _$BanListChange_LinkedArticleToJson(this); } diff --git a/lib/type/ban_list_change/BanStatusCard.dart b/lib/type/ban_list_change/BanStatusCard.dart index 00d72cd..4018e7e 100644 --- a/lib/type/ban_list_change/BanStatusCard.dart +++ b/lib/type/ban_list_change/BanStatusCard.dart @@ -8,6 +8,10 @@ part 'BanStatusCard.g.dart'; @HiveType(typeId: MyHive.ban_status_card) class BanStatusCard { + BanStatusCard(); + + factory BanStatusCard.fromJson(dynamic json) => _$BanStatusCardFromJson(json as Map); + @HiveField(0) @JsonKey(name: '_id') String oid = ''; @@ -15,9 +19,5 @@ class BanStatusCard { @HiveField(1) String? banStatus; - BanStatusCard(); - - factory BanStatusCard.fromJson(dynamic json) => _$BanStatusCardFromJson(json); - Map toJson() => _$BanStatusCardToJson(this); -} \ No newline at end of file +} diff --git a/lib/type/character/Character.dart b/lib/type/character/Character.dart index 44967b9..63f23c0 100644 --- a/lib/type/character/Character.dart +++ b/lib/type/character/Character.dart @@ -4,6 +4,11 @@ part 'Character.g.dart'; @JsonSerializable() class Character { + + Character(); + + factory Character.fromJson(dynamic json) => _$CharacterFromJson(json as Map); + @JsonKey(name: '_id', defaultValue: '') String oid = ''; @@ -19,16 +24,17 @@ class Character { @JsonKey(defaultValue: []) List worlds = []; - Character(); - - factory Character.fromJson(dynamic json) => _$CharacterFromJson(json); - Map toJson() => _$CharacterToJson(this); } @JsonSerializable() class Character_LinkedArticle { + + Character_LinkedArticle(); + + factory Character_LinkedArticle.fromJson(dynamic json) => _$Character_LinkedArticleFromJson(json as Map); + @JsonKey(defaultValue: '') String description = ''; @@ -42,16 +48,15 @@ class Character_LinkedArticle { @JsonKey(defaultValue: '') String image = ''; - - Character_LinkedArticle(); - - factory Character_LinkedArticle.fromJson(dynamic json) => _$Character_LinkedArticleFromJson(json); - Map toJson() => _$Character_LinkedArticleToJson(this); } @JsonSerializable() class Character_World { + + Character_World(); + + factory Character_World.fromJson(dynamic json) => _$Character_WorldFromJson(json as Map); @JsonKey(defaultValue: '') String name = ''; @@ -61,9 +66,5 @@ class Character_World { @JsonKey(name: '_id', defaultValue: '') String oid = ''; - Character_World(); - - factory Character_World.fromJson(dynamic json) => _$Character_WorldFromJson(json); - Map toJson() => _$Character_WorldToJson(this); } diff --git a/lib/type/deck_type/DeckType.dart b/lib/type/deck_type/DeckType.dart index a320bac..2cb47cf 100644 --- a/lib/type/deck_type/DeckType.dart +++ b/lib/type/deck_type/DeckType.dart @@ -25,7 +25,7 @@ class DeckType { DeckType(); - factory DeckType.fromJson(dynamic json) => _$DeckTypeFromJson(json); + factory DeckType.fromJson(dynamic json) => _$DeckTypeFromJson(json as Map); Map toJson() => _$DeckTypeToJson(this); } @@ -50,7 +50,7 @@ class DeckType_DeckBreakdown { DeckType_DeckBreakdown(); - factory DeckType_DeckBreakdown.fromJson(dynamic json) => _$DeckType_DeckBreakdownFromJson(json); + factory DeckType_DeckBreakdown.fromJson(Map json) => _$DeckType_DeckBreakdownFromJson(json); Map toJson() => _$DeckType_DeckBreakdownToJson(this); } @@ -78,7 +78,7 @@ class DeckType_DeckBreakdownCards { DeckType_DeckBreakdownCards(); - factory DeckType_DeckBreakdownCards.fromJson(dynamic json) => _$DeckType_DeckBreakdownCardsFromJson(json); + factory DeckType_DeckBreakdownCards.fromJson(Map json) => _$DeckType_DeckBreakdownCardsFromJson(json); Map toJson() => _$DeckType_DeckBreakdownCardsToJson(this); } @@ -100,7 +100,7 @@ class DeckType_DeckBreakdown_Skill { DeckType_DeckBreakdown_Skill(); - factory DeckType_DeckBreakdown_Skill.fromJson(dynamic json) => _$DeckType_DeckBreakdown_SkillFromJson(json); + factory DeckType_DeckBreakdown_Skill.fromJson(dynamic json) => _$DeckType_DeckBreakdown_SkillFromJson(json as Map); Map toJson() => _$DeckType_DeckBreakdown_SkillToJson(this); } diff --git a/lib/type/deck_type/DeckType.g.dart b/lib/type/deck_type/DeckType.g.dart index b8f44c4..e37e059 100644 --- a/lib/type/deck_type/DeckType.g.dart +++ b/lib/type/deck_type/DeckType.g.dart @@ -197,7 +197,8 @@ DeckType _$DeckTypeFromJson(Map json) => DeckType() ..name = json['name'] as String ..thumbnailImage = json['thumbnailImage'] as String ..oid = json['_id'] as String - ..deckBreakdown = DeckType_DeckBreakdown.fromJson(json['deckBreakdown']); + ..deckBreakdown = DeckType_DeckBreakdown.fromJson( + json['deckBreakdown'] as Map); Map _$DeckTypeToJson(DeckType instance) => { 'card': instance.card, @@ -217,7 +218,8 @@ DeckType_DeckBreakdown _$DeckType_DeckBreakdownFromJson( .map(DeckType_DeckBreakdown_Skill.fromJson) .toList() ..cards = (json['cards'] as List) - .map(DeckType_DeckBreakdownCards.fromJson) + .map((e) => + DeckType_DeckBreakdownCards.fromJson(e as Map)) .toList(); Map _$DeckType_DeckBreakdownToJson( diff --git a/lib/type/deck_type/TierList_PowerRanking.dart b/lib/type/deck_type/TierList_PowerRanking.dart index f59962c..aeffa04 100644 --- a/lib/type/deck_type/TierList_PowerRanking.dart +++ b/lib/type/deck_type/TierList_PowerRanking.dart @@ -25,7 +25,7 @@ class TierList_PowerRanking { TierList_PowerRanking(); - factory TierList_PowerRanking.fromJson(dynamic json)=> _$TierList_PowerRankingFromJson(json); + factory TierList_PowerRanking.fromJson(dynamic json)=> _$TierList_PowerRankingFromJson(json as Map); dynamic toJson() => _$TierList_PowerRankingToJson(this); } diff --git a/lib/type/pack_set/PackSet.dart b/lib/type/pack_set/PackSet.dart index 45ef705..9bb266d 100644 --- a/lib/type/pack_set/PackSet.dart +++ b/lib/type/pack_set/PackSet.dart @@ -10,7 +10,8 @@ class PackSet { PackSet(); - factory PackSet.fromJson(dynamic json) => _$PackSetFromJson(json); + factory PackSet.fromJson(dynamic json) => _$PackSetFromJson(json as Map); + @HiveField(0) @JsonKey(name: '_id', defaultValue: '') String oid = ''; @@ -41,7 +42,7 @@ class PackSet_Icon { PackSet_Icon(); - factory PackSet_Icon.fromJson(dynamic json) => _$PackSet_IconFromJson(json); + factory PackSet_Icon.fromJson(dynamic json) => _$PackSet_IconFromJson(json as Map); dynamic toJson() => _$PackSet_IconToJson(this); @HiveField(0) diff --git a/lib/type/skill/Skill.dart b/lib/type/skill/Skill.dart index 9508061..f3a4fc3 100644 --- a/lib/type/skill/Skill.dart +++ b/lib/type/skill/Skill.dart @@ -1,80 +1,110 @@ +import 'package:duel_links_meta/hive/MyHive.dart'; +import 'package:hive/hive.dart'; import 'package:json_annotation/json_annotation.dart'; part 'Skill.g.dart'; @JsonSerializable(includeIfNull: true) +@HiveType(typeId: MyHive.skill) class Skill { + Skill(); + + factory Skill.fromJson(dynamic json) => _$SkillFromJson(json as Map); + + @HiveField(0) @JsonKey(defaultValue: false) bool archive = false; + @HiveField(1) @JsonKey(defaultValue: false) bool rush = false; + @HiveField(2) @JsonKey(defaultValue: '') String source = ''; + @HiveField(3) @JsonKey(name: '_id') String oid=''; + @HiveField(4) String name = ''; + + @HiveField(5) String description = ''; + @HiveField(6) List relatedCards = []; + @HiveField(7) List characters = []; - factory Skill.fromJson(dynamic json) => _$SkillFromJson(json); dynamic toJson() => _$SkillToJson(this); - - Skill(); } +@HiveType(typeId: MyHive.skill_related_character) @JsonSerializable() class Skill_Character { + + Skill_Character(); + + factory Skill_Character + .fromJson(dynamic json) => _$Skill_CharacterFromJson(json as Map); + + @HiveField(0) @JsonKey(name: '_id') String oid = ''; + @HiveField(1) String how = ''; + @HiveField(2) Skill_Character_Character character = Skill_Character_Character(); - Skill_Character(); - - factory Skill_Character - .fromJson(dynamic json) => _$Skill_CharacterFromJson(json); - dynamic toJson() => _$Skill_CharacterToJson(this); } +@HiveType(typeId: MyHive.skill_related_character_character) @JsonSerializable() class Skill_Character_Character { + + Skill_Character_Character(); + + factory Skill_Character_Character + .fromJson(dynamic json) => _$Skill_Character_CharacterFromJson(json as Map); + + @HiveField(0) String name = ''; + + @HiveField(1) String thumbnailImage = ''; + + @HiveField(2) String victoryImage = ''; + + @HiveField(3) @JsonKey(name: '_id') String oid = ''; - Skill_Character_Character(); - - factory Skill_Character_Character - .fromJson(dynamic json) => _$Skill_Character_CharacterFromJson(json); - dynamic toJson() => _$Skill_Character_CharacterToJson(this); } +@HiveType(typeId: MyHive.skill_related_card) @JsonSerializable() class Skill_RelatedCard { + Skill_RelatedCard(); + + factory Skill_RelatedCard + .fromJson(dynamic json) => _$Skill_RelatedCardFromJson(json as Map); + + @HiveField(0) @JsonKey(name: '_id') String oid = ''; + @HiveField(1) String name = ''; - Skill_RelatedCard(); - - factory Skill_RelatedCard - .fromJson(dynamic json) => _$Skill_RelatedCardFromJson(json); - dynamic toJson() => _$Skill_RelatedCardToJson(this); } diff --git a/lib/type/skill/Skill.g.dart b/lib/type/skill/Skill.g.dart index 20a55db..367c55e 100644 --- a/lib/type/skill/Skill.g.dart +++ b/lib/type/skill/Skill.g.dart @@ -2,6 +2,182 @@ part of 'Skill.dart'; +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class SkillAdapter extends TypeAdapter { + @override + final int typeId = 22; + + @override + Skill read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return Skill() + ..archive = fields[0] as bool + ..rush = fields[1] as bool + ..source = fields[2] as String + ..oid = fields[3] as String + ..name = fields[4] as String + ..description = fields[5] as String + ..relatedCards = (fields[6] as List).cast() + ..characters = (fields[7] as List).cast(); + } + + @override + void write(BinaryWriter writer, Skill obj) { + writer + ..writeByte(8) + ..writeByte(0) + ..write(obj.archive) + ..writeByte(1) + ..write(obj.rush) + ..writeByte(2) + ..write(obj.source) + ..writeByte(3) + ..write(obj.oid) + ..writeByte(4) + ..write(obj.name) + ..writeByte(5) + ..write(obj.description) + ..writeByte(6) + ..write(obj.relatedCards) + ..writeByte(7) + ..write(obj.characters); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is SkillAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} + +class SkillCharacterAdapter extends TypeAdapter { + @override + final int typeId = 24; + + @override + Skill_Character read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return Skill_Character() + ..oid = fields[0] as String + ..how = fields[1] as String + ..character = fields[2] as Skill_Character_Character; + } + + @override + void write(BinaryWriter writer, Skill_Character obj) { + writer + ..writeByte(3) + ..writeByte(0) + ..write(obj.oid) + ..writeByte(1) + ..write(obj.how) + ..writeByte(2) + ..write(obj.character); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is SkillCharacterAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} + +class SkillCharacterCharacterAdapter + extends TypeAdapter { + @override + final int typeId = 25; + + @override + Skill_Character_Character read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return Skill_Character_Character() + ..name = fields[0] as String + ..thumbnailImage = fields[1] as String + ..victoryImage = fields[2] as String + ..oid = fields[3] as String; + } + + @override + void write(BinaryWriter writer, Skill_Character_Character obj) { + writer + ..writeByte(4) + ..writeByte(0) + ..write(obj.name) + ..writeByte(1) + ..write(obj.thumbnailImage) + ..writeByte(2) + ..write(obj.victoryImage) + ..writeByte(3) + ..write(obj.oid); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is SkillCharacterCharacterAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} + +class SkillRelatedCardAdapter extends TypeAdapter { + @override + final int typeId = 23; + + @override + Skill_RelatedCard read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return Skill_RelatedCard() + ..oid = fields[0] as String + ..name = fields[1] as String; + } + + @override + void write(BinaryWriter writer, Skill_RelatedCard obj) { + writer + ..writeByte(2) + ..writeByte(0) + ..write(obj.oid) + ..writeByte(1) + ..write(obj.name); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is SkillRelatedCardAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} + // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** diff --git a/lib/type/skill_stats/SkillStats.dart b/lib/type/skill_stats/SkillStats.dart index 8b32424..1278265 100644 --- a/lib/type/skill_stats/SkillStats.dart +++ b/lib/type/skill_stats/SkillStats.dart @@ -17,7 +17,7 @@ class SkillStats { int? tier; SkillStats(); - factory SkillStats.fromJson(dynamic json) => _$SkillStatsFromJson(json); + factory SkillStats.fromJson(dynamic json) => _$SkillStatsFromJson(json as Map); dynamic toJson() => _$SkillStatsToJson(this); } @@ -29,6 +29,6 @@ class SkillStats_CoverCard { String name = ''; SkillStats_CoverCard(); - factory SkillStats_CoverCard.fromJson(dynamic json) => _$SkillStats_CoverCardFromJson(json); + factory SkillStats_CoverCard.fromJson(dynamic json) => _$SkillStats_CoverCardFromJson(json as Map); dynamic toJson() => _$SkillStats_CoverCardToJson(this); } \ No newline at end of file diff --git a/lib/type/tier_list_top_tier/TierList_TopTier.dart b/lib/type/tier_list_top_tier/TierList_TopTier.dart index c65bb4b..b87c263 100644 --- a/lib/type/tier_list_top_tier/TierList_TopTier.dart +++ b/lib/type/tier_list_top_tier/TierList_TopTier.dart @@ -23,7 +23,7 @@ class TierList_TopTier { TierList_TopTier({required this.name, required this.oid, required this.tier}); - factory TierList_TopTier.fromJson(dynamic json) => _$TierList_TopTierFromJson(json); + factory TierList_TopTier.fromJson(dynamic json) => _$TierList_TopTierFromJson(json as Map); dynamic toJson() => _$TierList_TopTierToJson(this); } diff --git a/lib/type/top_deck/TopDeck.dart b/lib/type/top_deck/TopDeck.dart index 30cda7f..6587f70 100644 --- a/lib/type/top_deck/TopDeck.dart +++ b/lib/type/top_deck/TopDeck.dart @@ -51,7 +51,7 @@ class TopDeck { TopDeck(); - factory TopDeck.fromJson(dynamic json) => _$TopDeckFromJson(json); + factory TopDeck.fromJson(dynamic json) => _$TopDeckFromJson(json as Map); dynamic toJson() => _$TopDeckToJson(this); } @@ -66,7 +66,7 @@ class TopDeck_DeckType { TopDeck_DeckType(); - factory TopDeck_DeckType.fromJson(dynamic json) => _$TopDeck_DeckTypeFromJson(json); + factory TopDeck_DeckType.fromJson(Map json) => _$TopDeck_DeckTypeFromJson(json); dynamic toJson() => _$TopDeck_DeckTypeToJson(this); } @@ -81,7 +81,7 @@ class TopDeck_MainCard { TopDeck_MainCard(); - factory TopDeck_MainCard.fromJson(dynamic json) => _$TopDeck_MainCardFromJson(json); + factory TopDeck_MainCard.fromJson(Map json) => _$TopDeck_MainCardFromJson(json); dynamic toJson() => _$TopDeck_MainCardToJson(this); } @@ -93,7 +93,7 @@ class TopDeck_MainCard_Card { TopDeck_MainCard_Card(); - factory TopDeck_MainCard_Card.fromJson(dynamic json) => _$TopDeck_MainCard_CardFromJson(json); + factory TopDeck_MainCard_Card.fromJson(Map json) => _$TopDeck_MainCard_CardFromJson(json); dynamic toJson() => _$TopDeck_MainCard_CardToJson(this); } @@ -112,7 +112,7 @@ class TopDeck_Skill { TopDeck_Skill(); - factory TopDeck_Skill.fromJson(dynamic json) => _$TopDeck_SkillFromJson(json); + factory TopDeck_Skill.fromJson(Map json) => _$TopDeck_SkillFromJson(json); dynamic toJson() => _$TopDeck_SkillToJson(this); } @@ -137,7 +137,7 @@ class TopDeck_TournamentType { TopDeck_TournamentType(); - factory TopDeck_TournamentType.fromJson(dynamic json) => _$TopDeck_TournamentTypeFromJson(json); + factory TopDeck_TournamentType.fromJson(Map json) => _$TopDeck_TournamentTypeFromJson(json); dynamic toJson() => _$TopDeck_TournamentTypeToJson(this); } @@ -159,7 +159,7 @@ class TopDeck_RankedType { TopDeck_RankedType(); - factory TopDeck_RankedType.fromJson(dynamic json) => _$TopDeck_RankedTypeFromJson(json); + factory TopDeck_RankedType.fromJson(dynamic json) => _$TopDeck_RankedTypeFromJson(json as Map); dynamic toJson() => _$TopDeck_RankedTypeToJson(this); } diff --git a/lib/type/top_deck/TopDeck.g.dart b/lib/type/top_deck/TopDeck.g.dart index bc0414f..b5230d3 100644 --- a/lib/type/top_deck/TopDeck.g.dart +++ b/lib/type/top_deck/TopDeck.g.dart @@ -231,19 +231,22 @@ TopDeck _$TopDeckFromJson(Map json) => TopDeck() ..created = json['created'] == null ? null : DateTime.parse(json['created'] as String) ..customTournamentName = json['customTournamentName'] as String? - ..deckType = TopDeck_DeckType.fromJson(json['deckType']) + ..deckType = + TopDeck_DeckType.fromJson(json['deckType'] as Map) ..dollarsPrice = json['dollarsPrice'] as int ..extra = (json['extra'] as List?) - ?.map(TopDeck_MainCard.fromJson) + ?.map((e) => TopDeck_MainCard.fromJson(e as Map)) .toList() ?? [] ..gemsPrice = json['gemsPrice'] as int ..main = (json['main'] as List?) - ?.map(TopDeck_MainCard.fromJson) + ?.map((e) => TopDeck_MainCard.fromJson(e as Map)) .toList() ?? [] ..rush = json['rush'] as bool? - ..skill = json['skill'] == null ? null : TopDeck_Skill.fromJson(json['skill']) + ..skill = json['skill'] == null + ? null + : TopDeck_Skill.fromJson(json['skill'] as Map) ..tournamentNumber = json['tournamentNumber'] as String? ..tournamentPlacement = json['tournamentPlacement'] as String? ..rankedType = json['rankedType'] == null @@ -251,7 +254,8 @@ TopDeck _$TopDeckFromJson(Map json) => TopDeck() : TopDeck_RankedType.fromJson(json['rankedType']) ..tournamentType = json['tournamentType'] == null ? null - : TopDeck_TournamentType.fromJson(json['tournamentType']) + : TopDeck_TournamentType.fromJson( + json['tournamentType'] as Map) ..url = json['url'] as String?; Map _$TopDeckToJson(TopDeck instance) => { @@ -286,7 +290,8 @@ Map _$TopDeck_DeckTypeToJson(TopDeck_DeckType instance) => TopDeck_MainCard _$TopDeck_MainCardFromJson(Map json) => TopDeck_MainCard() ..amount = json['amount'] as int - ..card = TopDeck_MainCard_Card.fromJson(json['card']); + ..card = + TopDeck_MainCard_Card.fromJson(json['card'] as Map); Map _$TopDeck_MainCardToJson(TopDeck_MainCard instance) => { diff --git a/lib/type/top_deck/TopDeckSimple.dart b/lib/type/top_deck/TopDeckSimple.dart index 2050073..f91d653 100644 --- a/lib/type/top_deck/TopDeckSimple.dart +++ b/lib/type/top_deck/TopDeckSimple.dart @@ -19,6 +19,6 @@ class TopDeckSimple { TopDeckSimple(); - factory TopDeckSimple.fromJson(dynamic json) => _$TopDeckSimpleFromJson(json); + factory TopDeckSimple.fromJson(dynamic json) => _$TopDeckSimpleFromJson(json as Map); dynamic toJson() => _$TopDeckSimpleToJson(this); } diff --git a/lib/type/top_deck/TopDeckSimple.g.dart b/lib/type/top_deck/TopDeckSimple.g.dart index 7a048db..24b2695 100644 --- a/lib/type/top_deck/TopDeckSimple.g.dart +++ b/lib/type/top_deck/TopDeckSimple.g.dart @@ -12,13 +12,15 @@ TopDeckSimple _$TopDeckSimpleFromJson(Map json) => ..created = json['created'] == null ? null : DateTime.parse(json['created'] as String) - ..deckType = TopDeck_DeckType.fromJson(json['deckType']) + ..deckType = + TopDeck_DeckType.fromJson(json['deckType'] as Map) ..rankedType = json['rankedType'] == null ? null : TopDeck_RankedType.fromJson(json['rankedType']) ..tournamentType = json['tournamentType'] == null ? null - : TopDeck_TournamentType.fromJson(json['tournamentType']); + : TopDeck_TournamentType.fromJson( + json['tournamentType'] as Map); Map _$TopDeckSimpleToJson(TopDeckSimple instance) => { diff --git a/lib/type/world/World.dart b/lib/type/world/World.dart index 8d85aba..30e8e5f 100644 --- a/lib/type/world/World.dart +++ b/lib/type/world/World.dart @@ -18,7 +18,7 @@ class World { World(); - factory World.fromJson(dynamic json) => _$WorldFromJson(json); + factory World.fromJson(dynamic json) => _$WorldFromJson(json as Map); Map toJson() => _$WorldToJson(this); } diff --git a/lib/util/index.dart b/lib/util/index.dart index 9c42659..f5e3bba 100644 --- a/lib/util/index.dart +++ b/lib/util/index.dart @@ -1,7 +1,9 @@ import 'dart:developer'; import 'dart:io'; +import 'package:duel_links_meta/extension/String.dart'; import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; import 'package:get/get_connect/http/src/response/response.dart'; class Util { @@ -24,7 +26,28 @@ class Util { } } + static bool isReachBottom(ScrollController controller, {int threshold = 200}) { return controller.position.maxScrollExtent - controller.position.pixels <= threshold; } + + static List? decoderListCatch(dynamic data, T Function(dynamic _data) decoder) { + if (data == null) { + return null; + } + + try { + return (data as List).map(decoder).toList(); + } catch (err) { + if (kDebugMode) { + final msg = '[decoderListCatch] 解析json失败: $err'; + log(msg); + + msg.toast(); + } + + return null; + } + } + } diff --git a/lib/util/storage/LocalStorage.dart b/lib/util/storage/LocalStorage.dart deleted file mode 100644 index a170713..0000000 --- a/lib/util/storage/LocalStorage.dart +++ /dev/null @@ -1,39 +0,0 @@ -import 'dart:async'; - -import 'package:shared_preferences/shared_preferences.dart'; - -class LocalStorage_DarkMode { - static const String _key = 'app.setting.dark_mode'; - - static Future save(String value) { - return StringLocalStorage.save(_key, value); - } - - static Future get() { - return StringLocalStorage.get(_key); - } - - static Future remove() { - return StringLocalStorage.remove(_key); - } -} - -class StringLocalStorage { - static Future save(String key, String value) async { - var sp = await SharedPreferences.getInstance(); - - return sp.setString(key, value); - } - - static Future get(String key) async { - var sp = await SharedPreferences.getInstance(); - - return sp.getString(key); - } - - static Future remove(String key) async { - var sp = await SharedPreferences.getInstance(); - - return sp.remove(key); - } -} diff --git a/lib/util/storage/SharedPreferencesHelper.dart b/lib/util/storage/SharedPreferencesHelper.dart deleted file mode 100644 index 2c667d7..0000000 --- a/lib/util/storage/SharedPreferencesHelper.dart +++ /dev/null @@ -1,15 +0,0 @@ -import 'package:shared_preferences/shared_preferences.dart'; - -class SharedPreferencesHelper { - - static SharedPreferences? _prefs; - - - static get instance async{ - if (_prefs != null) { - return _prefs; - } - - return await SharedPreferences.getInstance(); - } -} \ No newline at end of file diff --git a/lib/widget/toast.dart b/lib/widget/toast.dart index 9a60b1b..7ca6a0a 100644 --- a/lib/widget/toast.dart +++ b/lib/widget/toast.dart @@ -1,25 +1,26 @@ import 'package:flutter/material.dart'; class ToastWidget extends StatelessWidget { - const ToastWidget({super.key, required this.msg}); + const ToastWidget({required this.msg, super.key}); final String msg; @override Widget build(BuildContext context) { final theme = Theme.of(context); + return Container( decoration: BoxDecoration( color: theme.colorScheme.primary, borderRadius: BorderRadius.circular(12), ), padding: const EdgeInsets.symmetric( - horizontal: 12.0, - vertical: 8.0, + horizontal: 12, + vertical: 8, ), child: Text( msg, - style: TextStyle(color: Colors.white), + style: const TextStyle(color: Colors.white), ), ); } diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index e71a16d..f6f23bf 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -6,6 +6,10 @@ #include "generated_plugin_registrant.h" +#include void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); + url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); } diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 2e1de87..f16b4c3 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + url_launcher_linux ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index eefcc6d..2505cc9 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,12 +5,14 @@ import FlutterMacOS import Foundation +import package_info_plus import path_provider_foundation -import shared_preferences_foundation import sqflite +import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) - SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) + UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) } diff --git a/pubspec.lock b/pubspec.lock index 751f9f5..4623f0a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -209,14 +209,6 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "3.0.3" - cupertino_icons: - dependency: "direct main" - description: - name: cupertino_icons - sha256: d57953e10f9f8327ce64a508a355f0b1ec902193f66288e8cb5070e7c47eeb2d - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.0.6" dart_style: dependency: transitive description: @@ -552,6 +544,22 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "2.1.0" + package_info_plus: + dependency: "direct main" + description: + name: package_info_plus + sha256: a75164ade98cb7d24cfd0a13c6408927c6b217fa60dee5a7ff5c116a58f28918 + url: "https://pub.flutter-io.cn" + source: hosted + version: "8.0.2" + package_info_plus_platform_interface: + dependency: transitive + description: + name: package_info_plus_platform_interface + sha256: ac1f4a4847f1ade8e6a87d1f39f5d7c67490738642e2542f559ec38c37489a66 + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.0.1" path: dependency: "direct main" description: @@ -680,62 +688,6 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "0.27.7" - shared_preferences: - dependency: "direct main" - description: - name: shared_preferences - sha256: "81429e4481e1ccfb51ede496e916348668fd0921627779233bd24cc3ff6abd02" - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.2.2" - shared_preferences_android: - dependency: transitive - description: - name: shared_preferences_android - sha256: "8568a389334b6e83415b6aae55378e158fbc2314e074983362d20c562780fb06" - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.2.1" - shared_preferences_foundation: - dependency: transitive - description: - name: shared_preferences_foundation - sha256: "7708d83064f38060c7b39db12aefe449cb8cdc031d6062280087bc4cdb988f5c" - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.3.5" - shared_preferences_linux: - dependency: transitive - description: - name: shared_preferences_linux - sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa" - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.3.2" - shared_preferences_platform_interface: - dependency: transitive - description: - name: shared_preferences_platform_interface - sha256: "22e2ecac9419b4246d7c22bfbbda589e3acf5c0351137d87dd2939d984d37c3b" - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.3.2" - shared_preferences_web: - dependency: transitive - description: - name: shared_preferences_web - sha256: "9aee1089b36bd2aafe06582b7d7817fd317ef05fc30e6ba14bff247d0933042a" - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.3.0" - shared_preferences_windows: - dependency: transitive - description: - name: shared_preferences_windows - sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59" - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.3.2" shelf: dependency: transitive description: @@ -790,7 +742,7 @@ packages: source: hosted version: "7.0.0" sqflite: - dependency: "direct main" + dependency: transitive description: name: sqflite sha256: "5ce2e1a15e822c3b4bfb5400455775e421da7098eed8adc8f26298ada7c9308c" @@ -893,6 +845,70 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "2.2.2" + url_launcher: + dependency: "direct main" + description: + name: url_launcher + sha256: "21b704ce5fa560ea9f3b525b43601c678728ba46725bab9b01187b4831377ed3" + url: "https://pub.flutter-io.cn" + source: hosted + version: "6.3.0" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: "17cd5e205ea615e2c6ea7a77323a11712dffa0720a8a90540db57a01347f9ad9" + url: "https://pub.flutter-io.cn" + source: hosted + version: "6.3.2" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: e43b677296fadce447e987a2f519dcf5f6d1e527dc35d01ffab4fff5b8a7063e + url: "https://pub.flutter-io.cn" + source: hosted + version: "6.3.1" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: e2b9622b4007f97f504cd64c0128309dfb978ae66adbe944125ed9e1750f06af + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.2.0" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: "9a1a42d5d2d95400c795b2914c36fdcb525870c752569438e4ebb09a2b5d90de" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.2.0" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.3.2" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: "772638d3b34c779ede05ba3d38af34657a05ac55b06279ea6edd409e323dca8e" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.3.3" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "49c10f879746271804767cb45551ec5592cdab00ee105c06dddde1a98f73b185" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.1.2" uuid: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 65bc82a..6e04713 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -34,20 +34,20 @@ dependencies: # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. - cupertino_icons: ^1.0.6 +# cupertino_icons: ^1.0.6 cached_network_image: ^3.2.3 webview_flutter: ^4.7.0 get: ^4.6.5 json_annotation: ^4.8.1 intl: ^0.19.0 flutter_svg: ^2.0.10+1 - shared_preferences: ^2.2.2 - sqflite: ^2.3.3 path: ^1.9.0 hive: ^2.2.3 hive_flutter: ^1.1.0 flutter_smart_dialog: ^4.9.6+1 animations: ^2.0.11 + url_launcher: 6.3.0 + package_info_plus: 8.0.2 dev_dependencies: flutter_test: @@ -91,6 +91,7 @@ flutter: # To add assets to your application, add an assets section, like this: assets: - assets/images/ + - assets/json/ # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/assets-and-images/#resolution-aware diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 8b6d468..4f78848 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,6 +6,9 @@ #include "generated_plugin_registrant.h" +#include void RegisterPlugins(flutter::PluginRegistry* registry) { + UrlLauncherWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UrlLauncherWindows")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index b93c4c3..88b22e5 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + url_launcher_windows ) list(APPEND FLUTTER_FFI_PLUGIN_LIST