@@ -9900,6 +9900,129 @@ fn is_local_fresh_for_remote(path: &std::path::Path, remote_updated_at: &str) ->
99009900 local_secs >= remote_dt. timestamp ( )
99019901}
99029902
9903+ #[ derive( Serialize , Deserialize , Clone , Debug ) ]
9904+ #[ serde( rename_all = "camelCase" ) ]
9905+ struct NetworkInfo {
9906+ region : String ,
9907+ ip : String ,
9908+ is_proxy : bool ,
9909+ #[ serde( skip_serializing_if = "Option::is_none" ) ]
9910+ proxy_type : Option < String > ,
9911+ }
9912+
9913+ static NETWORK_INFO_CACHE : LazyLock < Mutex < Option < ( std:: time:: SystemTime , NetworkInfo ) > > > =
9914+ LazyLock :: new ( || Mutex :: new ( None ) ) ;
9915+
9916+ const NETWORK_INFO_TTL : Duration = Duration :: from_secs ( 3600 ) ;
9917+
9918+ fn network_info_cache_path ( ) -> PathBuf {
9919+ dirs:: home_dir ( )
9920+ . unwrap_or_else ( || PathBuf :: from ( "." ) )
9921+ . join ( ".lovstudio/lovcode/cache/network.json" )
9922+ }
9923+
9924+ #[ derive( Serialize , Deserialize ) ]
9925+ struct NetworkInfoCacheFile {
9926+ fetched_at_unix : u64 ,
9927+ info : NetworkInfo ,
9928+ }
9929+
9930+ fn read_network_info_cache_file ( ) -> Option < ( std:: time:: SystemTime , NetworkInfo ) > {
9931+ let raw = std:: fs:: read ( network_info_cache_path ( ) ) . ok ( ) ?;
9932+ let parsed: NetworkInfoCacheFile = serde_json:: from_slice ( & raw ) . ok ( ) ?;
9933+ let fetched_at = std:: time:: UNIX_EPOCH + Duration :: from_secs ( parsed. fetched_at_unix ) ;
9934+ Some ( ( fetched_at, parsed. info ) )
9935+ }
9936+
9937+ fn write_network_info_cache_file ( fetched_at : std:: time:: SystemTime , info : & NetworkInfo ) {
9938+ let path = network_info_cache_path ( ) ;
9939+ if let Some ( parent) = path. parent ( ) {
9940+ let _ = std:: fs:: create_dir_all ( parent) ;
9941+ }
9942+ let unix = fetched_at
9943+ . duration_since ( std:: time:: UNIX_EPOCH )
9944+ . map ( |d| d. as_secs ( ) )
9945+ . unwrap_or ( 0 ) ;
9946+ let payload = NetworkInfoCacheFile { fetched_at_unix : unix, info : info. clone ( ) } ;
9947+ if let Ok ( bytes) = serde_json:: to_vec ( & payload) {
9948+ let _ = std:: fs:: write ( & path, bytes) ;
9949+ }
9950+ }
9951+
9952+ #[ tauri:: command]
9953+ async fn get_network_info ( ) -> Result < NetworkInfo , String > {
9954+ if let Ok ( mut guard) = NETWORK_INFO_CACHE . lock ( ) {
9955+ if guard. is_none ( ) {
9956+ * guard = read_network_info_cache_file ( ) ;
9957+ }
9958+ if let Some ( ( fetched_at, info) ) = guard. as_ref ( ) {
9959+ if fetched_at. elapsed ( ) . map ( |e| e < NETWORK_INFO_TTL ) . unwrap_or ( false ) {
9960+ return Ok ( info. clone ( ) ) ;
9961+ }
9962+ }
9963+ }
9964+
9965+ let client = reqwest:: Client :: builder ( )
9966+ . timeout ( Duration :: from_secs ( 5 ) )
9967+ . build ( )
9968+ . map_err ( |e| format ! ( "client build failed: {e}" ) ) ?;
9969+
9970+ let resp = client
9971+ . get ( "https://ipinfo.io/json" )
9972+ . send ( )
9973+ . await
9974+ . map_err ( |e| format ! ( "request failed: {e}" ) ) ?;
9975+
9976+ if !resp. status ( ) . is_success ( ) {
9977+ let status = resp. status ( ) ;
9978+ if let Ok ( guard) = NETWORK_INFO_CACHE . lock ( ) {
9979+ if let Some ( ( _, info) ) = guard. as_ref ( ) {
9980+ return Ok ( info. clone ( ) ) ;
9981+ }
9982+ }
9983+ return Err ( format ! ( "ipinfo returned {status}" ) ) ;
9984+ }
9985+
9986+ let data: Value = resp
9987+ . json ( )
9988+ . await
9989+ . map_err ( |e| format ! ( "parse failed: {e}" ) ) ?;
9990+
9991+ let city = data. get ( "city" ) . and_then ( |v| v. as_str ( ) ) ;
9992+ let country = data. get ( "country" ) . and_then ( |v| v. as_str ( ) ) ;
9993+ let region = match ( city, country) {
9994+ ( Some ( c) , Some ( co) ) => format ! ( "{c}, {co}" ) ,
9995+ ( None , Some ( co) ) => co. to_string ( ) ,
9996+ _ => "Unknown" . to_string ( ) ,
9997+ } ;
9998+ let ip = data. get ( "ip" ) . and_then ( |v| v. as_str ( ) ) . unwrap_or ( "" ) . to_string ( ) ;
9999+ let privacy = data. get ( "privacy" ) ;
10000+ let is_vpn = privacy. and_then ( |p| p. get ( "vpn" ) ) . and_then ( |v| v. as_bool ( ) ) . unwrap_or ( false ) ;
10001+ let is_proxy_flag = privacy. and_then ( |p| p. get ( "proxy" ) ) . and_then ( |v| v. as_bool ( ) ) . unwrap_or ( false ) ;
10002+ let proxy_type = if is_vpn {
10003+ Some ( "VPN" . to_string ( ) )
10004+ } else if is_proxy_flag {
10005+ Some ( "Proxy" . to_string ( ) )
10006+ } else {
10007+ None
10008+ } ;
10009+
10010+ let info = NetworkInfo {
10011+ region,
10012+ ip,
10013+ is_proxy : is_vpn || is_proxy_flag,
10014+ proxy_type,
10015+ } ;
10016+
10017+ let now = std:: time:: SystemTime :: now ( ) ;
10018+ if let Ok ( mut guard) = NETWORK_INFO_CACHE . lock ( ) {
10019+ * guard = Some ( ( now, info. clone ( ) ) ) ;
10020+ }
10021+ write_network_info_cache_file ( now, & info) ;
10022+
10023+ Ok ( info)
10024+ }
10025+
990310026#[ cfg_attr( mobile, tauri:: mobile_entry_point) ]
990410027pub fn run ( ) {
990510028 tauri:: Builder :: default ( )
@@ -10101,6 +10224,7 @@ pub fn run() {
1010110224 }
1010210225 } )
1010310226 . invoke_handler ( tauri:: generate_handler![
10227+ get_network_info,
1010410228 make_window_nonactivating_panel,
1010510229 list_projects,
1010610230 list_sessions,
0 commit comments