diff --git a/.gitignore b/.gitignore index 3ddb2de..4314849 100644 --- a/.gitignore +++ b/.gitignore @@ -41,6 +41,4 @@ app.*.map.json # Android Studio will place build artifacts here /android/app/debug /android/app/profile -/android/app/release - -/lib/secret_key.dart \ No newline at end of file +/android/app/release \ No newline at end of file diff --git a/lib/app/modules/home.dart b/lib/app/modules/home.dart index 2d97edc..00fcb63 100644 --- a/lib/app/modules/home.dart +++ b/lib/app/modules/home.dart @@ -85,154 +85,156 @@ class _HomePageState extends State with TickerProviderStateMixin { child: ScaffoldMessenger( key: globalKey, child: Scaffold( - appBar: AppBar( - centerTitle: true, - automaticallyImplyLeading: false, - leading: switch (tabIndex) { - 0 => IconButton( - onPressed: () { - Get.to(() => const SelectGeolocation(isStart: false), - transition: Transition.downToUp); + appBar: tabIndex == 2 + ? null + : AppBar( + centerTitle: true, + automaticallyImplyLeading: false, + leading: switch (tabIndex) { + 0 => IconButton( + onPressed: () { + Get.to(() => const SelectGeolocation(isStart: false), + transition: Transition.downToUp); + }, + icon: const Icon( + IconsaxPlusLinear.global_search, + size: 18, + ), + ), + int() => null, }, - icon: const Icon( - IconsaxPlusLinear.global_search, - size: 18, - ), - ), - int() => null, - }, - title: switch (tabIndex) { - 0 => visible - ? RawAutocomplete( - 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.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 onSelected, - Iterable 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, - ), + title: switch (tabIndex) { + 0 => visible + ? RawAutocomplete( + 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.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 onSelected, + Iterable 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, + ), + ), + ); + }, ), - ); - }, - ), - ), + ), + ), + ); + }, + ) + : 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, + ); + }, ), - ); - }, - ) - : 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, - ); - }, - ), - 1 => Text( - 'cities'.tr, - style: textStyle, + 1 => Text( + 'cities'.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, + }, ), - 2 => 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( child: TabBarView( controller: tabController, diff --git a/lib/app/modules/map/view/map.dart b/lib/app/modules/map/view/map.dart index 4eadf25..4144e21 100644 --- a/lib/app/modules/map/view/map.dart +++ b/lib/app/modules/map/view/map.dart @@ -1,17 +1,19 @@ import 'dart:io'; +import 'package:dio_cache_interceptor/dio_cache_interceptor.dart'; +import 'package:dio_cache_interceptor_file_store/dio_cache_interceptor_file_store.dart'; import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map_animations/flutter_map_animations.dart'; import 'package:flutter_map_cache/flutter_map_cache.dart'; import 'package:get/get.dart'; -import 'package:iconsax_plus/iconsax_plus.dart'; import 'package:latlong2/latlong.dart'; import 'package:path_provider/path_provider.dart'; import 'package:rain/app/controller/controller.dart'; -import 'package:dio_cache_interceptor/dio_cache_interceptor.dart'; -import 'package:dio_cache_interceptor_file_store/dio_cache_interceptor_file_store.dart'; +import 'package:rain/app/data/weather.dart'; +import 'package:rain/app/modules/cards/view/info_weather_card.dart'; import 'package:rain/app/modules/cards/widgets/create_card_weather.dart'; -import 'package:rain/secret_key.dart'; +import 'package:rain/app/modules/cards/widgets/weather_card_container.dart'; +import 'package:rain/app/widgets/status/status_weather.dart'; class MapWeather extends StatefulWidget { const MapWeather({super.key}); @@ -21,173 +23,230 @@ class MapWeather extends StatefulWidget { } class _MapWeatherState extends State with TickerProviderStateMixin { - late final _animatedMapController = AnimatedMapController(vsync: this); + late final AnimatedMapController _animatedMapController = + AnimatedMapController(vsync: this); final weatherController = Get.put(WeatherController()); - bool isDarkMode = Get.theme.brightness == Brightness.dark; - + final statusWeather = StatusWeather(); final Future _cacheStoreFuture = _getCacheStore(); + final bool _isDarkMode = Get.theme.brightness == Brightness.dark; + WeatherCard? _selectedWeatherCard; + bool _isCardVisible = false; + double _cardBottomPosition = -200; + static Future _getCacheStore() async { final dir = await getTemporaryDirectory(); return FileCacheStore('${dir.path}${Platform.pathSeparator}MapTiles'); } + void _onMarkerTap(WeatherCard weatherCard) { + setState(() { + _selectedWeatherCard = weatherCard; + _cardBottomPosition = 0; + _isCardVisible = true; + }); + } + + void _hideCard() { + setState(() { + _cardBottomPosition = -200; + }); + Future.delayed(const Duration(milliseconds: 300), () { + setState(() { + _isCardVisible = false; + }); + }); + } + + Marker _buildMainLocationMarker( + WeatherCard weatherCard, int hourOfDay, int dayOfNow) { + return Marker( + height: 40, + width: 40, + point: LatLng(weatherCard.lat!, weatherCard.lon!), + child: GestureDetector( + onTap: () => _onMarkerTap(weatherCard), + child: Container( + decoration: BoxDecoration( + color: context.theme.colorScheme.onSecondary, + shape: BoxShape.circle, + ), + child: Image.asset( + statusWeather.getImageNow( + weatherCard.weathercode![hourOfDay], + weatherCard.time![hourOfDay], + weatherCard.sunrise![dayOfNow], + weatherCard.sunset![dayOfNow], + ), + scale: 15, + ), + ), + ), + ); + } + + Marker _buildCardMarker(WeatherCard weatherCardList) { + return Marker( + height: 40, + width: 40, + point: LatLng(weatherCardList.lat!, weatherCardList.lon!), + child: GestureDetector( + onTap: () => _onMarkerTap(weatherCardList), + child: Container( + decoration: BoxDecoration( + color: context.theme.colorScheme.onSecondary, + shape: BoxShape.circle, + ), + child: Image.asset( + statusWeather.getImageNow( + weatherCardList.weathercode![weatherController.getTime( + weatherCardList.time!, weatherCardList.timezone!)], + weatherCardList.time![weatherController.getTime( + weatherCardList.time!, weatherCardList.timezone!)], + weatherCardList.sunrise![weatherController.getDay( + weatherCardList.timeDaily!, weatherCardList.timezone!)], + weatherCardList.sunset![weatherController.getDay( + weatherCardList.timeDaily!, weatherCardList.timezone!)], + ), + scale: 15, + ), + ), + ), + ); + } + + Widget _buildMapTileLayer(CacheStore cacheStore) { + return TileLayer( + urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', + userAgentPackageName: 'com.darkmoonight.rain', + tileProvider: CachedTileProvider( + store: cacheStore, + maxStale: const Duration(days: 30), + ), + ); + } + + Widget _buildWeatherCard() { + return AnimatedPositioned( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + left: 0, + right: 0, + bottom: _cardBottomPosition, + child: AnimatedOpacity( + opacity: _isCardVisible ? 1.0 : 0.0, + duration: const Duration(milliseconds: 300), + child: _isCardVisible + ? GestureDetector( + onTap: () => Get.to( + () => InfoWeatherCard(weatherCard: _selectedWeatherCard!), + transition: Transition.downToUp, + ), + child: WeatherCardContainer( + time: _selectedWeatherCard!.time!, + timeDaily: _selectedWeatherCard!.timeDaily!, + timeDay: _selectedWeatherCard!.sunrise!, + timeNight: _selectedWeatherCard!.sunset!, + weather: _selectedWeatherCard!.weathercode!, + degree: _selectedWeatherCard!.temperature2M!, + district: _selectedWeatherCard!.district!, + city: _selectedWeatherCard!.city!, + timezone: _selectedWeatherCard!.timezone!, + ), + ) + : const SizedBox.shrink(), + ), + ); + } + @override Widget build(BuildContext context) { final mainLocation = weatherController.location; - // final mainWeather = weatherController.mainWeather; - // final weatherCard = WeatherCard.fromJson({} - // ..addAll(mainWeather.toJson()) - // ..addAll(mainLocation.toJson())); + final mainWeather = weatherController.mainWeather; - Widget darkModeTilesContainerBuilder( - BuildContext context, - Widget tilesContainer, - ) { - return ColorFiltered( - colorFilter: const ColorFilter.matrix([ - -0.2126, -0.7152, -0.0722, 0, 255, // Red channel - -0.2126, -0.7152, -0.0722, 0, 255, // Green channel - -0.2126, -0.7152, -0.0722, 0, 255, // Blue channel - 0, 0, 0, 1, 0, // Alpha channel - ]), - child: tilesContainer, - ); - } - - Widget openStreetMapTileLayer(CacheStore cacheStore) { - return TileLayer( - urlTemplate: - 'https://api.mapbox.com/styles/v1/yoshimok/clzvnt6ae000s01qsh52veh8f/tiles/256/{z}/{x}/{y}@2x?access_token=$accessToken', - userAgentPackageName: 'com.darkmoonight.rain', - tileProvider: CachedTileProvider( - store: cacheStore, - maxStale: const Duration(days: 30), - )); - } + final hourOfDay = weatherController.hourOfDay.value; + final dayOfNow = weatherController.dayOfNow.value; return FutureBuilder( future: _cacheStoreFuture, builder: (context, snapshot) { - if (snapshot.hasData) { - final cacheStore = snapshot.data!; - return FlutterMap( - mapController: _animatedMapController.mapController, - options: MapOptions( - backgroundColor: context.theme.scaffoldBackgroundColor, - initialCenter: LatLng(mainLocation.lat!, mainLocation.lon!), - initialZoom: 12, - cameraConstraint: CameraConstraint.contain( - bounds: LatLngBounds( - const LatLng(-90, -180), - const LatLng(90, 180), - ), - ), - onLongPress: (tapPosition, point) => showModalBottomSheet( - context: context, - isScrollControlled: true, - enableDrag: false, - builder: (BuildContext context) => CreateWeatherCard( - latitude: '${point.latitude}', - longitude: '${point.longitude}', - ), - ), - ), - children: [ - isDarkMode - ? darkModeTilesContainerBuilder( - context, openStreetMapTileLayer(cacheStore)) - : openStreetMapTileLayer(cacheStore), - RichAttributionWidget( - animationConfig: const ScaleRAWA(), - attributions: [ - TextSourceAttribution( - 'Mapbox contributors', - onTap: () => weatherController - .urlLauncher('https://www.mapbox.com/legal/tos'), - ), - ], - ), - Obx( - () { - var weatherCards = weatherController.weatherCards.toList(); - return MarkerLayer( - markers: [ - Marker( - point: LatLng(mainLocation.lat!, mainLocation.lon!), - child: GestureDetector( - onTap: () {}, - child: Container( - decoration: BoxDecoration( - color: - context.theme.colorScheme.secondaryContainer, - borderRadius: BorderRadius.circular(10), - ), - child: Icon( - IconsaxPlusBold.home, - color: context - .theme.colorScheme.onSecondaryContainer, - ), - ), - ), - ), - ...weatherCards.map( - (weatherCardList) => Marker( - width: 35, - height: 35, - point: LatLng( - weatherCardList.lat!, weatherCardList.lon!), - child: GestureDetector( - onTap: () {}, - child: Container( - decoration: BoxDecoration( - color: context - .theme.colorScheme.secondaryContainer, - borderRadius: BorderRadius.circular(10), - ), - child: const Icon( - IconsaxPlusBold.location, - color: Colors.red, - ), - ), - ), - ), - ) - ], - ); - }, - ), - // Positioned( - // left: 0, - // right: 0, - // bottom: 0, - // child: GestureDetector( - // onTap: () => Get.to( - // () => InfoWeatherCard( - // weatherCard: weatherCard, - // ), - // transition: Transition.downToUp, - // ), - // child: WeatherCardContainer( - // time: mainWeather.time!, - // timeDaily: mainWeather.timeDaily!, - // timeDay: mainWeather.sunrise!, - // timeNight: mainWeather.sunset!, - // weather: mainWeather.weathercode!, - // degree: mainWeather.temperature2M!, - // district: mainLocation.district!, - // city: mainLocation.city!, - // timezone: mainWeather.timezone!, - // ), - // ), - // ) - ], - ); + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center(child: CircularProgressIndicator()); } + if (snapshot.hasError) { return Center(child: Text(snapshot.error.toString())); } - return const Center(child: CircularProgressIndicator()); + + final cacheStore = snapshot.data!; + + return FlutterMap( + mapController: _animatedMapController.mapController, + options: MapOptions( + backgroundColor: context.theme.scaffoldBackgroundColor, + initialCenter: LatLng(mainLocation.lat!, mainLocation.lon!), + initialZoom: 12, + cameraConstraint: CameraConstraint.contain( + bounds: LatLngBounds( + const LatLng(-90, -180), + const LatLng(90, 180), + ), + ), + onTap: (_, __) => _hideCard(), + onLongPress: (tapPosition, point) => showModalBottomSheet( + context: context, + isScrollControlled: true, + enableDrag: false, + builder: (BuildContext context) => CreateWeatherCard( + latitude: '${point.latitude}', + longitude: '${point.longitude}', + ), + ), + ), + children: [ + if (_isDarkMode) + ColorFiltered( + colorFilter: const ColorFilter.matrix([ + -0.2126, -0.7152, -0.0722, 0, 255, // Red channel + -0.2126, -0.7152, -0.0722, 0, 255, // Green channel + -0.2126, -0.7152, -0.0722, 0, 255, // Blue channel + 0, 0, 0, 1, 0, // Alpha channel + ]), + child: _buildMapTileLayer(cacheStore), + ) + else + _buildMapTileLayer(cacheStore), + RichAttributionWidget( + animationConfig: const ScaleRAWA(), + attributions: [ + TextSourceAttribution( + 'OpenStreetMap contributors', + onTap: () => weatherController + .urlLauncher('https://openstreetmap.org/copyright'), + ), + ], + ), + Obx(() { + final mainMarker = _buildMainLocationMarker( + WeatherCard.fromJson({ + ...mainWeather.toJson(), + ...mainLocation.toJson(), + }), + hourOfDay, + dayOfNow, + ); + + final cardMarkers = weatherController.weatherCards + .map((weatherCardList) => _buildCardMarker(weatherCardList)) + .toList(); + + return MarkerLayer( + markers: [mainMarker, ...cardMarkers], + ); + }), + _buildWeatherCard(), + ], + ); }, ); } diff --git a/lib/app/modules/settings/view/settings.dart b/lib/app/modules/settings/view/settings.dart index c9e8b5a..609ebfa 100644 --- a/lib/app/modules/settings/view/settings.dart +++ b/lib/app/modules/settings/view/settings.dart @@ -15,7 +15,6 @@ import 'package:rain/app/modules/settings/widgets/setting_card.dart'; import 'package:rain/main.dart'; import 'package:rain/theme/theme_controller.dart'; import 'package:rain/utils/color_converter.dart'; -import 'package:url_launcher/url_launcher.dart'; class SettingsPage extends StatefulWidget { const SettingsPage({super.key}); @@ -978,27 +977,15 @@ class _SettingsPageState extends State { elevation: 4, icon: const Icon(LineAwesomeIcons.discord), text: 'Discord', - onPressed: () async { - final Uri url = Uri.parse( - 'https://discord.gg/JMMa9aHh8f'); - if (!await launchUrl(url, - mode: LaunchMode.externalApplication)) { - throw Exception('Could not launch $url'); - } - }, + onPressed: () => weatherController.urlLauncher( + 'https://discord.gg/JMMa9aHh8f'), ), SettingCard( elevation: 4, icon: const Icon(LineAwesomeIcons.telegram), text: 'Telegram', - onPressed: () async { - final Uri url = - Uri.parse('https://t.me/darkmoonightX'); - if (!await launchUrl(url, - mode: LaunchMode.externalApplication)) { - throw Exception('Could not launch $url'); - } - }, + onPressed: () => weatherController + .urlLauncher('https://t.me/darkmoonightX'), ), const Gap(10), ],