Update application UI and functionality
This commit is contained in:
@@ -92,6 +92,11 @@ public sealed class TitleWeatherService(
|
||||
{
|
||||
private const double DistrictSearchMaxDistanceKm = 180;
|
||||
private const double CitySearchMaxDistanceKm = 500;
|
||||
private static readonly TimeSpan WeatherOperationTimeout = TimeSpan.FromSeconds(12);
|
||||
private static readonly HttpRequestPolicy WeatherRequestPolicy = new(
|
||||
TimeSpan.FromSeconds(7),
|
||||
MaxRetries: 0,
|
||||
CacheMode: HttpCacheMode.UseProtocol);
|
||||
|
||||
private static readonly Uri IpApiLocationUri = new("https://ipapi.co/json/");
|
||||
private static readonly Uri ClientLocationZhUri = BuildClientLocationUri("zh-Hans");
|
||||
@@ -114,12 +119,15 @@ public sealed class TitleWeatherService(
|
||||
|
||||
public async Task<TitleWeatherSnapshot> GetCurrentAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var operationCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
operationCts.CancelAfter(WeatherOperationTimeout);
|
||||
var requestToken = operationCts.Token;
|
||||
try
|
||||
{
|
||||
var location = await ResolveLocationAsync(cancellationToken).ConfigureAwait(false);
|
||||
var weatherPlace = await ResolveWeatherPlaceAsync(location, cancellationToken).ConfigureAwait(false);
|
||||
var location = await ResolveLocationAsync(requestToken).ConfigureAwait(false);
|
||||
var weatherPlace = await ResolveWeatherPlaceAsync(location, requestToken).ConfigureAwait(false);
|
||||
var uri = BuildForecastUri(weatherPlace);
|
||||
var forecast = await httpService.GetStringAsync(uri, cancellationToken).ConfigureAwait(false);
|
||||
var forecast = await GetWeatherStringAsync(uri, requestToken).ConfigureAwait(false);
|
||||
var snapshot = ParseForecast(location, weatherPlace, forecast);
|
||||
await WriteLogAsync(
|
||||
"Information",
|
||||
@@ -128,7 +136,7 @@ public sealed class TitleWeatherService(
|
||||
$"{snapshot.Location}; {snapshot.Condition}; query={weatherPlace.QueryLevel}").ConfigureAwait(false);
|
||||
return snapshot;
|
||||
}
|
||||
catch (Exception exception) when (exception is HttpRequestException or TaskCanceledException or JsonException or InvalidOperationException or FormatException)
|
||||
catch (Exception exception) when (IsRecoverableWeatherFailure(exception))
|
||||
{
|
||||
var safe = AppLocalizer.SanitizeSensitiveText(exception.Message, 180);
|
||||
await WriteLogAsync("Warning", "weather", "Title weather unavailable", safe).ConfigureAwait(false);
|
||||
@@ -177,10 +185,10 @@ public sealed class TitleWeatherService(
|
||||
{
|
||||
try
|
||||
{
|
||||
var content = await httpService.GetStringAsync(uri, cancellationToken).ConfigureAwait(false);
|
||||
var content = await GetWeatherStringAsync(uri, cancellationToken).ConfigureAwait(false);
|
||||
return ParseClientLocation(content);
|
||||
}
|
||||
catch (Exception exception) when (exception is HttpRequestException or TaskCanceledException or JsonException or InvalidOperationException or FormatException)
|
||||
catch (Exception exception) when (IsRecoverableWeatherFailure(exception))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
@@ -190,7 +198,7 @@ public sealed class TitleWeatherService(
|
||||
{
|
||||
try
|
||||
{
|
||||
var content = await httpService.GetStringAsync(IpApiLocationUri, cancellationToken).ConfigureAwait(false);
|
||||
var content = await GetWeatherStringAsync(IpApiLocationUri, cancellationToken).ConfigureAwait(false);
|
||||
using var document = JsonDocument.Parse(content);
|
||||
var root = document.RootElement;
|
||||
var latitude = GetDouble(root, "latitude");
|
||||
@@ -212,65 +220,20 @@ public sealed class TitleWeatherService(
|
||||
latitude.Value,
|
||||
longitude.Value);
|
||||
}
|
||||
catch (Exception exception) when (exception is HttpRequestException or TaskCanceledException or JsonException or InvalidOperationException or FormatException)
|
||||
catch (Exception exception) when (IsRecoverableWeatherFailure(exception))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<WeatherPlace> ResolveWeatherPlaceAsync(WeatherLocation location, CancellationToken cancellationToken)
|
||||
private Task<WeatherPlace> ResolveWeatherPlaceAsync(WeatherLocation location, CancellationToken cancellationToken)
|
||||
{
|
||||
if (location.DistrictGeoNameId is long districtId &&
|
||||
await TryGetGeocodedPlaceByIdAsync(districtId, cancellationToken).ConfigureAwait(false) is { } districtPlace &&
|
||||
IsNearExpectedLocation(districtPlace, location, DistrictSearchMaxDistanceKm))
|
||||
{
|
||||
return new WeatherPlace(
|
||||
FormatDisplayLocation(location, preferDistrict: true),
|
||||
AppLocalizer.T("区/县", "District"),
|
||||
districtPlace.Latitude,
|
||||
districtPlace.Longitude);
|
||||
}
|
||||
|
||||
if (location.CityGeoNameId is long cityId &&
|
||||
await TryGetGeocodedPlaceByIdAsync(cityId, cancellationToken).ConfigureAwait(false) is { } cityPlace &&
|
||||
IsNearExpectedLocation(cityPlace, location, CitySearchMaxDistanceKm))
|
||||
{
|
||||
return new WeatherPlace(
|
||||
FormatDisplayLocation(location, preferDistrict: false),
|
||||
AppLocalizer.T("市级", "City"),
|
||||
cityPlace.Latitude,
|
||||
cityPlace.Longitude);
|
||||
}
|
||||
|
||||
foreach (var query in BuildNameSearchQueries(location.QueryDistrict))
|
||||
{
|
||||
if (await TrySearchGeocodedPlaceAsync(query, location, DistrictSearchMaxDistanceKm, cancellationToken).ConfigureAwait(false) is { } place)
|
||||
{
|
||||
return new WeatherPlace(
|
||||
FormatDisplayLocation(location, preferDistrict: true),
|
||||
AppLocalizer.T("区/县", "District"),
|
||||
place.Latitude,
|
||||
place.Longitude);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var query in BuildNameSearchQueries(location.QueryCity))
|
||||
{
|
||||
if (await TrySearchGeocodedPlaceAsync(query, location, CitySearchMaxDistanceKm, cancellationToken).ConfigureAwait(false) is { } place)
|
||||
{
|
||||
return new WeatherPlace(
|
||||
FormatDisplayLocation(location, preferDistrict: false),
|
||||
AppLocalizer.T("市级", "City"),
|
||||
place.Latitude,
|
||||
place.Longitude);
|
||||
}
|
||||
}
|
||||
|
||||
return new WeatherPlace(
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
return Task.FromResult(new WeatherPlace(
|
||||
FormatDisplayLocation(location, preferDistrict: true),
|
||||
AppLocalizer.T("经纬度", "Coordinates"),
|
||||
location.Latitude,
|
||||
location.Longitude);
|
||||
location.Longitude));
|
||||
}
|
||||
|
||||
private async Task<GeocodedPlace?> TryGetGeocodedPlaceByIdAsync(long id, CancellationToken cancellationToken)
|
||||
@@ -278,11 +241,11 @@ public sealed class TitleWeatherService(
|
||||
try
|
||||
{
|
||||
var uri = new Uri($"https://geocoding-api.open-meteo.com/v1/get?id={id}&language=en&format=json");
|
||||
var content = await httpService.GetStringAsync(uri, cancellationToken).ConfigureAwait(false);
|
||||
var content = await GetWeatherStringAsync(uri, cancellationToken).ConfigureAwait(false);
|
||||
using var document = JsonDocument.Parse(content);
|
||||
return TryParseGeocodedPlace(document.RootElement, out var place) ? place : null;
|
||||
}
|
||||
catch (Exception exception) when (exception is HttpRequestException or TaskCanceledException or JsonException or InvalidOperationException or FormatException)
|
||||
catch (Exception exception) when (IsRecoverableWeatherFailure(exception))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
@@ -303,7 +266,7 @@ public sealed class TitleWeatherService(
|
||||
{
|
||||
var uri = new Uri("https://geocoding-api.open-meteo.com/v1/search" +
|
||||
$"?name={Uri.EscapeDataString(query)}&count=10&language=en&format=json");
|
||||
var content = await httpService.GetStringAsync(uri, cancellationToken).ConfigureAwait(false);
|
||||
var content = await GetWeatherStringAsync(uri, cancellationToken).ConfigureAwait(false);
|
||||
using var document = JsonDocument.Parse(content);
|
||||
if (!document.RootElement.TryGetProperty("results", out var results) ||
|
||||
results.ValueKind != JsonValueKind.Array)
|
||||
@@ -327,12 +290,31 @@ public sealed class TitleWeatherService(
|
||||
.Select(item => item.Place)
|
||||
.FirstOrDefault();
|
||||
}
|
||||
catch (Exception exception) when (exception is HttpRequestException or TaskCanceledException or JsonException or InvalidOperationException or FormatException)
|
||||
catch (Exception exception) when (IsRecoverableWeatherFailure(exception))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<string> GetWeatherStringAsync(Uri uri, CancellationToken cancellationToken)
|
||||
{
|
||||
var response = await httpService.SendAsync(
|
||||
uri,
|
||||
policy: WeatherRequestPolicy,
|
||||
cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
return response.Content;
|
||||
}
|
||||
|
||||
private static bool IsRecoverableWeatherFailure(Exception exception)
|
||||
=> exception is HttpRequestException or
|
||||
HttpRequestTimeoutException or
|
||||
TimeoutException or
|
||||
OperationCanceledException or
|
||||
JsonException or
|
||||
InvalidOperationException or
|
||||
FormatException or
|
||||
IOException;
|
||||
|
||||
private static Uri BuildForecastUri(WeatherPlace place)
|
||||
{
|
||||
var latitude = place.Latitude.ToString("0.####", CultureInfo.InvariantCulture);
|
||||
|
||||
Reference in New Issue
Block a user