Refactor code

This commit is contained in:
Yoshi 2025-05-28 17:42:15 +03:00
parent 33be8dcdc6
commit 9ba80c3609
69 changed files with 4478 additions and 4113 deletions

0
lib/app/api/api.dart Normal file → Executable file
View file

0
lib/app/api/city_api.dart Normal file → Executable file
View file

0
lib/app/api/weather_api.dart Normal file → Executable file
View file

0
lib/app/api/weather_api.freezed.dart Normal file → Executable file
View file

0
lib/app/api/weather_api.g.dart Normal file → Executable file
View file

147
lib/app/controller/controller.dart Normal file → Executable file
View file

@ -134,7 +134,7 @@ class WeatherController extends GetxController {
await readCache(); await readCache();
} }
Future<Map> getCurrentLocationSearch() async { Future<Map<String, dynamic>> getCurrentLocationSearch() async {
if (!(await isOnline.value)) { if (!(await isOnline.value)) {
showSnackBar(content: 'no_inter'.tr); showSnackBar(content: 'no_inter'.tr);
} }
@ -313,14 +313,13 @@ class WeatherController extends GetxController {
} }
Future<void> updateCacheCard(bool refresh) async { Future<void> updateCacheCard(bool refresh) async {
final weatherCard = final weatherCard = refresh
refresh ? isar.weatherCards.where().sortByIndex().findAllSync()
? isar.weatherCards.where().sortByIndex().findAllSync() : isar.weatherCards
: isar.weatherCards .filter()
.filter() .timestampLessThan(cacheExpiry)
.timestampLessThan(cacheExpiry) .sortByIndex()
.sortByIndex() .findAllSync();
.findAllSync();
if (!(await isOnline.value) || weatherCard.isEmpty) { if (!(await isOnline.value) || weatherCard.isEmpty) {
return; return;
@ -335,53 +334,52 @@ class WeatherController extends GetxController {
oldCard.timezone!, oldCard.timezone!,
); );
isar.writeTxnSync(() { isar.writeTxnSync(() {
oldCard _updateWeatherCard(oldCard, updatedCard);
..time = updatedCard.time
..weathercode = updatedCard.weathercode
..temperature2M = updatedCard.temperature2M
..apparentTemperature = updatedCard.apparentTemperature
..relativehumidity2M = updatedCard.relativehumidity2M
..precipitation = updatedCard.precipitation
..rain = updatedCard.rain
..surfacePressure = updatedCard.surfacePressure
..visibility = updatedCard.visibility
..evapotranspiration = updatedCard.evapotranspiration
..windspeed10M = updatedCard.windspeed10M
..winddirection10M = updatedCard.winddirection10M
..windgusts10M = updatedCard.windgusts10M
..cloudcover = updatedCard.cloudcover
..uvIndex = updatedCard.uvIndex
..dewpoint2M = updatedCard.dewpoint2M
..precipitationProbability = updatedCard.precipitationProbability
..shortwaveRadiation = updatedCard.shortwaveRadiation
..timeDaily = updatedCard.timeDaily
..weathercodeDaily = updatedCard.weathercodeDaily
..temperature2MMax = updatedCard.temperature2MMax
..temperature2MMin = updatedCard.temperature2MMin
..apparentTemperatureMax = updatedCard.apparentTemperatureMax
..apparentTemperatureMin = updatedCard.apparentTemperatureMin
..sunrise = updatedCard.sunrise
..sunset = updatedCard.sunset
..precipitationSum = updatedCard.precipitationSum
..precipitationProbabilityMax =
updatedCard.precipitationProbabilityMax
..windspeed10MMax = updatedCard.windspeed10MMax
..windgusts10MMax = updatedCard.windgusts10MMax
..uvIndexMax = updatedCard.uvIndexMax
..rainSum = updatedCard.rainSum
..winddirection10MDominant = updatedCard.winddirection10MDominant
..timestamp = DateTime.now();
isar.weatherCards.putSync(oldCard);
final newCard = oldCard;
final oldIdx = weatherCard.indexOf(oldCard);
weatherCards[oldIdx] = newCard;
weatherCards.refresh(); weatherCards.refresh();
}); });
} }
} }
void _updateWeatherCard(WeatherCard oldCard, WeatherCard updatedCard) {
oldCard
..time = updatedCard.time
..weathercode = updatedCard.weathercode
..temperature2M = updatedCard.temperature2M
..apparentTemperature = updatedCard.apparentTemperature
..relativehumidity2M = updatedCard.relativehumidity2M
..precipitation = updatedCard.precipitation
..rain = updatedCard.rain
..surfacePressure = updatedCard.surfacePressure
..visibility = updatedCard.visibility
..evapotranspiration = updatedCard.evapotranspiration
..windspeed10M = updatedCard.windspeed10M
..winddirection10M = updatedCard.winddirection10M
..windgusts10M = updatedCard.windgusts10M
..cloudcover = updatedCard.cloudcover
..uvIndex = updatedCard.uvIndex
..dewpoint2M = updatedCard.dewpoint2M
..precipitationProbability = updatedCard.precipitationProbability
..shortwaveRadiation = updatedCard.shortwaveRadiation
..timeDaily = updatedCard.timeDaily
..weathercodeDaily = updatedCard.weathercodeDaily
..temperature2MMax = updatedCard.temperature2MMax
..temperature2MMin = updatedCard.temperature2MMin
..apparentTemperatureMax = updatedCard.apparentTemperatureMax
..apparentTemperatureMin = updatedCard.apparentTemperatureMin
..sunrise = updatedCard.sunrise
..sunset = updatedCard.sunset
..precipitationSum = updatedCard.precipitationSum
..precipitationProbabilityMax = updatedCard.precipitationProbabilityMax
..windspeed10MMax = updatedCard.windspeed10MMax
..windgusts10MMax = updatedCard.windgusts10MMax
..uvIndexMax = updatedCard.uvIndexMax
..rainSum = updatedCard.rainSum
..winddirection10MDominant = updatedCard.winddirection10MDominant
..timestamp = DateTime.now();
isar.weatherCards.putSync(oldCard);
}
Future<void> updateCard(WeatherCard weatherCard) async { Future<void> updateCard(WeatherCard weatherCard) async {
if (!(await isOnline.value)) { if (!(await isOnline.value)) {
return; return;
@ -396,43 +394,7 @@ class WeatherController extends GetxController {
); );
isar.writeTxnSync(() { isar.writeTxnSync(() {
weatherCard _updateWeatherCard(weatherCard, updatedCard);
..time = updatedCard.time
..weathercode = updatedCard.weathercode
..temperature2M = updatedCard.temperature2M
..apparentTemperature = updatedCard.apparentTemperature
..relativehumidity2M = updatedCard.relativehumidity2M
..precipitation = updatedCard.precipitation
..rain = updatedCard.rain
..surfacePressure = updatedCard.surfacePressure
..visibility = updatedCard.visibility
..evapotranspiration = updatedCard.evapotranspiration
..windspeed10M = updatedCard.windspeed10M
..winddirection10M = updatedCard.winddirection10M
..windgusts10M = updatedCard.windgusts10M
..cloudcover = updatedCard.cloudcover
..uvIndex = updatedCard.uvIndex
..dewpoint2M = updatedCard.dewpoint2M
..precipitationProbability = updatedCard.precipitationProbability
..shortwaveRadiation = updatedCard.shortwaveRadiation
..timeDaily = updatedCard.timeDaily
..weathercodeDaily = updatedCard.weathercodeDaily
..temperature2MMax = updatedCard.temperature2MMax
..temperature2MMin = updatedCard.temperature2MMin
..apparentTemperatureMax = updatedCard.apparentTemperatureMax
..apparentTemperatureMin = updatedCard.apparentTemperatureMin
..sunrise = updatedCard.sunrise
..sunset = updatedCard.sunset
..precipitationSum = updatedCard.precipitationSum
..precipitationProbabilityMax = updatedCard.precipitationProbabilityMax
..windspeed10MMax = updatedCard.windspeed10MMax
..windgusts10MMax = updatedCard.windgusts10MMax
..uvIndexMax = updatedCard.uvIndexMax
..rainSum = updatedCard.rainSum
..winddirection10MDominant = updatedCard.winddirection10MDominant
..timestamp = DateTime.now();
isar.weatherCards.putSync(weatherCard);
}); });
} }
@ -512,8 +474,8 @@ class WeatherController extends GetxController {
void notificationCheck() async { void notificationCheck() async {
if (settings.notifications) { if (settings.notifications) {
final pendingNotificationRequests = final pendingNotificationRequests = await flutterLocalNotificationsPlugin
await flutterLocalNotificationsPlugin.pendingNotificationRequests(); .pendingNotificationRequests();
if (pendingNotificationRequests.isEmpty) { if (pendingNotificationRequests.isEmpty) {
notification(_mainWeather.value); notification(_mainWeather.value);
} }
@ -572,8 +534,9 @@ class WeatherController extends GetxController {
WeatherCardSchema, WeatherCardSchema,
], directory: (await getApplicationSupportDirectory()).path); ], directory: (await getApplicationSupportDirectory()).path);
final mainWeatherCache = final mainWeatherCache = isarWidget.mainWeatherCaches
isarWidget.mainWeatherCaches.where().findFirstSync(); .where()
.findFirstSync();
if (mainWeatherCache == null) return false; if (mainWeatherCache == null) return false;
final hour = getTime(mainWeatherCache.time!, mainWeatherCache.timezone!); final hour = getTime(mainWeatherCache.time!, mainWeatherCache.timezone!);

0
lib/app/data/db.dart Normal file → Executable file
View file

0
lib/app/data/db.g.dart Normal file → Executable file
View file

732
lib/app/ui/geolocation.dart Normal file → Executable file
View file

@ -33,6 +33,8 @@ class _SelectGeolocationState extends State<SelectGeolocation> {
final _controllerCity = TextEditingController(); final _controllerCity = TextEditingController();
final _controllerDistrict = TextEditingController(); final _controllerDistrict = TextEditingController();
static const kTextFieldElevation = 4.0;
static const colorFilter = ColorFilter.matrix(<double>[ static const colorFilter = ColorFilter.matrix(<double>[
-0.2, -0.7, -0.08, 0, 255, // Red channel -0.2, -0.7, -0.08, 0, 255, // Red channel
-0.2, -0.7, -0.08, 0, 255, // Green channel -0.2, -0.7, -0.08, 0, 255, // Green channel
@ -41,17 +43,16 @@ class _SelectGeolocationState extends State<SelectGeolocation> {
]); ]);
final bool _isDarkMode = Get.theme.brightness == Brightness.dark; final bool _isDarkMode = Get.theme.brightness == Brightness.dark;
final mapController = MapController(); final mapController = MapController();
textTrim(value) { void textTrim(TextEditingController value) {
value.text = value.text.trim(); value.text = value.text.trim();
while (value.text.contains(' ')) { while (value.text.contains(' ')) {
value.text = value.text.replaceAll(' ', ' '); value.text = value.text.replaceAll(' ', ' ');
} }
} }
void fillController(selection) { void fillController(Result selection) {
_controllerLat.text = '${selection.latitude}'; _controllerLat.text = '${selection.latitude}';
_controllerLon.text = '${selection.longitude}'; _controllerLon.text = '${selection.longitude}';
_controllerCity.text = selection.name; _controllerCity.text = selection.name;
@ -61,7 +62,7 @@ class _SelectGeolocationState extends State<SelectGeolocation> {
setState(() {}); setState(() {});
} }
void fillControllerGeo(location) { void fillControllerGeo(Map<String, dynamic> location) {
_controllerLat.text = '${location['lat']}'; _controllerLat.text = '${location['lat']}';
_controllerLon.text = '${location['lon']}'; _controllerLon.text = '${location['lon']}';
_controllerCity.text = location['district']; _controllerCity.text = location['district'];
@ -82,35 +83,310 @@ class _SelectGeolocationState extends State<SelectGeolocation> {
); );
} }
Widget _buildMap() {
return ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(20)),
child: SizedBox(
height: Get.size.height * 0.3,
child: FlutterMap(
mapController: mapController,
options: MapOptions(
backgroundColor: context.theme.colorScheme.surface,
initialCenter: const LatLng(55.7522, 37.6156),
initialZoom: 3,
interactionOptions: const InteractionOptions(
flags: InteractiveFlag.all & ~InteractiveFlag.rotate,
),
cameraConstraint: CameraConstraint.contain(
bounds: LatLngBounds(
const LatLng(-90, -180),
const LatLng(90, 180),
),
),
onLongPress: (tapPosition, point) =>
fillMap(point.latitude, point.longitude),
),
children: [
if (_isDarkMode)
ColorFiltered(
colorFilter: colorFilter,
child: _buildMapTileLayer(),
)
else
_buildMapTileLayer(),
RichAttributionWidget(
animationConfig: const ScaleRAWA(),
attributions: [
TextSourceAttribution(
'OpenStreetMap contributors',
onTap: () => weatherController.urlLauncher(
'https://openstreetmap.org/copyright',
),
),
],
),
],
),
),
);
}
Widget _buildSearchField() {
return RawAutocomplete<Result>(
focusNode: _focusNode,
textEditingController: _controller,
fieldViewBuilder:
(
BuildContext context,
TextEditingController fieldTextEditingController,
FocusNode fieldFocusNode,
VoidCallback onFieldSubmitted,
) {
return MyTextForm(
elevation: kTextFieldElevation,
labelText: 'search'.tr,
type: TextInputType.text,
icon: const Icon(IconsaxPlusLinear.global_search),
controller: _controller,
margin: const EdgeInsets.only(left: 10, right: 10, top: 10),
focusNode: _focusNode,
);
},
optionsBuilder: (TextEditingValue textEditingValue) {
if (textEditingValue.text.isEmpty) {
return const Iterable<Result>.empty();
}
return WeatherAPI().getCity(textEditingValue.text, locale);
},
onSelected: (Result selection) => fillController(selection),
displayStringForOption: (Result option) =>
'${option.name}, ${option.admin1}',
optionsViewBuilder:
(
BuildContext context,
AutocompleteOnSelected<Result> onSelected,
Iterable<Result> options,
) {
return _buildOptionsView(context, onSelected, options);
},
);
}
Widget _buildOptionsView(
BuildContext context,
AutocompleteOnSelected<Result> onSelected,
Iterable<Result> options,
) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
child: Align(
alignment: Alignment.topCenter,
child: Material(
borderRadius: BorderRadius.circular(20),
elevation: 4.0,
child: ListView.builder(
padding: EdgeInsets.zero,
shrinkWrap: true,
itemCount: options.length,
itemBuilder: (BuildContext context, int index) {
final Result option = options.elementAt(index);
return InkWell(
onTap: () => onSelected(option),
child: ListTile(
title: Text(
'${option.name}, ${option.admin1}',
style: context.textTheme.labelLarge,
),
),
);
},
),
),
),
);
}
Widget _buildLocationButton() {
return Card(
elevation: kTextFieldElevation,
margin: const EdgeInsets.only(top: 10, right: 10),
child: Container(
margin: const EdgeInsets.all(2.5),
child: IconButton(
onPressed: _handleLocationButtonPress,
icon: const Icon(IconsaxPlusLinear.location),
),
),
);
}
Future<void> _handleLocationButtonPress() async {
bool serviceEnabled = await Geolocator.isLocationServiceEnabled();
if (!serviceEnabled) {
if (!context.mounted) return;
await _showLocationDialog();
return;
}
setState(() => isLoading = true);
final location = await weatherController.getCurrentLocationSearch();
fillControllerGeo(location);
setState(() => isLoading = false);
}
Future<void> _showLocationDialog() async {
await showAdaptiveDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog.adaptive(
title: Text('location'.tr, style: context.textTheme.titleLarge),
content: Text('no_location'.tr, style: context.textTheme.titleMedium),
actions: [
TextButton(
onPressed: () => Get.back(result: false),
child: Text(
'cancel'.tr,
style: context.textTheme.titleMedium?.copyWith(
color: Colors.blueAccent,
),
),
),
TextButton(
onPressed: () {
Geolocator.openLocationSettings();
Get.back(result: true);
},
child: Text(
'settings'.tr,
style: context.textTheme.titleMedium?.copyWith(
color: Colors.green,
),
),
),
],
);
},
);
}
Widget _buildLatitudeField() {
return MyTextForm(
elevation: kTextFieldElevation,
controller: _controllerLat,
labelText: 'lat'.tr,
type: TextInputType.number,
icon: const Icon(IconsaxPlusLinear.location),
margin: const EdgeInsets.only(left: 10, right: 10, top: 10),
validator: (value) => _validateLatitude(value),
);
}
Widget _buildLongitudeField() {
return MyTextForm(
elevation: kTextFieldElevation,
controller: _controllerLon,
labelText: 'lon'.tr,
type: TextInputType.number,
icon: const Icon(IconsaxPlusLinear.location),
margin: const EdgeInsets.only(left: 10, right: 10, top: 10),
validator: (value) => _validateLongitude(value),
);
}
Widget _buildCityField() {
return MyTextForm(
elevation: kTextFieldElevation,
controller: _controllerCity,
labelText: 'city'.tr,
type: TextInputType.name,
icon: const Icon(IconsaxPlusLinear.building_3),
margin: const EdgeInsets.only(left: 10, right: 10, top: 10),
validator: (value) => _validateCity(value),
);
}
Widget _buildDistrictField() {
return MyTextForm(
elevation: kTextFieldElevation,
controller: _controllerDistrict,
labelText: 'district'.tr,
type: TextInputType.streetAddress,
icon: const Icon(IconsaxPlusLinear.global),
margin: const EdgeInsets.only(left: 10, right: 10, top: 10),
);
}
Widget _buildSubmitButton() {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
child: MyTextButton(buttonName: 'done'.tr, onPressed: _handleSubmit),
);
}
String? _validateLatitude(String? value) {
if (value == null || value.isEmpty) {
return 'validateValue'.tr;
}
double? numericValue = double.tryParse(value);
if (numericValue == null) {
return 'validateNumber'.tr;
}
if (numericValue < -90 || numericValue > 90) {
return 'validate90'.tr;
}
return null;
}
String? _validateLongitude(String? value) {
if (value == null || value.isEmpty) {
return 'validateValue'.tr;
}
double? numericValue = double.tryParse(value);
if (numericValue == null) {
return 'validateNumber'.tr;
}
if (numericValue < -180 || numericValue > 180) {
return 'validate180'.tr;
}
return null;
}
String? _validateCity(String? value) {
if (value == null || value.isEmpty) {
return 'validateName'.tr;
}
return null;
}
Future<void> _handleSubmit() async {
if (formKeySearch.currentState!.validate()) {
textTrim(_controllerLat);
textTrim(_controllerLon);
textTrim(_controllerCity);
textTrim(_controllerDistrict);
try {
await weatherController.deleteAll(true);
await weatherController.getLocation(
double.parse(_controllerLat.text),
double.parse(_controllerLon.text),
_controllerDistrict.text,
_controllerCity.text,
);
widget.isStart
? Get.off(() => const HomePage(), transition: Transition.downToUp)
: Get.back();
} catch (error) {
Future.error(error);
}
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
const kTextFieldElevation = 4.0;
return Form( return Form(
key: formKeySearch, key: formKeySearch,
child: Scaffold( child: Scaffold(
resizeToAvoidBottomInset: true, resizeToAvoidBottomInset: true,
appBar: AppBar( appBar: _buildAppBar(),
centerTitle: true,
leading:
widget.isStart
? null
: IconButton(
onPressed: () {
Get.back();
},
icon: const Icon(IconsaxPlusLinear.arrow_left_3, size: 20),
splashColor: Colors.transparent,
highlightColor: Colors.transparent,
),
automaticallyImplyLeading: false,
title: Text(
'searchCity'.tr,
style: context.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
fontSize: 18,
),
),
),
body: SafeArea( body: SafeArea(
child: Stack( child: Stack(
children: [ children: [
@ -128,66 +404,7 @@ class _SelectGeolocationState extends State<SelectGeolocation> {
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 10, horizontal: 10,
), ),
child: ClipRRect( child: _buildMap(),
borderRadius: const BorderRadius.all(
Radius.circular(20),
),
child: SizedBox(
height: Get.size.height * 0.3,
child: FlutterMap(
mapController: mapController,
options: MapOptions(
backgroundColor:
context.theme.colorScheme.surface,
initialCenter: const LatLng(
55.7522,
37.6156,
),
initialZoom: 3,
interactionOptions:
const InteractionOptions(
flags:
InteractiveFlag.all &
~InteractiveFlag.rotate,
),
cameraConstraint:
CameraConstraint.contain(
bounds: LatLngBounds(
const LatLng(-90, -180),
const LatLng(90, 180),
),
),
onLongPress:
(tapPosition, point) => fillMap(
point.latitude,
point.longitude,
),
),
children: [
if (_isDarkMode)
ColorFiltered(
colorFilter: colorFilter,
child: _buildMapTileLayer(),
)
else
_buildMapTileLayer(),
RichAttributionWidget(
animationConfig: const ScaleRAWA(),
attributions: [
TextSourceAttribution(
'OpenStreetMap contributors',
onTap:
() => weatherController
.urlLauncher(
'https://openstreetmap.org/copyright',
),
),
],
),
],
),
),
),
), ),
Padding( Padding(
padding: const EdgeInsets.fromLTRB( padding: const EdgeInsets.fromLTRB(
@ -205,284 +422,14 @@ class _SelectGeolocationState extends State<SelectGeolocation> {
), ),
Row( Row(
children: [ children: [
Flexible( Flexible(child: _buildSearchField()),
child: RawAutocomplete<Result>( _buildLocationButton(),
focusNode: _focusNode,
textEditingController: _controller,
fieldViewBuilder: (
BuildContext context,
TextEditingController
fieldTextEditingController,
FocusNode fieldFocusNode,
VoidCallback onFieldSubmitted,
) {
return MyTextForm(
elevation: kTextFieldElevation,
labelText: 'search'.tr,
type: TextInputType.text,
icon: const Icon(
IconsaxPlusLinear.global_search,
),
controller: _controller,
margin: const EdgeInsets.only(
left: 10,
right: 10,
top: 10,
),
focusNode: _focusNode,
);
},
optionsBuilder: (
TextEditingValue textEditingValue,
) {
if (textEditingValue.text.isEmpty) {
return const Iterable<
Result
>.empty();
}
return WeatherAPI().getCity(
textEditingValue.text,
locale,
);
},
onSelected:
(Result selection) =>
fillController(selection),
displayStringForOption:
(Result option) =>
'${option.name}, ${option.admin1}',
optionsViewBuilder: (
BuildContext context,
AutocompleteOnSelected<Result>
onSelected,
Iterable<Result> options,
) {
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 5,
),
child: Align(
alignment: Alignment.topCenter,
child: Material(
borderRadius:
BorderRadius.circular(20),
elevation: 4.0,
child: ListView.builder(
padding: EdgeInsets.zero,
shrinkWrap: true,
itemCount: options.length,
itemBuilder: (
BuildContext context,
int index,
) {
final Result option =
options.elementAt(
index,
);
return InkWell(
onTap:
() => onSelected(
option,
),
child: ListTile(
title: Text(
'${option.name}, ${option.admin1}',
style:
context
.textTheme
.labelLarge,
),
),
);
},
),
),
),
);
},
),
),
Card(
elevation: kTextFieldElevation,
margin: const EdgeInsets.only(
top: 10,
right: 10,
),
child: Container(
margin: const EdgeInsets.all(2.5),
child: IconButton(
onPressed: () async {
bool serviceEnabled =
await Geolocator.isLocationServiceEnabled();
if (!serviceEnabled) {
if (!context.mounted) return;
await showAdaptiveDialog(
context: context,
builder: (
BuildContext context,
) {
return AlertDialog.adaptive(
title: Text(
'location'.tr,
style:
context
.textTheme
.titleLarge,
),
content: Text(
'no_location'.tr,
style:
context
.textTheme
.titleMedium,
),
actions: [
TextButton(
onPressed:
() => Get.back(
result: false,
),
child: Text(
'cancel'.tr,
style: context
.textTheme
.titleMedium
?.copyWith(
color:
Colors
.blueAccent,
),
),
),
TextButton(
onPressed: () {
Geolocator.openLocationSettings();
Get.back(
result: true,
);
},
child: Text(
'settings'.tr,
style: context
.textTheme
.titleMedium
?.copyWith(
color:
Colors
.green,
),
),
),
],
);
},
);
return;
}
setState(() => isLoading = true);
final location =
await weatherController
.getCurrentLocationSearch();
fillControllerGeo(location);
setState(() => isLoading = false);
},
icon: const Icon(
IconsaxPlusLinear.location,
),
),
),
),
], ],
), ),
MyTextForm( _buildLatitudeField(),
elevation: kTextFieldElevation, _buildLongitudeField(),
controller: _controllerLat, _buildCityField(),
labelText: 'lat'.tr, _buildDistrictField(),
type: TextInputType.number,
icon: const Icon(IconsaxPlusLinear.location),
margin: const EdgeInsets.only(
left: 10,
right: 10,
top: 10,
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'validateValue'.tr;
}
double? numericValue = double.tryParse(
value,
);
if (numericValue == null) {
return 'validateNumber'.tr;
}
if (numericValue < -90 ||
numericValue > 90) {
return 'validate90'.tr;
}
return null;
},
),
MyTextForm(
elevation: kTextFieldElevation,
controller: _controllerLon,
labelText: 'lon'.tr,
type: TextInputType.number,
icon: const Icon(IconsaxPlusLinear.location),
margin: const EdgeInsets.only(
left: 10,
right: 10,
top: 10,
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'validateValue'.tr;
}
double? numericValue = double.tryParse(
value,
);
if (numericValue == null) {
return 'validateNumber'.tr;
}
if (numericValue < -180 ||
numericValue > 180) {
return 'validate180'.tr;
}
return null;
},
),
MyTextForm(
elevation: kTextFieldElevation,
controller: _controllerCity,
labelText: 'city'.tr,
type: TextInputType.name,
icon: const Icon(
IconsaxPlusLinear.building_3,
),
margin: const EdgeInsets.only(
left: 10,
right: 10,
top: 10,
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'validateName'.tr;
}
return null;
},
),
MyTextForm(
elevation: kTextFieldElevation,
controller: _controllerDistrict,
labelText: 'district'.tr,
type: TextInputType.streetAddress,
icon: const Icon(IconsaxPlusLinear.global),
margin: const EdgeInsets.only(
left: 10,
right: 10,
top: 10,
),
),
const Gap(20), const Gap(20),
], ],
), ),
@ -491,40 +438,7 @@ class _SelectGeolocationState extends State<SelectGeolocation> {
], ],
), ),
), ),
Padding( _buildSubmitButton(),
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 8,
),
child: MyTextButton(
buttonName: 'done'.tr,
onPressed: () async {
if (formKeySearch.currentState!.validate()) {
textTrim(_controllerLat);
textTrim(_controllerLon);
textTrim(_controllerCity);
textTrim(_controllerDistrict);
try {
await weatherController.deleteAll(true);
await weatherController.getLocation(
double.parse(_controllerLat.text),
double.parse(_controllerLon.text),
_controllerDistrict.text,
_controllerCity.text,
);
widget.isStart
? Get.off(
() => const HomePage(),
transition: Transition.downToUp,
)
: Get.back();
} catch (error) {
Future.error(error);
}
}
},
),
),
], ],
), ),
if (isLoading) if (isLoading)
@ -538,4 +452,28 @@ class _SelectGeolocationState extends State<SelectGeolocation> {
), ),
); );
} }
AppBar _buildAppBar() {
return AppBar(
centerTitle: true,
leading: widget.isStart
? null
: IconButton(
onPressed: () {
Get.back();
},
icon: const Icon(IconsaxPlusLinear.arrow_left_3, size: 20),
splashColor: Colors.transparent,
highlightColor: Colors.transparent,
),
automaticallyImplyLeading: false,
title: Text(
'searchCity'.tr,
style: context.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
fontSize: 18,
),
),
);
}
} }

