arblant
Railynx / lib / app / workbench_model.dart
workbench_model.dart190 lines · 5.7 KB
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190
import '../engine/vless_parser.dart';
import '../engine/vless_scanner.dart';

/// Workbench rules from ui.html, kept free of widgets so they can be tested.
const int genMin = 1;
const int genMax = 20;
const int defaultGenCount = 5;
const List<String> modeOrder = <String>['top', 'best', 'custom'];

const Map<String, String> modeExplainer = <String, String>{
  'top': 'Each config gets the next-best IP.',
  'best': 'Every config uses the fastest IP.',
  'custom': 'Pick the IP for each config yourself.',
};

/// One assigned row. [ip] is null when there are no results yet.
/// [configIndex] identifies the pasted config the row belongs to
/// (multi-config mode); it is 0 in the single-config flow.
class ConfigRow {
  const ConfigRow({this.ip, this.changed = false, this.stale = false, this.configIndex = 0});

  final String? ip;
  final bool changed;
  final bool stale;
  final int configIndex;
}

/// A "Test these" measurement for one IP.
class Tested {
  const Tested({this.delayMs, this.delta = 0, this.dead = false});

  final int? delayMs;
  final int delta;
  final bool dead;

  Tested withoutDelta() => Tested(delayMs: delayMs, delta: 0, dead: dead);
}

enum ChipKind { measuring, dead, stale, latency }

/// What the latency cell of a row shows.
class ChipInfo {
  const ChipInfo(this.kind, {this.ms, this.delta = 0});

  final ChipKind kind;
  final int? ms;
  final int delta;
}

class WorkbenchModel {
  WorkbenchModel._();

  static Map<String, int> delayByIp(List<ScanResult> results) {
    return <String, int>{for (final r in results) r.ip: r.delayMs};
  }

  static List<ConfigRow> assignments({
    required int count,
    required String mode,
    required List<ScanResult> results,
    required Map<int, String> custom,
  }) {
    final delays = delayByIp(results);
    final rows = <ConfigRow>[];
    for (var i = 0; i < count; i++) {
      final auto = results.isEmpty ? null : results[i % results.length].ip;
      if (mode == 'best') {
        rows.add(ConfigRow(ip: results.isEmpty ? null : results.first.ip));
      } else if (mode == 'custom' && custom[i] != null) {
        final pick = custom[i]!;
        rows.add(ConfigRow(
          ip: pick,
          changed: pick != auto,
          stale: !delays.containsKey(pick),
        ));
      } else {
        rows.add(ConfigRow(ip: auto));
      }
    }
    return rows;
  }

  /// Multi-config rows: one row per pasted config, assigned from ITS OWN
  /// server's results (configs that share a server share that server's
  /// result list, Top N rotating within the group by the config's
  /// position in it). Mirrors [assignments] rule for rule.
  static List<ConfigRow> assignmentsMulti({
    required List<VlessConfig> configs,
    required Map<String, List<ScanResult>> resultsByGroup,
    required String Function(VlessConfig) groupKey,
    required String mode,
    required Map<int, String> custom,
  }) {
    final seenInGroup = <String, int>{};
    final rows = <ConfigRow>[];
    for (var j = 0; j < configs.length; j++) {
      final key = groupKey(configs[j]);
      final results = resultsByGroup[key] ?? const <ScanResult>[];
      final pos = seenInGroup[key] ?? 0;
      seenInGroup[key] = pos + 1;
      final delays = delayByIp(results);
      final auto = results.isEmpty ? null : results[pos % results.length].ip;
      if (mode == 'best') {
        rows.add(ConfigRow(ip: results.isEmpty ? null : results.first.ip, configIndex: j));
      } else if (mode == 'custom' && custom[j] != null) {
        final pick = custom[j]!;
        rows.add(ConfigRow(
          ip: pick,
          changed: pick != auto,
          stale: !delays.containsKey(pick),
          configIndex: j,
        ));
      } else {
        rows.add(ConfigRow(ip: auto, configIndex: j));
      }
    }
    return rows;
  }

  /// Ran out of distinct IPs (any mode except Best-for-all): configs share.
  static bool shortage({
    required String mode,
    required int count,
    required int resultCount,
  }) {
    return mode != 'best' && resultCount > 0 && count > resultCount;
  }

  static String shortageText(int resultCount) {
    final plural = resultCount == 1 ? '' : 's';
    return 'Only $resultCount clean IP$plural found, so some configs will share one.';
  }

  static List<String> uniqueIps(List<ConfigRow> rows) {
    final seen = <String>{};
    final out = <String>[];
    for (final row in rows) {
      final ip = row.ip;
      if (ip != null && seen.add(ip)) {
        out.add(ip);
      }
    }
    return out;
  }

  /// Previous delay for the delta badge: the last good test, else the scan.
  static int? previousDelay(String ip, Map<String, Tested> tested, Map<String, int> delays) {
    final t = tested[ip];
    if (t != null && !t.dead) {
      return t.delayMs;
    }
    return delays[ip];
  }

  static Tested measured(int? previous, int newDelayMs) {
    final delta = previous == null ? 0 : previous - newDelayMs;
    return Tested(delayMs: newDelayMs, delta: delta, dead: false);
  }

  static String deltaLabel(int delta) {
    return delta > 0 ? '↓$delta' : '↑${-delta}';
  }

  static ChipInfo chipFor({
    required String? ip,
    required Map<String, Tested> tested,
    required Map<String, int> delays,
    required Set<String> testingIps,
    required bool testing,
  }) {
    if (ip == null) {
      return const ChipInfo(ChipKind.stale);
    }
    if (testing && testingIps.contains(ip)) {
      return const ChipInfo(ChipKind.measuring);
    }
    final t = tested[ip];
    if (t != null) {
      if (t.dead) {
        return const ChipInfo(ChipKind.dead);
      }
      return ChipInfo(ChipKind.latency, ms: t.delayMs, delta: t.delta);
    }
    final delay = delays[ip];
    if (delay == null) {
      return const ChipInfo(ChipKind.stale);
    }
    return ChipInfo(ChipKind.latency, ms: delay);
  }
}