From a3e6a55fbeefe0a1630ed671088fc8de7909b988 Mon Sep 17 00:00:00 2001 From: Feichtmeier Date: Sun, 21 Apr 2024 22:10:26 +0200 Subject: [PATCH] Add forecast --- lib/constants.dart | 2 + lib/main.dart | 2 + lib/src/app/app.dart | 43 ++-- lib/src/app/app_model.dart | 11 + lib/src/weather/view/city_search_field.dart | 40 +++- lib/src/weather/view/forecast_tile.dart | 9 +- lib/src/weather/view/today_tile.dart | 15 +- lib/src/weather/weather_model.dart | 2 +- lib/src/weather/weather_page.dart | 210 +++++++++++++------- linux/my_application.cc | 2 +- 10 files changed, 214 insertions(+), 122 deletions(-) create mode 100644 lib/src/app/app_model.dart diff --git a/lib/constants.dart b/lib/constants.dart index 72f65f2..585d68e 100644 --- a/lib/constants.dart +++ b/lib/constants.dart @@ -5,3 +5,5 @@ const kAppStateFileName = 'appstate.json'; const kSettingsFileName = 'settings.json'; const kFavLocationsFileName = 'favlocations.json'; const kLastLocation = 'lastLocation'; + +const kPaneWidth = 240.0; diff --git a/lib/main.dart b/lib/main.dart index 7dcd7fa..de30ab7 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -9,6 +9,7 @@ import 'package:watch_it/watch_it.dart'; import 'package:yaru/yaru.dart'; import 'src/app/app.dart'; +import 'src/app/app_model.dart'; import 'src/locations/locations_service.dart'; import 'src/weather/weather_model.dart'; @@ -23,6 +24,7 @@ Future main() async { LocationsService(), dispose: (s) => s.dispose(), ); + di.registerSingleton(AppModel()); final weatherModel = WeatherModel( locationsService: di(), openWeather: di(), diff --git a/lib/src/app/app.dart b/lib/src/app/app.dart index d59d3b2..19d5697 100644 --- a/lib/src/app/app.dart +++ b/lib/src/app/app.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:watch_it/watch_it.dart'; import 'package:yaru/yaru.dart'; +import '../../constants.dart'; import '../../weather.dart'; import '../weather/view/city_search_field.dart'; import '../weather/weather_model.dart'; @@ -46,6 +47,7 @@ class MasterDetailPage extends StatelessWidget with WatchItMixin { controller: YaruPageController( length: favLocationsLength == 0 ? 1 : favLocationsLength, ), + layoutDelegate: const YaruMasterFixedPaneDelegate(paneWidth: kPaneWidth), tileBuilder: (context, index, selected, availableWidth) { final location = favLocations.elementAt(index); return YaruMasterTile( @@ -56,17 +58,20 @@ class MasterDetailPage extends StatelessWidget with WatchItMixin { favLocations.elementAt(index), ), trailing: favLocationsLength > 1 - ? IconButton( - padding: EdgeInsets.zero, - onPressed: () { - model.removeFavLocation(location).then( - (value) => model.init( - cityName: favLocations.lastOrNull, - ), - ); - }, - icon: const Icon( - YaruIcons.window_close, + ? Center( + widthFactor: 0.1, + child: IconButton( + padding: EdgeInsets.zero, + onPressed: () { + model.removeFavLocation(location).then( + (value) => model.init( + cityName: favLocations.lastOrNull, + ), + ); + }, + icon: const Icon( + YaruIcons.window_close, + ), ), ) : null, @@ -79,21 +84,7 @@ class MasterDetailPage extends StatelessWidget with WatchItMixin { backgroundColor: YaruMasterDetailTheme.of(context).sideBarColor, border: BorderSide.none, style: YaruTitleBarStyle.undecorated, - leading: Center( - child: YaruIconButton( - padding: EdgeInsets.zero, - icon: const Icon( - Icons.location_on, - size: 16, - ), - onPressed: () => model.init(cityName: null), - ), - ), - titleSpacing: 0, - title: const Padding( - padding: EdgeInsets.only(right: 15), - child: CitySearchField(), - ), + title: const CitySearchField(), ), ); } diff --git a/lib/src/app/app_model.dart b/lib/src/app/app_model.dart new file mode 100644 index 0000000..857f73f --- /dev/null +++ b/lib/src/app/app_model.dart @@ -0,0 +1,11 @@ +import 'package:safe_change_notifier/safe_change_notifier.dart'; + +class AppModel extends SafeChangeNotifier { + int _tabIndex = 0; + int get tabIndex => _tabIndex; + set tabIndex(int value) { + if (value == _tabIndex) return; + _tabIndex = value; + notifyListeners(); + } +} diff --git a/lib/src/weather/view/city_search_field.dart b/lib/src/weather/view/city_search_field.dart index fe00c4e..ed91415 100644 --- a/lib/src/weather/view/city_search_field.dart +++ b/lib/src/weather/view/city_search_field.dart @@ -1,7 +1,8 @@ -import '../weather_model.dart'; import 'package:flutter/material.dart'; import 'package:watch_it/watch_it.dart'; -import 'package:yaru/icons.dart'; +import 'package:yaru/yaru.dart'; + +import '../weather_model.dart'; class CitySearchField extends StatefulWidget { const CitySearchField({ @@ -33,19 +34,46 @@ class _CitySearchFieldState extends State { var textField = TextField( onSubmitted: (value) => model.init(cityName: _controller.text), controller: _controller, + onTap: () { + _controller.selection = TextSelection( + baseOffset: 0, + extentOffset: _controller.value.text.length, + ); + }, style: Theme.of(context) .textTheme .bodyMedium ?.copyWith(fontWeight: FontWeight.w500), - decoration: const InputDecoration( - prefixIcon: Icon( + decoration: InputDecoration( + prefixIcon: const Icon( YaruIcons.search, size: 15, ), - prefixIconConstraints: BoxConstraints(minWidth: 35, minHeight: 30), - contentPadding: EdgeInsets.all(8), + prefixIconConstraints: + const BoxConstraints(minWidth: 35, minHeight: 30), filled: true, hintText: 'Cityname', + suffixIconConstraints: const BoxConstraints( + maxHeight: kYaruTitleBarItemHeight, + minHeight: kYaruTitleBarItemHeight, + minWidth: kYaruTitleBarItemHeight, + maxWidth: kYaruTitleBarItemHeight, + ), + suffixIcon: ClipRRect( + borderRadius: const BorderRadius.only( + topRight: Radius.circular(kYaruButtonRadius), + bottomRight: Radius.circular(kYaruButtonRadius), + ), + child: Material( + color: Colors.transparent, + child: InkWell( + child: const Icon( + YaruIcons.location, + ), + onTap: () => model.init(cityName: null), + ), + ), + ), ), ); return textField; diff --git a/lib/src/weather/view/forecast_tile.dart b/lib/src/weather/view/forecast_tile.dart index 09a0701..c3ba918 100644 --- a/lib/src/weather/view/forecast_tile.dart +++ b/lib/src/weather/view/forecast_tile.dart @@ -2,12 +2,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_weather_bg_null_safety/bg/weather_bg.dart'; import 'package:flutter_weather_bg_null_safety/flutter_weather_bg.dart'; import 'package:open_weather_client/models/weather_data.dart'; +import '../../../build_context_x.dart'; import '../weather_utils.dart'; import '../../../string_x.dart'; import '../weather_data_x.dart'; class ForecastTile extends StatefulWidget { - final List data; final WeatherData selectedData; final String? cityName; final double fontSize; @@ -31,7 +31,6 @@ class ForecastTile extends StatefulWidget { required this.padding, this.time, this.borderRadius = const BorderRadius.all(Radius.circular(10)), - required this.data, }); @override @@ -41,8 +40,8 @@ class ForecastTile extends StatefulWidget { class _ForecastTileState extends State { @override Widget build(BuildContext context) { - final theme = Theme.of(context); - final light = theme.brightness == Brightness.light; + final theme = context.theme; + final light = context.light; final style = theme.textTheme.headlineSmall?.copyWith( color: Colors.white, fontSize: 20, @@ -120,7 +119,7 @@ class _ForecastTileState extends State { child: Stack( children: [ Opacity( - opacity: light ? 1 : 0.4, + opacity: light ? 1 : 0.6, child: ClipRRect( borderRadius: widget.borderRadius, child: WeatherBg( diff --git a/lib/src/weather/view/today_tile.dart b/lib/src/weather/view/today_tile.dart index 7d5d98d..de982df 100644 --- a/lib/src/weather/view/today_tile.dart +++ b/lib/src/weather/view/today_tile.dart @@ -95,18 +95,19 @@ class TodayTile extends StatelessWidget { ], ), if (cityName != null) - Text( - cityName!, - style: style, - ) - else if (position != null) SizedBox( - width: 300, + width: width, child: Text( - position ?? '', + cityName!, style: style, textAlign: TextAlign.center, ), + ) + else if (position != null) + Text( + position ?? '', + style: style, + textAlign: TextAlign.center, ), ]; diff --git a/lib/src/weather/weather_model.dart b/lib/src/weather/weather_model.dart index 8c22133..ec54e36 100644 --- a/lib/src/weather/weather_model.dart +++ b/lib/src/weather/weather_model.dart @@ -121,7 +121,7 @@ class WeatherModel extends SafeChangeNotifier { } List? _fiveDaysForCast; - List todayForeCast() { + List get todayForeCast { if (_fiveDaysForCast == null) return []; final foreCast = _fiveDaysForCast!; diff --git a/lib/src/weather/weather_page.dart b/lib/src/weather/weather_page.dart index 52d6319..20b9ad8 100644 --- a/lib/src/weather/weather_page.dart +++ b/lib/src/weather/weather_page.dart @@ -5,6 +5,8 @@ import 'package:yaru/constants.dart'; import 'package:yaru/widgets.dart'; import '../../build_context_x.dart'; +import '../../constants.dart'; +import '../app/app_model.dart'; import 'view/forecast_tile.dart'; import 'view/today_tile.dart'; import 'weather_data_x.dart'; @@ -16,92 +18,148 @@ class WeatherPage extends StatelessWidget with WatchItMixin { @override Widget build(BuildContext context) { - final model = watchIt(); final mq = context.mq; + final theme = context.theme; + final data = watchPropertyValue((WeatherModel m) => m.data); + final initializing = watchPropertyValue((WeatherModel m) => m.initializing); + final error = watchPropertyValue((WeatherModel m) => m.error); + final cityFromPosition = + watchPropertyValue((WeatherModel m) => m.cityFromPosition); + final cityName = watchPropertyValue((WeatherModel m) => m.cityName); + final todayForeCast = + watchPropertyValue((WeatherModel m) => m.todayForeCast); + final notTodayForeCast = + watchPropertyValue((WeatherModel m) => m.notTodayForeCast); + final appModel = watchIt(); + final showToday = appModel.tabIndex == 0; - return Material( - color: Colors.transparent, - child: Stack( - children: [ - if (model.data != null) - Opacity( - opacity: context.light ? 0.4 : 0.3, - child: WeatherBg( - weatherType: getWeatherType(model.data!), - width: mq.size.width, - height: mq.size.height, + return DefaultTabController( + initialIndex: appModel.tabIndex, + length: 2, + child: Material( + color: Colors.transparent, + child: Stack( + children: [ + if (data != null) + Opacity( + opacity: context.light ? 0.4 : 0.3, + child: WeatherBg( + weatherType: getWeatherType(data), + width: mq.size.width, + height: mq.size.height, + ), ), - ), - Scaffold( - backgroundColor: Colors.transparent, - appBar: YaruWindowTitleBar( - leading: Navigator.of(context).canPop() - ? const YaruBackButton() - : null, + Scaffold( backgroundColor: Colors.transparent, - border: BorderSide.none, - ), - body: model.initializing == true - ? const Center( - child: YaruCircularProgressIndicator(), - ) - : model.data == null - ? Center( - child: model.error != null - ? Text(model.error!) - : const SizedBox.shrink(), - ) - : Column( - children: [ - Expanded( - child: TodayTile( - width: mq.size.width, - padding: const EdgeInsets.all(kYaruPagePadding), - day: 'Now', - height: mq.size.height, - position: model.cityFromPosition, - data: model.data!, - fontSize: 20, - cityName: model.cityName, - ), - ), - SizedBox( - height: 300, - width: mq.size.width, - child: ListView( - padding: const EdgeInsetsDirectional.all( - kYaruPagePadding, + appBar: YaruWindowTitleBar( + leading: Navigator.of(context).canPop() + ? const YaruBackButton() + : null, + backgroundColor: Colors.transparent, + border: BorderSide.none, + title: SizedBox( + width: 230, + child: YaruTabBar( + onTap: (v) => appModel.tabIndex = v, + tabs: const [ + Tab( + text: 'Today', + ), + Tab( + text: 'Forecast', + ), + ], + ), + ), + ), + body: initializing == true + ? Center( + child: YaruCircularProgressIndicator( + color: theme.colorScheme.onSurface, + strokeWidth: 3, + ), + ) + : data == null + ? Center( + child: error != null + ? Text(error) + : const SizedBox.shrink(), + ) + : Column( + children: [ + if (showToday) + Expanded( + child: Padding( + padding: const EdgeInsets.only(right: 20), + child: TodayTile( + width: mq.size.width - + kPaneWidth - + 2 * kYaruPagePadding, + padding: + const EdgeInsets.all(kYaruPagePadding), + day: 'Now', + height: mq.size.height, + position: cityFromPosition, + data: data, + fontSize: 20, + cityName: cityName, + ), + ), ), - scrollDirection: Axis.horizontal, - children: [ - if (model.todayForeCast().isNotEmpty == true) - for (final todayForecast - in model.todayForeCast()) - ForecastTile( + SizedBox( + height: showToday + ? 300 + : mq.size.height - kYaruPagePadding * 3, + width: mq.size.width, + child: ListView.builder( + itemCount: showToday + ? todayForeCast.length + : notTodayForeCast.length, + padding: const EdgeInsetsDirectional.all( + kYaruPagePadding, + ), + scrollDirection: Axis.horizontal, + itemBuilder: (context, index) { + if (showToday) { + return ForecastTile( width: 300, height: 400, padding: const EdgeInsets.only(right: 20), - day: todayForecast.getDate(context), - time: todayForecast.getTime(context), - selectedData: todayForecast, - data: const [], + day: + todayForeCast[index].getDate(context), + time: + todayForeCast[index].getTime(context), + selectedData: todayForeCast[index], fontSize: 15, - ), - ], + ); + } + return ForecastTile( + width: 300, + height: mq.size.height, + padding: const EdgeInsets.only(right: 20), + day: notTodayForeCast[index] + .getDate(context), + time: notTodayForeCast[index] + .getTime(context), + selectedData: notTodayForeCast[index], + fontSize: 15, + ); + }, + ), ), - ), - ], - ), - ), - ], + ], + ), + ), + ], + ), ), ); } } /* var foreCastTiles = [ - if (model.todayForeCast().isNotEmpty == true) - for (final todayForecast in model.todayForeCast()) + if (todayForeCast().isNotEmpty == true) + for (final todayForecast in todayForeCast()) ForecastTile( width: mq.size.width - 40, height: 200, @@ -112,17 +170,17 @@ class WeatherPage extends StatelessWidget with WatchItMixin { data: const [], fontSize: 15, ), - if (model.notTodayForeCast.isNotEmpty == true) - for (int i = 0; i < model.notTodayForeCast.length; i++) + if (notTodayForeCast.isNotEmpty == true) + for (int i = 0; i < notTodayForeCast.length; i++) ForecastTile( width: mq.size.width - 40, height: 200, padding: const EdgeInsets.only(bottom: 20), - day: model.notTodayForeCast[i].getDate(context), - time: model.notTodayForeCast[i].getTime(context), - selectedData: model.notTodayForeCast[i], + day: notTodayForeCast[i].getDate(context), + time: notTodayForeCast[i].getTime(context), + selectedData: notTodayForeCast[i], data: const [], fontSize: 15, - // borderRadius: getBorderRadius(i, model.notTodayForeCast), + // borderRadius: getBorderRadius(i, notTodayForeCast), ), ] */ diff --git a/linux/my_application.cc b/linux/my_application.cc index 9495b83..3657bbe 100644 --- a/linux/my_application.cc +++ b/linux/my_application.cc @@ -26,7 +26,7 @@ static void my_application_activate(GApplication* application) { gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(box)); GdkGeometry geometry_min; - geometry_min.min_width = 1200; + geometry_min.min_width = 800; geometry_min.min_height = 800; gtk_window_set_geometry_hints(window, nullptr, &geometry_min, GDK_HINT_MIN_SIZE); gtk_window_set_default_size(window, 1200, 800);