293
lib/app/ui/home.dart Normal file → Executable file
View file

@ -82,6 +82,148 @@ class _HomePageState extends State<HomePage> with TickerProviderStateMixin {
tabController.animateTo(tabIndex); tabController.animateTo(tabIndex);
} }
Widget _buildAppBarTitle(
int tabIndex,
TextStyle? textStyle,
TextStyle? labelLarge,
) {
switch (tabIndex) {
case 0:
return visible
? _buildSearchField(labelLarge)
: Obx(() {
final location = weatherController.location;
final city = location.city;
final district = location.district;
return Text(
weatherController.isLoading.isFalse
? district!.isEmpty
? '$city'
: city!.isEmpty
? district
: city == district
? city
: '$city, $district'
: settings.location
? 'search'.tr
: (isar.locationCaches.where().findAllSync()).isNotEmpty
? 'loading'.tr
: 'searchCity'.tr,
style: textStyle,
);
});
case 1:
return Text('cities'.tr, style: textStyle);
case 2:
return settings.hideMap
? Text('settings_full'.tr, style: textStyle)
: Text('map'.tr, style: textStyle);
case 3:
return Text('settings_full'.tr, style: textStyle);
default:
return const SizedBox.shrink();
}
}
Widget _buildSearchField(TextStyle? labelLarge) {
return RawAutocomplete<Result>(
focusNode: _focusNode,
textEditingController: _controller,
fieldViewBuilder: (_, __, ___, ____) {
return TextField(
controller: _controller,
focusNode: _focusNode,
style: labelLarge?.copyWith(fontSize: 16),
decoration: InputDecoration(hintText: 'search'.tr),
);
},
optionsBuilder: (TextEditingValue textEditingValue) {
if (textEditingValue.text.isEmpty) {
return const Iterable<Result>.empty();
}
return WeatherAPI().getCity(textEditingValue.text, locale);
},
onSelected: (Result selection) async {
await weatherController.deleteAll(true);
await weatherController.getLocation(
double.parse('${selection.latitude}'),
double.parse('${selection.longitude}'),
selection.admin1,
selection.name,
);
visible = false;
_controller.clear();
_focusNode.unfocus();
setState(() {});
},
displayStringForOption:
(Result option) => '${option.name}, ${option.admin1}',
optionsViewBuilder: (
BuildContext context,
AutocompleteOnSelected<Result> onSelected,
Iterable<Result> options,
) {
return _buildOptionsView(context, onSelected, options, labelLarge);
},
);
}
Widget _buildOptionsView(
BuildContext context,
AutocompleteOnSelected<Result> onSelected,
Iterable<Result> options,
TextStyle? labelLarge,
) {
return Align(
alignment: Alignment.topLeft,
child: Material(
borderRadius: BorderRadius.circular(20),
elevation: 4.0,
child: SizedBox(
width: 250,
child: ListView.builder(
padding: EdgeInsets.zero,
shrinkWrap: true,
itemCount: options.length,
itemBuilder: (BuildContext context, int index) {
final Result option = options.elementAt(index);
return InkWell(
onTap: () => onSelected(option),
child: ListTile(
title: Text(
'${option.name}, ${option.admin1}',
style: labelLarge,
),
),
);
},
),
),
),
);
}
Widget _buildSearchIconButton() {
return IconButton(
onPressed: () {
if (visible) {
_controller.clear();
_focusNode.unfocus();
visible = false;
} else {
visible = true;
}
setState(() {});
},
icon: Icon(
visible
? IconsaxPlusLinear.close_circle
: IconsaxPlusLinear.search_normal_1,
size: 18,
),
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final textTheme = context.textTheme; final textTheme = context.textTheme;
@ -100,146 +242,23 @@ class _HomePageState extends State<HomePage> with TickerProviderStateMixin {
appBar: AppBar( appBar: AppBar(
centerTitle: true, centerTitle: true,
automaticallyImplyLeading: false, automaticallyImplyLeading: false,
leading: switch (tabIndex) { leading:
0 => IconButton( tabIndex == 0
onPressed: () { ? IconButton(
Get.to( onPressed: () {
() => const SelectGeolocation(isStart: false), Get.to(
transition: Transition.downToUp, () => const SelectGeolocation(isStart: false),
); transition: Transition.downToUp,
},
icon: const Icon(IconsaxPlusLinear.global_search, size: 18),
),
int() => null,
},
title: switch (tabIndex) {
0 =>
visible
? RawAutocomplete<Result>(
focusNode: _focusNode,
textEditingController: _controller,
fieldViewBuilder: (_, __, ___, ____) {
return TextField(
controller: _controller,
focusNode: _focusNode,
style: labelLarge?.copyWith(fontSize: 16),
decoration: InputDecoration(hintText: 'search'.tr),
);
},
optionsBuilder: (TextEditingValue textEditingValue) {
if (textEditingValue.text.isEmpty) {
return const Iterable<Result>.empty();
}
return WeatherAPI().getCity(
textEditingValue.text,
locale,
);
},
onSelected: (Result selection) async {
await weatherController.deleteAll(true);
await weatherController.getLocation(
double.parse('${selection.latitude}'),
double.parse('${selection.longitude}'),
selection.admin1,
selection.name,
);
visible = false;
_controller.clear();
_focusNode.unfocus();
setState(() {});
},
displayStringForOption:
(Result option) => '${option.name}, ${option.admin1}',
optionsViewBuilder: (
BuildContext context,
AutocompleteOnSelected<Result> onSelected,
Iterable<Result> options,
) {
return Align(
alignment: Alignment.topLeft,
child: Material(
borderRadius: BorderRadius.circular(20),
elevation: 4.0,
child: SizedBox(
width: 250,
child: ListView.builder(
padding: EdgeInsets.zero,
shrinkWrap: true,
itemCount: options.length,
itemBuilder: (BuildContext context, int index) {
final Result option = options.elementAt(
index,
);
return InkWell(
onTap: () => onSelected(option),
child: ListTile(
title: Text(
'${option.name}, ${option.admin1}',
style: labelLarge,
),
),
);
},
),
),
),
); );
}, },
icon: const Icon(
IconsaxPlusLinear.global_search,
size: 18,
),
) )
: Obx(() { : null,
final location = weatherController.location; title: _buildAppBarTitle(tabIndex, textStyle, labelLarge),
final city = location.city; actions: tabIndex == 0 ? [_buildSearchIconButton()] : null,
final district = location.district;
return Text(
weatherController.isLoading.isFalse
? district!.isEmpty
? '$city'
: city!.isEmpty
? district
: city == district
? city
: '$city'
', $district'
: settings.location
? 'search'.tr
: (isar.locationCaches.where().findAllSync())
.isNotEmpty
? 'loading'.tr
: 'searchCity'.tr,
style: textStyle,
);
}),
1 => Text('cities'.tr, style: textStyle),
2 =>
settings.hideMap
? Text('settings_full'.tr, style: textStyle)
: Text('map'.tr, style: textStyle),
3 => Text('settings_full'.tr, style: textStyle),
int() => null,
},
actions: switch (tabIndex) {
0 => [
IconButton(
onPressed: () {
if (visible) {
_controller.clear();
_focusNode.unfocus();
visible = false;
} else {
visible = true;
}
setState(() {});
},
icon: Icon(
visible
? IconsaxPlusLinear.close_circle
: IconsaxPlusLinear.search_normal_1,
size: 18,
),
),
],
int() => null,
},
), ),
body: SafeArea( body: SafeArea(
child: TabBarView(controller: tabController, children: pages), child: TabBarView(controller: tabController, children: pages),

326
lib/app/ui/main/view/main.dart Normal file → Executable file
View file

@ -24,36 +24,12 @@ class _MainPageState extends State<MainPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return RefreshIndicator( return RefreshIndicator(
onRefresh: () async { onRefresh: _handleRefresh,
await weatherController.deleteAll(false);
await weatherController.setLocation();
setState(() {});
},
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 10), padding: const EdgeInsets.symmetric(horizontal: 10),
child: Obx(() { child: Obx(() {
if (weatherController.isLoading.isTrue) { if (weatherController.isLoading.isTrue) {
return ListView( return _buildLoadingView();
children: const [
MyShimmer(hight: 200),
MyShimmer(
hight: 130,
edgeInsetsMargin: EdgeInsets.symmetric(vertical: 15),
),
MyShimmer(
hight: 90,
edgeInsetsMargin: EdgeInsets.only(bottom: 15),
),
MyShimmer(
hight: 400,
edgeInsetsMargin: EdgeInsets.only(bottom: 15),
),
MyShimmer(
hight: 450,
edgeInsetsMargin: EdgeInsets.only(bottom: 15),
),
],
);
} }
final mainWeather = weatherController.mainWeather; final mainWeather = weatherController.mainWeather;
@ -65,114 +41,202 @@ class _MainPageState extends State<MainPage> {
final tempMax = mainWeather.temperature2MMax![dayOfNow]; final tempMax = mainWeather.temperature2MMax![dayOfNow];
final tempMin = mainWeather.temperature2MMin![dayOfNow]; final tempMin = mainWeather.temperature2MMin![dayOfNow];
return ListView( return _buildMainView(
children: [ context,
Now( mainWeather,
time: mainWeather.time![hourOfDay], weatherCard,
weather: mainWeather.weathercode![hourOfDay], hourOfDay,
degree: mainWeather.temperature2M![hourOfDay], dayOfNow,
feels: mainWeather.apparentTemperature![hourOfDay]!, sunrise,
timeDay: sunrise, sunset,
timeNight: sunset, tempMax!,
tempMax: tempMax!, tempMin!,
tempMin: tempMin!,
),
Card(
margin: const EdgeInsets.only(bottom: 15),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 5,
),
child: SizedBox(
height: 135,
child: ScrollablePositionedList.separated(
key: const PageStorageKey(0),
separatorBuilder: (BuildContext context, int index) {
return const VerticalDivider(
width: 10,
indent: 40,
endIndent: 40,
);
},
scrollDirection: Axis.horizontal,
itemScrollController:
weatherController.itemScrollController,
itemCount: mainWeather.time!.length,
itemBuilder: (ctx, i) {
final i24 = (i / 24).floor();
return GestureDetector(
onTap: () {
weatherController.hourOfDay.value = i;
weatherController.dayOfNow.value = i24;
setState(() {});
},
child: Container(
margin: const EdgeInsets.symmetric(vertical: 5),
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 5,
),
decoration: BoxDecoration(
color:
i == hourOfDay
? context
.theme
.colorScheme
.secondaryContainer
: Colors.transparent,
borderRadius: const BorderRadius.all(
Radius.circular(20),
),
),
child: Hourly(
time: mainWeather.time![i],
weather: mainWeather.weathercode![i],
degree: mainWeather.temperature2M![i],
timeDay: mainWeather.sunrise![i24],
timeNight: mainWeather.sunset![i24],
),
),
);
},
),
),
),
),
SunsetSunrise(timeSunrise: sunrise, timeSunset: sunset),
DescContainer(
humidity: mainWeather.relativehumidity2M?[hourOfDay],
wind: mainWeather.windspeed10M?[hourOfDay],
visibility: mainWeather.visibility?[hourOfDay],
feels: mainWeather.apparentTemperature?[hourOfDay],
evaporation: mainWeather.evapotranspiration?[hourOfDay],
precipitation: mainWeather.precipitation?[hourOfDay],
direction: mainWeather.winddirection10M?[hourOfDay],
pressure: mainWeather.surfacePressure?[hourOfDay],
rain: mainWeather.rain?[hourOfDay],
cloudcover: mainWeather.cloudcover?[hourOfDay],
windgusts: mainWeather.windgusts10M?[hourOfDay],
uvIndex: mainWeather.uvIndex?[hourOfDay],
dewpoint2M: mainWeather.dewpoint2M?[hourOfDay],
precipitationProbability:
mainWeather.precipitationProbability?[hourOfDay],
shortwaveRadiation: mainWeather.shortwaveRadiation?[hourOfDay],
initiallyExpanded: false,
title: 'hourlyVariables'.tr,
),
DailyContainer(
weatherData: weatherCard,
onTap:
() => Get.to(
() => DailyCardList(weatherData: weatherCard),
transition: Transition.downToUp,
),
),
],
); );
}), }),
), ),
); );
} }
Future<void> _handleRefresh() async {
await weatherController.deleteAll(false);
await weatherController.setLocation();
setState(() {});
}
Widget _buildLoadingView() {
return ListView(
children: const [
MyShimmer(height: 200),
MyShimmer(height: 130, margin: EdgeInsets.symmetric(vertical: 15)),
MyShimmer(height: 90, margin: EdgeInsets.only(bottom: 15)),
MyShimmer(height: 400, margin: EdgeInsets.only(bottom: 15)),
MyShimmer(height: 450, margin: EdgeInsets.only(bottom: 15)),
],
);
}
Widget _buildMainView(
BuildContext context,
MainWeatherCache mainWeather,
WeatherCard weatherCard,
int hourOfDay,
int dayOfNow,
String sunrise,
String sunset,
double tempMax,
double tempMin,
) {
return ListView(
children: [
_buildNowWidget(
mainWeather,
hourOfDay,
dayOfNow,
sunrise,
sunset,
tempMax,
tempMin,
),
_buildHourlyList(context, mainWeather, hourOfDay, dayOfNow),
_buildSunsetSunriseWidget(sunrise, sunset),
_buildHourlyDescContainer(mainWeather, hourOfDay),
_buildDailyContainer(weatherCard),
],
);
}
Widget _buildNowWidget(
MainWeatherCache mainWeather,
int hourOfDay,
int dayOfNow,
String sunrise,
String sunset,
double tempMax,
double tempMin,
) {
return Now(
time: mainWeather.time![hourOfDay],
weather: mainWeather.weathercode![hourOfDay],
degree: mainWeather.temperature2M![hourOfDay],
feels: mainWeather.apparentTemperature![hourOfDay]!,
timeDay: sunrise,
timeNight: sunset,
tempMax: tempMax,
tempMin: tempMin,
);
}
Widget _buildHourlyList(
BuildContext context,
MainWeatherCache mainWeather,
int hourOfDay,
int dayOfNow,
) {
return Card(
margin: const EdgeInsets.only(bottom: 15),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
child: SizedBox(
height: 135,
child: ScrollablePositionedList.separated(
key: const PageStorageKey(0),
separatorBuilder: (BuildContext context, int index) {
return const VerticalDivider(
width: 10,
indent: 40,
endIndent: 40,
);
},
scrollDirection: Axis.horizontal,
itemScrollController: weatherController.itemScrollController,
itemCount: mainWeather.time!.length,
itemBuilder: (ctx, i) {
return _buildHourlyItem(
context,
mainWeather,
i,
hourOfDay,
dayOfNow,
);
},
),
),
),
);
}
Widget _buildHourlyItem(
BuildContext context,
MainWeatherCache mainWeather,
int i,
int hourOfDay,
int dayOfNow,
) {
final i24 = (i / 24).floor();
return GestureDetector(
onTap: () {
weatherController.hourOfDay.value = i;
weatherController.dayOfNow.value = i24;
setState(() {});
},
child: Container(
margin: const EdgeInsets.symmetric(vertical: 5),
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 5),
decoration: BoxDecoration(
color: i == hourOfDay
? context.theme.colorScheme.secondaryContainer
: Colors.transparent,
borderRadius: const BorderRadius.all(Radius.circular(20)),
),
child: Hourly(
time: mainWeather.time![i],
weather: mainWeather.weathercode![i],
degree: mainWeather.temperature2M![i],
timeDay: mainWeather.sunrise![i24],
timeNight: mainWeather.sunset![i24],
),
),
);
}
Widget _buildSunsetSunriseWidget(String sunrise, String sunset) {
return SunsetSunrise(timeSunrise: sunrise, timeSunset: sunset);
}
Widget _buildHourlyDescContainer(
MainWeatherCache mainWeather,
int hourOfDay,
) {
return DescContainer(
humidity: mainWeather.relativehumidity2M?[hourOfDay],
wind: mainWeather.windspeed10M?[hourOfDay],
visibility: mainWeather.visibility?[hourOfDay],
feels: mainWeather.apparentTemperature?[hourOfDay],
evaporation: mainWeather.evapotranspiration?[hourOfDay],
precipitation: mainWeather.precipitation?[hourOfDay],
direction: mainWeather.winddirection10M?[hourOfDay],
pressure: mainWeather.surfacePressure?[hourOfDay],
rain: mainWeather.rain?[hourOfDay],
cloudcover: mainWeather.cloudcover?[hourOfDay],
windgusts: mainWeather.windgusts10M?[hourOfDay],
uvIndex: mainWeather.uvIndex?[hourOfDay],
dewpoint2M: mainWeather.dewpoint2M?[hourOfDay],
precipitationProbability:
mainWeather.precipitationProbability?[hourOfDay],
shortwaveRadiation: mainWeather.shortwaveRadiation?[hourOfDay],
initiallyExpanded: false,
title: 'hourlyVariables'.tr,
);
}
Widget _buildDailyContainer(WeatherCard weatherCard) {
return DailyContainer(
weatherData: weatherCard,
onTap: () => Get.to(
() => DailyCardList(weatherData: weatherCard),
transition: Transition.downToUp,
),
);
}
} }

211
lib/app/ui/map/view/map.dart Normal file → Executable file
View file

@ -113,7 +113,7 @@ class _MapPageState extends State<MapPage> with TickerProviderStateMixin {
_focusNode.unfocus(); _focusNode.unfocus();
} }
Widget _buidStyleMarkers( Widget _buildStyleMarkers(
int weathercode, int weathercode,
String time, String time,
String sunrise, String sunrise,
@ -154,7 +154,7 @@ class _MapPageState extends State<MapPage> with TickerProviderStateMixin {
point: LatLng(weatherCard.lat!, weatherCard.lon!), point: LatLng(weatherCard.lat!, weatherCard.lon!),
child: GestureDetector( child: GestureDetector(
onTap: () => _onMarkerTap(weatherCard), onTap: () => _onMarkerTap(weatherCard),
child: _buidStyleMarkers( child: _buildStyleMarkers(
weatherCard.weathercode![hourOfDay], weatherCard.weathercode![hourOfDay],
weatherCard.time![hourOfDay], weatherCard.time![hourOfDay],
weatherCard.sunrise![dayOfNow], weatherCard.sunrise![dayOfNow],
@ -166,33 +166,27 @@ class _MapPageState extends State<MapPage> with TickerProviderStateMixin {
} }
Marker _buildCardMarker(WeatherCard weatherCardList) { Marker _buildCardMarker(WeatherCard weatherCardList) {
final hourOfDay = weatherController.getTime(
weatherCardList.time!,
weatherCardList.timezone!,
);
final dayOfNow = weatherController.getDay(
weatherCardList.timeDaily!,
weatherCardList.timezone!,
);
return Marker( return Marker(
height: 50, height: 50,
width: 100, width: 100,
point: LatLng(weatherCardList.lat!, weatherCardList.lon!), point: LatLng(weatherCardList.lat!, weatherCardList.lon!),
child: GestureDetector( child: GestureDetector(
onTap: () => _onMarkerTap(weatherCardList), onTap: () => _onMarkerTap(weatherCardList),
child: _buidStyleMarkers( child: _buildStyleMarkers(
weatherCardList.weathercode![weatherController.getTime( weatherCardList.weathercode![hourOfDay],
weatherCardList.time!, weatherCardList.time![hourOfDay],
weatherCardList.timezone!, weatherCardList.sunrise![dayOfNow],
)], weatherCardList.sunset![dayOfNow],
weatherCardList.time![weatherController.getTime( weatherCardList.temperature2M![hourOfDay],
weatherCardList.time!,
weatherCardList.timezone!,
)],
weatherCardList.sunrise![weatherController.getDay(
weatherCardList.timeDaily!,
weatherCardList.timezone!,
)],
weatherCardList.sunset![weatherController.getDay(
weatherCardList.timeDaily!,
weatherCardList.timezone!,
)],
weatherCardList.temperature2M![weatherController.getTime(
weatherCardList.time!,
weatherCardList.timezone!,
)],
), ),
), ),
); );
@ -235,6 +229,91 @@ class _MapPageState extends State<MapPage> with TickerProviderStateMixin {
: const SizedBox.shrink(); : const SizedBox.shrink();
} }
Widget _buildSearchField() {
return RawAutocomplete<Result>(
focusNode: _focusNode,
textEditingController: _controllerSearch,
fieldViewBuilder: (
BuildContext context,
TextEditingController fieldTextEditingController,
FocusNode fieldFocusNode,
VoidCallback onFieldSubmitted,
) {
return MyTextForm(
labelText: 'search'.tr,
type: TextInputType.text,
icon: const Icon(IconsaxPlusLinear.global_search),
controller: _controllerSearch,
margin: const EdgeInsets.only(left: 10, right: 10, top: 10),
focusNode: _focusNode,
onChanged: (value) => setState(() {}),
iconButton:
_controllerSearch.text.isNotEmpty
? IconButton(
onPressed: () {
_controllerSearch.clear();
},
icon: const Icon(
IconsaxPlusLinear.close_circle,
color: Colors.grey,
size: 20,
),
)
: null,
);
},
optionsBuilder: (TextEditingValue textEditingValue) {
if (textEditingValue.text.isEmpty) {
return const Iterable<Result>.empty();
}
return WeatherAPI().getCity(textEditingValue.text, locale);
},
onSelected: (Result selection) {
_animatedMapController.mapController.move(
LatLng(selection.latitude, selection.longitude),
14,
);
_controllerSearch.clear();
_focusNode.unfocus();
},
displayStringForOption:
(Result option) => '${option.name}, ${option.admin1}',
optionsViewBuilder: (
BuildContext context,
AutocompleteOnSelected<Result> onSelected,
Iterable<Result> options,
) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
child: Align(
alignment: Alignment.topCenter,
child: Material(
borderRadius: BorderRadius.circular(20),
elevation: 4.0,
child: ListView.builder(
padding: EdgeInsets.zero,
shrinkWrap: true,
itemCount: options.length,
itemBuilder: (BuildContext context, int index) {
final Result option = options.elementAt(index);
return InkWell(
onTap: () => onSelected(option),
child: ListTile(
title: Text(
'${option.name}, ${option.admin1}',
style: context.textTheme.labelLarge,
),
),
);
},
),
),
),
);
},
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final mainLocation = weatherController.location; final mainLocation = weatherController.location;
@ -387,91 +466,7 @@ class _MapPageState extends State<MapPage> with TickerProviderStateMixin {
), ),
], ],
), ),
RawAutocomplete<Result>( _buildSearchField(),
focusNode: _focusNode,
textEditingController: _controllerSearch,
fieldViewBuilder: (
BuildContext context,
TextEditingController fieldTextEditingController,
FocusNode fieldFocusNode,
VoidCallback onFieldSubmitted,
) {
return MyTextForm(
labelText: 'search'.tr,
type: TextInputType.text,
icon: const Icon(IconsaxPlusLinear.global_search),
controller: _controllerSearch,
margin: const EdgeInsets.only(left: 10, right: 10, top: 10),
focusNode: _focusNode,
onChanged: (value) => setState(() {}),
iconButton:
_controllerSearch.text.isNotEmpty
? IconButton(
onPressed: () {
_controllerSearch.clear();
},
icon: const Icon(
IconsaxPlusLinear.close_circle,
color: Colors.grey,
size: 20,
),
)
: null,
);
},
optionsBuilder: (TextEditingValue textEditingValue) {
if (textEditingValue.text.isEmpty) {
return const Iterable<Result>.empty();
}
return WeatherAPI().getCity(textEditingValue.text, locale);
},
onSelected: (Result selection) {
_animatedMapController.mapController.move(
LatLng(selection.latitude, selection.longitude),
14,
);
_controllerSearch.clear();
_focusNode.unfocus();
},
displayStringForOption:
(Result option) => '${option.name}, ${option.admin1}',
optionsViewBuilder: (
BuildContext context,
AutocompleteOnSelected<Result> onSelected,
Iterable<Result> options,
) {
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 5,
),
child: Align(
alignment: Alignment.topCenter,
child: Material(
borderRadius: BorderRadius.circular(20),
elevation: 4.0,
child: ListView.builder(
padding: EdgeInsets.zero,
shrinkWrap: true,
itemCount: options.length,
itemBuilder: (BuildContext context, int index) {
final Result option = options.elementAt(index);
return InkWell(
onTap: () => onSelected(option),
child: ListTile(
title: Text(
'${option.name}, ${option.admin1}',
style: context.textTheme.labelLarge,
),
),
);
},
),
),
),
);
},
),
], ],
); );
}, },

