Add stats on target list
Please add stats / or estimates on target list screen. Also, as a bonus nice to have, please send stats / estimates as a note when exporting to Yata
Thank you!
Feature: Add Stats/Estimates to Target List Screen
The provided lib/pages/chaining/targets_page.dart, lib/widgets/chaining/targets_list.dart, and lib/widgets/chaining/target_card.dart confirm that the target list screen uses a ListView.builder with Slidable widgets containing TargetCard widgets. Each TargetCard is a StatefulWidget with a Card containing a Column of Row widgets for target details (name, level, respect, fair fight, life, status, notes, etc.). We'll modify TargetCard to include spy stats from SpiesController using YataSpyModel by adding a new row in the Column.
Changes to lib/widgets/chaining/target_card.dart
- Add imports at the top (after existing imports):
import 'package:get/get.dart';
import 'package:provider/provider.dart';
import 'package:torn_pda/providers/spies_controller.dart';
-
Modify the
buildmethod inTargetCardState(around line 78) to add a new row for spy stats in theColumnchildren (after the existing notes row, before the finalSizedBox).Original (partial,
Columnchildren around line 92):child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[ // LINE 1 Padding( padding: const EdgeInsetsDirectional.fromSTEB(12, 5, 10, 0), child: Row( // ... name, level, faction, refresh ), ), // LINE 2 Padding( padding: const EdgeInsetsDirectional.fromSTEB(16, 5, 15, 0), child: Row( // ... respect, fair fight, health ), ), // LINE 3 Padding( padding: const EdgeInsetsDirectional.fromSTEB(15, 5, 15, 0), child: Row( // ... travel, status, last updated ), ), // LINE 4 Padding( padding: const EdgeInsetsDirectional.fromSTEB(8, 5, 15, 0), child: Row( // ... notes, bounty, index ), ), const SizedBox(height: 10), ], ),New (add a new row after LINE 4, before
SizedBox):child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[ // LINE 1 Padding( padding: const EdgeInsetsDirectional.fromSTEB(12, 5, 10, 0), child: Row( children: <Widget>[ Row( mainAxisSize: MainAxisSize.min, children: <Widget>[ GestureDetector( onTap: () { if (_target!.status!.state!.contains("Federal") || _target!.status!.state!.contains("Fallen")) { _warnFedetalOrFallen(); } else { _startAttack(shortTap: true); } }, onLongPress: () { if (_target!.status!.state!.contains("Federal") || _target!.status!.state!.contains("Fallen")) { _warnFedetalOrFallen(); } else { _startAttack(shortTap: false); } }, child: Row( children: [ if (_target!.status!.state!.contains("Federal") || _target!.status!.state!.contains("Fallen")) const Icon(MdiIcons.graveStone, size: 18) else _attackIcon(), const Padding( padding: EdgeInsets.symmetric(horizontal: 5), ), SizedBox( width: 95, child: Text( '${_target!.name}', overflow: TextOverflow.ellipsis, style: const TextStyle( fontWeight: FontWeight.bold, ), ), ), ], ), ), ], ), Expanded( child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: <Widget>[ Row( mainAxisSize: MainAxisSize.min, children: [ const SizedBox(width: 5), OpenContainer( transitionDuration: const Duration(milliseconds: 500), transitionType: ContainerTransitionType.fadeThrough, openBuilder: (BuildContext context, VoidCallback _) { return TargetDetailsPage(target: _target); }, closedElevation: 0, closedShape: const RoundedRectangleBorder( borderRadius: BorderRadius.all( Radius.circular(56 / 2), ), ), closedColor: Colors.transparent, openColor: _themeProvider.canvas, closedBuilder: (BuildContext context, VoidCallback openContainer) { return const SizedBox( height: 22, width: 30, child: Icon( Icons.info_outline, size: 20, ), ); }, ), const SizedBox(width: 5), _factionIcon(), ], ), Text( 'Lvl ${_target!.level}', ), Padding( padding: const EdgeInsets.only(right: 3), child: SizedBox( height: 22, width: 22, child: _refreshIcon(), ), ), ], ), ), ], ), ), // LINE 2 Padding( padding: const EdgeInsetsDirectional.fromSTEB(16, 5, 15, 0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: <Widget>[ _returnRespectFF(_target!.respectGain, _target!.fairFight), _returnHealth(_target!), ], ), ), // LINE 3 Padding( padding: const EdgeInsetsDirectional.fromSTEB(15, 5, 15, 0), child: Row( children: <Widget>[ Row( children: <Widget>[ _travelIcon(), Container( width: 14, height: 14, decoration: BoxDecoration( color: _returnStatusColor(_target!.lastAction!.status), shape: BoxShape.circle, ), ), Padding( padding: const EdgeInsets.only(left: 13), child: Text( _target!.lastAction!.relative == "0 minutes ago" ? 'now' : _target!.lastAction!.relative!.replaceAll(' ago', ''), ), ), ], ), Expanded( child: Row( mainAxisAlignment: MainAxisAlignment.end, children: <Widget>[ const Icon(Icons.refresh, size: 15), Text( ' $_lastUpdatedString', style: TextStyle( color: _lastUpdatedMinutes <= 120 ? _themeProvider.mainText : _themeProvider.getTextColor(Colors.deepOrangeAccent), fontStyle: _lastUpdatedMinutes <= 120 ? FontStyle.normal : FontStyle.italic, ), ), ], ), ), ], ), ), // LINE 4 Padding( padding: const EdgeInsetsDirectional.fromSTEB(8, 5, 15, 0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Flexible( child: Row( children: <Widget>[ SizedBox( width: 30, height: 20, child: IconButton( padding: const EdgeInsets.all(0), iconSize: 20, icon: Icon( MdiIcons.notebookEditOutline, color: _returnTargetNoteColor(), ), onPressed: () { _showNotesDialog(); }, ), ), const SizedBox(width: 4), const Text('Notes: '), Flexible( child: Text( '${_target!.personalNote}', style: TextStyle( color: _returnTargetNoteColor(), ), ), ), ], ), ), const SizedBox(width: 10), if (_target?.basicicons?.icon13 != null) GestureDetector( onTap: () { BotToast.showText( clickClose: true, text: _target!.basicicons!.icon13!, textStyle: const TextStyle( fontSize: 14, color: Colors.white, ), contentColor: Colors.blue, duration: const Duration(seconds: 3), contentPadding: const EdgeInsets.all(10), ); }, child: const Padding( padding: EdgeInsets.only(right: 10), child: Image( image: AssetImage('images/icons/status/icon13.png'), width: 18, fit: BoxFit.fill, ), ), ), Padding( padding: const EdgeInsets.only(right: 2), child: Text( '${_targetsProvider.allTargets.indexOf(_target) + 1}' '/${_targetsProvider.allTargets.length}', style: TextStyle( color: Colors.brown[400], fontSize: 11, ), ), ), ], ), ), // LINE 5: Spy Stats Padding( padding: const EdgeInsetsDirectional.fromSTEB(8, 5, 15, 0), child: Consumer<SpiesController>( builder: (context, spiesController, child) { final spy = spiesController.getYataSpy(userId: _target!.playerId.toString()); return Row( children: [ Flexible( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (spy == null) Row( children: [ const Text('No stats estimate available'), IconButton( icon: const Icon(Icons.refresh, size: 15), onPressed: () async { await spiesController.fetchYataSpies(); }, ), ], ) else Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Est. Stats (from YATA):', style: TextStyle(color: _themeProvider.mainText), ), Text( 'Strength: ${spy.strength ?? "Unknown"}', style: TextStyle(color: _themeProvider.mainText), ), Text( 'Defense: ${spy.defense ?? "Unknown"}', style: TextStyle(color: _themeProvider.mainText), ), Text( 'Speed: ${spy.speed ?? "Unknown"}', style: TextStyle(color: _themeProvider.mainText), ), Text( 'Dexterity: ${spy.dexterity ?? "Unknown"}', style: TextStyle(color: _themeProvider.mainText), ), Text( 'Total: ${spy.total ?? "Unknown"}', style: TextStyle(color: _themeProvider.mainText), ), Text( '(Updated: ${spiesController.formatUpdateString(spy.update ?? 0)})', style: TextStyle(color: _themeProvider.mainText, fontSize: 12), ), ], ), ], ), ), ], ); }, ), ), const SizedBox(height: 10), ], ),
Notes
- Confirmed Details:
TargetCardis aStatefulWidgetwith aCardcontaining aColumnofRowwidgets for target details (name, level, respect, fair fight, life, status, notes, etc.), confirmed fromtarget_card.dart.TargetsListusesTargetCardwithin aSlidablein aListView.builder(fromtargets_list.dart).TargetModelhasplayerId,name,personalNote,personalNoteColor,life.current,life.maximum,status,faction,lastAction(fromtarget_model.dart).SpiesControllerhasgetYataSpyandfetchYataSpies(fromspies_controller.dart).YataSpyModelhasstrength,defense,speed,dexterity,total,update(fromyata_spy_model.dart).
- Behavior: Adds a new row (LINE 5) in the
TargetCardColumnto display spy stats below the notes row. If no spy data is available, shows a message with a refresh button to fetch from YATA. UsesConsumer<SpiesController>to react to spy data updates. - Backwards Compatibility: The new row is non-disruptive, preserving the existing layout and functionality. If spy data is unavailable, a fallback message is shown, maintaining usability.
- Testing Recommendations:
- Verify on the targets page with targets that have and lack spy data.
- Test the refresh button to ensure
fetchYataSpiestriggers API calls and updates the UI. - Check for API rate limit handling (managed by
SpiesController). - Ensure the card layout remains consistent in portrait and landscape orientations, with proper spacing and alignment.
- Implementation Details: The spy stats are added as a new
Paddingwidget with aConsumer<SpiesController>to dynamically display stats or a placeholder. The styling matches existing text (using_themeProvider.mainText) for consistency. The refresh button uses a smaller icon size (15) to align with other icons in the card.