Skip to content

Commit c4b835c

Browse files
committed
Integrate geo location API, fix overflow issue
1 parent b8364bd commit c4b835c

3 files changed

Lines changed: 179 additions & 18 deletions

File tree

lib/main.dart

Lines changed: 154 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import 'package:path_provider/path_provider.dart';
88
import 'package:logging/logging.dart';
99
import 'package:flutter_desktop_sleep/flutter_desktop_sleep.dart';
1010
import 'package:flutter_window_close/flutter_window_close.dart';
11+
import 'package:http/http.dart' as http;
12+
import 'dart:convert';
1113

1214
import 'myceliumflut_ffi_binding.dart';
1315

@@ -50,13 +52,21 @@ class _MyAppState extends State<MyApp> {
5052
String _nodeAddr = '';
5153
var privKey = Uint8List(0);
5254
List<String> peers = [];
55+
Map<String, String> _peerToCountry = {};
56+
Timer? _geoDebounce;
57+
late FocusNode _peersFocusNode;
58+
String _rawPeersText = '';
5359
late TextEditingController textEditController;
5460
final _flutterDesktopSleepPlugin = FlutterDesktopSleep();
55-
final ScrollController _scrollController = ScrollController();
61+
5662

5763
@override
5864
void initState() {
5965
textEditController = TextEditingController(text: '');
66+
_peersFocusNode = FocusNode();
67+
_peersFocusNode.addListener(() {
68+
setState(() {});
69+
});
6070
super.initState();
6171
initPlatformState();
6272
platform.setMethodCallHandler((MethodCall call) async {
@@ -152,7 +162,9 @@ class _MyAppState extends State<MyApp> {
152162
'tcp://5.223.43.251:9651'
153163
];
154164
}
155-
textEditController = TextEditingController(text: peers.join('\n'));
165+
_rawPeersText = peers.join('\n');
166+
textEditController = TextEditingController(text: _rawPeersText);
167+
await _fetchAndGroupPeersByCountry(peers);
156168

157169
String nodeAddr;
158170
if (isUseDylib()) {
@@ -173,6 +185,95 @@ class _MyAppState extends State<MyApp> {
173185
_nodeAddr = nodeAddr;
174186
});
175187
}
188+
Future<void> _fetchAndGroupPeersByCountry(List<String> peersList) async {
189+
try {
190+
final Map<String, String> p2c = {};
191+
for (final p in peersList) {
192+
final host = _extractHost(p);
193+
if (host.isEmpty) continue;
194+
final ip = await _resolveHost(host);
195+
if (ip == null) continue;
196+
final info = await _geoLookup(ip);
197+
final country = info['country_name']?.toString() ?? 'Unknown';
198+
p2c[p] = country;
199+
}
200+
if (mounted) {
201+
setState(() {
202+
_peerToCountry = p2c;
203+
});
204+
}
205+
} catch (e) {
206+
_logger.warning('geo overlay update failed');
207+
}
208+
}
209+
210+
String _extractHost(String peer) {
211+
try {
212+
final withoutScheme = peer.substring(peer.indexOf('://') + 3);
213+
if (withoutScheme.startsWith('[')) {
214+
final end = withoutScheme.indexOf(']');
215+
if (end > 1) return withoutScheme.substring(1, end);
216+
return '';
217+
}
218+
final colon = withoutScheme.lastIndexOf(':');
219+
if (colon > 0) return withoutScheme.substring(0, colon);
220+
return '';
221+
} catch (_) {
222+
return '';
223+
}
224+
}
225+
226+
Future<String?> _resolveHost(String host) async {
227+
try {
228+
final addresses = await InternetAddress.lookup(host);
229+
if (addresses.isNotEmpty) {
230+
return addresses.first.address;
231+
}
232+
} catch (_) {}
233+
return null;
234+
}
235+
236+
Future<Map<String, dynamic>> _geoLookup(String ip) async {
237+
final uri = Uri.parse('https://geoip.grid.tf/?ip=${Uri.encodeQueryComponent(ip)}');
238+
final resp = await http.get(uri, headers: {'Accept': 'application/json'});
239+
if (resp.statusCode != 200) {
240+
throw Exception('geo lookup failed: ${resp.statusCode}');
241+
}
242+
return json.decode(resp.body) as Map<String, dynamic>;
243+
}
244+
245+
Widget _buildGreyCountryOverlay(String rawText, Map<String, String> map) {
246+
final lines = rawText.split('\n');
247+
final spans = <TextSpan>[];
248+
for (var i = 0; i < lines.length; i++) {
249+
final line = lines[i];
250+
final trimmedLine = line.trim();
251+
if (trimmedLine.isEmpty) {
252+
spans.add(const TextSpan(text: '\n'));
253+
continue;
254+
}
255+
final country = map[trimmedLine];
256+
if (country != null) {
257+
spans.add(TextSpan(
258+
text: '$line — ',
259+
style: const TextStyle(fontSize: 14, color: Colors.black),
260+
));
261+
spans.add(TextSpan(
262+
text: country + (i < lines.length - 1 ? '\n' : ''),
263+
style: const TextStyle(fontSize: 14, color: Colors.grey),
264+
));
265+
} else {
266+
spans.add(TextSpan(
267+
text: line + (i < lines.length - 1 ? '\n' : ''),
268+
style: const TextStyle(fontSize: 14, color: Colors.black),
269+
));
270+
}
271+
}
272+
return RichText(
273+
text: TextSpan(children: spans),
274+
textAlign: TextAlign.left,
275+
);
276+
}
176277

177278
// start/stop mycelium button variables
178279
bool _isStarted = false;
@@ -187,6 +288,7 @@ class _MyAppState extends State<MyApp> {
187288
void dispose() {
188289
// Clean up the controller when the widget is disposed.
189290
textEditController.dispose();
291+
_peersFocusNode.dispose();
190292
super.dispose();
191293
}
192294

@@ -265,22 +367,52 @@ class _MyAppState extends State<MyApp> {
265367
constraints: BoxConstraints(
266368
maxHeight: 150,
267369
),
268-
child: SingleChildScrollView(
269-
controller: _scrollController,
270-
child: TextField(
271-
// peers address
272-
controller: textEditController,
273-
onTapOutside: (event) => {
274-
FocusManager.instance.primaryFocus?.unfocus(),
275-
},
276-
minLines: 1,
277-
maxLines: null,
278-
keyboardType: TextInputType.multiline,
279-
style: const TextStyle(fontSize: 14),
280-
decoration: const InputDecoration(
281-
border: OutlineInputBorder(),
282-
//labelText: 'Peers',
283-
),
370+
child: Container(
371+
decoration: BoxDecoration(
372+
border: Border.all(color: Colors.grey),
373+
borderRadius: BorderRadius.circular(4.0),
374+
),
375+
child: Stack(
376+
children: [
377+
TextField(
378+
controller: textEditController,
379+
focusNode: _peersFocusNode,
380+
onChanged: (v) {
381+
_rawPeersText = v;
382+
_geoDebounce?.cancel();
383+
_geoDebounce = Timer(const Duration(milliseconds: 500), () async {
384+
final currentPeers = preprocessPeers(getPeers(_rawPeersText));
385+
await _fetchAndGroupPeersByCountry(currentPeers);
386+
if (mounted) setState(() {});
387+
});
388+
},
389+
onTapOutside: (event) => {
390+
FocusManager.instance.primaryFocus?.unfocus(),
391+
},
392+
minLines: 1,
393+
maxLines: null,
394+
keyboardType: TextInputType.multiline,
395+
style: TextStyle(
396+
fontSize: 14,
397+
color: (!_peersFocusNode.hasFocus && _peerToCountry.isNotEmpty)
398+
? Colors.transparent
399+
: Colors.black,
400+
),
401+
decoration: const InputDecoration(
402+
border: InputBorder.none,
403+
contentPadding: EdgeInsets.all(12.0),
404+
),
405+
),
406+
if (!_peersFocusNode.hasFocus && _peerToCountry.isNotEmpty)
407+
Positioned.fill(
408+
child: IgnorePointer(
409+
child: Container(
410+
padding: const EdgeInsets.all(12.0),
411+
child: _buildGreyCountryOverlay(_rawPeersText, _peerToCountry),
412+
),
413+
),
414+
),
415+
],
284416
),
285417
),
286418
),
@@ -445,6 +577,10 @@ class _MyAppState extends State<MyApp> {
445577
sendPort.send('done');
446578
}
447579

580+
581+
582+
583+
448584
void stopMycelium() {
449585
try {
450586
stopVpn(platform);

pubspec.lock

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,22 @@ packages:
194194
url: "https://pub.dev"
195195
source: hosted
196196
version: "0.15.5"
197+
http:
198+
dependency: "direct main"
199+
description:
200+
name: http
201+
sha256: bb2ce4590bc2667c96f318d68cac1b5a7987ec819351d32b1c987239a815e007
202+
url: "https://pub.dev"
203+
source: hosted
204+
version: "1.5.0"
205+
http_parser:
206+
dependency: transitive
207+
description:
208+
name: http_parser
209+
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
210+
url: "https://pub.dev"
211+
source: hosted
212+
version: "4.1.2"
197213
image:
198214
dependency: transitive
199215
description:
@@ -452,6 +468,14 @@ packages:
452468
url: "https://pub.dev"
453469
source: hosted
454470
version: "14.3.0"
471+
web:
472+
dependency: transitive
473+
description:
474+
name: web
475+
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
476+
url: "https://pub.dev"
477+
source: hosted
478+
version: "1.1.1"
455479
webdriver:
456480
dependency: transitive
457481
description:

pubspec.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ dependencies:
3232
sdk: flutter
3333
convert: ^3.0.1
3434
crypto: ^3.0.1
35+
http: ^1.2.2
3536

3637
# The following adds the Cupertino Icons font to your application.
3738
# Use with the CupertinoIcons class for iOS style icons.

0 commit comments

Comments
 (0)