110
lib/app/ui/onboarding.dart Normal file → Executable file
View file

@ -1,9 +1,9 @@
import 'package:flutter/material.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:rain/app/data/db.dart'; import 'package:rain/app/data/db.dart';
import 'package:rain/app/ui/geolocation.dart'; import 'package:rain/app/ui/geolocation.dart';
import 'package:rain/app/ui/widgets/button.dart'; import 'package:rain/app/ui/widgets/button.dart';
import 'package:rain/main.dart'; import 'package:rain/main.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
class OnBording extends StatefulWidget { class OnBording extends StatefulWidget {
@ -19,8 +19,8 @@ class _OnBordingState extends State<OnBording> {
@override @override
void initState() { void initState() {
pageController = PageController(initialPage: 0);
super.initState(); super.initState();
pageController = PageController(initialPage: 0);
} }
@override @override
@ -45,55 +45,65 @@ class _OnBordingState extends State<OnBording> {
body: SafeArea( body: SafeArea(
child: Column( child: Column(
children: [ children: [
Expanded( _buildPageView(),
child: PageView.builder( _buildDotIndicators(),
controller: pageController, _buildActionButton(),
itemCount: data.length,
onPageChanged: (index) {
setState(() {
pageIndex = index;
});
},
itemBuilder:
(context, index) => OnboardContent(
image: data[index].image,
title: data[index].title,
description: data[index].description,
),
),
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
...List.generate(
data.length,
(index) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 5),
child: DotIndicator(isActive: index == pageIndex),
),
),
],
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
child: MyTextButton(
buttonName:
pageIndex == data.length - 1 ? 'start'.tr : 'next'.tr,
onPressed: () {
pageIndex == data.length - 1
? onBoardHome()
: pageController.nextPage(
duration: const Duration(milliseconds: 300),
curve: Curves.ease,
);
},
),
),
], ],
), ),
), ),
); );
} }
Widget _buildPageView() {
return Expanded(
child: PageView.builder(
controller: pageController,
itemCount: data.length,
onPageChanged: (index) {
setState(() {
pageIndex = index;
});
},
itemBuilder: (context, index) => OnboardContent(
image: data[index].image,
title: data[index].title,
description: data[index].description,
),
),
);
}
Widget _buildDotIndicators() {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(
data.length,
(index) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 5),
child: DotIndicator(isActive: index == pageIndex),
),
),
);
}
Widget _buildActionButton() {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
child: MyTextButton(
buttonName: pageIndex == data.length - 1 ? 'start'.tr : 'next'.tr,
onPressed: () {
if (pageIndex == data.length - 1) {
onBoardHome();
} else {
pageController.nextPage(
duration: const Duration(milliseconds: 300),
curve: Curves.ease,
);
}
},
),
);
}
} }
class DotIndicator extends StatelessWidget { class DotIndicator extends StatelessWidget {
@ -108,10 +118,9 @@ class DotIndicator extends StatelessWidget {
height: 8, height: 8,
width: 8, width: 8,
decoration: BoxDecoration( decoration: BoxDecoration(
color: color: isActive
isActive ? context.theme.colorScheme.secondary
? context.theme.colorScheme.secondary : context.theme.colorScheme.secondaryContainer,
: context.theme.colorScheme.secondaryContainer,
shape: BoxShape.circle, shape: BoxShape.circle,
), ),
); );
@ -153,6 +162,7 @@ class OnboardContent extends StatelessWidget {
required this.title, required this.title,
required this.description, required this.description,
}); });
final String image, title, description; final String image, title, description;
@override @override

266
lib/app/ui/places/view/place_info.dart Normal file → Executable file
View file

@ -56,138 +56,19 @@ class _PlaceInfoState extends State<PlaceInfo> {
final weatherCard = widget.weatherCard; final weatherCard = widget.weatherCard;
return RefreshIndicator( return RefreshIndicator(
onRefresh: () async { onRefresh: _handleRefresh,
await weatherController.updateCard(weatherCard);
getTime();
setState(() {});
},
child: Scaffold( child: Scaffold(
appBar: AppBar( appBar: _buildAppBar(context, weatherCard),
centerTitle: true,
automaticallyImplyLeading: false,
leading: IconButton(
onPressed: () => Get.back(),
icon: const Icon(IconsaxPlusLinear.arrow_left_3, size: 20),
),
title: Text(
weatherCard.district!.isNotEmpty
? '${weatherCard.city}'
', ${weatherCard.district}'
: '${weatherCard.city}',
style: context.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
fontSize: 18,
),
),
),
body: SafeArea( body: SafeArea(
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 10), padding: const EdgeInsets.symmetric(horizontal: 10),
child: ListView( child: ListView(
children: [ children: [
Now( _buildNowWidget(weatherCard),
time: weatherCard.time![timeNow], _buildHourlyList(weatherCard),
weather: weatherCard.weathercode![timeNow], _buildSunsetSunriseWidget(weatherCard),
degree: weatherCard.temperature2M![timeNow], _buildHourlyDescContainer(weatherCard),
feels: weatherCard.apparentTemperature![timeNow]!, _buildDailyContainer(weatherCard),
timeDay: weatherCard.sunrise![dayNow],
timeNight: weatherCard.sunset![dayNow],
tempMax: weatherCard.temperature2MMax![dayNow]!,
tempMin: weatherCard.temperature2MMin![dayNow]!,
),
Card(
margin: const EdgeInsets.only(bottom: 15),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 5,
),
child: SizedBox(
height: 135,
child: ScrollablePositionedList.separated(
key: const PageStorageKey(1),
separatorBuilder: (BuildContext context, int index) {
return const VerticalDivider(
width: 10,
indent: 40,
endIndent: 40,
);
},
scrollDirection: Axis.horizontal,
itemScrollController: itemScrollController,
itemCount: weatherCard.time!.length,
itemBuilder:
(ctx, i) => GestureDetector(
onTap: () {
timeNow = i;
dayNow = (i / 24).floor();
setState(() {});
},
child: Container(
margin: const EdgeInsets.symmetric(vertical: 5),
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 5,
),
decoration: BoxDecoration(
color:
i == timeNow
? context
.theme
.colorScheme
.secondaryContainer
: Colors.transparent,
borderRadius: const BorderRadius.all(
Radius.circular(20),
),
),
child: Hourly(
time: weatherCard.time![i],
weather: weatherCard.weathercode![i],
degree: weatherCard.temperature2M![i],
timeDay:
weatherCard.sunrise![(i / 24).floor()],
timeNight:
weatherCard.sunset![(i / 24).floor()],
),
),
),
),
),
),
),
SunsetSunrise(
timeSunrise: weatherCard.sunrise![dayNow],
timeSunset: weatherCard.sunset![dayNow],
),
DescContainer(
humidity: weatherCard.relativehumidity2M?[timeNow],
wind: weatherCard.windspeed10M?[timeNow],
visibility: weatherCard.visibility?[timeNow],
feels: weatherCard.apparentTemperature?[timeNow],
evaporation: weatherCard.evapotranspiration?[timeNow],
precipitation: weatherCard.precipitation?[timeNow],
direction: weatherCard.winddirection10M?[timeNow],
pressure: weatherCard.surfacePressure?[timeNow],
rain: weatherCard.rain?[timeNow],
cloudcover: weatherCard.cloudcover?[timeNow],
windgusts: weatherCard.windgusts10M?[timeNow],
uvIndex: weatherCard.uvIndex?[timeNow],
dewpoint2M: weatherCard.dewpoint2M?[timeNow],
precipitationProbability:
weatherCard.precipitationProbability?[timeNow],
shortwaveRadiation: weatherCard.shortwaveRadiation?[timeNow],
initiallyExpanded: false,
title: 'hourlyVariables'.tr,
),
DailyContainer(
weatherData: weatherCard,
onTap:
() => Get.to(
() => DailyCardList(weatherData: weatherCard),
transition: Transition.downToUp,
),
),
], ],
), ),
), ),
@ -195,4 +76,137 @@ class _PlaceInfoState extends State<PlaceInfo> {
), ),
); );
} }
Future<void> _handleRefresh() async {
await weatherController.updateCard(widget.weatherCard);
getTime();
setState(() {});
}
AppBar _buildAppBar(BuildContext context, WeatherCard weatherCard) {
return AppBar(
centerTitle: true,
automaticallyImplyLeading: false,
leading: IconButton(
onPressed: () => Get.back(),
icon: const Icon(IconsaxPlusLinear.arrow_left_3, size: 20),
),
title: Text(
weatherCard.district!.isNotEmpty
? '${weatherCard.city}, ${weatherCard.district}'
: '${weatherCard.city}',
style: context.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
fontSize: 18,
),
),
);
}
Widget _buildNowWidget(WeatherCard weatherCard) {
return Now(
time: weatherCard.time![timeNow],
weather: weatherCard.weathercode![timeNow],
degree: weatherCard.temperature2M![timeNow],
feels: weatherCard.apparentTemperature![timeNow]!,
timeDay: weatherCard.sunrise![dayNow],
timeNight: weatherCard.sunset![dayNow],
tempMax: weatherCard.temperature2MMax![dayNow]!,
tempMin: weatherCard.temperature2MMin![dayNow]!,
);
}
Widget _buildHourlyList(WeatherCard weatherCard) {
return Card(
margin: const EdgeInsets.only(bottom: 15),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
child: SizedBox(
height: 135,
child: ScrollablePositionedList.separated(
key: const PageStorageKey(1),
separatorBuilder: (BuildContext context, int index) {
return const VerticalDivider(
width: 10,
indent: 40,
endIndent: 40,
);
},
scrollDirection: Axis.horizontal,
itemScrollController: itemScrollController,
itemCount: weatherCard.time!.length,
itemBuilder: (ctx, i) => _buildHourlyItem(weatherCard, i),
),
),
),
);
}
Widget _buildHourlyItem(WeatherCard weatherCard, int i) {
return GestureDetector(
onTap: () {
timeNow = i;
dayNow = (i / 24).floor();
setState(() {});
},
child: Container(
margin: const EdgeInsets.symmetric(vertical: 5),
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 5),
decoration: BoxDecoration(
color:
i == timeNow
? context.theme.colorScheme.secondaryContainer
: Colors.transparent,
borderRadius: const BorderRadius.all(Radius.circular(20)),
),
child: Hourly(
time: weatherCard.time![i],
weather: weatherCard.weathercode![i],
degree: weatherCard.temperature2M![i],
timeDay: weatherCard.sunrise![(i / 24).floor()],
timeNight: weatherCard.sunset![(i / 24).floor()],
),
),
);
}
Widget _buildSunsetSunriseWidget(WeatherCard weatherCard) {
return SunsetSunrise(
timeSunrise: weatherCard.sunrise![dayNow],
timeSunset: weatherCard.sunset![dayNow],
);
}
Widget _buildHourlyDescContainer(WeatherCard weatherCard) {
return DescContainer(
humidity: weatherCard.relativehumidity2M?[timeNow],
wind: weatherCard.windspeed10M?[timeNow],
visibility: weatherCard.visibility?[timeNow],
feels: weatherCard.apparentTemperature?[timeNow],
evaporation: weatherCard.evapotranspiration?[timeNow],
precipitation: weatherCard.precipitation?[timeNow],
direction: weatherCard.winddirection10M?[timeNow],
pressure: weatherCard.surfacePressure?[timeNow],
rain: weatherCard.rain?[timeNow],
cloudcover: weatherCard.cloudcover?[timeNow],
windgusts: weatherCard.windgusts10M?[timeNow],
uvIndex: weatherCard.uvIndex?[timeNow],
dewpoint2M: weatherCard.dewpoint2M?[timeNow],
precipitationProbability: weatherCard.precipitationProbability?[timeNow],
shortwaveRadiation: weatherCard.shortwaveRadiation?[timeNow],
initiallyExpanded: false,
title: 'hourlyVariables'.tr,
);
}
Widget _buildDailyContainer(WeatherCard weatherCard) {
return DailyContainer(
weatherData: weatherCard,
onTap:
() => Get.to(
() => DailyCardList(weatherData: weatherCard),
transition: Transition.downToUp,
),
);
}
} }

154
lib/app/ui/places/view/place_list.dart Normal file → Executable file
View file

@ -14,91 +14,103 @@ class PlaceList extends StatefulWidget {
class _PlaceListState extends State<PlaceList> { class _PlaceListState extends State<PlaceList> {
final weatherController = Get.put(WeatherController()); final weatherController = Get.put(WeatherController());
TextEditingController searchTasks = TextEditingController(); final TextEditingController searchTasks = TextEditingController();
String filter = ''; String filter = '';
applyFilter(String value) async {
filter = value.toLowerCase();
setState(() {});
}
@override @override
void initState() { void initState() {
super.initState(); super.initState();
applyFilter(''); applyFilter('');
} }
void applyFilter(String value) {
filter = value.toLowerCase();
setState(() {});
}
void clearSearch() {
searchTasks.clear();
applyFilter('');
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final textTheme = context.textTheme; final textTheme = context.textTheme;
final titleMedium = textTheme.titleMedium; final titleMedium = textTheme.titleMedium;
return Obx(
() => return Obx(() => _buildContent(context, titleMedium));
weatherController.weatherCards.isEmpty }
? Center(
child: SingleChildScrollView( Widget _buildContent(BuildContext context, TextStyle? titleMedium) {
child: Column( if (weatherController.weatherCards.isEmpty) {
children: [ return _buildEmptyState(context, titleMedium);
Image.asset('assets/icons/City.png', scale: 6), } else {
SizedBox( return _buildListView(context);
width: Get.size.width * 0.8, }
child: Text( }
'noWeatherCard'.tr,
textAlign: TextAlign.center, Widget _buildEmptyState(BuildContext context, TextStyle? titleMedium) {
style: titleMedium?.copyWith( return Center(
fontWeight: FontWeight.w600, child: SingleChildScrollView(
fontSize: 18, child: Column(
), children: [
), Image.asset('assets/icons/City.png', scale: 6),
), SizedBox(
], width: Get.size.width * 0.8,
), child: Text(
), 'noWeatherCard'.tr,
) textAlign: TextAlign.center,
: NestedScrollView( style: titleMedium?.copyWith(
physics: const NeverScrollableScrollPhysics(), fontWeight: FontWeight.w600,
headerSliverBuilder: (context, innerBoxIsScrolled) { fontSize: 18,
return [
SliverToBoxAdapter(
child: MyTextForm(
labelText: 'search'.tr,
type: TextInputType.text,
icon: const Icon(
IconsaxPlusLinear.search_normal_1,
size: 20,
),
controller: searchTasks,
margin: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 5,
),
onChanged: applyFilter,
iconButton:
searchTasks.text.isNotEmpty
? IconButton(
onPressed: () {
searchTasks.clear();
applyFilter('');
},
icon: const Icon(
IconsaxPlusLinear.close_circle,
color: Colors.grey,
size: 20,
),
)
: null,
),
),
];
},
body: RefreshIndicator(
onRefresh: () async {
await weatherController.updateCacheCard(true);
setState(() {});
},
child: PlaceCardList(searchCity: filter),
), ),
), ),
),
],
),
),
); );
} }
Widget _buildListView(BuildContext context) {
return NestedScrollView(
physics: const NeverScrollableScrollPhysics(),
headerSliverBuilder: (context, innerBoxIsScrolled) {
return [_buildSearchField(context)];
},
body: RefreshIndicator(
onRefresh: _handleRefresh,
child: PlaceCardList(searchCity: filter),
),
);
}
Widget _buildSearchField(BuildContext context) {
return SliverToBoxAdapter(
child: MyTextForm(
labelText: 'search'.tr,
type: TextInputType.text,
icon: const Icon(IconsaxPlusLinear.search_normal_1, size: 20),
controller: searchTasks,
margin: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
onChanged: applyFilter,
iconButton:
searchTasks.text.isNotEmpty
? IconButton(
onPressed: clearSearch,
icon: const Icon(
IconsaxPlusLinear.close_circle,
color: Colors.grey,
size: 20,
),
)
: null,
),
);
}
Future<void> _handleRefresh() async {
await weatherController.updateCacheCard(true);
setState(() {});
}
} }

457
lib/app/ui/places/widgets/create_place.dart Normal file → Executable file
View file

@ -23,12 +23,14 @@ class _CreatePlaceState extends State<CreatePlace>
final formKey = GlobalKey<FormState>(); final formKey = GlobalKey<FormState>();
final _focusNode = FocusNode(); final _focusNode = FocusNode();
final weatherController = Get.put(WeatherController()); final weatherController = Get.put(WeatherController());
late final TextEditingController _controller = TextEditingController();
late TextEditingController _controllerLat = TextEditingController(); static const kTextFieldElevation = 4.0;
late TextEditingController _controllerLon = TextEditingController();
late final TextEditingController _controllerCity = TextEditingController(); late TextEditingController _controller;
late final TextEditingController _controllerDistrict = late TextEditingController _controllerLat;
TextEditingController(); late TextEditingController _controllerLon;
late TextEditingController _controllerCity;
late TextEditingController _controllerDistrict;
late AnimationController _animationController; late AnimationController _animationController;
late Animation<double> _animation; late Animation<double> _animation;
@ -36,10 +38,12 @@ class _CreatePlaceState extends State<CreatePlace>
@override @override
void initState() { void initState() {
super.initState(); super.initState();
if (widget.latitude != null && widget.longitude != null) { _controller = TextEditingController();
_controllerLat = TextEditingController(text: widget.latitude); _controllerLat = TextEditingController(text: widget.latitude);
_controllerLon = TextEditingController(text: widget.longitude); _controllerLon = TextEditingController(text: widget.longitude);
} _controllerCity = TextEditingController();
_controllerDistrict = TextEditingController();
_animationController = AnimationController( _animationController = AnimationController(
duration: const Duration(milliseconds: 300), duration: const Duration(milliseconds: 300),
vsync: this, vsync: this,
@ -78,20 +82,24 @@ class _CreatePlaceState extends State<CreatePlace>
setState(() {}); setState(() {});
} }
@override bool get showButton {
Widget build(BuildContext context) { return _controllerLon.text.isNotEmpty &&
const kTextFieldElevation = 4.0;
bool showButton =
_controllerLon.text.isNotEmpty &&
_controllerLat.text.isNotEmpty && _controllerLat.text.isNotEmpty &&
_controllerCity.text.isNotEmpty && _controllerCity.text.isNotEmpty &&
_controllerDistrict.text.isNotEmpty; _controllerDistrict.text.isNotEmpty;
}
void updateButtonVisibility() {
if (showButton) { if (showButton) {
_animationController.forward(); _animationController.forward();
} else { } else {
_animationController.reverse(); _animationController.reverse();
} }
}
@override
Widget build(BuildContext context) {
updateButtonVisibility();
return Padding( return Padding(
padding: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom), padding: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom),
@ -108,208 +116,13 @@ class _CreatePlaceState extends State<CreatePlace>
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Padding( _buildTitleText(),
padding: const EdgeInsets.only(top: 14, bottom: 7), _buildSearchField(),
child: Text( _buildLatitudeField(),
'create'.tr, _buildLongitudeField(),
style: context.textTheme.titleLarge?.copyWith( _buildCityField(),
fontWeight: FontWeight.bold, _buildDistrictField(),
), _buildSubmitButton(),
textAlign: TextAlign.center,
),
),
RawAutocomplete<Result>(
focusNode: _focusNode,
textEditingController: _controller,
fieldViewBuilder: (
BuildContext context,
TextEditingController fieldTextEditingController,
FocusNode fieldFocusNode,
VoidCallback onFieldSubmitted,
) {
return MyTextForm(
elevation: kTextFieldElevation,
labelText: 'search'.tr,
type: TextInputType.text,
icon: const Icon(IconsaxPlusLinear.global_search),
controller: _controller,
margin: const EdgeInsets.only(
left: 10,
right: 10,
top: 10,
),
focusNode: _focusNode,
);
},
optionsBuilder: (TextEditingValue textEditingValue) {
if (textEditingValue.text.isEmpty) {
return const Iterable<Result>.empty();
}
return WeatherAPI().getCity(
textEditingValue.text,
locale,
);
},
onSelected:
(Result selection) => fillController(selection),
displayStringForOption:
(Result option) => '${option.name}, ${option.admin1}',
optionsViewBuilder: (
BuildContext context,
AutocompleteOnSelected<Result> onSelected,
Iterable<Result> options,
) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 10),
child: Align(
alignment: Alignment.topCenter,
child: Material(
borderRadius: BorderRadius.circular(20),
elevation: 4.0,
child: ListView.builder(
padding: EdgeInsets.zero,
shrinkWrap: true,
itemCount: options.length,
itemBuilder: (BuildContext context, int index) {
final Result option = options.elementAt(
index,
);
return InkWell(
onTap: () => onSelected(option),
child: ListTile(
title: Text(
'${option.name}, ${option.admin1}',
style: context.textTheme.labelLarge,
),
),
);
},
),
),
),
);
},
),
MyTextForm(
elevation: kTextFieldElevation,
controller: _controllerLat,
labelText: 'lat'.tr,
type: TextInputType.number,
icon: const Icon(IconsaxPlusLinear.location),
onChanged: (value) => setState(() {}),
margin: const EdgeInsets.only(
left: 10,
right: 10,
top: 10,
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'validateValue'.tr;
}
double? numericValue = double.tryParse(value);
if (numericValue == null) {
return 'validateNumber'.tr;
}
if (numericValue < -90 || numericValue > 90) {
return 'validate90'.tr;
}
return null;
},
),
MyTextForm(
elevation: kTextFieldElevation,
controller: _controllerLon,
labelText: 'lon'.tr,
type: TextInputType.number,
icon: const Icon(IconsaxPlusLinear.location),
onChanged: (value) => setState(() {}),
margin: const EdgeInsets.only(
left: 10,
right: 10,
top: 10,
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'validateValue'.tr;
}
double? numericValue = double.tryParse(value);
if (numericValue == null) {
return 'validateNumber'.tr;
}
if (numericValue < -180 || numericValue > 180) {
return 'validate180'.tr;
}
return null;
},
),
MyTextForm(
elevation: kTextFieldElevation,
controller: _controllerCity,
labelText: 'city'.tr,
type: TextInputType.name,
icon: const Icon(IconsaxPlusLinear.building_3),
onChanged: (value) => setState(() {}),
margin: const EdgeInsets.only(
left: 10,
right: 10,
top: 10,
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'validateName'.tr;
}
return null;
},
),
MyTextForm(
elevation: kTextFieldElevation,
controller: _controllerDistrict,
labelText: 'district'.tr,
type: TextInputType.streetAddress,
icon: const Icon(IconsaxPlusLinear.global),
onChanged: (value) => setState(() {}),
margin: const EdgeInsets.only(
left: 10,
right: 10,
top: 10,
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'validateName'.tr;
}
return null;
},
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 10,
),
child: SizeTransition(
sizeFactor: _animation,
axisAlignment: -1.0,
child: MyTextButton(
buttonName: 'done'.tr,
onPressed: () async {
if (formKey.currentState!.validate()) {
textTrim(_controllerLat);
textTrim(_controllerLon);
textTrim(_controllerCity);
textTrim(_controllerDistrict);
setState(() => isLoading = true);
await weatherController.addCardWeather(
double.parse(_controllerLat.text),
double.parse(_controllerLon.text),
_controllerCity.text,
_controllerDistrict.text,
);
setState(() => isLoading = false);
Get.back();
}
},
),
),
),
], ],
), ),
), ),
@ -320,4 +133,214 @@ class _CreatePlaceState extends State<CreatePlace>
), ),
); );
} }
Widget _buildTitleText() {
return Padding(
padding: const EdgeInsets.only(top: 14, bottom: 7),
child: Text(
'create'.tr,
style: context.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
);
}
Widget _buildSearchField() {
return RawAutocomplete<Result>(
focusNode: _focusNode,
textEditingController: _controller,
fieldViewBuilder: (
BuildContext context,
TextEditingController fieldTextEditingController,
FocusNode fieldFocusNode,
VoidCallback onFieldSubmitted,
) {
return MyTextForm(
elevation: kTextFieldElevation,
labelText: 'search'.tr,
type: TextInputType.text,
icon: const Icon(IconsaxPlusLinear.global_search),
controller: _controller,
margin: const EdgeInsets.only(left: 10, right: 10, top: 10),
focusNode: _focusNode,
);
},
optionsBuilder: (TextEditingValue textEditingValue) {
if (textEditingValue.text.isEmpty) {
return const Iterable<Result>.empty();
}
return WeatherAPI().getCity(textEditingValue.text, locale);
},
onSelected: (Result selection) => fillController(selection),
displayStringForOption:
(Result option) => '${option.name}, ${option.admin1}',
optionsViewBuilder: (
BuildContext context,
AutocompleteOnSelected<Result> onSelected,
Iterable<Result> options,
) {
return _buildOptionsView(context, onSelected, options);
},
);
}
Widget _buildOptionsView(
BuildContext context,
AutocompleteOnSelected<Result> onSelected,
Iterable<Result> options,
) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 10),
child: Align(
alignment: Alignment.topCenter,
child: Material(
borderRadius: BorderRadius.circular(20),
elevation: 4.0,
child: ListView.builder(
padding: EdgeInsets.zero,
shrinkWrap: true,
itemCount: options.length,
itemBuilder: (BuildContext context, int index) {
final Result option = options.elementAt(index);
return InkWell(
onTap: () => onSelected(option),
child: ListTile(
title: Text(
'${option.name}, ${option.admin1}',
style: context.textTheme.labelLarge,
),
),
);
},
),
),
),
);
}
Widget _buildLatitudeField() {
return MyTextForm(
elevation: kTextFieldElevation,
controller: _controllerLat,
labelText: 'lat'.tr,
type: TextInputType.number,
icon: const Icon(IconsaxPlusLinear.location),
onChanged: (value) => setState(() {}),
margin: const EdgeInsets.only(left: 10, right: 10, top: 10),
validator: (value) => _validateLatitude(value),
);
}
Widget _buildLongitudeField() {
return MyTextForm(
elevation: kTextFieldElevation,
controller: _controllerLon,
labelText: 'lon'.tr,
type: TextInputType.number,
icon: const Icon(IconsaxPlusLinear.location),
onChanged: (value) => setState(() {}),
margin: const EdgeInsets.only(left: 10, right: 10, top: 10),
validator: (value) => _validateLongitude(value),
);
}
Widget _buildCityField() {
return MyTextForm(
elevation: kTextFieldElevation,
controller: _controllerCity,
labelText: 'city'.tr,
type: TextInputType.name,
icon: const Icon(IconsaxPlusLinear.building_3),
onChanged: (value) => setState(() {}),
margin: const EdgeInsets.only(left: 10, right: 10, top: 10),
validator: (value) => _validateCity(value),
);
}
Widget _buildDistrictField() {
return MyTextForm(
elevation: kTextFieldElevation,
controller: _controllerDistrict,
labelText: 'district'.tr,
type: TextInputType.streetAddress,
icon: const Icon(IconsaxPlusLinear.global),
onChanged: (value) => setState(() {}),
margin: const EdgeInsets.only(left: 10, right: 10, top: 10),
validator: (value) => _validateDistrict(value),
);
}
Widget _buildSubmitButton() {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
child: SizeTransition(
sizeFactor: _animation,
axisAlignment: -1.0,
child: MyTextButton(buttonName: 'done'.tr, onPressed: _handleSubmit),
),
);
}
String? _validateLatitude(String? value) {
if (value == null || value.isEmpty) {
return 'validateValue'.tr;
}
double? numericValue = double.tryParse(value);
if (numericValue == null) {
return 'validateNumber'.tr;
}
if (numericValue < -90 || numericValue > 90) {
return 'validate90'.tr;
}
return null;
}
String? _validateLongitude(String? value) {
if (value == null || value.isEmpty) {
return 'validateValue'.tr;
}
double? numericValue = double.tryParse(value);
if (numericValue == null) {
return 'validateNumber'.tr;
}
if (numericValue < -180 || numericValue > 180) {
return 'validate180'.tr;
}
return null;
}
String? _validateCity(String? value) {
if (value == null || value.isEmpty) {
return 'validateName'.tr;
}
return null;
}
String? _validateDistrict(String? value) {
if (value == null || value.isEmpty) {
return 'validateName'.tr;
}
return null;
}
Future<void> _handleSubmit() async {
if (formKey.currentState!.validate()) {
textTrim(_controllerLat);
textTrim(_controllerLon);
textTrim(_controllerCity);
textTrim(_controllerDistrict);
setState(() => isLoading = true);
await weatherController.addCardWeather(
double.parse(_controllerLat.text),
double.parse(_controllerLon.text),
_controllerCity.text,
_controllerDistrict.text,
);
setState(() => isLoading = false);
Get.back();
}
}
} }

