view changelogs, fixed background updates, updated strings, expressive loader

This commit is contained in:
Pranshul committed 2025-09-04 22:35:39 +05:30
1 parent dfda321aa6
commit f0bf80bb67
25 files changed
+936 -353

No files matched your search

-1
View File
@@ -79,7 +79,6 @@ flutter {
dependencies {
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4")
implementation("androidx.work:work-runtime-ktx:2.9.0")
implementation("androidx.glance:glance-appwidget:1.0.0-alpha05")
implementation("com.google.android.material:material:1.12.0")
}
+2 -1
View File
@@ -571,5 +571,6 @@
"insights_sub_49": "Very cold weather expected — possibly record lows.",
"insights_sub_50": "Temperatures are within the usual range — no records expected.",
"background_updates_not_working": "Background updates not working?",
"background_updates_not_working_sub": "Some device vendors restrict apps in the background. Check dontkillmyapp.com for steps to fix this."
"background_updates_not_working_sub": "Some device vendors restrict apps in the background. Check dontkillmyapp.com for steps to fix this.",
"about_weathermaster_sub": "Changelog, Version, License, and More"
}
+2 -1
View File
@@ -571,5 +571,6 @@
"insights_sub_49": "Very cold weather expected — possibly record lows.",
"insights_sub_50": "Temperatures are within the usual range — no records expected.",
"background_updates_not_working": "Background updates not working?",
"background_updates_not_working_sub": "Some device vendors restrict apps in the background. Check dontkillmyapp.com for steps to fix this."
"background_updates_not_working_sub": "Some device vendors restrict apps in the background. Check dontkillmyapp.com for steps to fix this.",
"about_weathermaster_sub": "Changelog, Version, License, and More"
}
+23 -10
View File
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import '../utils/theme.dart';
import 'package:material_color_utilities/material_color_utilities.dart';
List<LinearGradient> getGradients(
bool isLight, bool isFroggyLayout, iscurrentDay) {
@@ -16,8 +17,11 @@ List<LinearGradient> getGradients(
? iscurrentDay
? LinearGradient(
colors: [
Color(paletteWeather.secondary.get(30)),
Color(paletteWeather.secondary.get(15))
Color(CorePalette.of(
const Color.fromARGB(255, 31, 25, 0).toARGB32())
.secondary
.get(30)),
Color(paletteWeather.secondary.get(18))
],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
@@ -32,8 +36,11 @@ List<LinearGradient> getGradients(
stops: [0, 0.5])
: LinearGradient(
colors: [
Color(paletteWeather.secondary.get(25)),
Color(paletteWeather.secondary.get(20))
Color(CorePalette.of(
const Color.fromARGB(255, 31, 25, 0).toARGB32())
.secondary
.get(30)),
Color(paletteWeather.secondary.get(18))
],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
@@ -74,7 +81,10 @@ List<LinearGradient> getGradients(
stops: [0, 0.5],
)
: LinearGradient(
colors: [const Color.fromARGB(255, 4, 36, 83), Color(0xFF004a76)],
colors: [
const Color.fromARGB(255, 5, 47, 110),
Color.fromARGB(255, 0, 51, 92)
],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
stops: [0, 0.5]),
@@ -180,8 +190,8 @@ List<LinearGradient> getGradientsScrolled(
? iscurrentDay
? LinearGradient(
colors: [
Color(paletteWeather.secondary.get(15)),
Color(paletteWeather.secondary.get(15))
Color(paletteWeather.secondary.get(18)),
Color(paletteWeather.secondary.get(18))
],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
@@ -196,8 +206,8 @@ List<LinearGradient> getGradientsScrolled(
stops: [0, 0.5])
: LinearGradient(
colors: [
Color(paletteWeather.secondary.get(20)),
Color(paletteWeather.secondary.get(20))
Color(paletteWeather.secondary.get(18)),
Color(paletteWeather.secondary.get(18))
],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
@@ -239,7 +249,10 @@ List<LinearGradient> getGradientsScrolled(
stops: [0, 0.5],
)
: LinearGradient(
colors: [Color(0xFF004a76), Color(0xFF004a76)],
colors: [
Color.fromARGB(255, 0, 51, 92),
Color.fromARGB(255, 0, 51, 92)
],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
stops: [0, 0.5]),
+6
View File
@@ -24,6 +24,7 @@ import 'dart:ui' as ui;
import 'services/widget_service.dart';
import 'widget_background.dart';
import 'package:flutter_displaymode/flutter_displaymode.dart';
import 'package:home_widget/home_widget.dart';
final CorePalette paletteStartScreen = CorePalette.of(
const Color.fromARGB(255, 255, 196, 0).toARGB32(),
@@ -32,6 +33,8 @@ final CorePalette paletteStartScreen = CorePalette.of(
@pragma('vm:entry-point')
Future<void> workerUpdateWidget() async {
WidgetsFlutterBinding.ensureInitialized();
final dir = await getApplicationDocumentsDirectory();
Hive.init(dir.path);
await updateHomeWidget(null, updatedFromHome: false);
}
@@ -68,6 +71,7 @@ const easySupportedLocales = [
Locale('zh', 'CN'),
Locale('zh', 'TW'),
Locale('ca', 'ES'),
Locale('bg', 'BG')
];
final flutterSupportedLocales =
@@ -94,6 +98,8 @@ void main() async {
final dir = await getApplicationDocumentsDirectory();
Hive.init(dir.path);
await Hive.openBox('changelogs');
// widget------
bool useBackgroundUpdates = prefs.getBool("useBackgroundUpdates") ?? false;
+275 -6
View File
@@ -5,13 +5,18 @@ import '../utils/open_links.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
import 'package:hive/hive.dart';
import 'package:expressive_loading_indicator/expressive_loading_indicator.dart';
class AboutPage extends StatelessWidget {
const AboutPage({super.key});
@override
Widget build(BuildContext context) {
final colorTheme = Theme.of(context).colorScheme;
return Scaffold(
backgroundColor: Colors.transparent,
appBar: AppBar(
automaticallyImplyLeading: false,
backgroundColor: Theme.of(context).colorScheme.surfaceContainer,
@@ -31,10 +36,93 @@ class AboutPage extends StatelessWidget {
Text(
"WeatherMaster",
style: TextStyle(
color: Theme.of(context).colorScheme.secondary,
color: Theme.of(context).colorScheme.onSurface,
fontSize: 24),
),
CheckUpdateButton()
Row(
spacing: 2,
children: [
CheckUpdateButton(),
FilledButton.tonal(
onPressed: () {
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(28)),
),
builder: (context) {
return DraggableScrollableSheet(
initialChildSize: 0.4,
minChildSize: 0.4,
maxChildSize: 0.9,
snap: true,
builder: (context, scrollController) {
return Scaffold(
appBar: AppBar(
toolbarHeight: 65,
elevation: 1,
automaticallyImplyLeading: false,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(28),
topRight: Radius.circular(28))),
title: Text("Changelog"),
actions: [
IconButton.filledTonal(
onPressed: () {
Navigator.pop(context);
},
icon: Icon(Icons.close)),
SizedBox(
width: 10,
)
],
),
body: Container(
decoration: BoxDecoration(
color: colorTheme.surfaceContainerLow,
boxShadow: [
BoxShadow(
blurRadius: 10,
color: Colors.black26,
offset: Offset(0, -2),
),
],
),
child: ChangelogSheet(
scrollController: scrollController),
),
);
},
);
},
);
},
style: ButtonStyle(
padding: WidgetStateProperty.all(
EdgeInsets.symmetric(horizontal: 12, vertical: 5),
),
shape: WidgetStateProperty.all(
RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(3),
topRight: Radius.circular(50),
bottomLeft: Radius.circular(3),
bottomRight: Radius.circular(50)),
),
),
minimumSize: WidgetStateProperty.all(Size(0, 30)),
iconAlignment: IconAlignment.end,
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
child: Text("Whats new",
style: TextStyle(fontWeight: FontWeight.w700)),
),
],
)
]),
Container(
clipBehavior: Clip.hardEdge,
@@ -42,8 +130,8 @@ class AboutPage extends StatelessWidget {
BoxDecoration(borderRadius: BorderRadius.circular(50)),
child: Image.asset(
"assets/weather-icons/new_icon.png",
width: 80,
height: 80,
width: 76,
height: 76,
))
],
),
@@ -420,12 +508,21 @@ class _CheckUpdateButtonState extends State<CheckUpdateButton> {
Symbols.refresh,
weight: 700,
),
label: Text(isChecking ? 'checking'.tr() : currentVersion,
style: TextStyle(fontWeight: FontWeight.w700)),
label:
Text(currentVersion, style: TextStyle(fontWeight: FontWeight.w700)),
style: ButtonStyle(
padding: WidgetStateProperty.all(
EdgeInsets.symmetric(horizontal: 12, vertical: 5),
),
shape: WidgetStateProperty.all(
RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(50),
topRight: Radius.circular(3),
bottomLeft: Radius.circular(50),
bottomRight: Radius.circular(3)),
),
),
minimumSize: WidgetStateProperty.all(Size(0, 30)),
iconAlignment: IconAlignment.end,
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
@@ -433,3 +530,175 @@ class _CheckUpdateButtonState extends State<CheckUpdateButton> {
);
}
}
class ChangelogService {
final String githubRepo = "PranshulGG/WeatherMaster";
final Box _box = Hive.box('changelogs');
Future<List<Map<String, dynamic>>> getChangelogs() async {
final now = DateTime.now();
final lastFetch = _box.get('lastFetch') as DateTime?;
if (lastFetch != null &&
now.difference(lastFetch) < const Duration(hours: 48)) {
final cachedData =
List<Map<String, dynamic>>.from(_box.get('data', defaultValue: []));
await Future.delayed(const Duration(seconds: 2));
return cachedData;
}
final url = Uri.parse("https://api.github.com/repos/$githubRepo/releases");
final response = await http.get(url);
if (response.statusCode != 200) {
throw Exception("Failed to fetch releases");
}
final releases = jsonDecode(response.body) as List<dynamic>;
final parsed = releases
.where((r) => r['prerelease'] == false)
.take(10)
.map((r) => {
"tag": r["tag_name"],
"name": r["name"] ?? "",
"body": r["body"] ?? "",
"published_at": r["published_at"],
})
.toList();
await _box.put('data', parsed);
await _box.put('lastFetch', now);
return parsed;
}
}
class ChangelogSheet extends StatefulWidget {
final ScrollController scrollController;
const ChangelogSheet({super.key, required this.scrollController});
@override
State<ChangelogSheet> createState() => _ChangelogSheetState();
}
class _ChangelogSheetState extends State<ChangelogSheet> {
final service = ChangelogService();
late Future<List<Map<String, dynamic>>> _future;
@override
void initState() {
super.initState();
_future = service.getChangelogs();
}
@override
Widget build(BuildContext context) {
return FutureBuilder(
future: _future,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(
child: ExpressiveLoadingIndicator(
activeSize: 38,
color: Theme.of(context).colorScheme.primary,
));
}
if (snapshot.hasError) {
return Center(child: Text("Error loading changelog"));
}
final releases = snapshot.data as List<Map<String, dynamic>>;
if (releases.isEmpty) {
return const Center(child: Text("No changelogs available"));
}
final buffer = StringBuffer("");
for (final release in releases) {
buffer.writeln("# ${release['tag']}");
buffer.writeln(release['body']);
buffer.writeln("\n---\n");
}
final colorTheme = Theme.of(context).colorScheme;
return ListView.builder(
controller: widget.scrollController,
itemCount: releases.length,
itemBuilder: (context, index) {
final release = releases[index];
return Container(
margin: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
padding: EdgeInsets.all(12),
decoration: BoxDecoration(
color: colorTheme.surfaceContainerLowest,
borderRadius: BorderRadius.circular(16),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 6),
Row(spacing: 2.7, children: [
Container(
padding: const EdgeInsets.only(
left: 10, right: 10, bottom: 3, top: 3),
decoration: BoxDecoration(
color: colorTheme.primaryContainer,
borderRadius: BorderRadius.circular(50)),
child: Text(
release['tag'],
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color:
Theme.of(context).colorScheme.onPrimaryContainer,
),
),
),
if (index == 0)
Container(
padding: const EdgeInsets.only(
left: 10, right: 10, bottom: 3, top: 3),
decoration: BoxDecoration(
color: colorTheme.tertiary,
borderRadius: BorderRadius.circular(8)),
child: Text(
"Latest",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.onTertiary,
),
),
),
]),
SizedBox(height: 12),
MarkdownBody(
data: release['body'],
styleSheet: MarkdownStyleSheet.fromTheme(Theme.of(context))
.copyWith(
blockquote: TextStyle(
color: colorTheme.onSurfaceVariant,
fontStyle: FontStyle.italic,
),
blockquoteDecoration: BoxDecoration(
color: Colors.transparent,
border: Border(
left: BorderSide(
color: colorTheme.outline, width: 4)),
),
),
onTapLink: (text, href, title) {
if (href != null) openLink(href);
},
),
],
),
);
},
);
},
);
}
}
+2 -3
View File
@@ -40,7 +40,7 @@ class _BackgroundUpdatesPageState extends State<BackgroundUpdatesPage> {
}
final optionsInterval = {
30: "30 ${"minutes".tr()}",
15: "30 ${"minutes".tr()}",
60: "1 ${"hours".tr()}",
90: "1.5 ${"hours".tr()}",
120: "2 ${"hours".tr()}",
@@ -151,8 +151,7 @@ class _BackgroundUpdatesPageState extends State<BackgroundUpdatesPage> {
title: BatteryOptWidget(), // 0%
),
SettingSwitchTile(
title: Text(
"Show a snack message when a new version is released"),
title: Text("new_version_snack_message".tr()),
toggled: showNewVerNotification,
onChanged: (value) {
PreferencesHelper.setBool(
-1
View File
@@ -958,7 +958,6 @@ class ConditionsWidgetsForecast extends StatelessWidget {
crossAxisCount: 2,
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
padding: EdgeInsets.only(top: 8.5),
crossAxisSpacing: 12,
mainAxisSpacing: 12,
children: [
+52 -40
View File
@@ -95,47 +95,59 @@ class _ExtendWidgetState extends State<ExtendWidget> {
Widget build(BuildContext context) {
final colorTheme = Theme.of(context).colorScheme;
if (_showLoader) {
return Scaffold(
body: Center(
child: ExpressiveLoadingIndicator(
color: colorTheme.primary,
activeSize: 48,
),
));
}
// if (_showLoader) {
// return Scaffold(
// body: Center(
// child: ExpressiveLoadingIndicator(
// color: colorTheme.primary,
// activeSize: 48,
// ),
// ));
// }
return Scaffold(
body: CustomScrollView(slivers: [
SliverAppBar.large(
titleSpacing: 0,
automaticallyImplyLeading: false,
flexibleSpace: FlexibleSpaceBar(
title: Row(
spacing: 5,
children: [Icon(iconData), Text(extendedTitle)],
),
expandedTitleScale: 1.3,
titlePadding: EdgeInsets.all(16),
),
backgroundColor: Theme.of(context).colorScheme.surface,
expandedHeight: 100,
scrolledUnderElevation: 1,
actions: [
IconButton(
onPressed: () {
Navigator.pop(context);
},
icon: Icon(
Symbols.close,
weight: 600,
)),
SizedBox(width: 5)
],
),
SliverToBoxAdapter(
child: child,
)
]));
body: AnimatedSwitcher(
duration: const Duration(milliseconds: 500),
switchInCurve: Curves.easeIn,
switchOutCurve: Curves.easeOut,
child: _showLoader
? Center(
key: const ValueKey('loader'),
child: ExpressiveLoadingIndicator(
color: colorTheme.primary,
activeSize: 48,
),
)
: CustomScrollView(key: const ValueKey('content'), slivers: [
SliverAppBar.large(
titleSpacing: 0,
automaticallyImplyLeading: false,
flexibleSpace: FlexibleSpaceBar(
title: Row(
spacing: 5,
children: [Icon(iconData), Text(extendedTitle)],
),
expandedTitleScale: 1.3,
titlePadding: EdgeInsets.all(16),
),
backgroundColor: Theme.of(context).colorScheme.surface,
expandedHeight: 100,
scrolledUnderElevation: 1,
actions: [
IconButton(
onPressed: () {
Navigator.pop(context);
},
icon: Icon(
Symbols.close,
weight: 600,
)),
SizedBox(width: 5)
],
),
SliverToBoxAdapter(
child: child,
)
])));
}
Widget buildHumidityExtended() {
+121 -27
View File
@@ -123,7 +123,7 @@ class _WeatherHomeState extends State<WeatherHome> {
bool showAlertsPref = PreferencesHelper.getBool("showAlerts") ?? true;
bool widgetsUpdated = false;
bool? iscurrentDay = false;
bool? _cachedIsShowFrog;
bool? lastIsDay;
final WeatherFroggyManager _weatherManager = WeatherFroggyManager();
@@ -132,6 +132,9 @@ class _WeatherHomeState extends State<WeatherHome> {
bool _isLoadingFroggy = true;
bool layoutCreated = false;
bool isFirstAppBuild = true;
bool onLoadForceCall = false;
double? lat;
double? lon;
@@ -251,20 +254,30 @@ class _WeatherHomeState extends State<WeatherHome> {
isHomeLocation = false;
}
final hasInternet = await hasRealInternet();
final hasInternet = await hasNetworkConnection();
if (!hasInternet) {
_isAppFullyLoaded = true;
SnackUtil.showSnackBar(
context: context, message: "network_unavailable".tr());
if (!_istriggeredFromLocations) {
_isAppFullyLoaded = true;
} else {}
if (isFirstAppBuild) {
SnackUtil.showSnackBar(
context: context, message: "network_unavailable".tr());
isFirstAppBuild = false;
}
} else if (lastUpdated != null) {
final lastUpdateTime = DateTime.tryParse(lastUpdated);
final now = DateTime.now();
if (lastUpdateTime != null &&
now.difference(lastUpdateTime).inMinutes < 450) {
_isAppFullyLoaded = true;
if (!_istriggeredFromLocations) {
_isAppFullyLoaded = true;
}
isFirstAppBuild = false;
} else {
checkAndUpdateHomeLocation();
isFirstAppBuild = false;
}
}
return json.decode(cached);
@@ -306,7 +319,7 @@ class _WeatherHomeState extends State<WeatherHome> {
}
Future<void> _refreshWeatherData() async {
final hasInternet = await hasRealInternet();
final hasInternet = await hasNetworkConnection();
if (isViewLocation) {
if (!mounted) return;
@@ -353,8 +366,24 @@ class _WeatherHomeState extends State<WeatherHome> {
}
final weatherService = WeatherService();
final result = await weatherService.fetchWeather(lat!, lon!,
locationName: cacheKey, context: context);
Map<String, dynamic>? result;
try {
result = await weatherService.fetchWeather(lat!, lon!,
locationName: cacheKey, context: context);
} catch (e) {
setState(() {
_isAppFullyLoaded = true;
});
if (context != null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('data_fetch_error'.tr()),
duration: Duration(seconds: 5),
),
);
}
}
if (result == null) {
return;
@@ -460,9 +489,27 @@ class _WeatherHomeState extends State<WeatherHome> {
await saveLocation(saved);
final weatherService = WeatherService();
await weatherService.fetchWeather(currentLat, currentLon,
locationName: currentCacheKey, context: context);
try {
await weatherService.fetchWeather(
currentLat,
currentLon,
locationName: currentCacheKey,
context: context,
);
} catch (e) {
setState(() {
_isAppFullyLoaded = true;
});
if (context != null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('data_fetch_error'.tr()),
duration: Duration(seconds: 5),
),
);
}
}
setState(() {
_isAppFullyLoaded = false;
_istriggeredFromLocations = true;
@@ -657,14 +704,14 @@ class _WeatherHomeState extends State<WeatherHome> {
opacity: val,
child: RepaintBoundary(
child: Container(
padding: const EdgeInsets.all(6),
padding: const EdgeInsets.all(2),
decoration: BoxDecoration(
color: colorTheme.primaryContainer,
borderRadius: BorderRadius.circular(50),
),
child: ExpressiveLoadingIndicator(
color: colorTheme.primary,
activeSize: 48,
activeSize: 40,
),
),
),
@@ -748,7 +795,13 @@ class _WeatherHomeState extends State<WeatherHome> {
// cloudy
isLight
? paletteWeather.secondary.get(98)
: paletteWeather.secondary.get(useDarkerBackground ? 2 : 8),
: paletteWeather.secondary.get(useDarkerBackground
? 2
: !isShowFrog
? iscurrentDay!
? 8
: 3
: 8),
// overcast
isLight
@@ -845,10 +898,24 @@ class _WeatherHomeState extends State<WeatherHome> {
daily['precipitation_probability_max'];
final List<dynamic> dailyWeatherCodes = daily['weather_code'];
void maybeUpdateWeatherAnimation(Map<String, dynamic> current) {
void maybeUpdateWeatherAnimation(Map<String, dynamic> current,
{isForce = false}) {
final int weatherCode = current['weather_code'];
final int isDay = current['is_day'];
if (isForce) {
WidgetsBinding.instance.addPostFrameCallback((_) {
setState(() {
weatherAnimationWidget =
WeatherConditionAnimationMapper.build(
weatherCode: weatherCode,
isDay: isDay,
context: context,
setFullDisplay: !isShowFrog);
});
});
}
if (_cachedWeatherCode != weatherCode || _cachedIsDay != isDay) {
weatherAnimationWidget = WeatherConditionAnimationMapper.build(
weatherCode: weatherCode,
@@ -858,9 +925,22 @@ class _WeatherHomeState extends State<WeatherHome> {
_cachedWeatherCode = weatherCode;
_cachedIsDay = isDay;
_cachedIsShowFrog = isShowFrog;
onLoadForceCall = false;
}
}
if (onLoadForceCall) {
if (_cachedIsShowFrog != isShowFrog) {
maybeUpdateWeatherAnimation(current, isForce: true);
print('called');
_cachedIsShowFrog = isShowFrog;
}
} else {
onLoadForceCall = true;
}
final Map<String, (DateTime, DateTime)> daylightMap = {
for (int i = 0; i < dailyDates.length; i++)
dailyDates[i]: (
@@ -1340,18 +1420,32 @@ class _WeatherHomeState extends State<WeatherHome> {
await Future.delayed(Duration(milliseconds: 300));
if (result != null) {
if (result['viewLocaton'] == true) {
Map<String, dynamic>? result;
final weatherService = WeatherService();
final result =
await weatherService.fetchWeather(
PreferencesHelper.getJson(
'selectedViewLocation')?['lat'],
PreferencesHelper.getJson(
'selectedViewLocation')?['lon'],
locationName:
"${PreferencesHelper.getJson('selectedViewLocation')?['city']}, ${PreferencesHelper.getJson('selectedViewLocation')?['country']}",
context: context,
isOnlyView: true,
);
try {
result = await weatherService.fetchWeather(
lat!, lon!,
locationName: cacheKey,
context: context);
} catch (e) {
result = null;
setState(() {
_isAppFullyLoaded = true;
});
if (context != null) {
ScaffoldMessenger.of(context)
.showSnackBar(
SnackBar(
content:
Text('data_fetch_error'.tr()),
duration: Duration(seconds: 5),
),
);
}
}
if (result == null) {
} else {
setState(() {
@@ -1429,7 +1523,7 @@ class _WeatherHomeState extends State<WeatherHome> {
currentLastUpdated: formattedTime,
),
WeatherFrogIconWidget(iconUrl: _iconUrlFroggy),
const SizedBox(height: 13.8),
const SizedBox(height: 14),
SizedBox(
width: isFoldableLayout(context) ? 500 : null,
height: null,
+11 -4
View File
@@ -161,7 +161,7 @@ class TranslationProgressService {
final response = await http.get(
Uri.parse(
'https://api.crowdin.com/api/v2/projects/$projectId/languages/progress?limit=31&offset=0'),
'https://api.crowdin.com/api/v2/projects/$projectId/languages/progress?limit=32&offset=0'),
headers: {
'Authorization': 'Bearer $apiToken',
'Content-Type': 'application/json',
@@ -293,6 +293,9 @@ class _LanguageTileState extends State<LanguageTile> {
title: Text(
widget.languageName['native']!,
style: TextStyle(
fontFamily: widget.languageName['native'] == "English"
? 'FlexFontEn'
: 'DefaultFont',
color: _isSelected
? Theme.of(context).colorScheme.onPrimaryContainer
: Theme.of(context).colorScheme.onSurface),
@@ -419,6 +422,9 @@ Map<String, String> getLanguageNames(Locale locale) {
if (lang == 'ca' && country == 'ES') {
return {'native': 'Català', 'english': 'Catalan'};
}
if (lang == 'bg' && country == 'BG') {
return {'native': 'Български', 'english': 'Bulgarian'};
}
if (lang == 'de' && country == 'DE') {
return {'native': 'Deutsch', 'english': 'German'};
}
@@ -482,10 +488,10 @@ Map<String, String> getLanguageNames(Locale locale) {
if (lang == 'sl' && country == 'SI') {
return {'native': 'Slovenščina', 'english': 'Slovenian'};
}
if (lang == 'sr' && country == 'CS') {
if (lang == 'sr' && country == 'SP') {
return {'native': 'Српски (Ћирилица)', 'english': 'Serbian (Cyrillic)'};
}
if (lang == 'sr' && country == 'SP') {
if (lang == 'sr' && country == 'CS') {
return {'native': 'Српски', 'english': 'Serbian'};
}
if (lang == 'sv' && country == 'SE') {
@@ -554,7 +560,8 @@ String getLanguageCodeCrodwin(Locale locale) {
'tr', // Turkish
'uk', // Ukrainian
'vi', // Vietnamese
'ca'
'ca',
'bg'
].contains(lang)) {
return lang;
}
+18 -4
View File
@@ -14,6 +14,7 @@ import '../utils/condition_label_map.dart';
import 'package:material_symbols_icons/material_symbols_icons.dart';
import 'package:animations/animations.dart';
import '../widgets/dialog.dart';
import 'package:expressive_loading_indicator/expressive_loading_indicator.dart';
class LocationsScreen extends StatefulWidget {
const LocationsScreen({super.key});
@@ -28,12 +29,21 @@ final GlobalKey<ScaffoldMessengerState> _scaffoldMessengerKey =
class _LocationsScreenState extends State<LocationsScreen> {
List<SavedLocation> savedLocations = [];
bool _isFirstBuild = true;
bool _showLoader = true;
@override
void initState() {
super.initState();
loadSavedLocations();
Future.delayed(const Duration(milliseconds: 300), () {
if (mounted) {
setState(() {
_showLoader = false;
});
}
});
}
Future<void> loadSavedLocations() async {
@@ -159,9 +169,13 @@ class _LocationsScreenState extends State<LocationsScreen> {
),
);
},
child: isEditing
? buildReorderableListView(key: ValueKey('reorderable'))
: buildDismissibleListView(key: ValueKey('dismissible')),
child: _showLoader
? Center(
key: const ValueKey('loader'),
)
: isEditing
? buildReorderableListView(key: ValueKey('reorderable'))
: buildDismissibleListView(key: ValueKey('dismissible')),
),
floatingActionButtonLocation:
FloatingActionButtonLocation.centerFloat,
@@ -903,7 +917,7 @@ class _FadeInListItemState extends State<FadeInListItem> {
Widget build(BuildContext context) {
return AnimatedOpacity(
opacity: opacity,
duration: const Duration(milliseconds: 700),
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
child: widget.child,
);
+132 -48
View File
@@ -5,7 +5,6 @@ import 'package:http/http.dart' as http;
import 'package:shared_preferences/shared_preferences.dart';
import '../models/saved_location.dart';
import '../services/fetch_data.dart';
import '../utils/condition_label_map.dart';
import 'package:expressive_loading_indicator/expressive_loading_indicator.dart';
enum GeoProvider { nominatim, geonames, openMeteo }
@@ -75,7 +74,7 @@ class _SearchLocationsScreenState extends State<SearchLocationsScreen> {
Future<void> loadSavedProvider() async {
final prefs = await SharedPreferences.getInstance();
final index = prefs.getInt('geo_provider_index') ?? 0;
final index = prefs.getInt('geo_provider_index') ?? 1;
setState(() {
selectedProvider = GeoProvider.values[index];
});
@@ -87,6 +86,7 @@ class _SearchLocationsScreenState extends State<SearchLocationsScreen> {
}
Future<void> searchLocation(String input) async {
input = input.trim();
if (input.isEmpty) return;
setState(() {
@@ -172,51 +172,103 @@ class _SearchLocationsScreenState extends State<SearchLocationsScreen> {
setState(() => isLoading = false);
}
void openProviderDialog() async {
void openProviderSheet() async {
GeoProvider tempProvider = selectedProvider;
final colorTheme = Theme.of(context).colorScheme;
final confirmed = await showDialog<bool>(
final confirmed = await showModalBottomSheet<bool>(
context: context,
showDragHandle: true,
isScrollControlled: true,
backgroundColor: colorTheme.surfaceContainerLow,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(28)),
),
builder: (_) {
return AlertDialog(
title: Text("search_provider".tr()),
contentPadding:
EdgeInsets.only(left: 0, right: 0, top: 16, bottom: 5),
content: StatefulBuilder(
return Padding(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).padding.bottom + 10),
child: StatefulBuilder(
builder: (context, setState) {
return Column(
mainAxisSize: MainAxisSize.min,
children: GeoProvider.values.map((provider) {
return RadioListTile<GeoProvider>(
dense: true,
title: Text(
providerLabels[provider]!,
style: TextStyle(fontSize: 15),
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 18),
child: Text(
"search_provider".tr(),
style: TextStyle(fontSize: 22),
),
value: provider,
groupValue: tempProvider,
onChanged: (val) => setState(() => tempProvider = val!),
);
}).toList(),
),
SizedBox(height: 15),
...GeoProvider.values.asMap().entries.map((entry) {
final index = entry.key;
final provider = entry.value;
final isFirst = index == 0;
final isLast = index == GeoProvider.values.length - 1;
final isSelected = tempProvider == provider;
return Padding(
padding:
const EdgeInsets.only(left: 18, right: 18, bottom: 2),
child: ListTile(
shape: RoundedRectangleBorder(
borderRadius: isSelected
? BorderRadiusGeometry.circular(50)
: BorderRadius.only(
topRight:
Radius.circular(isFirst ? 18 : 2.6),
topLeft:
Radius.circular(isFirst ? 18 : 2.6),
bottomLeft:
Radius.circular(isLast ? 18 : 2.6),
bottomRight:
Radius.circular(isLast ? 18 : 2.6))),
tileColor: colorTheme.surfaceContainerLowest,
dense: true,
selected: isSelected,
selectedTileColor: colorTheme.primaryContainer,
title: Text(
providerLabels[provider]!,
style: TextStyle(
fontSize: 15,
color: isSelected
? colorTheme.onPrimaryContainer
: null,
),
),
onTap: () => setState(() => tempProvider = provider),
),
);
}).toList(),
SizedBox(height: 13),
// Divider(),
// SizedBox(height: 4),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 18),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
OutlinedButton(
onPressed: () => Navigator.pop(context, false),
child: Text(
'cancel'.tr(),
style: TextStyle(
fontSize: 16, fontWeight: FontWeight.w600),
)),
FilledButton(
onPressed: () => Navigator.pop(context, true),
child: Text(
'save'.tr(),
style: TextStyle(
fontSize: 16, fontWeight: FontWeight.w600),
)),
],
))
],
);
},
),
actions: [
TextButton(
child: Text(
"cancel".tr(),
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
),
onPressed: () => Navigator.pop(context, false),
),
TextButton(
child: Text(
"save".tr(),
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
),
onPressed: () => Navigator.pop(context, true),
),
],
);
},
);
@@ -251,7 +303,7 @@ class _SearchLocationsScreenState extends State<SearchLocationsScreen> {
title: TextField(
style: TextStyle(fontSize: 16, color: colorTheme.onSurface),
onChanged: (value) => query = value,
onSubmitted: searchLocation,
onSubmitted: (value) => searchLocation(value.trim()),
decoration: InputDecoration(
border: InputBorder.none,
focusedBorder: InputBorder.none,
@@ -339,7 +391,6 @@ class _SearchLocationsScreenState extends State<SearchLocationsScreen> {
color: colorTheme.primary,
)),
);
final location = results[index];
final lat = double.tryParse(
location['lat'] ?? '') ??
@@ -354,17 +405,33 @@ class _SearchLocationsScreenState extends State<SearchLocationsScreen> {
city: location['city'] ?? '',
country: location['country'] ?? '',
);
saveLocation(saved);
final cacheKey =
"${saved.city}_${saved.country}"
.toLowerCase()
.replaceAll(' ', '_');
final weatherService = WeatherService();
await weatherService.fetchWeather(
lat, lon,
locationName: cacheKey,
context: context);
try {
await weatherService.fetchWeather(
lat, lon,
locationName: cacheKey,
context: context);
} catch (e) {
Navigator.pop(context);
if (context != null) {
ScaffoldMessenger.of(context)
.showSnackBar(
SnackBar(
content:
Text('data_fetch_error'.tr()),
duration: Duration(seconds: 5),
),
);
}
return;
}
saveLocation(saved);
final prefs =
await SharedPreferences.getInstance();
@@ -455,13 +522,30 @@ class _SearchLocationsScreenState extends State<SearchLocationsScreen> {
)),
);
saveLocation(saved);
final cacheKey = "${saved.city}_${saved.country}"
.toLowerCase()
.replaceAll(' ', '_');
final weatherService = WeatherService();
await weatherService.fetchWeather(lat, lon,
locationName: cacheKey, context: context);
try {
await weatherService.fetchWeather(lat, lon,
locationName: cacheKey, context: context);
} catch (e) {
if (context != null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('data_fetch_error'.tr()),
duration: Duration(seconds: 5),
),
);
}
return;
}
saveLocation(saved);
// final weatherService = WeatherService();
// await weatherService.fetchWeather(lat, lon,
// locationName: cacheKey, context: context);
final prefs = await SharedPreferences.getInstance();
final jsonString =
@@ -541,7 +625,7 @@ class _SearchLocationsScreenState extends State<SearchLocationsScreen> {
],
),
FloatingActionButton(
onPressed: openProviderDialog,
onPressed: openProviderSheet,
child: Icon(Icons.tune),
)
],
+2 -1
View File
@@ -214,7 +214,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
isLight ? Color(0xffe6deff) : Color(0xff493e76),
isLight ? Color(0xff493e76) : Color(0xffe6deff)),
title: Text('${"about".tr()} WeatherMaster'),
description: Text('learn_about_app'.tr()),
description:
Text('Changelog, Version, License, and More'),
onTap: () {
Navigator.of(
context,
+109 -96
View File
@@ -59,117 +59,130 @@ class WeatherService {
'https://api.weatherapi.com/v1/astronomy.json?key=${dotenv.env['API_KEY_WEATHERAPI']!.toString()}&q=$lat,$lon');
// Prepare list of HTTP requests
final requests = <Future<http.Response>>[
http.get(uri),
http.get(airQualityUri),
http.get(astronomyUri),
];
try {
final requests = <Future<http.Response>>[
http.get(uri),
http.get(airQualityUri),
http.get(astronomyUri),
];
final responses = await Future.wait(requests);
final responses = await Future.wait(requests);
// Parse responses
final weatherData = json.decode(responses[0].body) as Map<String, dynamic>;
final airQualityData =
json.decode(responses[1].body) as Map<String, dynamic>;
final astronomyData =
json.decode(responses[2].body) as Map<String, dynamic>;
for (var response in responses) {
if (response.statusCode < 200 || response.statusCode >= 300) {
throw Exception(
'HTTP request failed with status: ${response.statusCode}');
}
}
// Parse responses
final weatherData =
json.decode(responses[0].body) as Map<String, dynamic>;
final airQualityData =
json.decode(responses[1].body) as Map<String, dynamic>;
final astronomyData =
json.decode(responses[2].body) as Map<String, dynamic>;
// Check if we need fallback data for missing fields
Map<String, dynamic> finalWeatherData = weatherData;
if (selectedModel != "best_match" && _hasIncompleteData(weatherData)) {
finalWeatherData = await _fetchWithFallback(
lat, lon, timezone, weatherData, selectedModel);
}
finalWeatherData['current'] = sanitizeCurrent(finalWeatherData['current']);
finalWeatherData['hourly'] = sanitizeHourly(finalWeatherData['hourly']);
finalWeatherData['daily'] = sanitizeDaily(finalWeatherData['daily']);
if (finalWeatherData['error'] == true ||
airQualityData['error'] == true ||
astronomyData['error'] == true) {
final reason = finalWeatherData['reason'] ??
airQualityData['reason'] ??
astronomyData['reason'] ??
'Unknown error';
if (context != null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
duration: Duration(seconds: 10),
content: Text("$reason. Please change your model"),
action: SnackBarAction(
label: 'Change model',
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (_) => const MeteoModelsPage()),
);
},
),
),
);
// Check if we need fallback data for missing fields
Map<String, dynamic> finalWeatherData = weatherData;
if (selectedModel != "best_match" && _hasIncompleteData(weatherData)) {
finalWeatherData = await _fetchWithFallback(
lat, lon, timezone, weatherData, selectedModel);
}
return null;
}
finalWeatherData['current'] =
sanitizeCurrent(finalWeatherData['current']);
finalWeatherData['hourly'] = sanitizeHourly(finalWeatherData['hourly']);
finalWeatherData['daily'] = sanitizeDaily(finalWeatherData['daily']);
final combinedDataForView = {
...finalWeatherData,
'air_quality': airQualityData,
'astronomy': astronomyData,
};
if (finalWeatherData['error'] == true ||
airQualityData['error'] == true ||
astronomyData['error'] == true) {
final reason = finalWeatherData['reason'] ??
airQualityData['reason'] ??
astronomyData['reason'] ??
'Unknown error';
if (context != null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
duration: Duration(seconds: 10),
content: Text("$reason. Please change your model"),
action: SnackBarAction(
label: 'Change model',
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (_) => const MeteoModelsPage()),
);
},
),
),
);
}
final nowForView = DateTime.now().toIso8601String();
return null;
}
if (isOnlyView) {
log("Only viewing data for $key. Not saving to cache.");
return {
'data': combinedDataForView,
'last_updated': nowForView,
'from_cache': false,
final combinedDataForView = {
...finalWeatherData,
'air_quality': airQualityData,
'astronomy': astronomyData,
};
}
final existingJson = box.get(key);
if (existingJson != null) {
final cachedMap = json.decode(existingJson);
final cachedData = cachedMap['data'];
final lastUpdated = cachedMap['last_updated'];
final nowForView = DateTime.now().toIso8601String();
if (json.encode(cachedData['current']) ==
json.encode(finalWeatherData['current']) &&
json.encode(cachedData['air_quality'] ?? {}) ==
json.encode(airQualityData) &&
json.encode(cachedData['astronomy'] ?? {}) ==
json.encode(astronomyData)) {
log("Hive: No update needed for $key");
if (isOnlyView) {
log("Only viewing data for $key. Not saving to cache.");
return {
'data': cachedData,
'last_updated': lastUpdated,
'from_cache': true,
'data': combinedDataForView,
'last_updated': nowForView,
'from_cache': false,
};
}
final existingJson = box.get(key);
if (existingJson != null) {
final cachedMap = json.decode(existingJson);
final cachedData = cachedMap['data'];
final lastUpdated = cachedMap['last_updated'];
if (json.encode(cachedData['current']) ==
json.encode(finalWeatherData['current']) &&
json.encode(cachedData['air_quality'] ?? {}) ==
json.encode(airQualityData) &&
json.encode(cachedData['astronomy'] ?? {}) ==
json.encode(astronomyData)) {
log("Hive: No update needed for $key");
return {
'data': cachedData,
'last_updated': lastUpdated,
'from_cache': true,
};
}
}
final now = DateTime.now().toIso8601String();
final combinedData = {
...finalWeatherData,
'air_quality': airQualityData,
'astronomy': astronomyData,
};
final wrappedData = {
'data': combinedData,
'last_updated': now,
};
await box.put(key, json.encode(wrappedData));
log("Hive: Updated cache for $key at $now");
return {
'data': combinedData,
'last_updated': now,
'from_cache': false,
};
} catch (e) {
// This will propagate the error to the caller
throw Exception('WeatherService.fetchWeather failed: $e');
}
final now = DateTime.now().toIso8601String();
final combinedData = {
...finalWeatherData,
'air_quality': airQualityData,
'astronomy': astronomyData,
};
final wrappedData = {
'data': combinedData,
'last_updated': now,
};
await box.put(key, json.encode(wrappedData));
log("Hive: Updated cache for $key at $now");
return {
'data': combinedData,
'last_updated': now,
'from_cache': false,
};
}
/// Check if weather data has missing or incomplete fields
+2 -1
View File
@@ -1,3 +1,4 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'open_links.dart';
import '../utils/condition_label_map.dart';
@@ -8,7 +9,7 @@ Widget homeBottomBar(BuildContext context, bool isLight) {
color: textColor,
fontSize: 18,
decoration: TextDecoration.underline,
decorationThickness: 3,
decorationThickness: context.locale.languageCode == "en" ? 2 : 3,
);
return RepaintBoundary(
+77 -65
View File
@@ -1,6 +1,7 @@
import 'dart:math';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'is_online.dart';
import 'dart:io';
class WeatherFroggyManager {
List<String> sunnyFrog = [];
@@ -14,103 +15,100 @@ class WeatherFroggyManager {
List<String> clearNightFrog = [];
List<String> partlyCloudyNightFrog = [];
Future<void> initializeIcons() async {
final online = await hasRealInternet();
if (online) {
_setOnlineIcons();
} else {
_setOfflineIcons();
}
Connectivity().onConnectivityChanged.listen((event) async {
if (await hasRealInternet()) {
Future<void> initializeIcons() async {
const testUrl =
"https://gitlab.com/bignutty/google-weather-icons/-/raw/main/froggie/v2/mobile/01-sunny/01-sunny-creek-swimming.png?ref_type=heads";
final online = await urlIsReachable(testUrl);
if (online) {
_setOnlineIcons();
} else {
_setOfflineIcons();
}
});
}
Connectivity().onConnectivityChanged.listen((event) async {
if (await hasNetworkConnection() && await urlIsReachable(testUrl)) {
_setOnlineIcons();
} else {
_setOfflineIcons();
}
});
}
void _setOnlineIcons() {
sunnyFrog = [
"https://gitlab.com/bignutty/google-weather-icons/-/raw/main/froggie/v2/mobile/01-sunny/01-sunny-creek-swimming.png?ref_type=heads",
"https://gitlab.com/bignutty/google-weather-icons/-/raw/main/froggie/v2/mobile/01-sunny/01-sunny-home-laundry_c.png?ref_type=heads",
"https://gitlab.com/bignutty/google-weather-icons/-/raw/main/froggie/v2/mobile/01-sunny/01-sunny-orchard-picking.png?ref_type=heads",
"https://gitlab.com/bignutty/google-weather-icons/-/raw/main/froggie/v2/mobile/01-sunny/01-sunny-home-laundry_f.png?ref_type=heads"
"https://gitlab.com/bignutty/google-weather-icons/-/raw/main/froggie/v2/mobile/01-sunny/01-sunny-creek-swimming.png?ref_type=heads",
"https://gitlab.com/bignutty/google-weather-icons/-/raw/main/froggie/v2/mobile/01-sunny/01-sunny-home-laundry_c.png?ref_type=heads",
"https://gitlab.com/bignutty/google-weather-icons/-/raw/main/froggie/v2/mobile/01-sunny/01-sunny-orchard-picking.png?ref_type=heads",
"https://gitlab.com/bignutty/google-weather-icons/-/raw/main/froggie/v2/mobile/01-sunny/01-sunny-home-laundry_f.png?ref_type=heads"
];
mostlySunnyFrog = [
"https://gitlab.com/bignutty/google-weather-icons/-/raw/main/froggie/v2/mobile/02-mostly-sunny/02-mostly-sunny-citypark-picnic.png?ref_type=heads",
"https://gitlab.com/bignutty/google-weather-icons/-/raw/main/froggie/v2/mobile/02-mostly-sunny/02-mostly-sunny-beach-sunscreen.png?ref_type=heads",
"https://gitlab.com/bignutty/google-weather-icons/-/raw/main/froggie/v2/mobile/02-mostly-sunny/02-mostly-sunny-home-laundry.png?ref_type=heads",
"https://gitlab.com/bignutty/google-weather-icons/-/raw/main/froggie/v2/mobile/02-mostly-sunny/02-mostly-sunny-beach-sandcastle.png?ref_type=heads",
"https://gitlab.com/bignutty/google-weather-icons/-/raw/main/froggie/v2/mobile/02-mostly-sunny/02-mostly-sunny-rooftop-pinacolada.png?ref_type=heads"
"https://gitlab.com/bignutty/google-weather-icons/-/raw/main/froggie/v2/mobile/02-mostly-sunny/02-mostly-sunny-citypark-picnic.png?ref_type=heads",
"https://gitlab.com/bignutty/google-weather-icons/-/raw/main/froggie/v2/mobile/02-mostly-sunny/02-mostly-sunny-beach-sunscreen.png?ref_type=heads",
"https://gitlab.com/bignutty/google-weather-icons/-/raw/main/froggie/v2/mobile/02-mostly-sunny/02-mostly-sunny-home-laundry.png?ref_type=heads",
"https://gitlab.com/bignutty/google-weather-icons/-/raw/main/froggie/v2/mobile/02-mostly-sunny/02-mostly-sunny-beach-sandcastle.png?ref_type=heads",
"https://gitlab.com/bignutty/google-weather-icons/-/raw/main/froggie/v2/mobile/02-mostly-sunny/02-mostly-sunny-rooftop-pinacolada.png?ref_type=heads"
];
partlyCloudyFrog = [
"https://gitlab.com/bignutty/google-weather-icons/-/raw/main/froggie/v2/mobile/03-partly-cloudy-day/03-partly-cloudy-day-field-hiking_f.png?ref_type=heads",
"https://gitlab.com/bignutty/google-weather-icons/-/raw/main/froggie/v2/mobile/03-partly-cloudy-day/03-partly-cloudy-day-home-flowers.png?ref_type=heads",
"https://gitlab.com/bignutty/google-weather-icons/-/raw/main/froggie/v2/mobile/03-partly-cloudy-day/03-partly-cloudy-day-creek-feet.png?ref_type=heads",
"https://gitlab.com/bignutty/google-weather-icons/-/raw/main/froggie/v2/mobile/03-partly-cloudy-day/03-partly-cloudy-day-field-biking_c.png?ref_type=heads",
"https://gitlab.com/bignutty/google-weather-icons/-/raw/main/froggie/v2/mobile/03-partly-cloudy-day/03-partly-cloudy-day-citypark-ukelele.png?ref_type=heads"
"https://gitlab.com/bignutty/google-weather-icons/-/raw/main/froggie/v2/mobile/03-partly-cloudy-day/03-partly-cloudy-day-field-hiking_f.png?ref_type=heads",
"https://gitlab.com/bignutty/google-weather-icons/-/raw/main/froggie/v2/mobile/03-partly-cloudy-day/03-partly-cloudy-day-home-flowers.png?ref_type=heads",
"https://gitlab.com/bignutty/google-weather-icons/-/raw/main/froggie/v2/mobile/03-partly-cloudy-day/03-partly-cloudy-day-creek-feet.png?ref_type=heads",
"https://gitlab.com/bignutty/google-weather-icons/-/raw/main/froggie/v2/mobile/03-partly-cloudy-day/03-partly-cloudy-day-field-biking_c.png?ref_type=heads",
"https://gitlab.com/bignutty/google-weather-icons/-/raw/main/froggie/v2/mobile/03-partly-cloudy-day/03-partly-cloudy-day-citypark-ukelele.png?ref_type=heads"
];
overcastFrog = [
"https://gitlab.com/bignutty/google-weather-icons/-/raw/main/froggie/v2/mobile/09-cloudy/09-cloudy-home-flowers.png?ref_type=heads",
"https://gitlab.com/bignutty/google-weather-icons/-/raw/main/froggie/v2/mobile/09-cloudy/09-cloudy-orchard-watching.png?ref_type=heads",
"https://gitlab.com/bignutty/google-weather-icons/-/raw/main/froggie/v2/mobile/04-mostly-cloudy-day/04-mostly-cloudy-day-home-flowers.png?ref_type=heads"
"https://gitlab.com/bignutty/google-weather-icons/-/raw/main/froggie/v2/mobile/09-cloudy/09-cloudy-home-flowers.png?ref_type=heads",
"https://gitlab.com/bignutty/google-weather-icons/-/raw/main/froggie/v2/mobile/09-cloudy/09-cloudy-orchard-watching.png?ref_type=heads",
"https://gitlab.com/bignutty/google-weather-icons/-/raw/main/froggie/v2/mobile/04-mostly-cloudy-day/04-mostly-cloudy-day-home-flowers.png?ref_type=heads"
];
fogFrog = [
"https://gitlab.com/bignutty/google-weather-icons/-/raw/main/froggie/v2/mobile/26-haze-fog-dust-smoke/26-haze-fog-dust-smoke-bridge.png?ref_type=heads",
"https://gitlab.com/bignutty/google-weather-icons/-/raw/main/froggie/v2/mobile/26-haze-fog-dust-smoke/26-haze-fog-dust-smoke-fruit-stand.png?ref_type=heads",
"https://gitlab.com/bignutty/google-weather-icons/-/raw/main/froggie/v2/mobile/26-haze-fog-dust-smoke/26-haze-fog-dust-smoke-mountain.png?ref_type=heads",
"https://gitlab.com/bignutty/google-weather-icons/-/raw/main/froggie/v2/mobile/26-haze-fog-dust-smoke/26-haze-fog-dust-smoke-pier.png?ref_type=heads",
"https://gitlab.com/bignutty/google-weather-icons/-/raw/main/froggie/v2/mobile/26-haze-fog-dust-smoke/26-haze-fog-dust-smoke-field-lantern.png?ref_type=heads"
"https://gitlab.com/bignutty/google-weather-icons/-/raw/main/froggie/v2/mobile/26-haze-fog-dust-smoke/26-haze-fog-dust-smoke-bridge.png?ref_type=heads",
"https://gitlab.com/bignutty/google-weather-icons/-/raw/main/froggie/v2/mobile/26-haze-fog-dust-smoke/26-haze-fog-dust-smoke-fruit-stand.png?ref_type=heads",
"https://gitlab.com/bignutty/google-weather-icons/-/raw/main/froggie/v2/mobile/26-haze-fog-dust-smoke/26-haze-fog-dust-smoke-mountain.png?ref_type=heads",
"https://gitlab.com/bignutty/google-weather-icons/-/raw/main/froggie/v2/mobile/26-haze-fog-dust-smoke/26-haze-fog-dust-smoke-pier.png?ref_type=heads",
"https://gitlab.com/bignutty/google-weather-icons/-/raw/main/froggie/v2/mobile/26-haze-fog-dust-smoke/26-haze-fog-dust-smoke-field-lantern.png?ref_type=heads"
];
rainFrog = [
"https://gitlab.com/bignutty/google-weather-icons/-/raw/main/froggie/v2/mobile/11-rain/11-rain-creek-leaf.png?ref_type=heads",
"https://gitlab.com/bignutty/google-weather-icons/-/raw/main/froggie/v2/mobile/11-rain/11-rain-home-inside.png?ref_type=heads",
"https://gitlab.com/bignutty/google-weather-icons/-/raw/main/froggie/v2/mobile/04-mostly-cloudy-day/04-mostly-cloudy-day-orchard-treeswing_f.png?ref_type=heads",
"https://gitlab.com/bignutty/google-weather-icons/-/raw/main/froggie/v2/mobile/12-heavy-rain/12-heavy-rain-busstop-umbrella.png?ref_type=heads",
"https://gitlab.com/bignutty/google-weather-icons/-/raw/main/froggie/v2/mobile/12-heavy-rain/12-heavy-rain-cafe-sitting-singing.png?ref_type=heads",
"https://gitlab.com/bignutty/google-weather-icons/-/raw/main/froggie/v2/mobile/11-rain/11-shower-rain-field-leaf.png?ref_type=heads"
"https://gitlab.com/bignutty/google-weather-icons/-/raw/main/froggie/v2/mobile/11-rain/11-rain-creek-leaf.png?ref_type=heads",
"https://gitlab.com/bignutty/google-weather-icons/-/raw/main/froggie/v2/mobile/11-rain/11-rain-home-inside.png?ref_type=heads",
"https://gitlab.com/bignutty/google-weather-icons/-/raw/main/froggie/v2/mobile/04-mostly-cloudy-day/04-mostly-cloudy-day-orchard-treeswing_f.png?ref_type=heads",
"https://gitlab.com/bignutty/google-weather-icons/-/raw/main/froggie/v2/mobile/12-heavy-rain/12-heavy-rain-busstop-umbrella.png?ref_type=heads",
"https://gitlab.com/bignutty/google-weather-icons/-/raw/main/froggie/v2/mobile/12-heavy-rain/12-heavy-rain-cafe-sitting-singing.png?ref_type=heads",
"https://gitlab.com/bignutty/google-weather-icons/-/raw/main/froggie/v2/mobile/11-rain/11-shower-rain-field-leaf.png?ref_type=heads"
];
snowFrog = [
"https://gitlab.com/bignutty/google-weather-icons/-/raw/main/froggie/v2/mobile/15-snow-showers-snow/15-snow-showers-snow-citypark-snowman.png?ref_type=heads",
"https://gitlab.com/bignutty/google-weather-icons/-/raw/main/froggie/v2/mobile/15-snow-showers-snow/15-snow-showers-snow-home-shoveling.png?ref_type=heads",
"https://gitlab.com/bignutty/google-weather-icons/-/raw/main/froggie/v2/mobile/15-snow-showers-snow/15-snow-showers-snow-creek-iceskating.png?ref_type=heads",
"https://gitlab.com/bignutty/google-weather-icons/-/raw/main/froggie/v2/mobile/17-heavy-snow-blizzard/17-heavy-snow-blizzard-home-inside.png?ref_type=heads",
"https://gitlab.com/bignutty/google-weather-icons/-/raw/main/froggie/v2/mobile/16-blowing-snow/16-blowing-snow-field-snowman.png?ref_type=heads"
"https://gitlab.com/bignutty/google-weather-icons/-/raw/main/froggie/v2/mobile/15-snow-showers-snow/15-snow-showers-snow-citypark-snowman.png?ref_type=heads",
"https://gitlab.com/bignutty/google-weather-icons/-/raw/main/froggie/v2/mobile/15-snow-showers-snow/15-snow-showers-snow-home-shoveling.png?ref_type=heads",
"https://gitlab.com/bignutty/google-weather-icons/-/raw/main/froggie/v2/mobile/15-snow-showers-snow/15-snow-showers-snow-creek-iceskating.png?ref_type=heads",
"https://gitlab.com/bignutty/google-weather-icons/-/raw/main/froggie/v2/mobile/17-heavy-snow-blizzard/17-heavy-snow-blizzard-home-inside.png?ref_type=heads",
"https://gitlab.com/bignutty/google-weather-icons/-/raw/main/froggie/v2/mobile/16-blowing-snow/16-blowing-snow-field-snowman.png?ref_type=heads"
];
thunderStormFrog = [
"https://gitlab.com/bignutty/google-weather-icons/-/raw/main/froggie/v2/mobile/22-iso-thunderstorms/22-iso-thunderstorms-home-inside.png?ref_type=heads",
"https://gitlab.com/bignutty/google-weather-icons/-/raw/main/froggie/v2/mobile/22-iso-thunderstorms/22-iso-thunderstorms-cafe-looking-outside.png?ref_type=heads",
"https://gitlab.com/bignutty/google-weather-icons/-/raw/main/froggie/v2/mobile/22-iso-thunderstorms/22-iso-thunderstorms-busstop-newspaper.png?ref_type=heads"
"https://gitlab.com/bignutty/google-weather-icons/-/raw/main/froggie/v2/mobile/22-iso-thunderstorms/22-iso-thunderstorms-home-inside.png?ref_type=heads",
"https://gitlab.com/bignutty/google-weather-icons/-/raw/main/froggie/v2/mobile/22-iso-thunderstorms/22-iso-thunderstorms-cafe-looking-outside.png?ref_type=heads",
"https://gitlab.com/bignutty/google-weather-icons/-/raw/main/froggie/v2/mobile/22-iso-thunderstorms/22-iso-thunderstorms-busstop-newspaper.png?ref_type=heads"
];
clearNightFrog = [
"https://gitlab.com/bignutty/google-weather-icons/-/raw/main/froggie/v2/mobile/05-clear/05-clear-creek-stars.png?ref_type=heads",
"https://gitlab.com/bignutty/google-weather-icons/-/raw/main/froggie/v2/mobile/05-clear/05-clear-field-lanterns.png?ref_type=heads",
"https://gitlab.com/bignutty/google-weather-icons/-/raw/main/froggie/v2/mobile/05-clear/05-clear-orchard-fireflies.png?ref_type=heads",
"https://gitlab.com/bignutty/google-weather-icons/-/raw/main/froggie/v2/mobile/05-clear/05-clear-home-lounging.png?ref_type=heads",
"https://gitlab.com/bignutty/google-weather-icons/-/raw/main/froggie/v2/mobile/05-clear/05-clear-hills-telescope.png?ref_type=heads"
"https://gitlab.com/bignutty/google-weather-icons/-/raw/main/froggie/v2/mobile/05-clear/05-clear-creek-stars.png?ref_type=heads",
"https://gitlab.com/bignutty/google-weather-icons/-/raw/main/froggie/v2/mobile/05-clear/05-clear-field-lanterns.png?ref_type=heads",
"https://gitlab.com/bignutty/google-weather-icons/-/raw/main/froggie/v2/mobile/05-clear/05-clear-orchard-fireflies.png?ref_type=heads",
"https://gitlab.com/bignutty/google-weather-icons/-/raw/main/froggie/v2/mobile/05-clear/05-clear-home-lounging.png?ref_type=heads",
"https://gitlab.com/bignutty/google-weather-icons/-/raw/main/froggie/v2/mobile/05-clear/05-clear-hills-telescope.png?ref_type=heads"
];
partlyCloudyNightFrog = [
"https://gitlab.com/bignutty/google-weather-icons/-/raw/main/froggie/v2/mobile/07-partly-cloudy-night/07-partly-cloudy-night-creek-fireflies.png?ref_type=heads",
"https://gitlab.com/bignutty/google-weather-icons/-/raw/main/froggie/v2/mobile/07-partly-cloudy-night/07-partly-cloudy-night-field-fireflies.png?ref_type=heads",
"https://gitlab.com/bignutty/google-weather-icons/-/raw/main/froggie/v2/mobile/07-partly-cloudy-night/07-partly-cloudy-night-hills-smores.png?ref_type=heads",
"https://gitlab.com/bignutty/google-weather-icons/-/raw/main/froggie/v2/mobile/07-partly-cloudy-night/07-partly-cloudy-night-orchard-eating.png?ref_type=heads"
"https://gitlab.com/bignutty/google-weather-icons/-/raw/main/froggie/v2/mobile/07-partly-cloudy-night/07-partly-cloudy-night-creek-fireflies.png?ref_type=heads",
"https://gitlab.com/bignutty/google-weather-icons/-/raw/main/froggie/v2/mobile/07-partly-cloudy-night/07-partly-cloudy-night-field-fireflies.png?ref_type=heads",
"https://gitlab.com/bignutty/google-weather-icons/-/raw/main/froggie/v2/mobile/07-partly-cloudy-night/07-partly-cloudy-night-hills-smores.png?ref_type=heads",
"https://gitlab.com/bignutty/google-weather-icons/-/raw/main/froggie/v2/mobile/07-partly-cloudy-night/07-partly-cloudy-night-orchard-eating.png?ref_type=heads"
];
}
void _setOfflineIcons() {
@@ -126,7 +124,7 @@ Future<void> initializeIcons() async {
partlyCloudyNightFrog = ['assets/froggie/02n.png'];
}
final Random _random = Random();
final Random _random = Random();
String getFroggieIcon(int iconCode, bool isDay) {
if (isDay) {
@@ -140,7 +138,8 @@ Future<void> initializeIcons() async {
return _randomIcon(overcastFrog);
} else if ([45, 48].contains(iconCode)) {
return _randomIcon(fogFrog);
} else if ([51, 53, 55, 56, 57, 61, 63, 65, 80, 81, 82].contains(iconCode)) {
} else if ([51, 53, 55, 56, 57, 61, 63, 65, 80, 81, 82]
.contains(iconCode)) {
return _randomIcon(rainFrog);
} else if ([66, 67, 71, 73, 75, 77, 85, 86].contains(iconCode)) {
return _randomIcon(snowFrog);
@@ -152,11 +151,12 @@ Future<void> initializeIcons() async {
return _randomIcon(clearNightFrog);
} else if ([1, 2].contains(iconCode)) {
return _randomIcon(partlyCloudyNightFrog);
} else if (iconCode == 3){
} else if (iconCode == 3) {
return _randomIcon(overcastFrog);
} else if ([45, 48].contains(iconCode)) {
return _randomIcon(fogFrog);
} else if ([51, 53, 55, 56, 57, 61, 63, 65, 80, 81, 82].contains(iconCode)) {
} else if ([51, 53, 55, 56, 57, 61, 63, 65, 80, 81, 82]
.contains(iconCode)) {
return _randomIcon(rainFrog);
} else if ([66, 67, 71, 73, 75, 77, 85, 86].contains(iconCode)) {
return _randomIcon(snowFrog);
@@ -173,3 +173,15 @@ Future<void> initializeIcons() async {
return list[_random.nextInt(list.length)];
}
}
Future<bool> urlIsReachable(String url,
{Duration timeout = const Duration(seconds: 5)}) async {
try {
final uri = Uri.parse(url);
final request = await HttpClient().headUrl(uri).timeout(timeout);
final response = await request.close();
return response.statusCode >= 200 && response.statusCode < 400;
} catch (_) {
return false;
}
}
+10 -35
View File
@@ -1,39 +1,14 @@
import 'dart:io';
import 'package:http/http.dart' as http;
import 'package:connectivity_plus/connectivity_plus.dart';
// Checks for actual internet access
Future<bool> hasRealInternet() async {
final testUrls = [
'http://connect.rom.miui.com/generate_204', // MIUI
'http://captive.apple.com', // Apple
'http://example.com', // Lightweight
'http://baidu.com', // China
'http://clients3.google.com/generate_204', // Android
'http://www.msftconnecttest.com/connecttest.txt', // Windows
'http://www.gstatic.com/generate_204', // Google lightweight
'https://example.com', // HTTPS fallback
];
Future<bool> hasNetworkConnection() async {
final result = await Connectivity().checkConnectivity();
try {
final dnsResult = await InternetAddress.lookup('example.com')
.timeout(const Duration(seconds: 3));
if (dnsResult.isEmpty || dnsResult[0].rawAddress.isEmpty) return false;
for (final url in testUrls) {
try {
final response =
await http.get(Uri.parse(url)).timeout(const Duration(seconds: 3));
if (response.statusCode == 204 ||
response.statusCode == 200 ||
response.statusCode == 302) {
return true;
}
} catch (_) {
// try next
}
}
return false;
} catch (_) {
return false;
if (result is List) {
final first = result.isNotEmpty ? result.first : ConnectivityResult.none;
return first == ConnectivityResult.mobile ||
first == ConnectivityResult.wifi;
} else {
return result == ConnectivityResult.mobile ||
result == ConnectivityResult.wifi;
}
}
+3 -4
View File
@@ -3,9 +3,9 @@ import 'services/fetch_data.dart';
import 'utils/preferences_helper.dart';
import 'package:flutter/material.dart';
import 'package:home_widget/home_widget.dart';
import 'utils/unit_converter.dart';
import 'package:path_provider/path_provider.dart';
import 'package:hive/hive.dart';
import 'utils/unit_converter.dart';
import 'utils/condition_label_map.dart';
import 'package:flutter/services.dart' show rootBundle;
@@ -14,9 +14,6 @@ Future<void> updateHomeWidget(weather, {bool updatedFromHome = false}) async {
try {
WidgetsFlutterBinding.ensureInitialized();
final dir = await getApplicationDocumentsDirectory();
Hive.init(dir.path);
await PreferencesHelper.init();
await HomeWidget.setAppGroupId('com.pranshulgg.weather_master_app');
@@ -289,6 +286,8 @@ Future<void> updateHomeWidget(weather, {bool updatedFromHome = false}) async {
name: 'clockDateWidgetProvider', iOSName: null);
await HomeWidget.updateWidget(
name: 'DateCurrentWidgetProvider', iOSName: null);
await HomeWidget.updateWidget(
name: 'ClockHourlyWidgetProvider', iOSName: null);
print('[WidgetUpdate] Called updateWidget');
} catch (e, stack) {
+6 -5
View File
@@ -295,7 +295,7 @@ class AnimatedTemperature extends StatelessWidget {
color: Theme.of(context).brightness == Brightness.light
? Theme.of(context).colorScheme.inverseSurface
: Theme.of(context).colorScheme.primary,
fontSize: isShowFrog ? 60 : 130,
fontSize: isShowFrog ? 65 : 136,
fontWeight: isShowFrog ? null : FontWeight.bold,
height: isShowFrog ? 1.3 : 1,
),
@@ -370,7 +370,7 @@ class _WeatherTopCardStateVertical extends State<WeatherTopCardVertical> {
final colorTheme = Theme.of(context).colorScheme;
return Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 14),
padding: const EdgeInsets.fromLTRB(16, 16, 16, 12),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
@@ -392,6 +392,7 @@ class _WeatherTopCardStateVertical extends State<WeatherTopCardVertical> {
height: 6,
),
Row(
spacing: 3,
mainAxisAlignment: MainAxisAlignment.center,
children: [
PreferencesHelper.getBool("useTempAnimation") == false
@@ -401,13 +402,13 @@ class _WeatherTopCardStateVertical extends State<WeatherTopCardVertical> {
Brightness.light
? Theme.of(context).colorScheme.inverseSurface
: Theme.of(context).colorScheme.primary,
fontSize: 130,
fontSize: 136,
fontWeight: isShowFrog ? null : FontWeight.bold,
height: 1.3))
: AnimatedTemperature(
targetTemp: convertedTemp.toDouble()),
Padding(
padding: const EdgeInsets.only(bottom: 40),
padding: const EdgeInsets.only(bottom: 50),
child: SvgPicture.asset(
WeatherIconMapper.getIcon(
widget.currentWeatherIconCode, widget.currentisDay),
@@ -418,7 +419,7 @@ class _WeatherTopCardStateVertical extends State<WeatherTopCardVertical> {
],
),
SizedBox(
height: 16,
height: 10,
),
Text(
"${'feels_like'.tr()} ${widget.currentFeelsLike == 0000 ? '--' : '$convertedfeelsLikeTemp°'}",
@@ -0,0 +1,16 @@
# C/C++ build system timings
create_cxx_tasks
create-initial-cxx-model
create-module-model 11ms
[gap of 87ms]
create-initial-cxx-model completed in 98ms
create_cxx_tasks completed in 102ms
# C/C++ build system timings
create_cxx_tasks
create-initial-cxx-model
create-variant-model 19ms
[gap of 65ms]
create-initial-cxx-model completed in 88ms
create_cxx_tasks completed in 90ms
@@ -675,3 +675,70 @@ create_cxx_tasks
create-initial-cxx-model 83ms
create_cxx_tasks completed in 86ms
# C/C++ build system timings
create_cxx_tasks
create-initial-cxx-model
create-module-model 32ms
create-variant-model 13ms
create-ARMEABI_V7A-model 36ms
[gap of 76ms]
create-ARM64_V8A-model 12ms
[gap of 13ms]
create-initial-cxx-model completed in 186ms
[gap of 22ms]
create_cxx_tasks completed in 215ms
# C/C++ build system timings
create_cxx_tasks
create-initial-cxx-model 86ms
create_cxx_tasks completed in 91ms
# C/C++ build system timings
create_cxx_tasks
create-initial-cxx-model
create-module-model 32ms
create-variant-model 15ms
create-ARMEABI_V7A-model 43ms
[gap of 15ms]
create-X86_64-model 10ms
[gap of 24ms]
create-X86-model 13ms
[gap of 29ms]
create-ARM64_V8A-model 15ms
[gap of 10ms]
create-X86_64-model 13ms
create-initial-cxx-model completed in 228ms
[gap of 28ms]
create_cxx_tasks completed in 263ms
# C/C++ build system timings
create_cxx_tasks
create-initial-cxx-model 111ms
create_cxx_tasks completed in 116ms
# C/C++ build system timings
create_cxx_tasks
create-initial-cxx-model
create-module-model 39ms
create-variant-model 16ms
create-ARMEABI_V7A-model 44ms
[gap of 110ms]
create-initial-cxx-model completed in 215ms
[gap of 26ms]
create_cxx_tasks completed in 251ms
# C/C++ build system timings
create_cxx_tasks
create-initial-cxx-model
create-module-model 13ms
[gap of 18ms]
create-ARM64_V8A-model 10ms
create-X86_64-model 10ms
[gap of 33ms]
create-X86-model 11ms
create-module-model 15ms
create-ARMEABI_V7A-model 10ms
create-X86-model 13ms
create-initial-cxx-model completed in 175ms
create_cxx_tasks completed in 182ms