22using System ;
33using System . Collections . Generic ;
44using System . Diagnostics ;
5+ using System . Linq ;
56using System . Threading ;
67using System . Threading . Tasks ;
78using System . Windows ;
89
910namespace Flow . Launcher . Plugin . SpeedTest
1011{
11- public class Main : IAsyncPlugin , IPluginI18n
12+ public class Main : IAsyncPlugin , IPluginI18n , ISettingProvider
1213 {
1314 private PluginInitContext ? _context ;
1415 private Settings ? _settings ;
@@ -20,40 +21,36 @@ public class Main : IAsyncPlugin, IPluginI18n
2021 private double _uploadProgress ;
2122 private double _currentDownloadSpeed ;
2223 private double _currentUploadSpeed ;
23- private Timer ? _updateTimer ;
2424 private string ? _lastError ;
2525 private DateTime _lastQueryTime ;
2626 private bool _isDarkTheme ;
27+ private string _currentQuery = string . Empty ;
28+ private Timer ? _refreshTimer ;
2729
2830 public Task InitAsync ( PluginInitContext context )
2931 {
3032 _context = context ;
3133 _settings = context . API . LoadSettingJsonStorage < Settings > ( ) ;
3234
33- var dispatcher = Application . Current ? . Dispatcher ;
34- if ( dispatcher != null )
35- {
36- if ( dispatcher . CheckAccess ( ) )
37- _isDarkTheme = context . API . IsApplicationDarkTheme ( ) ;
38- else
39- dispatcher . Invoke ( ( ) => _isDarkTheme = context . API . IsApplicationDarkTheme ( ) ) ;
40- }
41-
35+ UpdateIcon ( ) ;
4236 context . API . ActualApplicationThemeChanged += ( _ , __ ) =>
4337 {
44- var disp = Application . Current ? . Dispatcher ;
45- if ( disp != null )
46- {
47- if ( disp . CheckAccess ( ) )
48- _isDarkTheme = _context . API . IsApplicationDarkTheme ( ) ;
49- else
50- disp . Invoke ( ( ) => _isDarkTheme = _context . API . IsApplicationDarkTheme ( ) ) ;
51- }
38+ UpdateIcon ( ) ;
39+ try { _context ? . API . ChangeQuery ( _context . CurrentPluginMetadata . ActionKeyword , true ) ; } catch { }
5240 } ;
5341
5442 return Task . CompletedTask ;
5543 }
5644
45+ private void UpdateIcon ( )
46+ {
47+ var dispatcher = Application . Current ? . Dispatcher ;
48+ if ( dispatcher ? . CheckAccess ( ) == true )
49+ _isDarkTheme = _context . API . IsApplicationDarkTheme ( ) ;
50+ else
51+ dispatcher ? . Invoke ( ( ) => _isDarkTheme = _context . API . IsApplicationDarkTheme ( ) ) ;
52+ }
53+
5754 private string GetIcon ( ) => _isDarkTheme ? "icon-dark.png" : "icon-light.png" ;
5855
5956 public async Task < List < Result > > QueryAsync ( Query query , CancellationToken token )
@@ -68,18 +65,116 @@ public async Task<List<Result>> QueryAsync(Query query, CancellationToken token)
6865 }
6966 _lastQueryTime = DateTime . Now ;
7067
71- if ( string . IsNullOrWhiteSpace ( query . Search ) && ! _isTestRunning && _lastResult == null && _lastError == null )
68+ var q = ( query . Search ?? string . Empty ) . Trim ( ) ;
69+
70+ if ( string . IsNullOrWhiteSpace ( q ) )
7271 {
73- _currentStatus = "Connecting to server..." ;
74- RunTest ( ) ;
72+ // don't auto-run tests: show usage and last result
73+ if ( _lastResult != null )
74+ {
75+ var timeSince = DateTime . Now - _lastTestTime ;
76+ var timeStr = timeSince . TotalMinutes < 60 ? $ "{ ( int ) timeSince . TotalMinutes } m ago" : $ "{ ( int ) timeSince . TotalHours } h ago";
77+ results . Add ( new Result
78+ {
79+ Title = $ "↓ { _lastResult . DownloadSpeed : F1} Mbps ↑ { _lastResult . UploadSpeed : F1} Mbps",
80+ SubTitle = $ "{ timeStr } • start • history • ip",
81+ IcoPath = GetIcon ( )
82+ } ) ;
83+ }
84+ else
85+ {
86+ results . Add ( new Result
87+ {
88+ Title = "Type start to test your speed" ,
89+ SubTitle = "start • history • ip" ,
90+ IcoPath = GetIcon ( )
91+ } ) ;
92+ }
7593
76- results . Add ( new Result
94+ return results ;
95+ }
96+
97+ // command handling
98+ var cmd = q . ToLowerInvariant ( ) ;
99+ if ( cmd == "start" )
100+ {
101+ if ( ! _isTestRunning )
77102 {
78- Title = "Testing your internet speed..." ,
79- SubTitle = "Connecting to nearest server..." ,
80- IcoPath = GetIcon ( )
81- } ) ;
103+ _currentQuery = query . RawQuery ;
104+ _currentStatus = "Connecting to server..." ;
105+ RunTest ( ) ;
106+
107+ // Set up auto-refresh timer to update progress
108+ _refreshTimer ? . Dispose ( ) ;
109+ _refreshTimer = new Timer ( _ =>
110+ {
111+ if ( _isTestRunning && _context != null )
112+ {
113+ try
114+ {
115+ // Trigger refresh by changing query to current query
116+ _context . API . ChangeQuery ( _currentQuery , true ) ;
117+ }
118+ catch { }
119+ }
120+ } , null , 300 , 300 ) ;
121+
122+ results . Add ( new Result { Title = "Testing your internet speed..." , SubTitle = "Connecting to nearest server..." , IcoPath = GetIcon ( ) } ) ;
123+ return results ;
124+ }
125+ // if test is running, fall through to show progress
126+ }
127+
128+ if ( cmd == "history" )
129+ {
130+ var hist = _settings ? . History ?? new List < HistoryEntry > ( ) ;
131+ if ( hist . Count == 0 )
132+ {
133+ results . Add ( new Result { Title = "No history" , SubTitle = "No previous tests recorded" , IcoPath = GetIcon ( ) } ) ;
134+ return results ;
135+ }
136+
137+ // show all history entries
138+ foreach ( var entry in hist . AsReadOnly ( ) . Reverse ( ) )
139+ {
140+ results . Add ( new Result
141+ {
142+ Title = $ "↓ { entry . DownloadSpeed : F1} Mbps ↑ { entry . UploadSpeed : F1} Mbps",
143+ SubTitle = $ "{ entry . Time : g} • Ping: { entry . Ping : F0} ms",
144+ IcoPath = GetIcon ( )
145+ } ) ;
146+ }
147+
148+ return results ;
149+ }
82150
151+ if ( cmd == "ip" )
152+ {
153+ var internalIp = await GetInternalIpAsync ( ) ;
154+ results . Add ( new Result
155+ {
156+ Title = "Internal IP" ,
157+ SubTitle = $ "{ internalIp } • Press Enter to copy",
158+ IcoPath = GetIcon ( ) ,
159+ Action = _ =>
160+ {
161+ try { System . Windows . Clipboard . SetText ( internalIp ) ; } catch { }
162+ return true ;
163+ }
164+ } ) ;
165+
166+ var externalIp = await GetExternalIpAsync ( ) ;
167+ results . Add ( new Result
168+ {
169+ Title = "External IP" ,
170+ SubTitle = $ "{ externalIp } • Press Enter to copy",
171+ IcoPath = GetIcon ( ) ,
172+ Action = _ =>
173+ {
174+ try { System . Windows . Clipboard . SetText ( externalIp ) ; } catch { }
175+ return true ;
176+ }
177+ } ) ;
83178 return results ;
84179 }
85180
@@ -94,21 +189,16 @@ public async Task<List<Result>> QueryAsync(Query query, CancellationToken token)
94189 }
95190 else if ( _lastResult != null )
96191 {
97- var timeSince = DateTime . Now - _lastTestTime ;
98- var timeStr = timeSince . TotalMinutes < 60
99- ? $ "{ ( int ) timeSince . TotalMinutes } m ago"
100- : $ "{ ( int ) timeSince . TotalHours } h ago";
101-
102192 results . Add ( new Result
103193 {
104194 Title = $ "↓ { _lastResult . DownloadSpeed : F1} Mbps ↑ { _lastResult . UploadSpeed : F1} Mbps",
105- SubTitle = $ "Ping: { _lastResult . Ping : F0} ms • { _lastResult . ServerName } • { timeStr } • Enter to retest",
195+ SubTitle = $ "Ping: { _lastResult . Ping : F0} ms • { _lastResult . ServerName } • Enter to retest",
106196 IcoPath = GetIcon ( ) ,
107197 Action = _ =>
108198 {
109199 _lastResult = null ;
110200 _lastError = null ;
111- RunTest ( ) ;
201+ _context ? . API . ChangeQuery ( _context . CurrentPluginMetadata . ActionKeyword + " start" , true ) ;
112202 return false ;
113203 }
114204 } ) ;
@@ -196,18 +286,6 @@ private void RunTest()
196286 _currentDownloadSpeed = 0 ;
197287 _currentUploadSpeed = 0 ;
198288
199- _updateTimer = new Timer ( _ =>
200- {
201- if ( _isTestRunning && _context != null )
202- {
203- try
204- {
205- _context . API . ChangeQuery ( _context . CurrentPluginMetadata . ActionKeyword + " " , true ) ;
206- }
207- catch { }
208- }
209- } , null , 300 , 300 ) ;
210-
211289 var cliPath = await SpeedTestCLI . DownloadIfMissing ( _context ! ) ;
212290
213291 var result = await SpeedTestCLI . Run (
@@ -227,6 +305,29 @@ private void RunTest()
227305 _lastResult = result ;
228306 _lastTestTime = DateTime . Now ;
229307 _lastError = null ;
308+
309+ // record history with IPs from speedtest result
310+ var entry = new HistoryEntry
311+ {
312+ Time = DateTime . Now ,
313+ DownloadSpeed = result ? . DownloadSpeed ?? 0 ,
314+ UploadSpeed = result ? . UploadSpeed ?? 0 ,
315+ Ping = result ? . Ping ?? 0 ,
316+ ServerName = result ? . ServerName ?? string . Empty ,
317+ ResultUrl = result ? . ResultUrl ?? string . Empty ,
318+ InternalIP = result ? . InternalIP ?? "unknown" ,
319+ ExternalIP = result ? . ExternalIP ?? "unknown"
320+ } ;
321+
322+ _settings ??= new Settings ( ) ;
323+ _settings . History . Add ( entry ) ;
324+ // keep only the last N entries
325+ var max = _settings . MaxHistoryEntries > 0 ? _settings . MaxHistoryEntries : 20 ;
326+ if ( _settings . History . Count > max )
327+ _settings . History . RemoveRange ( 0 , _settings . History . Count - max ) ;
328+
329+ try { _context ? . API . SaveSettingJsonStorage < Settings > ( ) ; } catch { }
330+
230331 }
231332 catch ( Exception ex )
232333 {
@@ -237,25 +338,74 @@ private void RunTest()
237338 finally
238339 {
239340 _isTestRunning = false ;
240- _updateTimer ? . Dispose ( ) ;
241- _updateTimer = null ;
242-
243- await Task . Delay ( 50 ) ;
341+ _refreshTimer ? . Dispose ( ) ;
342+ _refreshTimer = null ;
343+
344+ // Trigger final refresh to show full detailed results
244345 if ( _context != null )
245346 {
347+ await Task . Delay ( 100 ) ;
246348 try
247349 {
248- _context . API . ChangeQuery ( _context . CurrentPluginMetadata . ActionKeyword , false ) ;
350+ _context . API . ChangeQuery ( _context . CurrentPluginMetadata . ActionKeyword + " result" , true ) ;
249351 }
250352 catch { }
251353 }
252354 }
253355 } ) ;
254356 }
255357
358+ private async Task < string > GetExternalIpAsync ( )
359+ {
360+ try
361+ {
362+ using var http = new System . Net . Http . HttpClient { Timeout = TimeSpan . FromSeconds ( 5 ) } ;
363+ return ( await http . GetStringAsync ( "https://api.ipify.org" ) ) . Trim ( ) ;
364+ }
365+ catch { return "unknown" ; }
366+ }
367+
368+ private Task < string > GetInternalIpAsync ( )
369+ {
370+ try
371+ {
372+ var host = System . Net . Dns . GetHostEntry ( System . Net . Dns . GetHostName ( ) ) ;
373+ foreach ( var ip in host . AddressList )
374+ {
375+ if ( ip . AddressFamily == System . Net . Sockets . AddressFamily . InterNetwork && ! System . Net . IPAddress . IsLoopback ( ip ) )
376+ return Task . FromResult ( ip . ToString ( ) ) ;
377+ }
378+ return Task . FromResult ( "unknown" ) ;
379+ }
380+ catch
381+ {
382+ return Task . FromResult ( "unknown" ) ;
383+ }
384+ }
385+
256386 public string GetTranslatedPluginTitle ( ) => "Speed Test" ;
257387 public string GetTranslatedPluginDescription ( ) => "Test your internet connection speed" ;
388+
389+ public System . Windows . Controls . Control CreateSettingPanel ( )
390+ {
391+ return new SettingsControl ( _context ! ) ;
392+ }
393+ }
394+
395+ public class Settings {
396+ public List < HistoryEntry > History { get ; set ; } = new List < HistoryEntry > ( ) ;
397+ public int MaxHistoryEntries { get ; set ; } = 20 ;
258398 }
259399
260- public class Settings { }
400+ public class HistoryEntry
401+ {
402+ public DateTime Time { get ; set ; }
403+ public double DownloadSpeed { get ; set ; }
404+ public double UploadSpeed { get ; set ; }
405+ public double Ping { get ; set ; }
406+ public string ServerName { get ; set ; } = "" ;
407+ public string ResultUrl { get ; set ; } = "" ;
408+ public string InternalIP { get ; set ; } = "" ;
409+ public string ExternalIP { get ; set ; } = "" ;
410+ }
261411}
0 commit comments