188
lib/app/ui/places/widgets/place_card.dart Normal file → Executable file
View file

@ -19,6 +19,7 @@ class PlaceCard extends StatefulWidget {
required this.timeNight, required this.timeNight,
required this.timeDaily, required this.timeDaily,
}); });
final List<String> time; final List<String> time;
final List<String> timeDay; final List<String> timeDay;
final List<String> timeNight; final List<String> timeNight;
@ -40,104 +41,115 @@ class _PlaceCardState extends State<PlaceCard> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final currentTimeIndex = weatherController.getTime(
widget.time,
widget.timezone,
);
final currentDayIndex = weatherController.getDay(
widget.timeDaily,
widget.timezone,
);
return Card( return Card(
margin: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), margin: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(vertical: 15, horizontal: 20), padding: const EdgeInsets.symmetric(vertical: 15, horizontal: 20),
child: Row( child: Row(
children: [ children: [
Expanded( _buildWeatherInfo(context, currentTimeIndex, currentDayIndex),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
statusData.getDegree(
widget
.degree[weatherController.getTime(
widget.time,
widget.timezone,
)]
.round()
.toInt(),
),
style: context.textTheme.titleLarge?.copyWith(
fontSize: 22,
fontWeight: FontWeight.w600,
),
),
const Gap(7),
Text(
statusWeather.getText(
widget.weather[weatherController.getTime(
widget.time,
widget.timezone,
)],
),
style: context.textTheme.titleMedium?.copyWith(
color: Colors.grey,
fontWeight: FontWeight.w400,
),
),
],
),
const Gap(10),
Text(
widget.district.isEmpty
? widget.city
: widget.city.isEmpty
? widget.district
: widget.city == widget.district
? widget.city
: '${widget.city}'
', ${widget.district}',
style: context.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w500,
),
),
const Gap(5),
StreamBuilder(
stream: Stream.periodic(const Duration(seconds: 1)),
builder: (context, snapshot) {
return Text(
'${'time'.tr}: ${statusData.getTimeFormatTz(tz.TZDateTime.now(tz.getLocation(widget.timezone)))}',
style: context.textTheme.titleMedium?.copyWith(
color: Colors.grey,
fontWeight: FontWeight.w400,
),
);
},
),
],
),
),
const Gap(5), const Gap(5),
Image.asset( _buildWeatherImage(currentTimeIndex, currentDayIndex),
statusWeather.getImageNow(
widget.weather[weatherController.getTime(
widget.time,
widget.timezone,
)],
widget.time[weatherController.getTime(
widget.time,
widget.timezone,
)],
widget.timeDay[weatherController.getDay(
widget.timeDaily,
widget.timezone,
)],
widget.timeNight[weatherController.getDay(
widget.timeDaily,
widget.timezone,
)],
),
scale: 6.5,
),
], ],
), ),
), ),
); );
} }
Widget _buildWeatherInfo(
BuildContext context,
int currentTimeIndex,
int currentDayIndex,
) {
return Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
statusData.getDegree(
widget.degree[currentTimeIndex].round().toInt(),
),
style: context.textTheme.titleLarge?.copyWith(
fontSize: 22,
fontWeight: FontWeight.w600,
),
),
const Gap(7),
Text(
statusWeather.getText(widget.weather[currentTimeIndex]),
style: context.textTheme.titleMedium?.copyWith(
color: Colors.grey,
fontWeight: FontWeight.w400,
),
),
],
),
const Gap(10),
_buildLocationText(),
const Gap(5),
_buildCurrentTimeText(context),
],
),
);
}
Widget _buildLocationText() {
String locationText;
if (widget.district.isEmpty) {
locationText = widget.city;
} else if (widget.city.isEmpty) {
locationText = widget.district;
} else if (widget.city == widget.district) {
locationText = widget.city;
} else {
locationText = '${widget.city}, ${widget.district}';
}
return Text(
locationText,
style: context.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w500,
),
);
}
Widget _buildCurrentTimeText(BuildContext context) {
return StreamBuilder(
stream: Stream.periodic(const Duration(seconds: 1)),
builder: (context, snapshot) {
return Text(
'${'time'.tr}: ${statusData.getTimeFormatTz(tz.TZDateTime.now(tz.getLocation(widget.timezone)))}',
style: context.textTheme.titleMedium?.copyWith(
color: Colors.grey,
fontWeight: FontWeight.w400,
),
);
},
);
}
Widget _buildWeatherImage(int currentTimeIndex, int currentDayIndex) {
return Image.asset(
statusWeather.getImageNow(
widget.weather[currentTimeIndex],
widget.time[currentTimeIndex],
widget.timeDay[currentDayIndex],
widget.timeNight[currentDayIndex],
),
scale: 6.5,
);
}
} }

205
lib/app/ui/places/widgets/place_card_list.dart Normal file → Executable file
View file

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:iconsax_plus/iconsax_plus.dart'; import 'package:iconsax_plus/iconsax_plus.dart';
import 'package:rain/app/controller/controller.dart'; import 'package:rain/app/controller/controller.dart';
import 'package:rain/app/data/db.dart';
import 'package:rain/app/ui/places/view/place_info.dart'; import 'package:rain/app/ui/places/view/place_info.dart';
import 'package:rain/app/ui/places/widgets/place_card.dart'; import 'package:rain/app/ui/places/widgets/place_card.dart';
@ -21,92 +22,134 @@ class _PlaceCardListState extends State<PlaceCardList> {
final textTheme = context.textTheme; final textTheme = context.textTheme;
final titleMedium = textTheme.titleMedium; final titleMedium = textTheme.titleMedium;
var weatherCards = final weatherCards = _filterWeatherCards(
weatherController.weatherCards weatherController.weatherCards,
.where( widget.searchCity,
(weatherCard) => );
(widget.searchCity.isEmpty ||
weatherCard.city!.toLowerCase().contains(
widget.searchCity,
)),
)
.toList()
.obs;
return ReorderableListView( return ReorderableListView(
onReorder: onReorder:
(oldIndex, newIndex) => weatherController.reorder(oldIndex, newIndex), (oldIndex, newIndex) => weatherController.reorder(oldIndex, newIndex),
children: [ children: _buildWeatherCardList(
...weatherCards.map( weatherCards,
(weatherCardList) => Dismissible( context,
key: ValueKey(weatherCardList), textTheme,
direction: DismissDirection.endToStart, titleMedium,
background: Container( ),
alignment: Alignment.centerRight, );
child: const Padding( }
padding: EdgeInsets.only(right: 15),
child: Icon(IconsaxPlusLinear.trash_square, color: Colors.red), List<WeatherCard> _filterWeatherCards(
), List<WeatherCard> weatherCards,
), String searchCity,
confirmDismiss: (DismissDirection direction) async { ) {
return await showAdaptiveDialog( return weatherCards
context: context, .where(
builder: (BuildContext context) { (weatherCard) =>
return AlertDialog.adaptive( (searchCity.isEmpty ||
title: Text( weatherCard.city!.toLowerCase().contains(searchCity)),
'deletedCardWeather'.tr, )
style: textTheme.titleLarge, .toList();
), }
content: Text(
'deletedCardWeatherQuery'.tr, List<Widget> _buildWeatherCardList(
style: titleMedium, List<WeatherCard> weatherCards,
), BuildContext context,
actions: [ TextTheme textTheme,
TextButton( TextStyle? titleMedium,
onPressed: () => Get.back(result: false), ) {
child: Text( return weatherCards
'cancel'.tr, .map(
style: titleMedium?.copyWith( (weatherCardList) => _buildDismissibleCard(
color: Colors.blueAccent, context,
), weatherCardList,
), textTheme,
), titleMedium,
TextButton(
onPressed: () => Get.back(result: true),
child: Text(
'delete'.tr,
style: titleMedium?.copyWith(color: Colors.red),
),
),
],
);
},
);
},
onDismissed: (DismissDirection direction) async {
await weatherController.deleteCardWeather(weatherCardList);
},
child: GestureDetector(
onTap:
() => Get.to(
() => PlaceInfo(weatherCard: weatherCardList),
transition: Transition.downToUp,
),
child: PlaceCard(
time: weatherCardList.time!,
timeDaily: weatherCardList.timeDaily!,
timeDay: weatherCardList.sunrise!,
timeNight: weatherCardList.sunset!,
weather: weatherCardList.weathercode!,
degree: weatherCardList.temperature2M!,
district: weatherCardList.district!,
city: weatherCardList.city!,
timezone: weatherCardList.timezone!,
),
),
), ),
), )
], .toList();
}
Widget _buildDismissibleCard(
BuildContext context,
WeatherCard weatherCardList,
TextTheme textTheme,
TextStyle? titleMedium,
) {
return Dismissible(
key: ValueKey(weatherCardList),
direction: DismissDirection.endToStart,
background: _buildDismissibleBackground(),
confirmDismiss:
(DismissDirection direction) =>
_showDeleteConfirmationDialog(context, textTheme, titleMedium),
onDismissed: (DismissDirection direction) async {
await weatherController.deleteCardWeather(weatherCardList);
},
child: _buildCardGestureDetector(weatherCardList),
);
}
Widget _buildDismissibleBackground() {
return Container(
alignment: Alignment.centerRight,
child: const Padding(
padding: EdgeInsets.only(right: 15),
child: Icon(IconsaxPlusLinear.trash_square, color: Colors.red),
),
);
}
Future<bool> _showDeleteConfirmationDialog(
BuildContext context,
TextTheme textTheme,
TextStyle? titleMedium,
) async {
return await showAdaptiveDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog.adaptive(
title: Text('deletedCardWeather'.tr, style: textTheme.titleLarge),
content: Text('deletedCardWeatherQuery'.tr, style: titleMedium),
actions: [
TextButton(
onPressed: () => Get.back(result: false),
child: Text(
'cancel'.tr,
style: titleMedium?.copyWith(color: Colors.blueAccent),
),
),
TextButton(
onPressed: () => Get.back(result: true),
child: Text(
'delete'.tr,
style: titleMedium?.copyWith(color: Colors.red),
),
),
],
);
},
);
}
Widget _buildCardGestureDetector(WeatherCard weatherCardList) {
return GestureDetector(
onTap:
() => Get.to(
() => PlaceInfo(weatherCard: weatherCardList),
transition: Transition.downToUp,
),
child: PlaceCard(
time: weatherCardList.time!,
timeDaily: weatherCardList.timeDaily!,
timeDay: weatherCardList.sunrise!,
timeNight: weatherCardList.sunset!,
weather: weatherCardList.weathercode!,
degree: weatherCardList.temperature2M!,
district: weatherCardList.district!,
city: weatherCardList.city!,
timezone: weatherCardList.timezone!,
),
); );
} }
} }

2396
lib/app/ui/settings/view/settings.dart Normal file → Executable file

File diff suppressed because it is too large Load diff

98
lib/app/ui/settings/widgets/setting_card.dart Normal file → Executable file
View file

@ -14,12 +14,13 @@ class SettingCard extends StatelessWidget {
this.elevation, this.elevation,
this.dropdownName, this.dropdownName,
this.dropdownList, this.dropdownList,
this.dropdownCange, this.dropdownChange,
this.value, this.value,
this.onPressed, this.onPressed,
this.onChange, this.onChange,
this.infoWidget, this.infoWidget,
}); });
final Widget icon; final Widget icon;
final String text; final String text;
final bool switcher; final bool switcher;
@ -29,10 +30,10 @@ class SettingCard extends StatelessWidget {
final Widget? infoWidget; final Widget? infoWidget;
final String? dropdownName; final String? dropdownName;
final List<String>? dropdownList; final List<String>? dropdownList;
final Function(String?)? dropdownCange; final ValueChanged<String?>? dropdownChange;
final bool? value; final bool? value;
final Function()? onPressed; final VoidCallback? onPressed;
final Function(bool)? onChange; final ValueChanged<bool>? onChange;
final double? elevation; final double? elevation;
@override @override
@ -49,45 +50,58 @@ class SettingCard extends StatelessWidget {
style: context.textTheme.titleMedium, style: context.textTheme.titleMedium,
overflow: TextOverflow.visible, overflow: TextOverflow.visible,
), ),
trailing: trailing: _buildTrailingWidget(context),
switcher
? Transform.scale(
scale: 0.8,
child: Switch(value: value!, onChanged: onChange),
)
: dropdown
? DropdownButton<String>(
icon: const Padding(
padding: EdgeInsets.only(left: 7),
child: Icon(IconsaxPlusLinear.arrow_down),
),
iconSize: 15,
alignment: AlignmentDirectional.centerEnd,
borderRadius: const BorderRadius.all(Radius.circular(15)),
underline: Container(),
value: dropdownName,
items:
dropdownList!.map<DropdownMenuItem<String>>((
String value,
) {
return DropdownMenuItem<String>(
value: value,
child: Text(value),
);
}).toList(),
onChanged: dropdownCange,
)
: info
? infoSettings
? Wrap(
children: [
infoWidget!,
const Icon(IconsaxPlusLinear.arrow_right_3, size: 18),
],
)
: infoWidget!
: const Icon(IconsaxPlusLinear.arrow_right_3, size: 18),
), ),
); );
} }
Widget _buildTrailingWidget(BuildContext context) {
if (switcher) {
return _buildSwitchWidget();
} else if (dropdown) {
return _buildDropdownWidget();
} else if (info) {
return _buildInfoWidget();
} else {
return const Icon(IconsaxPlusLinear.arrow_right_3, size: 18);
}
}
Widget _buildSwitchWidget() {
return Transform.scale(
scale: 0.8,
child: Switch(value: value!, onChanged: onChange),
);
}
Widget _buildDropdownWidget() {
return DropdownButton<String>(
icon: const Padding(
padding: EdgeInsets.only(left: 7),
child: Icon(IconsaxPlusLinear.arrow_down),
),
iconSize: 15,
alignment: AlignmentDirectional.centerEnd,
borderRadius: const BorderRadius.all(Radius.circular(15)),
underline: Container(),
value: dropdownName,
items: dropdownList!.map<DropdownMenuItem<String>>((String value) {
return DropdownMenuItem<String>(value: value, child: Text(value));
}).toList(),
onChanged: dropdownChange,
);
}
Widget _buildInfoWidget() {
if (infoSettings) {
return Wrap(
children: [
infoWidget!,
const Icon(IconsaxPlusLinear.arrow_right_3, size: 18),
],
);
} else {
return infoWidget!;
}
}
} }

21
lib/app/ui/widgets/button.dart Normal file → Executable file
View file

@ -6,25 +6,32 @@ class MyTextButton extends StatelessWidget {
super.key, super.key,
required this.buttonName, required this.buttonName,
required this.onPressed, required this.onPressed,
this.height = 50.0,
}); });
final String buttonName; final String buttonName;
final VoidCallback? onPressed; final VoidCallback? onPressed;
final double height;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SizedBox( return SizedBox(
height: 50, height: height,
width: double.infinity, width: double.infinity,
child: ElevatedButton( child: ElevatedButton(
style: ButtonStyle( style: _buildButtonStyle(context),
shadowColor: const WidgetStatePropertyAll(Colors.transparent),
backgroundColor: WidgetStatePropertyAll(
context.theme.colorScheme.secondaryContainer.withAlpha(80),
),
),
onPressed: onPressed, onPressed: onPressed,
child: Text(buttonName, style: context.textTheme.titleMedium), child: Text(buttonName, style: context.textTheme.titleMedium),
), ),
); );
} }
ButtonStyle _buildButtonStyle(BuildContext context) {
return ButtonStyle(
shadowColor: const WidgetStatePropertyAll(Colors.transparent),
backgroundColor: WidgetStatePropertyAll(
context.theme.colorScheme.secondaryContainer.withAlpha(80),
),
);
}
} }

13
lib/app/ui/widgets/shimmer.dart Normal file → Executable file
View file

@ -3,16 +3,21 @@ import 'package:get/get.dart';
import 'package:shimmer/shimmer.dart'; import 'package:shimmer/shimmer.dart';
class MyShimmer extends StatelessWidget { class MyShimmer extends StatelessWidget {
const MyShimmer({super.key, required this.hight, this.edgeInsetsMargin}); const MyShimmer({super.key, required this.height, this.margin});
final double hight;
final EdgeInsets? edgeInsetsMargin; final double height;
final EdgeInsets? margin;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Shimmer.fromColors( return Shimmer.fromColors(
baseColor: context.theme.cardColor, baseColor: context.theme.cardColor,
highlightColor: context.theme.primaryColor, highlightColor: context.theme.primaryColor,
child: Card(margin: edgeInsetsMargin, child: SizedBox(height: hight)), child: _buildShimmerCard(),
); );
} }
Widget _buildShimmerCard() {
return Card(margin: margin, child: SizedBox(height: height));
}
} }

40
lib/app/ui/widgets/text_form.dart Normal file → Executable file
View file

@ -15,6 +15,7 @@ class MyTextForm extends StatelessWidget {
this.focusNode, this.focusNode,
this.onChanged, this.onChanged,
}); });
final String labelText; final String labelText;
final TextInputType type; final TextInputType type;
final Icon icon; final Icon icon;
@ -31,23 +32,28 @@ class MyTextForm extends StatelessWidget {
return Card( return Card(
elevation: elevation, elevation: elevation,
margin: margin, margin: margin,
child: TextFormField( child: _buildTextFormField(context),
focusNode: focusNode, );
controller: controller, }
keyboardType: type,
style: context.textTheme.labelLarge, Widget _buildTextFormField(BuildContext context) {
decoration: InputDecoration( return TextFormField(
contentPadding: const EdgeInsets.symmetric( focusNode: focusNode,
horizontal: 12.5, controller: controller,
vertical: 0, keyboardType: type,
), style: context.textTheme.labelLarge,
prefixIcon: icon, decoration: _buildInputDecoration(),
suffixIcon: iconButton, validator: validator,
labelText: labelText, onChanged: onChanged,
), );
validator: validator, }
onChanged: onChanged,
), InputDecoration _buildInputDecoration() {
return InputDecoration(
contentPadding: const EdgeInsets.symmetric(horizontal: 12.5, vertical: 0),
prefixIcon: icon,
suffixIcon: iconButton,
labelText: labelText,
); );
} }
} }

111
lib/app/ui/widgets/weather/daily/daily_card.dart Normal file → Executable file
View file

@ -14,6 +14,7 @@ class DailyCard extends StatefulWidget {
required this.temperature2MMax, required this.temperature2MMax,
required this.temperature2MMin, required this.temperature2MMin,
}); });
final DateTime timeDaily; final DateTime timeDaily;
final int? weathercodeDaily; final int? weathercodeDaily;
final double? temperature2MMax; final double? temperature2MMax;
@ -29,54 +30,70 @@ class _DailyCardState extends State<DailyCard> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return widget.weathercodeDaily == null if (widget.weathercodeDaily == null) {
? Container() return Container();
: Card( }
margin: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
child: Padding( return Card(
padding: const EdgeInsets.symmetric(vertical: 15, horizontal: 20), margin: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
child: Row( child: Padding(
children: [ padding: const EdgeInsets.symmetric(vertical: 15, horizontal: 20),
Expanded( child: Row(
child: Column( children: [
crossAxisAlignment: CrossAxisAlignment.start, _buildTemperatureInfo(context),
children: [ const Gap(5),
Text( _buildWeatherImage(),
'${statusData.getDegree(widget.temperature2MMin?.round())} / ${statusData.getDegree(widget.temperature2MMax?.round())}', ],
style: context.textTheme.titleLarge?.copyWith( ),
fontSize: 22, ),
fontWeight: FontWeight.w600, );
), }
),
const Gap(5), Widget _buildTemperatureInfo(BuildContext context) {
Text( return Expanded(
DateFormat.MMMMEEEEd( child: Column(
locale.languageCode, crossAxisAlignment: CrossAxisAlignment.start,
).format(widget.timeDaily), children: [
style: context.textTheme.titleMedium?.copyWith( Text(
color: Colors.grey, '${statusData.getDegree(widget.temperature2MMin?.round())} / ${statusData.getDegree(widget.temperature2MMax?.round())}',
fontWeight: FontWeight.w400, style: context.textTheme.titleLarge?.copyWith(
), fontSize: 22,
), fontWeight: FontWeight.w600,
const Gap(5),
Text(
statusWeather.getText(widget.weathercodeDaily),
style: context.textTheme.titleMedium?.copyWith(
color: Colors.grey,
fontWeight: FontWeight.w400,
),
),
],
),
),
const Gap(5),
Image.asset(
statusWeather.getImageNowDaily(widget.weathercodeDaily),
scale: 6.5,
),
],
), ),
), ),
); const Gap(5),
_buildDateText(context),
const Gap(5),
_buildWeatherDescription(context),
],
),
);
}
Widget _buildDateText(BuildContext context) {
return Text(
DateFormat.MMMMEEEEd(locale.languageCode).format(widget.timeDaily),
style: context.textTheme.titleMedium?.copyWith(
color: Colors.grey,
fontWeight: FontWeight.w400,
),
);
}
Widget _buildWeatherDescription(BuildContext context) {
return Text(
statusWeather.getText(widget.weathercodeDaily),
style: context.textTheme.titleMedium?.copyWith(
color: Colors.grey,
fontWeight: FontWeight.w400,
),
);
}
Widget _buildWeatherImage() {
return Image.asset(
statusWeather.getImageNowDaily(widget.weathercodeDaily),
scale: 6.5,
);
} }
} }

