torn-pda icon indicating copy to clipboard operation
torn-pda copied to clipboard

Add stats on target list

Open bogdanstoik opened this issue 6 months ago • 1 comments

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!

bogdanstoik avatar Jun 27 '25 21:06 bogdanstoik

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

  1. 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';
  1. Modify the build method in TargetCardState (around line 78) to add a new row for spy stats in the Column children (after the existing notes row, before the final SizedBox).

    Original (partial, Column children 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:
    • 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.), confirmed from target_card.dart.
    • TargetsList uses TargetCard within a Slidable in a ListView.builder (from targets_list.dart).
    • TargetModel has playerId, name, personalNote, personalNoteColor, life.current, life.maximum, status, faction, lastAction (from target_model.dart).
    • SpiesController has getYataSpy and fetchYataSpies (from spies_controller.dart).
    • YataSpyModel has strength, defense, speed, dexterity, total, update (from yata_spy_model.dart).
  • Behavior: Adds a new row (LINE 5) in the TargetCard Column to display spy stats below the notes row. If no spy data is available, shows a message with a refresh button to fetch from YATA. Uses Consumer<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 fetchYataSpies triggers 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 Padding widget with a Consumer<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.

alxspiker avatar Jul 17 '25 00:07 alxspiker