From c6f05ad60bbb53d848bc1409d7b4d85ca00a3c66 Mon Sep 17 00:00:00 2001 From: Feichtmeier Date: Sun, 21 Apr 2024 19:57:25 +0200 Subject: [PATCH 1/4] Sidebar layout part 1 --- lib/build_context_x.dart | 7 + lib/constants.dart | 7 + lib/main.dart | 25 +- lib/src/app/app.dart | 71 ++++- lib/src/locations/locations_service.dart | 55 ++++ lib/src/weather/view/city_search_field.dart | 18 +- lib/src/weather/view/forecast_tile.dart | 2 +- lib/src/weather/view/today_tile.dart | 53 ++-- lib/src/weather/weather_model.dart | 56 +++- lib/src/weather/weather_page.dart | 268 +++++++----------- .../{utils.dart => weather_utils.dart} | 0 lib/utils.dart | 233 +++++++++++++++ linux/my_application.cc | 2 +- pubspec.lock | 52 +++- pubspec.yaml | 3 + 15 files changed, 614 insertions(+), 238 deletions(-) create mode 100644 lib/build_context_x.dart create mode 100644 lib/constants.dart create mode 100644 lib/src/locations/locations_service.dart rename lib/src/weather/{utils.dart => weather_utils.dart} (100%) create mode 100644 lib/utils.dart diff --git a/lib/build_context_x.dart b/lib/build_context_x.dart new file mode 100644 index 0000000..4b9a7c2 --- /dev/null +++ b/lib/build_context_x.dart @@ -0,0 +1,7 @@ +import 'package:flutter/material.dart'; + +extension BuildContextX on BuildContext { + ThemeData get theme => Theme.of(this); + bool get light => theme.brightness == Brightness.light; + MediaQueryData get mq => MediaQuery.of(this); +} diff --git a/lib/constants.dart b/lib/constants.dart new file mode 100644 index 0000000..72f65f2 --- /dev/null +++ b/lib/constants.dart @@ -0,0 +1,7 @@ +const kAppName = 'pulse'; +const kAppTitle = 'Pulse'; + +const kAppStateFileName = 'appstate.json'; +const kSettingsFileName = 'settings.json'; +const kFavLocationsFileName = 'favlocations.json'; +const kLastLocation = 'lastLocation'; diff --git a/lib/main.dart b/lib/main.dart index d8d1b26..7dcd7fa 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -3,23 +3,38 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:geocoding_resolver/geocoding_resolver.dart'; +import 'package:geolocator/geolocator.dart'; import 'package:open_weather_client/open_weather.dart'; import 'package:watch_it/watch_it.dart'; import 'package:yaru/yaru.dart'; import 'src/app/app.dart'; +import 'src/locations/locations_service.dart'; import 'src/weather/weather_model.dart'; Future main() async { await YaruWindowTitleBar.ensureInitialized(); final apiKey = await loadApiKey(); if (apiKey != null && apiKey.isNotEmpty) { - di.registerSingleton(OpenWeather(apiKey: apiKey)); - di.registerSingleton(GeoCoder()); - final weatherModel = - WeatherModel(openWeather: di(), geoCoder: di()); + di.registerSingleton(OpenWeather(apiKey: apiKey)); + di.registerSingleton(GeoCoder()); + di.registerSingleton(GeolocatorPlatform.instance); + di.registerSingleton( + LocationsService(), + dispose: (s) => s.dispose(), + ); + final weatherModel = WeatherModel( + locationsService: di(), + openWeather: di(), + geoCoder: di(), + geolocatorPlatform: di(), + ); await weatherModel.init(); - di.registerSingleton(weatherModel); + + di.registerSingleton( + weatherModel, + dispose: (s) => s.dispose(), + ); runApp(const App()); } else { diff --git a/lib/src/app/app.dart b/lib/src/app/app.dart index c91bb1f..08da6fe 100644 --- a/lib/src/app/app.dart +++ b/lib/src/app/app.dart @@ -1,9 +1,12 @@ import 'dart:ui'; import 'package:flutter/material.dart'; +import 'package:watch_it/watch_it.dart'; import 'package:yaru/yaru.dart'; import '../../weather.dart'; +import '../weather/view/city_search_field.dart'; +import '../weather/weather_model.dart'; class App extends StatelessWidget { const App({super.key}); @@ -15,7 +18,7 @@ class App extends StatelessWidget { debugShowCheckedModeBanner: false, theme: yaruLight, darkTheme: yaruDark, - home: const WeatherPage(), + home: const MasterDetailPage(), scrollBehavior: const MaterialScrollBehavior().copyWith( dragDevices: { PointerDeviceKind.mouse, @@ -28,3 +31,69 @@ class App extends StatelessWidget { ); } } + +class MasterDetailPage extends StatelessWidget with WatchItMixin { + const MasterDetailPage({super.key}); + + @override + Widget build(BuildContext context) { + final model = di(); + final favLocationsLength = + watchPropertyValue((WeatherModel m) => m.favLocations.length); + final favLocations = watchPropertyValue((WeatherModel m) => m.favLocations); + final lastLocation = watchPropertyValue((WeatherModel m) => m.lastLocation); + return YaruMasterDetailPage( + controller: YaruPageController( + length: favLocationsLength == 0 ? 1 : favLocationsLength, + ), + tileBuilder: (context, index, selected, availableWidth) { + final location = favLocations.elementAt(index); + return YaruMasterTile( + // TODO: assign pages to location + onTap: () {}, + selected: lastLocation == location, + title: Text( + favLocations.elementAt(index), + ), + trailing: favLocationsLength > 1 + ? IconButton( + onPressed: () { + model.removeFavLocation(location).then( + (value) => model.init( + cityName: favLocations.lastOrNull, + ), + ); + }, + icon: const Icon( + YaruIcons.window_close, + ), + ) + : null, + ); + }, + pageBuilder: (context, index) { + return const WeatherPage(); + }, + appBar: YaruDialogTitleBar( + 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(), + ), + ), + ); + } +} diff --git a/lib/src/locations/locations_service.dart b/lib/src/locations/locations_service.dart new file mode 100644 index 0000000..9bd8b66 --- /dev/null +++ b/lib/src/locations/locations_service.dart @@ -0,0 +1,55 @@ +import 'dart:async'; + +import '../../constants.dart'; +import '../../utils.dart'; + +class LocationsService { + String? _lastLocation; + String? get lastLocation => _lastLocation; + void setLastLocation(String? value) { + if (value == _lastLocation) return; + writeAppState(kLastLocation, value).then((_) { + _lastLocationController.add(true); + _lastLocation = value; + }); + } + + final _lastLocationController = StreamController.broadcast(); + Stream get lastLocationChanged => _lastLocationController.stream; + + Set _favLocations = {}; + Set get favLocations => _favLocations; + bool isFavLocation(String value) => _favLocations.contains(value); + final _favLocationsController = StreamController.broadcast(); + Stream get favLocationsChanged => _favLocationsController.stream; + + void addFavLocation(String name) { + if (favLocations.contains(name)) return; + _favLocations.add(name); + writeStringIterable( + iterable: _favLocations, + filename: kFavLocationsFileName, + ).then((_) => _favLocationsController.add(true)); + } + + Future removeFavLocation(String name) async { + if (!favLocations.contains(name)) return; + _favLocations.remove(name); + return writeStringIterable( + iterable: _favLocations, + filename: kFavLocationsFileName, + ).then((_) => _favLocationsController.add(true)); + } + + Future init() async { + _lastLocation = (await readAppState(kLastLocation)) as String?; + _favLocations = Set.from( + (await readStringIterable(filename: kFavLocationsFileName) ?? {}), + ); + } + + Future dispose() async { + await _favLocationsController.close(); + await _lastLocationController.close(); + } +} diff --git a/lib/src/weather/view/city_search_field.dart b/lib/src/weather/view/city_search_field.dart index a21a571..fe00c4e 100644 --- a/lib/src/weather/view/city_search_field.dart +++ b/lib/src/weather/view/city_search_field.dart @@ -37,25 +37,15 @@ class _CitySearchFieldState extends State { .textTheme .bodyMedium ?.copyWith(fontWeight: FontWeight.w500), - decoration: InputDecoration( - prefixIcon: const Icon( + decoration: const InputDecoration( + prefixIcon: Icon( YaruIcons.search, size: 15, ), - prefixIconConstraints: - const BoxConstraints(minWidth: 35, minHeight: 30), - contentPadding: const EdgeInsets.all(8), + prefixIconConstraints: BoxConstraints(minWidth: 35, minHeight: 30), + contentPadding: EdgeInsets.all(8), filled: true, hintText: 'Cityname', - enabledBorder: OutlineInputBorder( - borderSide: const BorderSide(color: Colors.transparent), - borderRadius: BorderRadius.circular(40), - ), - focusedBorder: OutlineInputBorder( - borderSide: const BorderSide(color: Colors.transparent), - borderRadius: BorderRadius.circular(40), - ), - fillColor: Theme.of(context).colorScheme.onSurface.withOpacity(0.05), ), ); return textField; diff --git a/lib/src/weather/view/forecast_tile.dart b/lib/src/weather/view/forecast_tile.dart index 28fbbf9..09a0701 100644 --- a/lib/src/weather/view/forecast_tile.dart +++ b/lib/src/weather/view/forecast_tile.dart @@ -2,7 +2,7 @@ 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 '../utils.dart'; +import '../weather_utils.dart'; import '../../../string_x.dart'; import '../weather_data_x.dart'; diff --git a/lib/src/weather/view/today_tile.dart b/lib/src/weather/view/today_tile.dart index 4b8ba59..7d5d98d 100644 --- a/lib/src/weather/view/today_tile.dart +++ b/lib/src/weather/view/today_tile.dart @@ -1,8 +1,7 @@ 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 '../utils.dart'; +import '../../../build_context_x.dart'; +import '../weather_utils.dart'; import '../../../string_x.dart'; import '../weather_data_x.dart'; @@ -16,6 +15,7 @@ class TodayTile extends StatelessWidget { final String? day; final EdgeInsets padding; final String? time; + final BorderRadiusGeometry? borderRadius; const TodayTile({ super.key, @@ -28,18 +28,18 @@ class TodayTile extends StatelessWidget { this.day, required this.padding, this.time, + this.borderRadius, }); @override Widget build(BuildContext context) { - final theme = Theme.of(context); - final light = theme.brightness == Brightness.light; + final theme = context.theme; final style = theme.textTheme.headlineSmall?.copyWith( color: Colors.white, fontSize: 20, shadows: [ Shadow( - color: Colors.black.withOpacity(0.8), + color: Colors.black.withOpacity(0.9), offset: const Offset(0, 1), blurRadius: 3, ), @@ -110,41 +110,22 @@ class TodayTile extends StatelessWidget { ), ]; - final banner = Card( - child: Stack( - children: [ - Opacity( - opacity: light ? 1 : 0.4, - child: ClipRRect( - borderRadius: BorderRadius.circular(10), - child: WeatherBg( - weatherType: getWeatherType(data), - width: width ?? double.infinity, - height: height ?? double.infinity, - ), - ), - ), - Center( - child: Wrap( - direction: Axis.vertical, - alignment: WrapAlignment.center, - crossAxisAlignment: WrapCrossAlignment.center, - spacing: 20, - runSpacing: 20, - runAlignment: WrapAlignment.center, - children: children, - ), - ), - ], - ), - ); - return SizedBox( width: width, height: height, child: Padding( padding: padding, - child: banner, + child: Center( + child: Wrap( + direction: Axis.vertical, + alignment: WrapAlignment.center, + crossAxisAlignment: WrapCrossAlignment.center, + spacing: 20, + runSpacing: 20, + runAlignment: WrapAlignment.center, + children: children, + ), + ), ), ); } diff --git a/lib/src/weather/weather_model.dart b/lib/src/weather/weather_model.dart index 34f0972..8c22133 100644 --- a/lib/src/weather/weather_model.dart +++ b/lib/src/weather/weather_model.dart @@ -1,19 +1,36 @@ +import 'dart:async'; + import 'package:geocoding_resolver/geocoding_resolver.dart'; import 'package:geolocator/geolocator.dart'; import 'package:open_weather_client/open_weather.dart'; import 'package:safe_change_notifier/safe_change_notifier.dart'; +import '../locations/locations_service.dart'; import 'weather_data_x.dart'; import 'weekday.dart'; class WeatherModel extends SafeChangeNotifier { - WeatherModel({required OpenWeather openWeather, required GeoCoder geoCoder}) - : _openWeather = openWeather, - _geoCoder = geoCoder; + WeatherModel({ + required OpenWeather openWeather, + required GeoCoder geoCoder, + required LocationsService locationsService, + required GeolocatorPlatform geolocatorPlatform, + }) : _openWeather = openWeather, + _geoCoder = geoCoder, + _geolocatorPlatform = geolocatorPlatform, + _locationsService = locationsService; final GeoCoder _geoCoder; final OpenWeather _openWeather; - final GeolocatorPlatform _geolocatorPlatform = GeolocatorPlatform.instance; + final GeolocatorPlatform _geolocatorPlatform; + final LocationsService _locationsService; + StreamSubscription? _lastLocationChangedSub; + StreamSubscription? _favLocationsChangedSub; + + String? get lastLocation => _locationsService.lastLocation; + Set get favLocations => _locationsService.favLocations; + Future removeFavLocation(String location) => + _locationsService.removeFavLocation(location); Position? _position; Position? get position => _position; @@ -37,8 +54,7 @@ class WeatherModel extends SafeChangeNotifier { WeatherData? _weatherData; WeatherData? get data => _weatherData; - String? _cityName; - String? get cityName => _cityName; + String? get cityName => _locationsService.lastLocation; bool? _initializing; bool? get initializing => _initializing; @@ -50,11 +66,14 @@ class WeatherModel extends SafeChangeNotifier { Future init({String? cityName}) async { initializing = true; - _cityName = cityName; - _position = await _getCurrentPosition(); + _lastLocationChangedSub ??= + _locationsService.lastLocationChanged.listen((_) => notifyListeners()); + _favLocationsChangedSub ??= + _locationsService.favLocationsChanged.listen((_) => notifyListeners()); - if (_position != null && (cityName == null || cityName.isEmpty)) { + if (cityName == null || _position == null) { + _position = await _getCurrentPosition(); _weatherData = await loadWeatherByPosition( latitude: position!.latitude, longitude: position!.longitude, @@ -64,15 +83,30 @@ class WeatherModel extends SafeChangeNotifier { latitude: position!.latitude, ); _cityFromPosition = await loadCityFromPosition(); + if (_cityFromPosition != null) { + _locationsService.setLastLocation(_cityFromPosition); + _locationsService.addFavLocation(_cityFromPosition!); + } } else { - _weatherData = await loadWeatherFromCityName(cityName!); - _cityName = cityName; + _weatherData = await loadWeatherFromCityName(cityName); + _locationsService.setLastLocation(cityName); _fiveDaysForCast = await loadForeCastByCityName(cityName: cityName); + if (_weatherData != null) { + _locationsService.setLastLocation(cityName); + _locationsService.addFavLocation(cityName); + } } initializing = false; } + @override + Future dispose() async { + super.dispose(); + await _lastLocationChangedSub?.cancel(); + await _favLocationsChangedSub?.cancel(); + } + Future loadWeatherFromCityName(String cityName) async { try { WeatherData? weatherData = await _openWeather.currentWeatherByCityName( diff --git a/lib/src/weather/weather_page.dart b/lib/src/weather/weather_page.dart index 1ac98df..b1791a5 100644 --- a/lib/src/weather/weather_page.dart +++ b/lib/src/weather/weather_page.dart @@ -1,13 +1,15 @@ import 'package:flutter/material.dart'; -import 'package:open_weather_client/models/weather_data.dart'; -import 'view/city_search_field.dart'; +import 'package:flutter_weather_bg_null_safety/bg/weather_bg.dart'; +import 'package:watch_it/watch_it.dart'; +import 'package:yaru/constants.dart'; +import 'package:yaru/widgets.dart'; + +import '../../build_context_x.dart'; import 'view/forecast_tile.dart'; import 'view/today_tile.dart'; -import 'utils.dart'; -import 'weather_model.dart'; import 'weather_data_x.dart'; -import 'package:watch_it/watch_it.dart'; -import 'package:yaru/widgets.dart'; +import 'weather_model.dart'; +import 'weather_utils.dart'; class WeatherPage extends StatelessWidget with WatchItMixin { const WeatherPage({super.key}); @@ -15,177 +17,109 @@ class WeatherPage extends StatelessWidget with WatchItMixin { @override Widget build(BuildContext context) { final model = watchIt(); - final mq = MediaQuery.of(context); - final theme = Theme.of(context); - final light = theme.brightness == Brightness.light; + final mq = context.mq; - final locationButton = Center( - child: SizedBox( - height: 35, - width: 35, - child: YaruIconButton( - icon: const Icon( - Icons.location_on, - size: 16, - ), - onPressed: () => model.init(cityName: null), - ), - ), - ); - - var foreCastTiles = [ - if (model.todayForeCast().isNotEmpty == true) - for (final todayForecast in model.todayForeCast()) - ForecastTile( - width: mq.size.width - 40, - height: 200, - padding: const EdgeInsets.only(bottom: 20), - day: todayForecast.getDate(context), - time: todayForecast.getTime(context), - selectedData: todayForecast, - data: const [], - fontSize: 15, - ), - if (model.notTodayForeCast.isNotEmpty == true) - for (int i = 0; i < model.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], - data: const [], - fontSize: 15, - // borderRadius: getBorderRadius(i, model.notTodayForeCast), - ), - ]; - final scaffold = Scaffold( - backgroundColor: model.data == null - ? null - : getColor(model.data!).withOpacity(light ? 0.1 : 0.05), - appBar: YaruWindowTitleBar( - backgroundColor: Colors.transparent, - border: BorderSide.none, - leading: locationButton, - title: const SizedBox( - width: 300, - child: CitySearchField(), - ), - ), - body: model.initializing == true - ? const Center( - child: YaruCircularProgressIndicator(), - ) - : model.data == null - ? Center( - child: model.error != null - ? Text(model.error!) - : const SizedBox.shrink(), - ) - : SizedBox( - // height: size.height, - child: LayoutBuilder( - builder: (context, constraints) { - var column = ListView( - padding: - const EdgeInsets.only(top: 10, right: 20, left: 20), + 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, + ), + ), + Scaffold( + backgroundColor: Colors.transparent, + appBar: const YaruWindowTitleBar( + 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: [ - TodayTile( - width: mq.size.width - 40, - padding: const EdgeInsets.only(bottom: 20), - day: 'Now', - height: 300, - position: model.cityFromPosition, - data: model.data!, - fontSize: 20, - cityName: model.cityName, - ), - ...foreCastTiles, - ], - ); - - var row = Padding( - padding: - const EdgeInsets.only(top: 20, right: 20, left: 20), - child: Row( - children: [ - TodayTile( - padding: const EdgeInsets.only( - bottom: 20, - ), + Expanded( + child: TodayTile( + width: mq.size.width, + padding: EdgeInsets.zero, day: 'Now', - width: 500, - height: mq.size.height - 40, + height: mq.size.height, position: model.cityFromPosition, data: model.data!, fontSize: 20, cityName: model.cityName, ), - const SizedBox( - width: 20, - ), - Expanded( - child: ListView( - children: foreCastTiles, + ), + SizedBox( + height: 300, + width: mq.size.width, + child: ListView( + padding: const EdgeInsetsDirectional.all( + kYaruPagePadding, ), + scrollDirection: Axis.horizontal, + children: [ + if (model.todayForeCast().isNotEmpty == true) + for (final todayForecast + in model.todayForeCast()) + ForecastTile( + width: 300, + height: 400, + padding: const EdgeInsets.only(right: 20), + day: todayForecast.getDate(context), + time: todayForecast.getTime(context), + selectedData: todayForecast, + data: const [], + fontSize: 15, + ), + ], ), - ], - ), - ); - return constraints.maxWidth < 1000 ? column : row; - }, - ), - ), + ), + ], + ), + ), + ], + ), ); - - return scaffold; - } - - BorderRadius getBorderRadius(int i, List data) { - const radius = Radius.circular(10); - if (i < data.length - 1 && data.length > 1) { - if (i == 0) { - return const BorderRadius.only(topLeft: radius, topRight: radius); - } - if (i >= 1) { - if (data[i].getWD() == data[i + 1].getWD() && - data[i].getWD() == data[i - 1].getWD()) { - return BorderRadius.zero; - } else if (data[i].getWD() == data[i + 1].getWD()) { - return const BorderRadius.only(topLeft: radius, topRight: radius); - } else if (data[i].getWD() == data[i - 1].getWD()) { - return const BorderRadius.only( - bottomLeft: radius, - bottomRight: radius, - ); - } - } - } - - return BorderRadius.circular(10); - } - - EdgeInsets getPadding(int i, List data) { - const value = 20.0; - - if (i == data.length - 1) { - return const EdgeInsets.only(bottom: value); - } else if (i == 0) { - return const EdgeInsets.only(top: 10); - } - - if (i < data.length - 1 && i >= 1) { - if (data[i].getWD() == data[i + 1].getWD() && - data[i].getWD() == data[i - 1].getWD()) { - return EdgeInsets.zero; - } else if (data[i].getWD() == data[i + 1].getWD()) { - return const EdgeInsets.only(top: value); - } else if (data[i].getWD() == data[i - 1].getWD()) { - return const EdgeInsets.only(bottom: value); - } - } - - return EdgeInsets.zero; } } + +/* var foreCastTiles = [ + if (model.todayForeCast().isNotEmpty == true) + for (final todayForecast in model.todayForeCast()) + ForecastTile( + width: mq.size.width - 40, + height: 200, + padding: const EdgeInsets.only(bottom: 20), + day: todayForecast.getDate(context), + time: todayForecast.getTime(context), + selectedData: todayForecast, + data: const [], + fontSize: 15, + ), + if (model.notTodayForeCast.isNotEmpty == true) + for (int i = 0; i < model.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], + data: const [], + fontSize: 15, + // borderRadius: getBorderRadius(i, model.notTodayForeCast), + ), +] */ \ No newline at end of file diff --git a/lib/src/weather/utils.dart b/lib/src/weather/weather_utils.dart similarity index 100% rename from lib/src/weather/utils.dart rename to lib/src/weather/weather_utils.dart diff --git a/lib/utils.dart b/lib/utils.dart new file mode 100644 index 0000000..8fb5722 --- /dev/null +++ b/lib/utils.dart @@ -0,0 +1,233 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:path/path.dart' as p; +import 'package:path_provider/path_provider.dart'; +import 'package:xdg_directories/xdg_directories.dart'; + +import 'constants.dart'; + +String formatTime(Duration duration) { + String twoDigits(int n) => n.toString().padLeft(2, '0'); + final hours = twoDigits(duration.inHours); + final minutes = twoDigits(duration.inMinutes.remainder(60)); + final seconds = twoDigits(duration.inSeconds.remainder(60)); + + return [if (duration.inHours > 0) hours, minutes, seconds].join(':'); +} + +String? _workingDir; +Future getWorkingDir() async { + if (_workingDir != null) return Future.value(_workingDir!); + if (Platform.isLinux) { + final workingDir = p.join(configHome.path, kAppName); + if (!Directory(workingDir).existsSync()) { + await Directory(workingDir).create(); + } + _workingDir = workingDir; + return workingDir; + } else if (Platform.isMacOS || Platform.isIOS) { + final libDirPath = (await getLibraryDirectory()).path; + final workingDirPath = p.join(libDirPath, kAppName); + if (!Directory(workingDirPath).existsSync()) { + await Directory(workingDirPath).create(); + } + _workingDir = workingDirPath; + return workingDirPath; + } else { + final docDirPath = (await getApplicationSupportDirectory()).path; + final workingDirPath = p.join(docDirPath, kAppName); + if (!Directory(workingDirPath).existsSync()) { + Directory(workingDirPath).createSync(); + } + _workingDir = workingDirPath; + return workingDirPath; + } +} + +Future getMusicDir() async { + if (Platform.isLinux) { + return getUserDirectory('MUSIC')?.path; + } + return null; +} + +Future getDownloadsDir() async { + String? path; + if (Platform.isLinux) { + path = getUserDirectory('DOWNLOAD')?.path; + } else if (Platform.isMacOS || Platform.isIOS || Platform.isWindows) { + path = (await getDownloadsDirectory())?.path; + } + if (path != null) { + return p.join(path, 'musicpod'); + } + return null; +} + +Future writeAppState(String key, dynamic value) async => + await writeSetting(key, value, kAppStateFileName); + +Future writeSetting( + String? key, + dynamic value, [ + String filename = kSettingsFileName, +]) async { + if (key == null || value == null) return; + final oldSettings = await getSettings(filename); + if (oldSettings.containsKey(key)) { + oldSettings.update(key, (v) => value); + } else { + oldSettings.putIfAbsent(key, () => value); + } + final jsonStr = jsonEncode(oldSettings); + + final workingDir = await getWorkingDir(); + + final file = File(p.join(workingDir, filename)); + + if (!file.existsSync()) { + file.create(); + } + + await file.writeAsString(jsonStr); +} + +Future readAppState(String key) => readSetting(key, kAppStateFileName); + +Future readSetting( + dynamic key, [ + String filename = kSettingsFileName, +]) async { + if (key == null) return null; + final oldSettings = await getSettings(filename); + return oldSettings[key]; +} + +Future> getSettings([ + String filename = kSettingsFileName, +]) async { + final workingDir = await getWorkingDir(); + + final file = File(p.join(workingDir, filename)); + + if (file.existsSync()) { + final jsonStr = await file.readAsString(); + + final map = jsonDecode(jsonStr) as Map; + + final m = map.map( + (key, value) => MapEntry( + key, + value, + ), + ); + + return m; + } else { + return {}; + } +} + +Future writeStringIterable({ + required Iterable iterable, + required String filename, +}) async { + final workingDir = await getWorkingDir(); + final file = File('$workingDir/$filename'); + if (!file.existsSync()) { + file.create(); + } + await file.writeAsString(iterable.join('\n')); +} + +Future?> readStringIterable({ + required String filename, +}) async { + final workingDir = await getWorkingDir(); + final file = File(p.join(workingDir, filename)); + + if (!file.existsSync()) return Future.value(null); + + final content = await file.readAsLines(); + + return content; +} + +Future writeJsonToFile(Map json, String fileName) async { + final jsonStr = jsonEncode(json); + + final workingDir = await getWorkingDir(); + + final file = File(p.join(workingDir, fileName)); + + if (!file.existsSync()) { + file.createSync(); + } + + await file.writeAsString(jsonStr); +} + +Future> readStringMap(String fileName) async { + final workingDir = await getWorkingDir(); + + try { + final file = File(p.join(workingDir, fileName)); + + if (file.existsSync()) { + final jsonStr = await file.readAsString(); + + final map = jsonDecode(jsonStr) as Map; + + final m = map.map( + (key, value) => MapEntry( + key, + value as String, + ), + ); + + return m; + } else { + return {}; + } + } on Exception catch (_) { + return {}; + } +} + +Future writeStringMap(Map map, String fileName) async { + final dynamicMap = map.map( + (key, value) => MapEntry( + key, + value as dynamic, + ), + ); + + final jsonStr = jsonEncode(dynamicMap); + + final workingDir = await getWorkingDir(); + + final file = File(p.join(workingDir, fileName)); + + if (!file.existsSync()) { + file.createSync(); + } + + await file.writeAsString(jsonStr); +} + +Duration? parseDuration(String? durationAsString) { + if (durationAsString == null || durationAsString == 'null') return null; + int hours = 0; + int minutes = 0; + int micros; + List parts = durationAsString.split(':'); + if (parts.length > 2) { + hours = int.parse(parts[parts.length - 3]); + } + if (parts.length > 1) { + minutes = int.parse(parts[parts.length - 2]); + } + micros = (double.parse(parts[parts.length - 1]) * 1000000).round(); + return Duration(hours: hours, minutes: minutes, microseconds: micros); +} diff --git a/linux/my_application.cc b/linux/my_application.cc index a6faf0e..f8bf8dd 100644 --- a/linux/my_application.cc +++ b/linux/my_application.cc @@ -29,7 +29,7 @@ static void my_application_activate(GApplication* application) { geometry_min.min_width = 680; geometry_min.min_height = 600; gtk_window_set_geometry_hints(window, nullptr, &geometry_min, GDK_HINT_MIN_SIZE); - gtk_window_set_default_size(window, 600, 800); + gtk_window_set_default_size(window, 1200, 800); g_autoptr(FlDartProject) project = fl_dart_project_new(); fl_dart_project_set_dart_entrypoint_arguments( diff --git a/pubspec.lock b/pubspec.lock index 9816c68..66e5f51 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -394,7 +394,7 @@ packages: source: hosted version: "2.0.1" path: - dependency: transitive + dependency: "direct main" description: name: path sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" @@ -409,6 +409,54 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.1" + path_provider: + dependency: "direct main" + description: + name: path_provider + sha256: c9e7d3a4cd1410877472158bee69963a4579f78b68c65a2b7d40d1a7a88bb161 + url: "https://pub.dev" + source: hosted + version: "2.1.3" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: a248d8146ee5983446bf03ed5ea8f6533129a12b11f12057ad1b4a67a2b3b41d + url: "https://pub.dev" + source: hosted + version: "2.2.4" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "5a7999be66e000916500be4f15a3633ebceb8302719b47b9cc49ce924125350f" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" + url: "https://pub.dev" + source: hosted + version: "2.2.1" petitparser: dependency: transitive description: @@ -599,7 +647,7 @@ packages: source: hosted version: "0.3.8" xdg_directories: - dependency: transitive + dependency: "direct main" description: name: xdg_directories sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d diff --git a/pubspec.yaml b/pubspec.yaml index 4af40ca..676c3b5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -29,6 +29,9 @@ dependencies: ref: b9e3943904596809b957b44a3733b3794a43517b geocoding_resolver: ^0.0.3+2 watch_it: ^1.4.1 + xdg_directories: ^1.0.4 + path_provider: ^2.1.3 + path: ^1.9.0 dev_dependencies: flutter_lints: ^3.0.1 From a325bb2fccf68506d7f34b7b2114da118b872a3c Mon Sep 17 00:00:00 2001 From: Feichtmeier Date: Sun, 21 Apr 2024 20:12:46 +0200 Subject: [PATCH 2/4] Limit size for now, add ontap --- lib/src/app/app.dart | 3 ++- lib/src/weather/weather_page.dart | 7 +++++-- linux/my_application.cc | 4 ++-- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/lib/src/app/app.dart b/lib/src/app/app.dart index 08da6fe..d59d3b2 100644 --- a/lib/src/app/app.dart +++ b/lib/src/app/app.dart @@ -50,13 +50,14 @@ class MasterDetailPage extends StatelessWidget with WatchItMixin { final location = favLocations.elementAt(index); return YaruMasterTile( // TODO: assign pages to location - onTap: () {}, + onTap: () => model.init(cityName: location), selected: lastLocation == location, title: Text( favLocations.elementAt(index), ), trailing: favLocationsLength > 1 ? IconButton( + padding: EdgeInsets.zero, onPressed: () { model.removeFavLocation(location).then( (value) => model.init( diff --git a/lib/src/weather/weather_page.dart b/lib/src/weather/weather_page.dart index b1791a5..153987b 100644 --- a/lib/src/weather/weather_page.dart +++ b/lib/src/weather/weather_page.dart @@ -34,7 +34,10 @@ class WeatherPage extends StatelessWidget with WatchItMixin { ), Scaffold( backgroundColor: Colors.transparent, - appBar: const YaruWindowTitleBar( + appBar: YaruWindowTitleBar( + leading: Navigator.of(context).canPop() + ? const YaruBackButton() + : null, backgroundColor: Colors.transparent, border: BorderSide.none, ), @@ -53,7 +56,7 @@ class WeatherPage extends StatelessWidget with WatchItMixin { Expanded( child: TodayTile( width: mq.size.width, - padding: EdgeInsets.zero, + padding: const EdgeInsets.all(kYaruPagePadding), day: 'Now', height: mq.size.height, position: model.cityFromPosition, diff --git a/linux/my_application.cc b/linux/my_application.cc index f8bf8dd..9495b83 100644 --- a/linux/my_application.cc +++ b/linux/my_application.cc @@ -26,8 +26,8 @@ static void my_application_activate(GApplication* application) { gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(box)); GdkGeometry geometry_min; - geometry_min.min_width = 680; - geometry_min.min_height = 600; + geometry_min.min_width = 1200; + 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); From 8ee68901bb07fed3fbc8e1979f3b60f664ac262b Mon Sep 17 00:00:00 2001 From: Feichtmeier Date: Sun, 21 Apr 2024 20:14:52 +0200 Subject: [PATCH 3/4] format --- lib/src/weather/weather_page.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/weather/weather_page.dart b/lib/src/weather/weather_page.dart index 153987b..52d6319 100644 --- a/lib/src/weather/weather_page.dart +++ b/lib/src/weather/weather_page.dart @@ -125,4 +125,4 @@ class WeatherPage extends StatelessWidget with WatchItMixin { fontSize: 15, // borderRadius: getBorderRadius(i, model.notTodayForeCast), ), -] */ \ No newline at end of file +] */ From aa72cfbb14e0d1a9236f02e7e6aa21280ac3fa1a Mon Sep 17 00:00:00 2001 From: Feichtmeier Date: Sun, 21 Apr 2024 20:16:01 +0200 Subject: [PATCH 4/4] re-add empty key file --- .gitignore | 1 - assets/apikey.json | 3 +++ 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 assets/apikey.json diff --git a/.gitignore b/.gitignore index fd23e4c..2f36ff0 100644 --- a/.gitignore +++ b/.gitignore @@ -44,4 +44,3 @@ app.*.map.json /android/app/release .vscode/settings.json -assets/apikey.json diff --git a/assets/apikey.json b/assets/apikey.json new file mode 100644 index 0000000..e370bb6 --- /dev/null +++ b/assets/apikey.json @@ -0,0 +1,3 @@ +{ + "apiKey": "" +} \ No newline at end of file