458
lib/app/ui/widgets/weather/daily/daily_card_info.dart Normal file → Executable file
View file

@ -52,32 +52,11 @@ class _DailyCardInfoState extends State<DailyCardInfo> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final weatherData = widget.weatherData; final weatherData = widget.weatherData;
final timeDaily = weatherData.timeDaily ?? []; final timeDaily = weatherData.timeDaily ?? [];
final weatherCodeDaily = weatherData.weathercodeDaily ?? [];
final textTheme = context.textTheme; final textTheme = context.textTheme;
return Scaffold( return Scaffold(
appBar: AppBar( appBar: _buildAppBar(context, textTheme, timeDaily),
automaticallyImplyLeading: false,
centerTitle: true,
leading: IconButton(
onPressed: () {
Get.back();
},
icon: const Icon(IconsaxPlusLinear.arrow_left_3, size: 20),
splashColor: Colors.transparent,
highlightColor: Colors.transparent,
),
title: Text(
DateFormat.MMMMEEEEd(
locale.languageCode,
).format(timeDaily[pageIndex]),
style: textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
fontSize: 18,
),
),
),
body: SafeArea( body: SafeArea(
child: PageView.builder( child: PageView.builder(
controller: pageController, controller: pageController,
@ -89,172 +68,281 @@ class _DailyCardInfoState extends State<DailyCardInfo> {
}, },
itemCount: timeDaily.length, itemCount: timeDaily.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final indexedWeatherCodeDaily = weatherCodeDaily[index]; return _buildPageContent(context, weatherData, index);
final temperature2MMin = weatherData.temperature2MMin?[index];
final temperature2MMax = weatherData.temperature2MMax?[index];
final apparentTemperatureMin =
weatherData.apparentTemperatureMin?[index];
final apparentTemperatureMax =
weatherData.apparentTemperatureMax?[index];
final uvIndexMax = weatherData.uvIndexMax?[index];
final windDirection10MDominant =
weatherData.winddirection10MDominant?[index];
final windSpeed10MMax = weatherData.windspeed10MMax?[index];
final windGusts10MMax = weatherData.windgusts10MMax?[index];
final precipitationProbabilityMax =
weatherData.precipitationProbabilityMax?[index];
final rainSum = weatherData.rainSum?[index];
final precipitationSum = weatherData.precipitationSum?[index];
final sunrise = weatherData.sunrise![index];
final sunset = weatherData.sunset![index];
final startIndex = index * 24;
return indexedWeatherCodeDaily == null
? null
: Container(
margin: const EdgeInsets.symmetric(horizontal: 10),
child: ListView(
children: [
Now(
weather:
weatherData.weathercode![startIndex + hourOfDay],
degree:
weatherData.temperature2M![startIndex + hourOfDay],
feels:
weatherData.apparentTemperature![startIndex +
hourOfDay]!,
time: weatherData.time![startIndex + hourOfDay],
timeDay: sunrise,
timeNight: sunset,
tempMax: temperature2MMax!,
tempMin: temperature2MMin!,
),
Card(
margin: const EdgeInsets.only(bottom: 15),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 5,
),
child: SizedBox(
height: 135,
child: ScrollablePositionedList.separated(
separatorBuilder: (
BuildContext context,
int index,
) {
return const VerticalDivider(
width: 10,
indent: 40,
endIndent: 40,
);
},
scrollDirection: Axis.horizontal,
itemCount: 24,
itemBuilder: (ctx, i) {
int hourlyIndex = startIndex + i;
return GestureDetector(
onTap: () {
hourOfDay = i;
setState(() {});
},
child: Container(
margin: const EdgeInsets.symmetric(
vertical: 5,
),
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 5,
),
decoration: BoxDecoration(
color:
i == hourOfDay
? context
.theme
.colorScheme
.secondaryContainer
: Colors.transparent,
borderRadius: const BorderRadius.all(
Radius.circular(20),
),
),
child: Hourly(
time: weatherData.time![hourlyIndex],
weather:
weatherData.weathercode![hourlyIndex],
degree:
weatherData
.temperature2M![hourlyIndex],
timeDay: sunrise,
timeNight: sunset,
),
),
);
},
),
),
),
),
SunsetSunrise(timeSunrise: sunrise, timeSunset: sunset),
DescContainer(
humidity:
weatherData.relativehumidity2M?[startIndex +
hourOfDay],
wind: weatherData.windspeed10M?[startIndex + hourOfDay],
visibility:
weatherData.visibility?[startIndex + hourOfDay],
feels:
weatherData.apparentTemperature?[startIndex +
hourOfDay],
evaporation:
weatherData.evapotranspiration?[startIndex +
hourOfDay],
precipitation:
weatherData.precipitation?[startIndex + hourOfDay],
direction:
weatherData.winddirection10M?[startIndex +
hourOfDay],
pressure:
weatherData.surfacePressure?[startIndex +
hourOfDay],
rain: weatherData.rain?[startIndex + hourOfDay],
cloudcover:
weatherData.cloudcover?[startIndex + hourOfDay],
windgusts:
weatherData.windgusts10M?[startIndex + hourOfDay],
uvIndex: weatherData.uvIndex?[startIndex + hourOfDay],
dewpoint2M:
weatherData.dewpoint2M?[startIndex + hourOfDay],
precipitationProbability:
weatherData.precipitationProbability?[startIndex +
hourOfDay],
shortwaveRadiation:
weatherData.shortwaveRadiation?[startIndex +
hourOfDay],
initiallyExpanded: true,
title: 'hourlyVariables'.tr,
),
DescContainer(
apparentTemperatureMin: apparentTemperatureMin,
apparentTemperatureMax: apparentTemperatureMax,
uvIndexMax: uvIndexMax,
windDirection10MDominant: windDirection10MDominant,
windSpeed10MMax: windSpeed10MMax,
windGusts10MMax: windGusts10MMax,
precipitationProbabilityMax:
precipitationProbabilityMax,
rainSum: rainSum,
precipitationSum: precipitationSum,
initiallyExpanded: true,
title: 'dailyVariables'.tr,
),
],
),
);
}, },
), ),
), ),
); );
} }
AppBar _buildAppBar(
BuildContext context,
TextTheme textTheme,
List<DateTime> timeDaily,
) {
return AppBar(
automaticallyImplyLeading: false,
centerTitle: true,
leading: IconButton(
onPressed: () => Get.back(),
icon: const Icon(IconsaxPlusLinear.arrow_left_3, size: 20),
splashColor: Colors.transparent,
highlightColor: Colors.transparent,
),
title: Text(
DateFormat.MMMMEEEEd(locale.languageCode).format(timeDaily[pageIndex]),
style: textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
fontSize: 18,
),
),
);
}
Widget _buildPageContent(
BuildContext context,
WeatherCard weatherData,
int index,
) {
final weatherCodeDaily = weatherData.weathercodeDaily?[index];
if (weatherCodeDaily == null) {
return Container();
}
final startIndex = index * 24;
final temperature2MMin = weatherData.temperature2MMin?[index];
final temperature2MMax = weatherData.temperature2MMax?[index];
final apparentTemperatureMin = weatherData.apparentTemperatureMin?[index];
final apparentTemperatureMax = weatherData.apparentTemperatureMax?[index];
final uvIndexMax = weatherData.uvIndexMax?[index];
final windDirection10MDominant =
weatherData.winddirection10MDominant?[index];
final windSpeed10MMax = weatherData.windspeed10MMax?[index];
final windGusts10MMax = weatherData.windgusts10MMax?[index];
final precipitationProbabilityMax =
weatherData.precipitationProbabilityMax?[index];
final rainSum = weatherData.rainSum?[index];
final precipitationSum = weatherData.precipitationSum?[index];
final sunrise = weatherData.sunrise?[index];
final sunset = weatherData.sunset?[index];
if (sunrise == null || sunset == null) {
return Container();
}
return Container(
margin: const EdgeInsets.symmetric(horizontal: 10),
child: ListView(
children: [
_buildNowWidget(
weatherData,
index,
startIndex,
hourOfDay,
sunrise,
sunset,
),
_buildHourlyList(context, weatherData, startIndex, sunrise, sunset),
_buildSunsetSunriseWidget(sunrise, sunset),
_buildHourlyDescContainer(weatherData, startIndex, hourOfDay),
_buildDailyDescContainer(
weatherData,
temperature2MMin,
temperature2MMax,
apparentTemperatureMin,
apparentTemperatureMax,
uvIndexMax,
windDirection10MDominant,
windSpeed10MMax,
windGusts10MMax,
precipitationProbabilityMax,
rainSum,
precipitationSum,
),
],
),
);
}
Widget _buildNowWidget(
WeatherCard weatherData,
int index,
int startIndex,
int hourOfDay,
String sunrise,
String sunset,
) {
final weatherCode = weatherData.weathercode?[startIndex + hourOfDay];
final temperature = weatherData.temperature2M?[startIndex + hourOfDay];
final feels = weatherData.apparentTemperature?[startIndex + hourOfDay];
final time = weatherData.time?[startIndex + hourOfDay];
final tempMax = weatherData.temperature2MMax?[index];
final tempMin = weatherData.temperature2MMin?[index];
if (weatherCode == null ||
temperature == null ||
feels == null ||
time == null ||
tempMax == null ||
tempMin == null) {
return Container();
}
return Now(
weather: weatherCode,
degree: temperature,
feels: feels,
time: time,
timeDay: sunrise,
timeNight: sunset,
tempMax: tempMax,
tempMin: tempMin,
);
}
Widget _buildHourlyList(
BuildContext context,
WeatherCard weatherData,
int startIndex,
String sunrise,
String sunset,
) {
return Card(
margin: const EdgeInsets.only(bottom: 15),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
child: SizedBox(
height: 135,
child: ScrollablePositionedList.separated(
separatorBuilder: (BuildContext context, int index) {
return const VerticalDivider(
width: 10,
indent: 40,
endIndent: 40,
);
},
scrollDirection: Axis.horizontal,
itemCount: 24,
itemBuilder: (ctx, i) {
return _buildHourlyItem(
context,
weatherData,
startIndex,
i,
sunrise,
sunset,
);
},
),
),
),
);
}
Widget _buildHourlyItem(
BuildContext context,
WeatherCard weatherData,
int startIndex,
int i,
String sunrise,
String sunset,
) {
int hourlyIndex = startIndex + i;
bool isSelected = i == hourOfDay;
final time = weatherData.time?[hourlyIndex];
final weatherCode = weatherData.weathercode?[hourlyIndex];
final temperature = weatherData.temperature2M?[hourlyIndex];
if (time == null || weatherCode == null || temperature == null) {
return Container();
}
return GestureDetector(
onTap: () {
setState(() {
hourOfDay = i;
});
},
child: Container(
margin: const EdgeInsets.symmetric(vertical: 5),
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 5),
decoration: BoxDecoration(
color: isSelected
? context.theme.colorScheme.secondaryContainer
: Colors.transparent,
borderRadius: const BorderRadius.all(Radius.circular(20)),
),
child: Hourly(
time: time,
weather: weatherCode,
degree: temperature,
timeDay: sunrise,
timeNight: sunset,
),
),
);
}
Widget _buildSunsetSunriseWidget(String sunrise, String sunset) {
return SunsetSunrise(timeSunrise: sunrise, timeSunset: sunset);
}
Widget _buildHourlyDescContainer(
WeatherCard weatherData,
int startIndex,
int hourOfDay,
) {
final hourlyIndex = startIndex + hourOfDay;
return DescContainer(
humidity: weatherData.relativehumidity2M?[hourlyIndex],
wind: weatherData.windspeed10M?[hourlyIndex],
visibility: weatherData.visibility?[hourlyIndex],
feels: weatherData.apparentTemperature?[hourlyIndex],
evaporation: weatherData.evapotranspiration?[hourlyIndex],
precipitation: weatherData.precipitation?[hourlyIndex],
direction: weatherData.winddirection10M?[hourlyIndex],
pressure: weatherData.surfacePressure?[hourlyIndex],
rain: weatherData.rain?[hourlyIndex],
cloudcover: weatherData.cloudcover?[hourlyIndex],
windgusts: weatherData.windgusts10M?[hourlyIndex],
uvIndex: weatherData.uvIndex?[hourlyIndex],
dewpoint2M: weatherData.dewpoint2M?[hourlyIndex],
precipitationProbability:
weatherData.precipitationProbability?[hourlyIndex],
shortwaveRadiation: weatherData.shortwaveRadiation?[hourlyIndex],
initiallyExpanded: true,
title: 'hourlyVariables'.tr,
);
}
Widget _buildDailyDescContainer(
WeatherCard weatherData,
double? temperature2MMin,
double? temperature2MMax,
double? apparentTemperatureMin,
double? apparentTemperatureMax,
double? uvIndexMax,
int? windDirection10MDominant,
double? windSpeed10MMax,
double? windGusts10MMax,
int? precipitationProbabilityMax,
double? rainSum,
double? precipitationSum,
) {
return DescContainer(
apparentTemperatureMin: apparentTemperatureMin,
apparentTemperatureMax: apparentTemperatureMax,
uvIndexMax: uvIndexMax,
windDirection10MDominant: windDirection10MDominant,
windSpeed10MMax: windSpeed10MMax,
windGusts10MMax: windGusts10MMax,
precipitationProbabilityMax: precipitationProbabilityMax,
rainSum: rainSum,
precipitationSum: precipitationSum,
initiallyExpanded: true,
title: 'dailyVariables'.tr,
);
}
} }

89
lib/app/ui/widgets/weather/daily/daily_card_list.dart Normal file → Executable file
View file

@ -16,50 +16,69 @@ class DailyCardList extends StatefulWidget {
class _DailyCardListState extends State<DailyCardList> { class _DailyCardListState extends State<DailyCardList> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
const transparent = Colors.transparent;
final weatherData = widget.weatherData; final weatherData = widget.weatherData;
final timeDaily = weatherData.timeDaily ?? []; final timeDaily = weatherData.timeDaily ?? [];
return Scaffold( return Scaffold(
appBar: AppBar( appBar: _buildAppBar(context),
automaticallyImplyLeading: false,
centerTitle: true,
leading: IconButton(
onPressed: () {
Get.back();
},
icon: const Icon(IconsaxPlusLinear.arrow_left_3, size: 20),
splashColor: transparent,
highlightColor: transparent,
),
title: Text(
'weatherMore'.tr,
style: context.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
fontSize: 18,
),
),
),
body: SafeArea( body: SafeArea(
child: ListView.builder( child: ListView.builder(
itemCount: timeDaily.length, itemCount: timeDaily.length,
itemBuilder: itemBuilder: (context, index) =>
(context, index) => GestureDetector( _buildDailyCardItem(context, weatherData, index),
onTap:
() => Get.to(
() =>
DailyCardInfo(weatherData: weatherData, index: index),
transition: Transition.downToUp,
),
child: DailyCard(
timeDaily: timeDaily[index],
weathercodeDaily: weatherData.weathercodeDaily![index],
temperature2MMax: weatherData.temperature2MMax![index],
temperature2MMin: weatherData.temperature2MMin![index],
),
),
), ),
), ),
); );
} }
AppBar _buildAppBar(BuildContext context) {
return AppBar(
automaticallyImplyLeading: false,
centerTitle: true,
leading: IconButton(
onPressed: () => Get.back(),
icon: const Icon(IconsaxPlusLinear.arrow_left_3, size: 20),
splashColor: Colors.transparent,
highlightColor: Colors.transparent,
),
title: Text(
'weatherMore'.tr,
style: context.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
fontSize: 18,
),
),
);
}
Widget _buildDailyCardItem(
BuildContext context,
WeatherCard weatherData,
int index,
) {
final timeDaily = weatherData.timeDaily?[index];
final weathercodeDaily = weatherData.weathercodeDaily?[index];
final temperature2MMax = weatherData.temperature2MMax?[index];
final temperature2MMin = weatherData.temperature2MMin?[index];
if (timeDaily == null ||
weathercodeDaily == null ||
temperature2MMax == null ||
temperature2MMin == null) {
return Container();
}
return GestureDetector(
onTap: () => Get.to(
() => DailyCardInfo(weatherData: weatherData, index: index),
transition: Transition.downToUp,
),
child: DailyCard(
timeDaily: timeDaily,
weathercodeDaily: weathercodeDaily,
temperature2MMax: temperature2MMax,
temperature2MMin: temperature2MMin,
),
);
}
} }

249
lib/app/ui/widgets/weather/daily/daily_container.dart Normal file → Executable file
View file

@ -28,7 +28,9 @@ class _DailyContainerState extends State<DailyContainer> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final splashColor = context.theme.colorScheme.primary.withOpacity(0.4); final splashColor = context.theme.colorScheme.primary.withValues(
alpha: 0.4,
);
const inkWellBorderRadius = BorderRadius.all(Radius.circular(16)); const inkWellBorderRadius = BorderRadius.all(Radius.circular(16));
final weatherData = widget.weatherData; final weatherData = widget.weatherData;
@ -42,104 +44,163 @@ class _DailyContainerState extends State<DailyContainer> {
padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 5), padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 5),
child: Column( child: Column(
children: [ children: [
ListView.builder( _buildDailyListView(
shrinkWrap: true, context,
physics: const NeverScrollableScrollPhysics(), weatherData,
itemCount: 7, weatherCodeDaily,
itemBuilder: (ctx, index) { labelLarge,
return InkWell(
splashColor: splashColor,
borderRadius: inkWellBorderRadius,
onTap:
() => Get.to(
() => DailyCardInfo(
weatherData: weatherData,
index: index,
),
transition: Transition.downToUp,
),
child: Container(
margin: const EdgeInsets.symmetric(vertical: 12),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
DateFormat.EEEE(
locale.languageCode,
).format((weatherData.timeDaily ?? [])[index]),
style: labelLarge,
overflow: TextOverflow.ellipsis,
),
),
Expanded(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset(
statusWeather.getImage7Day(
weatherCodeDaily[index],
),
scale: 3,
),
const Gap(5),
Expanded(
child: Text(
statusWeather.getText(
weatherCodeDaily[index],
),
style: labelLarge,
overflow: TextOverflow.ellipsis,
),
),
],
),
),
Expanded(
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Text(
statusData.getDegree(
(weatherData.temperature2MMin ?? [])[index]
?.round(),
),
style: labelLarge,
),
Text(' / ', style: labelLarge),
Text(
statusData.getDegree(
(weatherData.temperature2MMax ?? [])[index]
?.round(),
),
style: labelLarge,
),
],
),
),
],
),
),
);
},
), ),
const Divider(), const Divider(),
InkWell( _buildMoreInfoButton(context, splashColor, inkWellBorderRadius),
splashColor: splashColor,
borderRadius: inkWellBorderRadius,
onTap: widget.onTap,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 10),
child: Text(
'weatherMore'.tr,
style: textTheme.titleMedium,
overflow: TextOverflow.ellipsis,
),
),
),
], ],
), ),
), ),
); );
} }
Widget _buildDailyListView(
BuildContext context,
WeatherCard weatherData,
List<int?> weatherCodeDaily,
TextStyle? labelLarge,
) {
return ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: 7,
itemBuilder: (ctx, index) {
return _buildDailyItem(
context,
weatherData,
weatherCodeDaily,
index,
labelLarge,
);
},
);
}
Widget _buildDailyItem(
BuildContext context,
WeatherCard weatherData,
List<int?> weatherCodeDaily,
int index,
TextStyle? labelLarge,
) {
final splashColor = context.theme.colorScheme.primary.withValues(
alpha: 0.4,
);
const inkWellBorderRadius = BorderRadius.all(Radius.circular(16));
return InkWell(
splashColor: splashColor,
borderRadius: inkWellBorderRadius,
onTap: () => Get.to(
() => DailyCardInfo(weatherData: weatherData, index: index),
transition: Transition.downToUp,
),
child: Container(
margin: const EdgeInsets.symmetric(vertical: 12),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_buildDayText(weatherData, index, labelLarge),
_buildWeatherInfo(weatherCodeDaily, index, labelLarge),
_buildTemperatureRange(weatherData, index, labelLarge),
],
),
),
);
}
Widget _buildDayText(
WeatherCard weatherData,
int index,
TextStyle? labelLarge,
) {
return Expanded(
child: Text(
DateFormat.EEEE(
locale.languageCode,
).format((weatherData.timeDaily ?? [])[index]),
style: labelLarge,
overflow: TextOverflow.ellipsis,
),
);
}
Widget _buildWeatherInfo(
List<int?> weatherCodeDaily,
int index,
TextStyle? labelLarge,
) {
final weatherCode = weatherCodeDaily[index];
if (weatherCode == null) {
return const Expanded(child: SizedBox.shrink());
}
return Expanded(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset(statusWeather.getImage7Day(weatherCode), scale: 3),
const Gap(5),
Expanded(
child: Text(
statusWeather.getText(weatherCode),
style: labelLarge,
overflow: TextOverflow.ellipsis,
),
),
],
),
);
}
Widget _buildTemperatureRange(
WeatherCard weatherData,
int index,
TextStyle? labelLarge,
) {
return Expanded(
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Text(
statusData.getDegree(
(weatherData.temperature2MMax ?? [])[index]?.round(),
),
style: labelLarge,
),
Text(' / ', style: labelLarge),
Text(
statusData.getDegree(
(weatherData.temperature2MMin ?? [])[index]?.round(),
),
style: labelLarge,
),
],
),
);
}
Widget _buildMoreInfoButton(
BuildContext context,
Color splashColor,
BorderRadius inkWellBorderRadius,
) {
return InkWell(
splashColor: splashColor,
borderRadius: inkWellBorderRadius,
onTap: widget.onTap,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 10),
child: Text(
'weatherMore'.tr,
style: context.textTheme.titleMedium,
overflow: TextOverflow.ellipsis,
),
),
);
}
} }

51
lib/app/ui/widgets/weather/desc/desc.dart Normal file → Executable file
View file

@ -10,6 +10,7 @@ class DescWeather extends StatefulWidget {
required this.desc, required this.desc,
this.message = '', this.message = '',
}); });
final String imageName; final String imageName;
final String value; final String value;
final String desc; final String desc;
@ -26,34 +27,42 @@ class _DescWeatherState extends State<DescWeather> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final textTheme = context.textTheme; final textTheme = context.textTheme;
return GestureDetector( return GestureDetector(
onTap: () => setState(() => hide = !hide), onTap: _toggleDescriptionVisibility,
child: Tooltip( child: Tooltip(
message: widget.message, message: widget.message,
child: SizedBox( child: SizedBox(
height: 90, height: 90,
width: 100, width: 100,
child: Column( child: _buildContent(textTheme),
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset(widget.imageName, scale: 20),
const Gap(5),
Text(
widget.value,
style: textTheme.labelLarge,
overflow: TextOverflow.ellipsis,
),
Expanded(
child: Text(
widget.desc,
style: textTheme.bodySmall,
overflow: hide ? TextOverflow.ellipsis : TextOverflow.visible,
textAlign: TextAlign.center,
),
),
],
),
), ),
), ),
); );
} }
void _toggleDescriptionVisibility() {
setState(() => hide = !hide);
}
Widget _buildContent(TextTheme textTheme) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset(widget.imageName, scale: 20),
const Gap(5),
Text(
widget.value,
style: textTheme.labelLarge,
overflow: TextOverflow.ellipsis,
),
Expanded(
child: Text(
widget.desc,
style: textTheme.bodySmall,
overflow: hide ? TextOverflow.ellipsis : TextOverflow.visible,
textAlign: TextAlign.center,
),
),
],
);
}
} }

384
lib/app/ui/widgets/weather/desc/desc_container.dart Normal file → Executable file
View file

@ -50,7 +50,6 @@ class DescContainer extends StatefulWidget {
final double? dewpoint2M; final double? dewpoint2M;
final int? precipitationProbability; final int? precipitationProbability;
final double? shortwaveRadiation; final double? shortwaveRadiation;
final double? apparentTemperatureMin; final double? apparentTemperatureMin;
final double? apparentTemperatureMax; final double? apparentTemperatureMax;
final double? uvIndexMax; final double? uvIndexMax;
@ -60,7 +59,6 @@ class DescContainer extends StatefulWidget {
final int? precipitationProbabilityMax; final int? precipitationProbabilityMax;
final double? rainSum; final double? rainSum;
final double? precipitationSum; final double? precipitationSum;
final bool initiallyExpanded; final bool initiallyExpanded;
final String title; final String title;
@ -74,230 +72,192 @@ class _DescContainerState extends State<DescContainer> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final dewpoint2M = widget.dewpoint2M?.round();
final feels = widget.feels;
final visibility = widget.visibility;
final direction = widget.direction;
final wind = widget.wind;
final windgusts = widget.windgusts;
final evaporation = widget.evaporation;
final precipitation = widget.precipitation;
final rain = widget.rain;
final precipitationProbability = widget.precipitationProbability;
final humidity = widget.humidity;
final cloudcover = widget.cloudcover;
final pressure = widget.pressure;
final uvIndex = widget.uvIndex;
final shortwaveRadiation = widget.shortwaveRadiation;
final apparentTemperatureMin = widget.apparentTemperatureMin;
final apparentTemperatureMax = widget.apparentTemperatureMax;
final uvIndexMax = widget.uvIndexMax;
final windDirection10MDominant = widget.windDirection10MDominant;
final windSpeed10MMax = widget.windSpeed10MMax;
final windGusts10MMax = widget.windGusts10MMax;
final precipitationProbabilityMax = widget.precipitationProbabilityMax;
final rainSum = widget.rainSum;
final precipitationSum = widget.precipitationSum;
final initiallyExpanded = widget.initiallyExpanded;
final title = widget.title;
return Card( return Card(
margin: const EdgeInsets.only(bottom: 15), margin: const EdgeInsets.only(bottom: 15),
child: ExpansionTile( child: ExpansionTile(
shape: const Border(), shape: const Border(),
title: Text(title, style: context.textTheme.labelLarge), title: Text(widget.title, style: context.textTheme.labelLarge),
initiallyExpanded: initiallyExpanded, initiallyExpanded: widget.initiallyExpanded,
children: [ children: [
Padding( Padding(
padding: const EdgeInsets.only(top: 20, bottom: 5), padding: const EdgeInsets.only(top: 20, bottom: 5),
child: Wrap( child: Wrap(
alignment: WrapAlignment.spaceEvenly, alignment: WrapAlignment.spaceEvenly,
spacing: 5, spacing: 5,
children: [ children: _buildWeatherDescriptions(context),
apparentTemperatureMin == null
? Container()
: DescWeather(
imageName: 'assets/images/cold.png',
value: statusData.getDegree(
apparentTemperatureMin.round(),
),
desc: 'apparentTemperatureMin'.tr,
),
apparentTemperatureMax == null
? Container()
: DescWeather(
imageName: 'assets/images/hot.png',
value: statusData.getDegree(
apparentTemperatureMax.round(),
),
desc: 'apparentTemperatureMax'.tr,
),
uvIndexMax == null
? Container()
: DescWeather(
imageName: 'assets/images/uv.png',
value: '${uvIndexMax.round()}',
desc: 'uvIndex'.tr,
message: message.getUvIndex(uvIndexMax.round()),
),
windDirection10MDominant == null
? Container()
: DescWeather(
imageName: 'assets/images/windsock.png',
value: '$windDirection10MDominant°',
desc: 'direction'.tr,
message: message.getDirection(windDirection10MDominant),
),
windSpeed10MMax == null
? Container()
: DescWeather(
imageName: 'assets/images/wind.png',
value: statusData.getSpeed(windSpeed10MMax.round()),
desc: 'wind'.tr,
),
windGusts10MMax == null
? Container()
: DescWeather(
imageName: 'assets/images/windgusts.png',
value: statusData.getSpeed(windGusts10MMax.round()),
desc: 'windgusts'.tr,
),
precipitationProbabilityMax == null
? Container()
: DescWeather(
imageName: 'assets/images/precipitation_probability.png',
value: '$precipitationProbabilityMax%',
desc: 'precipitationProbability'.tr,
),
rainSum == null
? Container()
: DescWeather(
imageName: 'assets/images/water.png',
value: statusData.getPrecipitation(rainSum),
desc: 'rain'.tr,
),
precipitationSum == null
? Container()
: DescWeather(
imageName: 'assets/images/rainfall.png',
value: statusData.getPrecipitation(precipitationSum),
desc: 'precipitation'.tr,
),
dewpoint2M == null
? Container()
: DescWeather(
imageName: 'assets/images/dew.png',
value: statusData.getDegree(dewpoint2M.round()),
desc: 'dewpoint'.tr,
),
feels == null
? Container()
: DescWeather(
imageName: 'assets/images/temperature.png',
value: statusData.getDegree(feels.round()),
desc: 'feels'.tr,
),
visibility == null
? Container()
: DescWeather(
imageName: 'assets/images/fog.png',
value: statusData.getVisibility(visibility),
desc: 'visibility'.tr,
),
direction == null
? Container()
: DescWeather(
imageName: 'assets/images/windsock.png',
value: '$direction°',
desc: 'direction'.tr,
message: message.getDirection(direction),
),
wind == null
? Container()
: DescWeather(
imageName: 'assets/images/wind.png',
value: statusData.getSpeed(wind.round()),
desc: 'wind'.tr,
),
windgusts == null
? Container()
: DescWeather(
imageName: 'assets/images/windgusts.png',
value: statusData.getSpeed(windgusts.round()),
desc: 'windgusts'.tr,
),
evaporation == null
? Container()
: DescWeather(
imageName: 'assets/images/evaporation.png',
value: statusData.getPrecipitation(evaporation.abs()),
desc: 'evaporation'.tr,
),
precipitation == null
? Container()
: DescWeather(
imageName: 'assets/images/rainfall.png',
value: statusData.getPrecipitation(precipitation),
desc: 'precipitation'.tr,
),
rain == null
? Container()
: DescWeather(
imageName: 'assets/images/water.png',
value: statusData.getPrecipitation(rain),
desc: 'rain'.tr,
),
precipitationProbability == null
? Container()
: DescWeather(
imageName: 'assets/images/precipitation_probability.png',
value: '$precipitationProbability%',
desc: 'precipitationProbability'.tr,
),
humidity == null
? Container()
: DescWeather(
imageName: 'assets/images/humidity.png',
value: '$humidity%',
desc: 'humidity'.tr,
),
cloudcover == null
? Container()
: DescWeather(
imageName: 'assets/images/cloudy.png',
value: '$cloudcover%',
desc: 'cloudcover'.tr,
),
pressure == null
? Container()
: DescWeather(
imageName: 'assets/images/atmospheric.png',
value: statusData.getPressure(pressure.round()),
desc: 'pressure'.tr,
message: message.getPressure(pressure.round()),
),
uvIndex == null
? Container()
: DescWeather(
imageName: 'assets/images/uv.png',
value: '${uvIndex.round()}',
desc: 'uvIndex'.tr,
message: message.getUvIndex(uvIndex.round()),
),
shortwaveRadiation == null
? Container()
: DescWeather(
imageName: 'assets/images/shortwave_radiation.png',
value: '${shortwaveRadiation.round()} ${'W/m2'.tr}',
desc: 'shortwaveRadiation'.tr,
),
],
), ),
), ),
], ],
), ),
); );
} }
List<Widget> _buildWeatherDescriptions(BuildContext context) {
final List<Widget> descriptions = [];
void addDescriptionIfNotNull({
required dynamic value,
required String imageName,
required String desc,
String? message,
}) {
if (value != null &&
value != '' &&
value != 'null°C' &&
value != 'null°F' &&
value != 'null°' &&
value != 'null%' &&
value != 'null ${'W/m2'.tr}') {
descriptions.add(
DescWeather(
imageName: imageName,
value: value.toString(),
desc: desc,
message: message ?? '',
),
);
} else {
descriptions.add(Container());
}
}
final weatherData = [
{
'value': statusData.getDegree(widget.apparentTemperatureMin?.round()),
'imageName': 'assets/images/cold.png',
'desc': 'apparentTemperatureMin'.tr,
},
{
'value': statusData.getDegree(widget.apparentTemperatureMax?.round()),
'imageName': 'assets/images/hot.png',
'desc': 'apparentTemperatureMax'.tr,
},
{
'value': widget.uvIndexMax?.round(),
'imageName': 'assets/images/uv.png',
'desc': 'uvIndex'.tr,
'message': message.getUvIndex(widget.uvIndexMax?.round()),
},
{
'value': '${widget.windDirection10MDominant}°',
'imageName': 'assets/images/windsock.png',
'desc': 'direction'.tr,
'message': message.getDirection(widget.windDirection10MDominant),
},
{
'value': statusData.getSpeed(widget.windSpeed10MMax?.round()),
'imageName': 'assets/images/wind.png',
'desc': 'wind'.tr,
},
{
'value': statusData.getSpeed(widget.windGusts10MMax?.round()),
'imageName': 'assets/images/windgusts.png',
'desc': 'windgusts'.tr,
},
{
'value': '${widget.precipitationProbabilityMax}%',
'imageName': 'assets/images/precipitation_probability.png',
'desc': 'precipitationProbability'.tr,
},
{
'value': statusData.getPrecipitation(widget.rainSum),
'imageName': 'assets/images/water.png',
'desc': 'rain'.tr,
},
{
'value': statusData.getPrecipitation(widget.precipitationSum),
'imageName': 'assets/images/rainfall.png',
'desc': 'precipitation'.tr,
},
{
'value': statusData.getDegree(widget.dewpoint2M?.round()),
'imageName': 'assets/images/dew.png',
'desc': 'dewpoint'.tr,
},
{
'value': statusData.getDegree(widget.feels?.round()),
'imageName': 'assets/images/temperature.png',
'desc': 'feels'.tr,
},
{
'value': statusData.getVisibility(widget.visibility),
'imageName': 'assets/images/fog.png',
'desc': 'visibility'.tr,
},
{
'value': '${widget.direction}°',
'imageName': 'assets/images/windsock.png',
'desc': 'direction'.tr,
'message': message.getDirection(widget.direction),
},
{
'value': statusData.getSpeed(widget.wind?.round()),
'imageName': 'assets/images/wind.png',
'desc': 'wind'.tr,
},
{
'value': statusData.getSpeed(widget.windgusts?.round()),
'imageName': 'assets/images/windgusts.png',
'desc': 'windgusts'.tr,
},
{
'value': statusData.getPrecipitation(widget.evaporation?.abs()),
'imageName': 'assets/images/evaporation.png',
'desc': 'evaporation'.tr,
},
{
'value': statusData.getPrecipitation(widget.precipitation),
'imageName': 'assets/images/rainfall.png',
'desc': 'precipitation'.tr,
},
{
'value': statusData.getPrecipitation(widget.rain),
'imageName': 'assets/images/water.png',
'desc': 'rain'.tr,
},
{
'value': '${widget.precipitationProbability}%',
'imageName': 'assets/images/precipitation_probability.png',
'desc': 'precipitationProbability'.tr,
},
{
'value': '${widget.humidity}%',
'imageName': 'assets/images/humidity.png',
'desc': 'humidity'.tr,
},
{
'value': '${widget.cloudcover}%',
'imageName': 'assets/images/cloudy.png',
'desc': 'cloudcover'.tr,
},
{
'value': statusData.getPressure(widget.pressure?.round()),
'imageName': 'assets/images/atmospheric.png',
'desc': 'pressure'.tr,
'message': message.getPressure(widget.pressure?.round()),
},
{
'value': widget.uvIndex?.round(),
'imageName': 'assets/images/uv.png',
'desc': 'uvIndex'.tr,
'message': message.getUvIndex(widget.uvIndex?.round()),
},
{
'value': '${widget.shortwaveRadiation?.round()} ${'W/m2'.tr}',
'imageName': 'assets/images/shortwave_radiation.png',
'desc': 'shortwaveRadiation'.tr,
},
];
for (var data in weatherData) {
addDescriptionIfNotNull(
value: data['value'],
imageName: '${data['imageName']}',
desc: '${data['desc']}',
message: '${data['message']}',
);
}
return descriptions;
}
} }

96
lib/app/ui/widgets/weather/desc/message.dart Normal file → Executable file
View file

@ -2,58 +2,64 @@ import 'package:get/get.dart';
class Message { class Message {
String getPressure(int? pressure) { String getPressure(int? pressure) {
if (pressure != null) { return _getPressureDescription(pressure);
if (pressure < 1000) {
return 'low'.tr;
} else if (pressure > 1020) {
return 'high'.tr;
} else {
return 'normal'.tr;
}
} else {
return '';
}
} }
String getUvIndex(int? uvIndex) { String getUvIndex(int? uvIndex) {
if (uvIndex != null) { return _getUvIndexDescription(uvIndex);
if (uvIndex < 3) {
return 'uvLow'.tr;
} else if (uvIndex < 6) {
return 'uvAverage'.tr;
} else if (uvIndex < 8) {
return 'uvHigh'.tr;
} else if (uvIndex < 11) {
return 'uvVeryHigh'.tr;
} else {
return 'uvExtreme'.tr;
}
} else {
return '';
}
} }
String getDirection(int? direction) { String getDirection(int? direction) {
if (direction != null) { return _getDirectionDescription(direction);
if (direction >= 337.5 || direction < 22.5) { }
return 'north'.tr;
} else if (direction >= 22.5 && direction < 67.5) { String _getPressureDescription(int? pressure) {
return 'northeast'.tr; if (pressure == null) return '';
} else if (direction >= 67.5 && direction < 112.5) {
return 'east'.tr; if (pressure < 1000) {
} else if (direction >= 112.5 && direction < 157.5) { return 'low'.tr;
return 'southeast'.tr; } else if (pressure > 1020) {
} else if (direction >= 157.5 && direction < 202.5) { return 'high'.tr;
return 'south'.tr;
} else if (direction >= 202.5 && direction < 247.5) {
return 'southwest'.tr;
} else if (direction >= 247.5 && direction < 292.5) {
return 'west'.tr;
} else {
return 'northwest'.tr;
}
} else { } else {
return ''; return 'normal'.tr;
}
}
String _getUvIndexDescription(int? uvIndex) {
if (uvIndex == null) return '';
if (uvIndex < 3) {
return 'uvLow'.tr;
} else if (uvIndex < 6) {
return 'uvAverage'.tr;
} else if (uvIndex < 8) {
return 'uvHigh'.tr;
} else if (uvIndex < 11) {
return 'uvVeryHigh'.tr;
} else {
return 'uvExtreme'.tr;
}
}
String _getDirectionDescription(int? direction) {
if (direction == null) return '';
if (direction >= 337.5 || direction < 22.5) {
return 'north'.tr;
} else if (direction >= 22.5 && direction < 67.5) {
return 'northeast'.tr;
} else if (direction >= 67.5 && direction < 112.5) {
return 'east'.tr;
} else if (direction >= 112.5 && direction < 157.5) {
return 'southeast'.tr;
} else if (direction >= 157.5 && direction < 202.5) {
return 'south'.tr;
} else if (direction >= 202.5 && direction < 247.5) {
return 'southwest'.tr;
} else if (direction >= 247.5 && direction < 292.5) {
return 'west'.tr;
} else {
return 'northwest'.tr;
} }
} }
} }

57
lib/app/ui/widgets/weather/hourly.dart Normal file → Executable file
View file

@ -14,6 +14,7 @@ class Hourly extends StatefulWidget {
required this.timeDay, required this.timeDay,
required this.timeNight, required this.timeNight,
}); });
final String time; final String time;
final String timeDay; final String timeDay;
final String timeNight; final String timeNight;
@ -32,35 +33,45 @@ class _HourlyState extends State<Hourly> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final textTheme = context.textTheme; final textTheme = context.textTheme;
final time = widget.time; final time = widget.time;
return Column( return Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly, mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [ children: [
Column( _buildTimeText(textTheme, time),
children: [ _buildWeatherImage(),
Text(statusData.getTimeFormat(time), style: textTheme.labelLarge), _buildTemperatureText(textTheme),
Text( ],
DateFormat( );
'E', }
locale.languageCode,
).format(DateTime.tryParse(time)!), Widget _buildTimeText(TextTheme textTheme, String time) {
style: textTheme.labelLarge?.copyWith(color: Colors.grey), return Column(
), children: [
], Text(statusData.getTimeFormat(time), style: textTheme.labelLarge),
),
Image.asset(
statusWeather.getImageToday(
widget.weather,
time,
widget.timeDay,
widget.timeNight,
),
scale: 3,
),
Text( Text(
statusData.getDegree(widget.degree.round()), DateFormat('E', locale.languageCode).format(DateTime.tryParse(time)!),
style: textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600), style: textTheme.labelLarge?.copyWith(color: Colors.grey),
), ),
], ],
); );
} }
Widget _buildWeatherImage() {
return Image.asset(
statusWeather.getImageToday(
widget.weather,
widget.time,
widget.timeDay,
widget.timeNight,
),
scale: 3,
);
}
Widget _buildTemperatureText(TextTheme textTheme) {
return Text(
statusData.getDegree(widget.degree.round()),
style: textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600),
);
}
} }

261
lib/app/ui/widgets/weather/now.dart Normal file → Executable file
View file

@ -18,6 +18,7 @@ class Now extends StatefulWidget {
required this.tempMin, required this.tempMin,
required this.feels, required this.feels,
}); });
final String time; final String time;
final String timeDay; final String timeDay;
final String timeNight; final String timeNight;
@ -38,132 +39,144 @@ class _NowState extends State<Now> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return largeElement return largeElement
? Padding( ? _buildLargeElementLayout(context)
padding: const EdgeInsets.only(bottom: 15), : _buildCompactElementLayout(context);
child: Column( }
mainAxisAlignment: MainAxisAlignment.center,
children: [ Widget _buildLargeElementLayout(BuildContext context) {
const Gap(15), return Padding(
Image( padding: const EdgeInsets.only(bottom: 15),
image: AssetImage( child: Column(
statusWeather.getImageNow( mainAxisAlignment: MainAxisAlignment.center,
widget.weather, children: [
widget.time, const Gap(15),
widget.timeDay, _buildWeatherImage(200),
widget.timeNight, _buildTemperatureText(context, widget.degree, 90),
), Text(
), statusWeather.getText(widget.weather),
fit: BoxFit.fill, style: context.textTheme.titleLarge,
height: 200,
),
Text(
'${roundDegree ? widget.degree.round() : widget.degree}',
style: context.textTheme.displayLarge?.copyWith(
fontSize: 90,
fontWeight: FontWeight.w800,
shadows: const [Shadow(blurRadius: 15, offset: Offset(5, 5))],
),
),
Text(
statusWeather.getText(widget.weather),
style: context.textTheme.titleLarge,
),
const Gap(5),
Text(
DateFormat.MMMMEEEEd(
locale.languageCode,
).format(DateTime.parse(widget.time)),
style: context.textTheme.labelLarge?.copyWith(
color: Colors.grey,
),
),
],
), ),
) const Gap(5),
: Card( _buildDateText(context),
margin: const EdgeInsets.only(bottom: 15), ],
child: Padding( ),
padding: const EdgeInsets.only( );
top: 18, }
bottom: 18,
left: 25, Widget _buildCompactElementLayout(BuildContext context) {
right: 15, return Card(
), margin: const EdgeInsets.only(bottom: 15),
child: Row( child: Padding(
crossAxisAlignment: CrossAxisAlignment.center, padding: const EdgeInsets.only(
children: [ top: 18,
Expanded( bottom: 18,
child: Column( left: 25,
crossAxisAlignment: CrossAxisAlignment.start, right: 15,
children: [ ),
Text( child: Row(
DateFormat.MMMMEEEEd( crossAxisAlignment: CrossAxisAlignment.center,
locale.languageCode, children: [
).format(DateTime.parse(widget.time)), Expanded(
style: context.textTheme.labelLarge?.copyWith( child: Column(
color: Colors.grey, crossAxisAlignment: CrossAxisAlignment.start,
), children: [
), _buildDateText(context),
const Gap(5), const Gap(5),
Text( Text(
statusWeather.getText(widget.weather), statusWeather.getText(widget.weather),
style: context.textTheme.titleLarge?.copyWith( style: context.textTheme.titleLarge?.copyWith(fontSize: 20),
fontSize: 20,
),
),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('feels'.tr, style: context.textTheme.bodyMedium),
Text('', style: context.textTheme.bodyMedium),
Text(
statusData.getDegree(widget.feels.round()),
style: context.textTheme.bodyMedium,
),
],
),
const Gap(30),
Text(
statusData.getDegree(
roundDegree ? widget.degree.round() : widget.degree,
),
style: context.textTheme.displayMedium?.copyWith(
fontWeight: FontWeight.w800,
),
),
const Gap(5),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
statusData.getDegree((widget.tempMin.round())),
style: context.textTheme.labelLarge,
),
Text(' / ', style: context.textTheme.labelLarge),
Text(
statusData.getDegree((widget.tempMax.round())),
style: context.textTheme.labelLarge,
),
],
),
],
), ),
), _buildFeelsLikeText(context),
Image( const Gap(30),
image: AssetImage( _buildTemperatureCompactText(context, widget.degree),
statusWeather.getImageNow( const Gap(5),
widget.weather, _buildMinMaxTemperatureText(context),
widget.time, ],
widget.timeDay, ),
widget.timeNight,
),
),
fit: BoxFit.fill,
height: 140,
),
],
), ),
), _buildWeatherImage(140),
); ],
),
),
);
}
Widget _buildWeatherImage(double height) {
return Image(
image: AssetImage(
statusWeather.getImageNow(
widget.weather,
widget.time,
widget.timeDay,
widget.timeNight,
),
),
fit: BoxFit.fill,
height: height,
);
}
Widget _buildTemperatureText(
BuildContext context,
double degree,
double? fontSize,
) {
return Text(
'${roundDegree ? degree.round() : degree}',
style: context.textTheme.displayLarge?.copyWith(
fontSize: fontSize,
fontWeight: FontWeight.w800,
shadows: const [Shadow(blurRadius: 15, offset: Offset(5, 5))],
),
);
}
Widget _buildTemperatureCompactText(BuildContext context, double degree) {
return Text(
statusData.getDegree(roundDegree ? widget.degree.round() : widget.degree),
style: context.textTheme.displayMedium?.copyWith(
fontWeight: FontWeight.w800,
),
);
}
Widget _buildDateText(BuildContext context) {
return Text(
DateFormat.MMMMEEEEd(
locale.languageCode,
).format(DateTime.parse(widget.time)),
style: context.textTheme.labelLarge?.copyWith(color: Colors.grey),
);
}
Widget _buildFeelsLikeText(BuildContext context) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('feels'.tr, style: context.textTheme.bodyMedium),
Text('', style: context.textTheme.bodyMedium),
Text(
statusData.getDegree(widget.feels.round()),
style: context.textTheme.bodyMedium,
),
],
);
}
Widget _buildMinMaxTemperatureText(BuildContext context) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
statusData.getDegree((widget.tempMin.round())),
style: context.textTheme.labelLarge,
),
Text(' / ', style: context.textTheme.labelLarge),
Text(
statusData.getDegree((widget.tempMax.round())),
style: context.textTheme.labelLarge,
),
],
);
} }
} }

95
lib/app/ui/widgets/weather/status/status_data.dart Normal file → Executable file
View file

@ -4,7 +4,35 @@ import 'package:rain/main.dart';
import 'package:timezone/timezone.dart'; import 'package:timezone/timezone.dart';
class StatusData { class StatusData {
String getDegree(degree) { String getDegree(dynamic degree) {
return _formatDegree(degree);
}
String getSpeed(int? speed) {
return _formatSpeed(speed);
}
String getPressure(int? pressure) {
return _formatPressure(pressure);
}
String getVisibility(double? length) {
return _formatVisibility(length);
}
String getPrecipitation(double? precipitation) {
return _formatPrecipitation(precipitation);
}
String getTimeFormat(String time) {
return _formatTime(time);
}
String getTimeFormatTz(TZDateTime time) {
return _formatTimeTz(time);
}
String _formatDegree(dynamic degree) {
switch (settings.degrees) { switch (settings.degrees) {
case 'celsius': case 'celsius':
return '$degree°C'; return '$degree°C';
@ -15,11 +43,13 @@ class StatusData {
} }
} }
String getSpeed(int? speed) { String _formatSpeed(int? speed) {
if (speed == null) return '';
switch (settings.measurements) { switch (settings.measurements) {
case 'metric': case 'metric':
return settings.wind == 'm/s' return settings.wind == 'm/s'
? '${(speed! * (5 / 18)).toPrecision(1)} ${'m/s'.tr}' ? '${(speed * (5 / 18)).toPrecision(1)} ${'m/s'.tr}'
: '$speed ${'kph'.tr}'; : '$speed ${'kph'.tr}';
case 'imperial': case 'imperial':
return '$speed ${'mph'.tr}'; return '$speed ${'mph'.tr}';
@ -28,28 +58,38 @@ class StatusData {
} }
} }
String getPressure(int? pressure) { String _formatPressure(int? pressure) {
if (pressure == null) return '';
return settings.pressure == 'mmHg' return settings.pressure == 'mmHg'
? '${(pressure! * (3 / 4)).toPrecision(1)} ${'mmHg'.tr}' ? '${(pressure * (3 / 4)).toPrecision(1)} ${'mmHg'.tr}'
: '$pressure ${'hPa'.tr}'; : '$pressure ${'hPa'.tr}';
} }
String getVisibility(double? length) { String _formatVisibility(double? length) {
if (length != null) { if (length == null) return '';
switch (settings.measurements) {
case 'metric': switch (settings.measurements) {
return '${length > 1000 ? (length / 1000).round() : (length / 1000).toStringAsFixed(2)} ${'km'.tr}'; case 'metric':
case 'imperial': return _formatMetricVisibility(length);
return '${length > 5280 ? (length / 5280).round() : (length / 5280).toStringAsFixed(2)} ${'mi'.tr}'; case 'imperial':
default: return _formatImperialVisibility(length);
return '${length > 1000 ? (length / 1000).round() : (length / 1000).toStringAsFixed(2)} ${'km'.tr}'; default:
} return _formatMetricVisibility(length);
} else {
return '';
} }
} }
String getPrecipitation(double? precipitation) { String _formatMetricVisibility(double length) {
return '${length > 1000 ? (length / 1000).round() : (length / 1000).toStringAsFixed(2)} ${'km'.tr}';
}
String _formatImperialVisibility(double length) {
return '${length > 5280 ? (length / 5280).round() : (length / 5280).toStringAsFixed(2)} ${'mi'.tr}';
}
String _formatPrecipitation(double? precipitation) {
if (precipitation == null) return '';
switch (settings.measurements) { switch (settings.measurements) {
case 'metric': case 'metric':
return '$precipitation ${'mm'.tr}'; return '$precipitation ${'mm'.tr}';
@ -60,24 +100,21 @@ class StatusData {
} }
} }
String getTimeFormat(String time) { String _formatTime(String time) {
final parsedTime = DateTime.tryParse(time);
if (parsedTime == null) return '';
switch (settings.timeformat) { switch (settings.timeformat) {
case '12': case '12':
return DateFormat.jm( return DateFormat.jm(locale.languageCode).format(parsedTime);
locale.languageCode,
).format(DateTime.tryParse(time)!);
case '24': case '24':
return DateFormat.Hm( return DateFormat.Hm(locale.languageCode).format(parsedTime);
locale.languageCode,
).format(DateTime.tryParse(time)!);
default: default:
return DateFormat.Hm( return DateFormat.Hm(locale.languageCode).format(parsedTime);
locale.languageCode,
).format(DateTime.tryParse(time)!);
} }
} }
String getTimeFormatTz(TZDateTime time) { String _formatTimeTz(TZDateTime time) {
switch (settings.timeformat) { switch (settings.timeformat) {
case '12': case '12':
return DateFormat.jm(locale.languageCode).format(time); return DateFormat.jm(locale.languageCode).format(time);

519
lib/app/ui/widgets/weather/status/status_weather.dart Normal file → Executable file
View file

@ -9,120 +9,17 @@ class StatusWeather {
String timeDay, String timeDay,
String timeNight, String timeNight,
) { ) {
final currentTime = DateTime.parse(time); return _getImageBasedOnTime(
final day = DateTime.parse(timeDay); weather,
final night = DateTime.parse(timeNight); time,
timeDay,
final dayTime = DateTime( timeNight,
day.year, _getDayNightImagePaths,
day.month,
day.day,
day.hour,
day.minute,
); );
final nightTime = DateTime(
night.year,
night.month,
night.day,
night.hour,
night.minute,
);
switch (weather) {
case 0:
if (currentTime.isAfter(dayTime) && currentTime.isBefore(nightTime)) {
return '${assetImageRoot}sun.png';
} else {
return '${assetImageRoot}full-moon.png';
}
case 1:
case 2:
case 3:
if (currentTime.isAfter(dayTime) && currentTime.isBefore(nightTime)) {
return '${assetImageRoot}cloud.png';
} else {
return '${assetImageRoot}moon.png';
}
case 45:
case 48:
if (currentTime.isAfter(dayTime) && currentTime.isBefore(nightTime)) {
return '${assetImageRoot}fog.png';
} else {
return '${assetImageRoot}fog_moon.png';
}
case 51:
case 53:
case 55:
case 56:
case 57:
case 61:
case 63:
case 65:
case 66:
case 67:
return '${assetImageRoot}rain.png';
case 80:
case 81:
case 82:
return '${assetImageRoot}rain-fall.png';
case 71:
case 73:
case 75:
case 77:
case 85:
case 86:
return '${assetImageRoot}snow.png';
case 95:
return '${assetImageRoot}thunder.png';
case 96:
case 99:
return '${assetImageRoot}storm.png';
default:
return '';
}
} }
String getImageNowDaily(int? weather) { String getImageNowDaily(int? weather) {
switch (weather) { return _getDailyImage(weather);
case 0:
return '${assetImageRoot}sun.png';
case 1:
case 2:
case 3:
return '${assetImageRoot}cloud.png';
case 45:
case 48:
return '${assetImageRoot}fog.png';
case 51:
case 53:
case 55:
case 56:
case 57:
case 61:
case 63:
case 65:
case 66:
case 67:
return '${assetImageRoot}rain.png';
case 80:
case 81:
case 82:
return '${assetImageRoot}rain-fall.png';
case 71:
case 73:
case 75:
case 77:
case 85:
case 86:
return '${assetImageRoot}snow.png';
case 95:
return '${assetImageRoot}thunder.png';
case 96:
case 99:
return '${assetImageRoot}storm.png';
default:
return '';
}
} }
String getImageToday( String getImageToday(
@ -130,6 +27,45 @@ class StatusWeather {
String time, String time,
String timeDay, String timeDay,
String timeNight, String timeNight,
) {
return _getImageBasedOnTime(
weather,
time,
timeDay,
timeNight,
_getTodayImagePaths,
);
}
String getImage7Day(int? weather) {
return _getDailyImage(weather, isDay: true);
}
String getText(int? weather) {
return _getWeatherText(weather);
}
String getImageNotification(
int weather,
String time,
String timeDay,
String timeNight,
) {
return _getImageBasedOnTime(
weather,
time,
timeDay,
timeNight,
_getNotificationImagePaths,
);
}
String _getImageBasedOnTime(
int weather,
String time,
String timeDay,
String timeNight,
Map<int, Map<bool, String>> imagePaths,
) { ) {
final currentTime = DateTime.parse(time); final currentTime = DateTime.parse(time);
final day = DateTime.parse(timeDay); final day = DateTime.parse(timeDay);
@ -150,28 +86,23 @@ class StatusWeather {
night.minute, night.minute,
); );
final isDayTime =
currentTime.isAfter(dayTime) && currentTime.isBefore(nightTime);
return imagePaths[weather]?[isDayTime] ?? '';
}
String _getDailyImage(int? weather, {bool isDay = false}) {
switch (weather) { switch (weather) {
case 0: case 0:
if (currentTime.isAfter(dayTime) && currentTime.isBefore(nightTime)) { return '$assetImageRoot${isDay ? 'clear_day' : 'sun'}.png';
return '${assetImageRoot}clear_day.png';
} else {
return '${assetImageRoot}clear_night.png';
}
case 1: case 1:
case 2: case 2:
case 3: case 3:
if (currentTime.isAfter(dayTime) && currentTime.isBefore(nightTime)) { return '$assetImageRoot${isDay ? 'cloudy_day' : 'cloud'}.png';
return '${assetImageRoot}cloudy_day.png';
} else {
return '${assetImageRoot}cloudy_night.png';
}
case 45: case 45:
case 48: case 48:
if (currentTime.isAfter(dayTime) && currentTime.isBefore(nightTime)) { return '${assetImageRoot}fog${isDay ? '_day' : ''}.png';
return '${assetImageRoot}fog_day.png';
} else {
return '${assetImageRoot}fog_night.png';
}
case 51: case 51:
case 53: case 53:
case 55: case 55:
@ -185,77 +116,24 @@ class StatusWeather {
case 80: case 80:
case 81: case 81:
case 82: case 82:
if (currentTime.isAfter(dayTime) && currentTime.isBefore(nightTime)) { return '${assetImageRoot}rain${isDay ? '_day' : ''}.png';
return '${assetImageRoot}rain_day.png';
} else {
return '${assetImageRoot}rain_night.png';
}
case 71: case 71:
case 73: case 73:
case 75: case 75:
case 77: case 77:
case 85: case 85:
case 86: case 86:
if (currentTime.isAfter(dayTime) && currentTime.isBefore(nightTime)) { return '${assetImageRoot}snow${isDay ? '_day' : ''}.png';
return '${assetImageRoot}snow_day.png';
} else {
return '${assetImageRoot}snow_night.png';
}
case 95: case 95:
case 96: case 96:
case 99: case 99:
if (currentTime.isAfter(dayTime) && currentTime.isBefore(nightTime)) { return '${assetImageRoot}thunder${isDay ? '_day' : ''}.png';
return '${assetImageRoot}thunder_day.png';
} else {
return '${assetImageRoot}thunder_night.png';
}
default: default:
return ''; return '';
} }
} }
String getImage7Day(int? weather) { String _getWeatherText(int? weather) {
switch (weather) {
case 0:
return '${assetImageRoot}clear_day.png';
case 1:
case 2:
case 3:
return '${assetImageRoot}cloudy_day.png';
case 45:
case 48:
return '${assetImageRoot}fog_day.png';
case 51:
case 53:
case 55:
case 56:
case 57:
case 61:
case 63:
case 65:
case 66:
case 67:
case 80:
case 81:
case 82:
return '${assetImageRoot}rain_day.png';
case 71:
case 73:
case 75:
case 77:
case 85:
case 86:
return '${assetImageRoot}snow_day.png';
case 95:
case 96:
case 99:
return '${assetImageRoot}thunder_day.png';
default:
return '';
}
}
String getText(int? weather) {
switch (weather) { switch (weather) {
case 0: case 0:
return 'clear_sky'.tr; return 'clear_sky'.tr;
@ -301,82 +179,207 @@ class StatusWeather {
} }
} }
String getImageNotification( final Map<int, Map<bool, String>> _getDayNightImagePaths = {
int weather, 0: {
String time, true: '${assetImageRoot}sun.png',
String timeDay, false: '${assetImageRoot}full-moon.png',
String timeNight, },
) { 1: {true: '${assetImageRoot}cloud.png', false: '${assetImageRoot}moon.png'},
final currentTime = DateTime.parse(time); 2: {true: '${assetImageRoot}cloud.png', false: '${assetImageRoot}moon.png'},
final day = DateTime.parse(timeDay); 3: {true: '${assetImageRoot}cloud.png', false: '${assetImageRoot}moon.png'},
final night = DateTime.parse(timeNight); 45: {
true: '${assetImageRoot}fog.png',
false: '${assetImageRoot}fog_moon.png',
},
48: {
true: '${assetImageRoot}fog.png',
false: '${assetImageRoot}fog_moon.png',
},
51: {true: '${assetImageRoot}rain.png', false: '${assetImageRoot}rain.png'},
53: {true: '${assetImageRoot}rain.png', false: '${assetImageRoot}rain.png'},
55: {true: '${assetImageRoot}rain.png', false: '${assetImageRoot}rain.png'},
56: {true: '${assetImageRoot}rain.png', false: '${assetImageRoot}rain.png'},
57: {true: '${assetImageRoot}rain.png', false: '${assetImageRoot}rain.png'},
61: {true: '${assetImageRoot}rain.png', false: '${assetImageRoot}rain.png'},
63: {true: '${assetImageRoot}rain.png', false: '${assetImageRoot}rain.png'},
65: {true: '${assetImageRoot}rain.png', false: '${assetImageRoot}rain.png'},
66: {true: '${assetImageRoot}rain.png', false: '${assetImageRoot}rain.png'},
67: {true: '${assetImageRoot}rain.png', false: '${assetImageRoot}rain.png'},
80: {
true: '${assetImageRoot}rain-fall.png',
false: '${assetImageRoot}rain-fall.png',
},
81: {
true: '${assetImageRoot}rain-fall.png',
false: '${assetImageRoot}rain-fall.png',
},
82: {
true: '${assetImageRoot}rain-fall.png',
false: '${assetImageRoot}rain-fall.png',
},
71: {true: '${assetImageRoot}snow.png', false: '${assetImageRoot}snow.png'},
73: {true: '${assetImageRoot}snow.png', false: '${assetImageRoot}snow.png'},
75: {true: '${assetImageRoot}snow.png', false: '${assetImageRoot}snow.png'},
77: {true: '${assetImageRoot}snow.png', false: '${assetImageRoot}snow.png'},
85: {true: '${assetImageRoot}snow.png', false: '${assetImageRoot}snow.png'},
86: {true: '${assetImageRoot}snow.png', false: '${assetImageRoot}snow.png'},
95: {
true: '${assetImageRoot}thunder.png',
false: '${assetImageRoot}thunder.png',
},
96: {
true: '${assetImageRoot}storm.png',
false: '${assetImageRoot}storm.png',
},
99: {
true: '${assetImageRoot}storm.png',
false: '${assetImageRoot}storm.png',
},
};
final dayTime = DateTime( final Map<int, Map<bool, String>> _getTodayImagePaths = {
day.year, 0: {
day.month, true: '${assetImageRoot}clear_day.png',
day.day, false: '${assetImageRoot}clear_night.png',
day.hour, },
day.minute, 1: {
); true: '${assetImageRoot}cloudy_day.png',
final nightTime = DateTime( false: '${assetImageRoot}cloudy_night.png',
night.year, },
night.month, 2: {
night.day, true: '${assetImageRoot}cloudy_day.png',
night.hour, false: '${assetImageRoot}cloudy_night.png',
night.minute, },
); 3: {
true: '${assetImageRoot}cloudy_day.png',
false: '${assetImageRoot}cloudy_night.png',
},
45: {
true: '${assetImageRoot}fog_day.png',
false: '${assetImageRoot}fog_night.png',
},
48: {
true: '${assetImageRoot}fog_day.png',
false: '${assetImageRoot}fog_night.png',
},
51: {
true: '${assetImageRoot}rain_day.png',
false: '${assetImageRoot}rain_night.png',
},
53: {
true: '${assetImageRoot}rain_day.png',
false: '${assetImageRoot}rain_night.png',
},
55: {
true: '${assetImageRoot}rain_day.png',
false: '${assetImageRoot}rain_night.png',
},
56: {
true: '${assetImageRoot}rain_day.png',
false: '${assetImageRoot}rain_night.png',
},
57: {
true: '${assetImageRoot}rain_day.png',
false: '${assetImageRoot}rain_night.png',
},
61: {
true: '${assetImageRoot}rain_day.png',
false: '${assetImageRoot}rain_night.png',
},
63: {
true: '${assetImageRoot}rain_day.png',
false: '${assetImageRoot}rain_night.png',
},
65: {
true: '${assetImageRoot}rain_day.png',
false: '${assetImageRoot}rain_night.png',
},
66: {
true: '${assetImageRoot}rain_day.png',
false: '${assetImageRoot}rain_night.png',
},
67: {
true: '${assetImageRoot}rain_day.png',
false: '${assetImageRoot}rain_night.png',
},
80: {
true: '${assetImageRoot}rain_day.png',
false: '${assetImageRoot}rain_night.png',
},
81: {
true: '${assetImageRoot}rain_day.png',
false: '${assetImageRoot}rain_night.png',
},
82: {
true: '${assetImageRoot}rain_day.png',
false: '${assetImageRoot}rain_night.png',
},
71: {
true: '${assetImageRoot}snow_day.png',
false: '${assetImageRoot}snow_night.png',
},
73: {
true: '${assetImageRoot}snow_day.png',
false: '${assetImageRoot}snow_night.png',
},
75: {
true: '${assetImageRoot}snow_day.png',
false: '${assetImageRoot}snow_night.png',
},
77: {
true: '${assetImageRoot}snow_day.png',
false: '${assetImageRoot}snow_night.png',
},
85: {
true: '${assetImageRoot}snow_day.png',
false: '${assetImageRoot}snow_night.png',
},
86: {
true: '${assetImageRoot}snow_day.png',
false: '${assetImageRoot}snow_night.png',
},
95: {
true: '${assetImageRoot}thunder_day.png',
false: '${assetImageRoot}thunder_night.png',
},
96: {
true: '${assetImageRoot}thunder_day.png',
false: '${assetImageRoot}thunder_night.png',
},
99: {
true: '${assetImageRoot}thunder_day.png',
false: '${assetImageRoot}thunder_night.png',
},
};
switch (weather) { final Map<int, Map<bool, String>> _getNotificationImagePaths = {
case 0: 0: {true: 'sun.png', false: 'full-moon.png'},
if (currentTime.isAfter(dayTime) && currentTime.isBefore(nightTime)) { 1: {true: 'cloud.png', false: 'moon.png'},
return 'sun.png'; 2: {true: 'cloud.png', false: 'moon.png'},
} else { 3: {true: 'cloud.png', false: 'moon.png'},
return 'full-moon.png'; 45: {true: 'fog.png', false: 'fog_moon.png'},
} 48: {true: 'fog.png', false: 'fog_moon.png'},
case 1: 51: {true: 'rain.png', false: 'rain.png'},
case 2: 53: {true: 'rain.png', false: 'rain.png'},
case 3: 55: {true: 'rain.png', false: 'rain.png'},
if (currentTime.isAfter(dayTime) && currentTime.isBefore(nightTime)) { 56: {true: 'rain.png', false: 'rain.png'},
return 'cloud.png'; 57: {true: 'rain.png', false: 'rain.png'},
} else { 61: {true: 'rain.png', false: 'rain.png'},
return 'moon.png'; 63: {true: 'rain.png', false: 'rain.png'},
} 65: {true: 'rain.png', false: 'rain.png'},
case 45: 66: {true: 'rain.png', false: 'rain.png'},
case 48: 67: {true: 'rain.png', false: 'rain.png'},
if (currentTime.isAfter(dayTime) && currentTime.isBefore(nightTime)) { 80: {true: 'rain-fall.png', false: 'rain-fall.png'},
return 'fog.png'; 81: {true: 'rain-fall.png', false: 'rain-fall.png'},
} else { 82: {true: 'rain-fall.png', false: 'rain-fall.png'},
return 'fog_moon.png'; 71: {true: 'snow.png', false: 'snow.png'},
} 73: {true: 'snow.png', false: 'snow.png'},
case 51: 75: {true: 'snow.png', false: 'snow.png'},
case 53: 77: {true: 'snow.png', false: 'snow.png'},
case 55: 85: {true: 'snow.png', false: 'snow.png'},
case 56: 86: {true: 'snow.png', false: 'snow.png'},
case 57: 95: {true: 'thunder.png', false: 'thunder.png'},
case 61: 96: {true: 'storm.png', false: 'storm.png'},
case 63: 99: {true: 'storm.png', false: 'storm.png'},
case 65: };
case 66:
case 67:
return 'rain.png';
case 80:
case 81:
case 82:
return 'rain-fall.png';
case 71:
case 73:
case 75:
case 77:
case 85:
case 86:
return 'snow.png';
case 95:
return 'thunder.png';
case 96:
case 99:
return 'storm.png';
default:
return '';
}
}
} }

97
lib/app/ui/widgets/weather/sunset_sunrise.dart Normal file → Executable file
View file

@ -32,65 +32,54 @@ class _SunsetSunriseState extends State<SunsetSunrise> {
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 20), padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 20),
child: Row( child: Row(
children: [ children: [
Expanded( _buildSunTimeColumn(
child: Row( context,
mainAxisAlignment: MainAxisAlignment.center, 'sunrise'.tr,
children: [ statusData.getTimeFormat(widget.timeSunrise),
Expanded( 'assets/images/sunrise.png',
child: Column( titleSmall,
crossAxisAlignment: CrossAxisAlignment.start, titleLarge,
children: [
Text(
'sunrise'.tr,
style: titleSmall,
overflow: TextOverflow.ellipsis,
),
const Gap(2),
Text(
statusData.getTimeFormat(widget.timeSunrise),
style: titleLarge,
),
],
),
),
const Gap(5),
Flexible(
child: Image.asset('assets/images/sunrise.png', scale: 10),
),
],
),
), ),
Expanded( _buildSunTimeColumn(
child: Row( context,
mainAxisAlignment: MainAxisAlignment.center, 'sunset'.tr,
children: [ statusData.getTimeFormat(widget.timeSunset),
Expanded( 'assets/images/sunset.png',
child: Column( titleSmall,
crossAxisAlignment: CrossAxisAlignment.start, titleLarge,
children: [
Text(
'sunset'.tr,
style: titleSmall,
overflow: TextOverflow.ellipsis,
),
const Gap(2),
Text(
statusData.getTimeFormat(widget.timeSunset),
style: titleLarge,
),
],
),
),
const Gap(5),
Flexible(
child: Image.asset('assets/images/sunset.png', scale: 10),
),
],
),
), ),
], ],
), ),
), ),
); );
} }
Widget _buildSunTimeColumn(
BuildContext context,
String label,
String time,
String imagePath,
TextStyle? labelStyle,
TextStyle? timeStyle,
) {
return Expanded(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label, style: labelStyle, overflow: TextOverflow.ellipsis),
const Gap(2),
Text(time, style: timeStyle),
],
),
),
const Gap(5),
Flexible(child: Image.asset(imagePath, scale: 10)),
],
),
);
}
} }

0
lib/app/utils/color_converter.dart Normal file → Executable file
View file

0
lib/app/utils/device_info.dart Normal file → Executable file
View file

65
lib/app/utils/notification.dart Normal file → Executable file
View file

@ -4,38 +4,55 @@ import 'package:rain/main.dart';
import 'package:timezone/timezone.dart' as tz; import 'package:timezone/timezone.dart' as tz;
class NotificationShow { class NotificationShow {
Future showNotification( static const String _channelId = 'Rain';
static const String _channelName = 'DARK NIGHT';
Future<void> showNotification(
int id, int id,
String title, String title,
String body, String body,
DateTime date, DateTime date,
String icon, String icon,
) async { ) async {
final imagePath = await WeatherController().getLocalImagePath(icon); try {
final imagePath = await _getLocalImagePath(icon);
final notificationDetails = await _buildNotificationDetails(imagePath);
final scheduledTime = _getScheduledTime(date);
AndroidNotificationDetails androidNotificationDetails = await flutterLocalNotificationsPlugin.zonedSchedule(
AndroidNotificationDetails( id,
'Rain', title,
'DARK NIGHT', body,
priority: Priority.high, scheduledTime,
importance: Importance.max, notificationDetails,
playSound: false, androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle,
enableVibration: false, payload: imagePath,
largeIcon: FilePathAndroidBitmap(imagePath), );
); } catch (e) {
NotificationDetails notificationDetails = NotificationDetails( print('Error showing notification: $e');
android: androidNotificationDetails, }
); }
var scheduledTime = tz.TZDateTime.from(date, tz.local); Future<String> _getLocalImagePath(String icon) async {
flutterLocalNotificationsPlugin.zonedSchedule( return await WeatherController().getLocalImagePath(icon);
id, }
title,
body, Future<NotificationDetails> _buildNotificationDetails(
scheduledTime, String imagePath,
notificationDetails, ) async {
androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle, final androidNotificationDetails = AndroidNotificationDetails(
payload: imagePath, _channelId,
_channelName,
priority: Priority.high,
importance: Importance.max,
playSound: false,
enableVibration: false,
largeIcon: FilePathAndroidBitmap(imagePath),
); );
return NotificationDetails(android: androidNotificationDetails);
}
tz.TZDateTime _getScheduledTime(DateTime date) {
return tz.TZDateTime.from(date, tz.local);
} }
} }

12
lib/app/utils/show_snack_bar.dart Normal file → Executable file
View file

@ -1,20 +1,16 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
final globalKey = GlobalKey<ScaffoldMessengerState>(); final GlobalKey<ScaffoldMessengerState> globalKey =
GlobalKey<ScaffoldMessengerState>();
void showSnackBar({required String content, Function? onPressed}) { void showSnackBar({required String content, VoidCallback? onPressed}) {
globalKey.currentState?.showSnackBar( globalKey.currentState?.showSnackBar(
SnackBar( SnackBar(
content: Text(content), content: Text(content),
action: action:
onPressed != null onPressed != null
? SnackBarAction( ? SnackBarAction(label: 'settings'.tr, onPressed: onPressed)
label: 'settings'.tr,
onPressed: () {
onPressed();
},
)
: null, : null,
), ),
); );

24
lib/main.dart Normal file → Executable file
View file

@ -86,24 +86,24 @@ void callbackDispatcher() {
void main() async { void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
await _initializeApp(); await initializeApp();
runApp(const MyApp()); runApp(const MyApp());
} }
Future<void> _initializeApp() async { Future<void> initializeApp() async {
_setupConnectivityListener(); setupConnectivityListener();
await _initializeTimeZone(); await initializeTimeZone();
await _initializeIsar(); await initializeIsar();
await _initializeNotifications(); await initializeNotifications();
if (Platform.isAndroid) { if (Platform.isAndroid) {
await _setOptimalDisplayMode(); await setOptimalDisplayMode();
Workmanager().initialize(callbackDispatcher, isInDebugMode: kDebugMode); Workmanager().initialize(callbackDispatcher, isInDebugMode: kDebugMode);
HomeWidget.setAppGroupId(appGroupId); HomeWidget.setAppGroupId(appGroupId);
} }
DeviceFeature().init(); DeviceFeature().init();
} }
void _setupConnectivityListener() { void setupConnectivityListener() {
Connectivity().onConnectivityChanged.listen((result) { Connectivity().onConnectivityChanged.listen((result) {
isOnline.value = isOnline.value =
result.contains(ConnectivityResult.none) result.contains(ConnectivityResult.none)
@ -112,13 +112,13 @@ void _setupConnectivityListener() {
}); });
} }
Future<void> _initializeTimeZone() async { Future<void> initializeTimeZone() async {
final timeZoneName = await FlutterTimezone.getLocalTimezone(); final timeZoneName = await FlutterTimezone.getLocalTimezone();
tz.initializeTimeZones(); tz.initializeTimeZones();
tz.setLocalLocation(tz.getLocation(timeZoneName)); tz.setLocalLocation(tz.getLocation(timeZoneName));
} }
Future<void> _initializeIsar() async { Future<void> initializeIsar() async {
isar = await Isar.open([ isar = await Isar.open([
SettingsSchema, SettingsSchema,
MainWeatherCacheSchema, MainWeatherCacheSchema,
@ -140,7 +140,7 @@ Future<void> _initializeIsar() async {
} }
} }
Future<void> _initializeNotifications() async { Future<void> initializeNotifications() async {
const initializationSettings = InitializationSettings( const initializationSettings = InitializationSettings(
android: AndroidInitializationSettings('@mipmap/ic_launcher'), android: AndroidInitializationSettings('@mipmap/ic_launcher'),
iOS: DarwinInitializationSettings(), iOS: DarwinInitializationSettings(),
@ -149,7 +149,7 @@ Future<void> _initializeNotifications() async {
await flutterLocalNotificationsPlugin.initialize(initializationSettings); await flutterLocalNotificationsPlugin.initialize(initializationSettings);
} }
Future<void> _setOptimalDisplayMode() async { Future<void> setOptimalDisplayMode() async {
final supported = await FlutterDisplayMode.supported; final supported = await FlutterDisplayMode.supported;
final active = await FlutterDisplayMode.active; final active = await FlutterDisplayMode.active;
final sameResolution = final sameResolution =

224
lib/theme/theme.dart Normal file → Executable file
View file

@ -3,7 +3,7 @@ import 'package:flutter/services.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
import 'package:dynamic_color/dynamic_color.dart'; import 'package:dynamic_color/dynamic_color.dart';
final ThemeData baseLigth = ThemeData.light(useMaterial3: true); final ThemeData baseLight = ThemeData.light(useMaterial3: true);
final ThemeData baseDark = ThemeData.dark(useMaterial3: true); final ThemeData baseDark = ThemeData.dark(useMaterial3: true);
const Color lightColor = Colors.white; const Color lightColor = Colors.white;
@ -14,6 +14,7 @@ ColorScheme colorSchemeLight = ColorScheme.fromSeed(
seedColor: Colors.deepPurple, seedColor: Colors.deepPurple,
brightness: Brightness.light, brightness: Brightness.light,
); );
ColorScheme colorSchemeDark = ColorScheme.fromSeed( ColorScheme colorSchemeDark = ColorScheme.fromSeed(
seedColor: Colors.deepPurple, seedColor: Colors.deepPurple,
brightness: Brightness.dark, brightness: Brightness.dark,
@ -24,65 +25,12 @@ ThemeData lightTheme(
ColorScheme? colorScheme, ColorScheme? colorScheme,
bool edgeToEdgeAvailable, bool edgeToEdgeAvailable,
) { ) {
return baseLigth.copyWith( return _buildTheme(
baseTheme: baseLight,
brightness: Brightness.light, brightness: Brightness.light,
colorScheme: color: color,
colorScheme colorScheme: colorScheme,
?.copyWith( edgeToEdgeAvailable: edgeToEdgeAvailable,
brightness: Brightness.light,
surface: baseLigth.colorScheme.surface,
)
.harmonized(),
textTheme: GoogleFonts.ubuntuTextTheme(baseLigth.textTheme),
appBarTheme: AppBarTheme(
backgroundColor: color,
foregroundColor: baseLigth.colorScheme.onSurface,
shadowColor: Colors.transparent,
surfaceTintColor: Colors.transparent,
elevation: 0,
systemOverlayStyle: SystemUiOverlayStyle(
statusBarIconBrightness: Brightness.dark,
statusBarColor: Colors.transparent,
systemStatusBarContrastEnforced: false,
systemNavigationBarContrastEnforced: false,
systemNavigationBarDividerColor: Colors.transparent,
systemNavigationBarIconBrightness: Brightness.dark,
systemNavigationBarColor:
edgeToEdgeAvailable ? Colors.transparent : colorScheme?.surface,
),
),
primaryColor: color,
canvasColor: color,
scaffoldBackgroundColor: color,
cardTheme: baseLigth.cardTheme.copyWith(
color: color,
surfaceTintColor:
color == oledColor ? Colors.transparent : colorScheme?.surfaceTint,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
shadowColor: Colors.transparent,
),
bottomSheetTheme: baseLigth.bottomSheetTheme.copyWith(
backgroundColor: color,
surfaceTintColor:
color == oledColor ? Colors.transparent : colorScheme?.surfaceTint,
),
navigationRailTheme: baseLigth.navigationRailTheme.copyWith(
backgroundColor: color,
),
navigationBarTheme: baseLigth.navigationBarTheme.copyWith(
backgroundColor: color,
surfaceTintColor:
color == oledColor ? Colors.transparent : colorScheme?.surfaceTint,
),
inputDecorationTheme: baseLigth.inputDecorationTheme.copyWith(
labelStyle: WidgetStateTextStyle.resolveWith((Set<WidgetState> states) {
return const TextStyle(fontSize: 14);
}),
border: InputBorder.none,
focusedBorder: InputBorder.none,
enabledBorder: InputBorder.none,
),
indicatorColor: Colors.black,
); );
} }
@ -91,63 +39,121 @@ ThemeData darkTheme(
ColorScheme? colorScheme, ColorScheme? colorScheme,
bool edgeToEdgeAvailable, bool edgeToEdgeAvailable,
) { ) {
return baseDark.copyWith( return _buildTheme(
baseTheme: baseDark,
brightness: Brightness.dark, brightness: Brightness.dark,
colorScheme: color: color,
colorScheme colorScheme: colorScheme,
?.copyWith( edgeToEdgeAvailable: edgeToEdgeAvailable,
brightness: Brightness.dark, );
surface: baseDark.colorScheme.surface, }
)
.harmonized(), ThemeData _buildTheme({
textTheme: GoogleFonts.ubuntuTextTheme(baseDark.textTheme), required ThemeData baseTheme,
appBarTheme: AppBarTheme( required Brightness brightness,
backgroundColor: color, required Color? color,
foregroundColor: baseDark.colorScheme.onSurface, required ColorScheme? colorScheme,
shadowColor: Colors.transparent, required bool edgeToEdgeAvailable,
surfaceTintColor: Colors.transparent, }) {
elevation: 0, final harmonizedColorScheme =
systemOverlayStyle: SystemUiOverlayStyle( colorScheme
statusBarIconBrightness: Brightness.light, ?.copyWith(
statusBarColor: Colors.transparent, brightness: brightness,
systemStatusBarContrastEnforced: false, surface: baseTheme.colorScheme.surface,
systemNavigationBarContrastEnforced: false, )
systemNavigationBarDividerColor: Colors.transparent, .harmonized();
systemNavigationBarIconBrightness: Brightness.light,
systemNavigationBarColor: return baseTheme.copyWith(
edgeToEdgeAvailable ? Colors.transparent : colorScheme?.surface, brightness: brightness,
), colorScheme: harmonizedColorScheme,
textTheme: GoogleFonts.ubuntuTextTheme(baseTheme.textTheme),
appBarTheme: _buildAppBarTheme(
color,
baseTheme.colorScheme.onSurface,
edgeToEdgeAvailable,
brightness,
harmonizedColorScheme,
), ),
primaryColor: color, primaryColor: color,
canvasColor: color, canvasColor: color,
scaffoldBackgroundColor: color, scaffoldBackgroundColor: color,
cardTheme: baseDark.cardTheme.copyWith( cardTheme: _buildCardTheme(color, harmonizedColorScheme),
color: color, bottomSheetTheme: _buildBottomSheetTheme(color, harmonizedColorScheme),
surfaceTintColor: navigationRailTheme: baseTheme.navigationRailTheme.copyWith(
color == oledColor ? Colors.transparent : colorScheme?.surfaceTint,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
shadowColor: Colors.transparent,
),
bottomSheetTheme: baseDark.bottomSheetTheme.copyWith(
backgroundColor: color,
surfaceTintColor:
color == oledColor ? Colors.transparent : colorScheme?.surfaceTint,
),
navigationRailTheme: baseDark.navigationRailTheme.copyWith(
backgroundColor: color, backgroundColor: color,
), ),
navigationBarTheme: baseDark.navigationBarTheme.copyWith( navigationBarTheme: _buildNavigationBarTheme(color, harmonizedColorScheme),
backgroundColor: color, inputDecorationTheme: _buildInputDecorationTheme(),
surfaceTintColor: );
color == oledColor ? Colors.transparent : colorScheme?.surfaceTint, }
),
inputDecorationTheme: baseDark.inputDecorationTheme.copyWith( AppBarTheme _buildAppBarTheme(
labelStyle: WidgetStateTextStyle.resolveWith((Set<WidgetState> states) { Color? color,
return const TextStyle(fontSize: 14); Color? onSurfaceColor,
}), bool edgeToEdgeAvailable,
border: InputBorder.none, Brightness brightness,
focusedBorder: InputBorder.none, ColorScheme? colorScheme,
enabledBorder: InputBorder.none, ) {
return AppBarTheme(
backgroundColor: color,
foregroundColor: onSurfaceColor,
shadowColor: Colors.transparent,
surfaceTintColor: Colors.transparent,
elevation: 0,
systemOverlayStyle: SystemUiOverlayStyle(
statusBarIconBrightness:
brightness == Brightness.light ? Brightness.dark : Brightness.light,
statusBarColor: Colors.transparent,
systemStatusBarContrastEnforced: false,
systemNavigationBarContrastEnforced: false,
systemNavigationBarDividerColor: Colors.transparent,
systemNavigationBarIconBrightness:
brightness == Brightness.light ? Brightness.dark : Brightness.light,
systemNavigationBarColor:
edgeToEdgeAvailable ? Colors.transparent : colorScheme?.surface,
), ),
); );
} }
CardThemeData _buildCardTheme(Color? color, ColorScheme? colorScheme) {
return CardThemeData(
color: color,
surfaceTintColor:
color == oledColor ? Colors.transparent : colorScheme?.surfaceTint,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
shadowColor: Colors.transparent,
);
}
BottomSheetThemeData _buildBottomSheetTheme(
Color? color,
ColorScheme? colorScheme,
) {
return BottomSheetThemeData(
backgroundColor: color,
surfaceTintColor:
color == oledColor ? Colors.transparent : colorScheme?.surfaceTint,
);
}
NavigationBarThemeData _buildNavigationBarTheme(
Color? color,
ColorScheme? colorScheme,
) {
return NavigationBarThemeData(
backgroundColor: color,
surfaceTintColor:
color == oledColor ? Colors.transparent : colorScheme?.surfaceTint,
);
}
InputDecorationTheme _buildInputDecorationTheme() {
return InputDecorationTheme(
labelStyle: WidgetStateTextStyle.resolveWith((Set<WidgetState> states) {
return const TextStyle(fontSize: 14);
}),
border: InputBorder.none,
focusedBorder: InputBorder.none,
enabledBorder: InputBorder.none,
);
}

32
lib/theme/theme_controller.dart Normal file → Executable file
View file

@ -4,29 +4,37 @@ import 'package:rain/app/data/db.dart';
import 'package:rain/main.dart'; import 'package:rain/main.dart';
class ThemeController extends GetxController { class ThemeController extends GetxController {
ThemeMode get theme => ThemeMode get theme => _getThemeMode();
settings.theme == 'system'
? ThemeMode.system
: settings.theme == 'dark'
? ThemeMode.dark
: ThemeMode.light;
void saveOledTheme(bool isOled) { void saveOledTheme(bool isOled) {
settings.amoledTheme = isOled; _updateSetting((settings) => settings.amoledTheme = isOled);
isar.writeTxnSync(() => isar.settings.putSync(settings));
} }
void saveMaterialTheme(bool isMaterial) { void saveMaterialTheme(bool isMaterial) {
settings.materialColor = isMaterial; _updateSetting((settings) => settings.materialColor = isMaterial);
isar.writeTxnSync(() => isar.settings.putSync(settings));
} }
void saveTheme(String themeMode) { void saveTheme(String themeMode) {
settings.theme = themeMode; _updateSetting((settings) => settings.theme = themeMode);
isar.writeTxnSync(() => isar.settings.putSync(settings));
} }
void changeTheme(ThemeData theme) => Get.changeTheme(theme); void changeTheme(ThemeData theme) => Get.changeTheme(theme);
void changeThemeMode(ThemeMode themeMode) => Get.changeThemeMode(themeMode); void changeThemeMode(ThemeMode themeMode) => Get.changeThemeMode(themeMode);
ThemeMode _getThemeMode() {
switch (settings.theme) {
case 'system':
return ThemeMode.system;
case 'dark':
return ThemeMode.dark;
default:
return ThemeMode.light;
}
}
void _updateSetting(void Function(Settings) update) {
update(settings);
isar.writeTxnSync(() => isar.settings.putSync(settings));
}
} }

0
lib/translation/bn_in.dart Normal file → Executable file
View file

0
lib/translation/cs_cz.dart Normal file → Executable file
View file

0
lib/translation/da_dk.dart Normal file → Executable file
View file

0
lib/translation/de_de.dart Normal file → Executable file
View file

0
lib/translation/en_us.dart Normal file → Executable file
View file

0
lib/translation/es_es.dart Normal file → Executable file
View file

0
lib/translation/fa_ir.dart Normal file → Executable file
View file

0
lib/translation/fr_fr.dart Normal file → Executable file
View file

0
lib/translation/ga_ie.dart Normal file → Executable file
View file

0
lib/translation/hi_in.dart Normal file → Executable file
View file

0
lib/translation/hu_hu.dart Normal file → Executable file
View file

0
lib/translation/it_it.dart Normal file → Executable file
View file

0
lib/translation/ka_ge.dart Normal file → Executable file
View file

0
lib/translation/ko_kr.dart Normal file → Executable file
View file

0
lib/translation/nl_nl.dart Normal file → Executable file
View file

0
lib/translation/pl_pl.dart Normal file → Executable file
View file

0
lib/translation/pt_br.dart Normal file → Executable file
View file

0
lib/translation/ro_ro.dart Normal file → Executable file
View file

0
lib/translation/ru_ru.dart Normal file → Executable file
View file

0
lib/translation/sk_sk.dart Normal file → Executable file
View file

0
lib/translation/tr_tr.dart Normal file → Executable file
View file

0
lib/translation/translation.dart Normal file → Executable file
View file

0
lib/translation/ur_pk.dart Normal file → Executable file
View file

0
lib/translation/zh_ch.dart Normal file → Executable file
View file

0
lib/translation/zh_tw.dart Normal file → Executable file
View file

View file

@ -50,10 +50,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: async name: async
sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.12.0" version: "2.13.0"
boolean_selector: boolean_selector:
dependency: transitive dependency: transitive
description: description:
@ -122,10 +122,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: built_value name: built_value
sha256: ea90e81dc4a25a043d9bee692d20ed6d1c4a1662a28c03a96417446c093ed6b4 sha256: "082001b5c3dc495d4a42f1d5789990505df20d8547d42507c29050af6933ee27"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "8.9.5" version: "8.10.1"
characters: characters:
dependency: transitive dependency: transitive
description: description:
@ -298,10 +298,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: fake_async name: fake_async
sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.2" version: "1.3.3"
ffi: ffi:
dependency: transitive dependency: transitive
description: description:
@ -343,10 +343,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: flutter_expandable_fab name: flutter_expandable_fab
sha256: "4d03f54e5384897e32606e9959cef5e7857e5a203e24684f95dfbb5f7fb9b88e" sha256: c2936d398169166064d025df91a3bb417109a859e725d9b80c6ef7f04e01b6ab
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.4.1" version: "2.5.1"
flutter_hsvcolor_picker: flutter_hsvcolor_picker:
dependency: "direct main" dependency: "direct main"
description: description:
@ -367,18 +367,18 @@ packages:
dependency: "direct dev" dependency: "direct dev"
description: description:
name: flutter_lints name: flutter_lints
sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.0.0" version: "6.0.0"
flutter_local_notifications: flutter_local_notifications:
dependency: "direct main" dependency: "direct main"
description: description:
name: flutter_local_notifications name: flutter_local_notifications
sha256: "33b3e0269ae9d51669957a923f2376bee96299b09915d856395af8c4238aebfa" sha256: b94a50aabbe56ef254f95f3be75640f99120429f0a153b2dc30143cffc9bfdf3
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "19.1.0" version: "19.2.1"
flutter_local_notifications_linux: flutter_local_notifications_linux:
dependency: transitive dependency: transitive
description: description:
@ -449,10 +449,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: flutter_timezone name: flutter_timezone
sha256: bc286cecb0366d88e6c4644e3962ebd1ce1d233abc658eb1e0cd803389f84b64 sha256: "13b2109ad75651faced4831bf262e32559e44aa549426eab8a597610d385d934"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.1.0" version: "4.1.1"
flutter_web_plugins: flutter_web_plugins:
dependency: transitive dependency: transitive
description: flutter description: flutter
@ -494,26 +494,26 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: geocoding name: geocoding
sha256: d580c801cba9386b4fac5047c4c785a4e19554f46be42f4f5e5b7deacd088a66 sha256: "606be036287842d779d7ec4e2f6c9435fc29bbbd3c6da6589710f981d8852895"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.0" version: "4.0.0"
geocoding_android: geocoding_android:
dependency: transitive dependency: transitive
description: description:
name: geocoding_android name: geocoding_android
sha256: "1b13eca79b11c497c434678fed109c2be020b158cec7512c848c102bc7232603" sha256: fe7df2e35dc49a5176af634ff9c7b0312d9a2adc94320b936a56241f8028bbbc
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.3.1" version: "4.0.0"
geocoding_ios: geocoding_ios:
dependency: transitive dependency: transitive
description: description:
name: geocoding_ios name: geocoding_ios
sha256: "94ddba60387501bd1c11e18dca7c5a9e8c645d6e3da9c38b9762434941870c24" sha256: "43bde988312feb1a3cb6c3d514e9f4b04b564d1884fa56bd8241030bbb3bde36"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.1" version: "3.0.2"
geocoding_platform_interface: geocoding_platform_interface:
dependency: transitive dependency: transitive
description: description:
@ -526,10 +526,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: geolocator name: geolocator
sha256: e7ebfa04ce451daf39b5499108c973189a71a919aa53c1204effda1c5b93b822 sha256: ee2212a3df8292ec4c90b91183b8001d3f5a800823c974b570c5f9344ca320dc
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "14.0.0" version: "14.0.1"
geolocator_android: geolocator_android:
dependency: transitive dependency: transitive
description: description:
@ -606,10 +606,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: home_widget name: home_widget
sha256: "7430f7549d42cef2e729bd3c779de748b93f1eb78b1abfe6bca8fffd1cfce3e9" sha256: ad9634ef5894f3bac73f04d59e2e5151a39798f49985399fd928dadc828d974a
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.7.0+1" version: "0.8.0"
html: html:
dependency: transitive dependency: transitive
description: description:
@ -622,10 +622,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: http name: http
sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f sha256: "2c11f3f94c687ee9bad77c171151672986360b2b001d109814ee7140b2cf261b"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.0" version: "1.4.0"
http_cache_core: http_cache_core:
dependency: transitive dependency: transitive
description: description:
@ -678,18 +678,18 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: internet_connection_checker_plus name: internet_connection_checker_plus
sha256: eb3a6f03e7b1641589f580993d29aee0b3c4920fc618f7556de359fedb87b02e sha256: "5aea4a1ee0fcca736980a7d04d96fe8c0b53dea330690053305a5c5392230112"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.7.1" version: "2.7.2"
intl: intl:
dependency: "direct main" dependency: "direct main"
description: description:
name: intl name: intl
sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.19.0" version: "0.20.2"
io: io:
dependency: transitive dependency: transitive
description: description:
@ -766,10 +766,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: leak_tracker name: leak_tracker
sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "10.0.8" version: "10.0.9"
leak_tracker_flutter_testing: leak_tracker_flutter_testing:
dependency: transitive dependency: transitive
description: description:
@ -798,10 +798,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: lints name: lints
sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7 sha256: a5e2b223cb7c9c8efdc663ef484fdd95bb243bff242ef5b13e26883547fce9a0
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.1.1" version: "6.0.0"
lists: lists:
dependency: transitive dependency: transitive
description: description:
@ -1179,10 +1179,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: timezone name: timezone
sha256: ffc9d5f4d1193534ef051f9254063fa53d588609418c84299956c3db9383587d sha256: dd14a3b83cfd7cb19e7888f1cbc20f258b8d71b54c06f79ac585f14093a287d1
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.10.0" version: "0.10.1"
timing: timing:
dependency: transitive dependency: transitive
description: description:
@ -1299,10 +1299,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: vm_service name: vm_service
sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "14.3.1" version: "15.0.0"
watcher: watcher:
dependency: transitive dependency: transitive
description: description:
@ -1323,10 +1323,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: web_socket name: web_socket
sha256: bfe6f435f6ec49cb6c01da1e275ae4228719e59a6b067048c51e72d9d63bcc4b sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.0" version: "1.0.1"
web_socket_channel: web_socket_channel:
dependency: transitive dependency: transitive
description: description:
@ -1339,10 +1339,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: win32 name: win32
sha256: dc6ecaa00a7c708e5b4d10ee7bec8c270e9276dfcab1783f57e9962d7884305f sha256: "329edf97fdd893e0f1e3b9e88d6a0e627128cc17cc316a8d67fda8f1451178ba"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.12.0" version: "5.13.0"
win32_registry: win32_registry:
dependency: transitive dependency: transitive
description: description:
@ -1364,7 +1364,7 @@ packages:
description: description:
path: workmanager path: workmanager
ref: main ref: main
resolved-ref: "4ce065135dc1b91fee918f81596b42a56850391d" resolved-ref: "387d30698678dbf40585fa4732be60a74f9e07ef"
url: "https://github.com/fluttercommunity/flutter_workmanager.git" url: "https://github.com/fluttercommunity/flutter_workmanager.git"
source: git source: git
version: "0.5.2" version: "0.5.2"
@ -1401,5 +1401,5 @@ packages:
source: hosted source: hosted
version: "3.1.3" version: "3.1.3"
sdks: sdks:
dart: ">=3.7.2 <4.0.0" dart: ">=3.8.0 <4.0.0"
flutter: ">=3.27.0" flutter: ">=3.27.0"

View file

@ -6,7 +6,7 @@ publish_to: "none"
version: 1.3.8+41 version: 1.3.8+41
environment: environment:
sdk: ">=3.7.2 <4.0.0" sdk: ">=3.8.0 <4.0.0"
dependencies: dependencies:
flutter: flutter:
@ -16,13 +16,13 @@ dependencies:
get: ^4.7.2 get: ^4.7.2
gap: ^3.0.1 gap: ^3.0.1
dio: ^5.8.0+1 dio: ^5.8.0+1
intl: ^0.19.0 intl: ^0.20.2
shimmer: ^3.0.0 shimmer: ^3.0.0
latlong2: ^0.9.1 latlong2: ^0.9.1
timezone: ^0.10.0 timezone: ^0.10.1
geocoding: ^3.0.0 geocoding: ^4.0.0
geolocator: ^14.0.0 geolocator: ^14.0.1
home_widget: ^0.7.0+1 home_widget: ^0.8.0
restart_app: ^1.3.2 restart_app: ^1.3.2
flutter_map: ^8.1.1 flutter_map: ^8.1.1
google_fonts: ^6.2.1 google_fonts: ^6.2.1
@ -32,7 +32,7 @@ dependencies:
path_provider: ^2.1.5 path_provider: ^2.1.5
# quick_settings: ^1.0.1 # quick_settings: ^1.0.1
json_annotation: ^4.9.0 json_annotation: ^4.9.0
flutter_timezone: ^4.1.0 flutter_timezone: ^4.1.1
device_info_plus: ^11.4.0 device_info_plus: ^11.4.0
package_info_plus: ^8.3.0 package_info_plus: ^8.3.0
connectivity_plus: ^6.1.4 connectivity_plus: ^6.1.4
@ -44,11 +44,11 @@ dependencies:
dio_cache_interceptor: ^4.0.3 dio_cache_interceptor: ^4.0.3
http_cache_file_store: ^2.0.1 http_cache_file_store: ^2.0.1
flutter_map_animations: ^0.9.0 flutter_map_animations: ^0.9.0
flutter_expandable_fab: ^2.4.1 flutter_expandable_fab: ^2.5.1
flutter_hsvcolor_picker: ^1.5.1 flutter_hsvcolor_picker: ^1.5.1
scrollable_positioned_list: ^0.3.8 scrollable_positioned_list: ^0.3.8
flutter_local_notifications: ^19.1.0 flutter_local_notifications: ^19.2.1
internet_connection_checker_plus: ^2.7.1 internet_connection_checker_plus: ^2.7.2
isar: isar:
version: ^3.1.8 version: ^3.1.8
hosted: https://pub.isar-community.dev/ hosted: https://pub.isar-community.dev/
@ -74,7 +74,7 @@ dev_dependencies:
sdk: flutter sdk: flutter
freezed: ^3.0.0-0.0.dev freezed: ^3.0.0-0.0.dev
build_runner: ^2.4.15 build_runner: ^2.4.15
flutter_lints: ^5.0.0 flutter_lints: ^6.0.0
json_serializable: ^6.9.0 json_serializable: ^6.9.0
flutter_native_splash: ^2.4.6 flutter_native_splash: ^2.4.6
flutter_launcher_icons: ^0.14.3 flutter_launcher_icons: ^0.14.3