diff --git a/android/app/build.gradle b/android/app/build.gradle index 19cf245..9a8b448 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -28,6 +28,8 @@ apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { compileSdkVersion flutter.compileSdkVersion + compileSdkVersion 33 + compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index f5aa7ca..ec5490a 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -7,6 +7,7 @@ + diff --git a/lib/main.dart b/lib/main.dart index a320819..3d0a82e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter_phoenix/flutter_phoenix.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:get/get.dart'; import 'package:hive_flutter/adapters.dart'; @@ -17,24 +18,10 @@ import 'pages/splash/splash_screen.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); + await initializeHive(); + await initializeGetX(); - // Initialize Hive and Hive Flutter - await Hive.initFlutter(); - registerAdapters(); - Hive.openBox('user'); - Hive.openBox('collection'); - Hive.openBox('watchlist'); - Hive.openBox('history'); - Hive.openBox('artists'); - - // Initialize the controllers - Get.put(MainController()); - Get.put(HomeDataController()); - Get.put(SearchBarController()); - Get.put(ProfileController()); - Get.put(CacheData()); - - runApp(const ProviderScope(child: App())); + runApp(ProviderScope(child: Phoenix(child: const App()))); } class App extends StatelessWidget { @@ -54,3 +41,24 @@ class App extends StatelessWidget { ); } } + +Future? initializeHive() async { + // Initialize Hive and Hive Flutter + await Hive.initFlutter(); + registerAdapters(); + Hive.openBox('user'); + Hive.openBox('collection'); + Hive.openBox('watchlist'); + Hive.openBox('history'); + Hive.openBox('artists'); +} + +Future? initializeGetX() { + // Initialize the controllers + Get.put(MainController()); + Get.put(HomeDataController()); + Get.put(SearchBarController()); + Get.put(ProfileController()); + Get.put(CacheData()); + return null; +} diff --git a/lib/models/hive/models/user.dart b/lib/models/hive/models/user.dart index 0bf2379..351fb78 100644 --- a/lib/models/hive/models/user.dart +++ b/lib/models/hive/models/user.dart @@ -13,4 +13,8 @@ class HiveUser extends HiveObject { late String username; @HiveField(UserFields.imageUrl) late String imageUrl; + + // factory HiveUser.fromJson(Map json) { + // return HiveUser..name = json['name']..username = json['username']..imageUrl = json['imageUrl']; + // } } diff --git a/lib/models/show_models/show_preview_model.dart b/lib/models/show_models/show_preview_model.dart index 2e4e818..1bb8e26 100644 --- a/lib/models/show_models/show_preview_model.dart +++ b/lib/models/show_models/show_preview_model.dart @@ -98,6 +98,22 @@ class ShowPreview { domestic: json['domestic'] ?? "", foreignLifetimeGross: json['foreignLifetimeGross'] ?? "", foreign: json['foreign'] ?? "", + companies: json['companies'] ?? "", + contentRating: json['contentRating'] ?? "", + countries: json['countries'] ?? "", + genres: json['genres'] ?? "", + languages: json['languages'] ?? "", + similars: json['similars'] == null + ? null + : [ + for (var similar in json['similars']!) + ShowPreview.fromJson(similar) + ], + watchDate: + json["watchDate"] == null ? null : DateTime.parse(json["watchDate"]), + watchTime: json['watchTime'] == null + ? null + : TimeOfDay.fromDateTime(DateTime.parse(json["watchDate"])), ); } @@ -105,10 +121,26 @@ class ShowPreview { 'id': show.id, 'rank': show.rank, 'title': show.title, + 'type': show.type, 'crew': show.crew, 'image': show.image, 'year': show.year, 'imDbRating': show.imDbRating, + 'imDbVotes': show.imDbVotes, + 'released': show.released, + 'seasonNumber': show.seasonNumber, + 'episodeNumber': show.episodeNumber, + 'plot': show.plot, + 'genres': show.genres, + 'countries': show.countries, + 'languages': show.languages, + 'companies': show.companies, + 'contentRating': show.contentRating, + 'watchDate': show.watchDate?.toString(), + 'watchTime': show.watchTime?.toString(), + 'similars': [ + for (var similar in show.similars!) ShowPreview.toMap(similar) + ] }; static String encode(List shows) => json.encode( diff --git a/lib/modules/Recommender/Recommender.dart b/lib/modules/Recommender/Recommender.dart index 55bf31f..a307c7e 100644 --- a/lib/modules/Recommender/Recommender.dart +++ b/lib/modules/Recommender/Recommender.dart @@ -8,6 +8,7 @@ Future recommender() async { PreferencesShareholder preferencesShareholder = PreferencesShareholder(); HomeDataController homeDataController = Get.find(); List> allLists = await preferencesShareholder.getAllLists(); + print(allLists); List allSimilars = await getAllSimilars(allLists: allLists); Map allSimilarsStats = {}; int i = 0; diff --git a/lib/modules/preferences/backup.dart b/lib/modules/preferences/backup.dart new file mode 100644 index 0000000..4b72bf8 --- /dev/null +++ b/lib/modules/preferences/backup.dart @@ -0,0 +1,185 @@ +import 'dart:convert'; +import 'dart:io'; +import 'package:external_path/external_path.dart'; +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/foundation.dart'; +import 'package:hive_flutter/hive_flutter.dart'; +import 'package:movielab/models/hive/models/show_preview.dart'; +import 'package:movielab/models/hive/models/user.dart'; +import 'package:movielab/models/show_models/show_preview_model.dart'; +import 'package:movielab/models/user_model/user_model.dart'; +import 'package:movielab/modules/preferences/preferences_shareholder.dart'; +import 'package:movielab/modules/recommender/recommender.dart'; +import 'package:movielab/pages/splash/get_user_data.dart'; +import 'package:permission_handler/permission_handler.dart'; + +Future createBackup() async { + try { + Map userData = {}; + + PreferencesShareholder preferencesShareholder = PreferencesShareholder(); + User user = await preferencesShareholder.getUser(); + userData['personal'] = { + "name": user.name, + "username": user.username, + "imageUrl": user.imageUrl + }; + + List watchlist = + await preferencesShareholder.getList(listName: "watchlist"); + userData['watchlist'] = [ + for (ShowPreview show in watchlist) ShowPreview.toMap(show) + ]; + List history = + await preferencesShareholder.getList(listName: "history"); + userData['history'] = [ + for (ShowPreview show in history) ShowPreview.toMap(show) + ]; + List collection = + await preferencesShareholder.getList(listName: "collection"); + userData['collection'] = [ + for (ShowPreview show in collection) ShowPreview.toMap(show) + ]; + List artists = + await preferencesShareholder.getList(listName: "artists"); + userData['artists'] = [ + for (ShowPreview show in artists) ShowPreview.toMap(show) + ]; + String jsonData = jsonEncode(userData); + await Permission.storage.request(); + String formattedDate = DateTime.now() + .toString() + .replaceAll('.', '-') + .replaceAll(' ', '-') + .replaceAll(':', '-'); + String? dir = await FilePicker.platform.getDirectoryPath(); + String path = '$dir/MovieLab-backup-$formattedDate.db'; + File backupFile = File(path); + await backupFile.writeAsString(jsonData); + return true; + } catch (e) { + return false; + } +} + +Future restoreBackup() async { + PreferencesShareholder shareholder = PreferencesShareholder(); + FilePickerResult? file = await FilePicker.platform.pickFiles( + allowMultiple: false, + initialDirectory: await ExternalPath.getExternalStoragePublicDirectory( + ExternalPath.DIRECTORY_DOCUMENTS), + dialogTitle: "MovieLab backup file"); + bool success = false; + if (file != null) { + File files = File(file.files.single.path.toString()); + + await backupFileTester(files).then((value) async { + if (value) { + Map backup = jsonDecode(await files.readAsString()); + Box user = Hive.box('user'); + Box watchlist = Hive.box('watchlist'); + Box history = Hive.box('history'); + Box collection = + Hive.box('collection'); + Box artists = Hive.box('artists'); + + user.deleteAt(0); + shareholder.deleteList("watchlist"); + shareholder.deleteList("history"); + shareholder.deleteList("collection"); + shareholder.deleteList("artists"); + + // print("history: ${history.length}"); + // for (int i = 0; i <= history.length + 1; i++) { + // print("history $i deliting"); + // history.delete(i); + // print("done"); + // } + // print(collection.length); + // for (int i = 0; i <= collection.length + 1; i++) { + // collection.delete(i); + // } + // print(artists.length); + // for (int i = 0; i <= artists.length; i++) { + // artists.delete(i); + // } + user.put( + 0, + HiveUser() + ..name = backup['personal']['name'] + ..username = backup['personal']['username'] + ..imageUrl = backup['personal']['imageUrl']); + + for (var item in backup['watchlist']) { + shareholder.addShowToList( + listName: 'watchlist', + showPreview: ShowPreview.fromJson(item), + genres: item['genres'], + countries: item['countries'], + languages: item['languages'], + companies: item['companies'], + contentRating: item['contentRating'], + similars: [ + for (Map show in item['similars']) + ShowPreview.fromJson(show) + ], + ); + } + + for (var item in backup['history']) { + shareholder.addShowToList( + listName: 'history', + showPreview: ShowPreview.fromJson(item), + genres: item['genres'], + countries: item['countries'], + languages: item['languages'], + companies: item['companies'], + contentRating: item['contentRating'], + similars: [ + for (Map show in item['similars']) + ShowPreview.fromJson(show) + ], + ); + } + for (var item in backup['collection']) { + shareholder.addShowToList( + listName: 'collection', + showPreview: ShowPreview.fromJson(item), + genres: item['genres'], + countries: item['countries'], + languages: item['languages'], + companies: item['companies'], + contentRating: item['contentRating'], + similars: [ + for (Map show in item['similars']) + ShowPreview.fromJson(show) + ], + ); + } + + recommender(); + getUserData(); + success = true; + } + }); + return success; + } else { + return success; + } +} + +Future backupFileTester(final File files) async { + try { + Map backup = jsonDecode(await files.readAsString()); + backup['personal']; + if (kDebugMode) { + print("The imported file is a real backup."); + } + return true; + } catch (e) { + if (kDebugMode) { + print('There\'s a problem with the imported file.'); + } + return false; + } +} diff --git a/lib/modules/preferences/preferences_shareholder.dart b/lib/modules/preferences/preferences_shareholder.dart index 5633db2..61d6cb9 100644 --- a/lib/modules/preferences/preferences_shareholder.dart +++ b/lib/modules/preferences/preferences_shareholder.dart @@ -1,13 +1,13 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:hive_flutter/adapters.dart'; +import 'package:movielab/models/hive/convertor.dart'; +import 'package:movielab/models/hive/models/show_preview.dart'; import 'package:movielab/models/hive/models/user.dart'; import 'package:movielab/models/show_models/show_preview_model.dart'; import 'package:movielab/models/user_model/user_model.dart'; import 'package:movielab/modules/Recommender/Recommender.dart'; import 'package:movielab/pages/splash/get_user_data.dart'; -import '../../models/hive/convertor.dart'; -import '../../models/hive/models/show_preview.dart'; class PreferencesShareholder { // Delete all items of a list in the shared preferences diff --git a/lib/pages/shared/settings_page/sections/backup_page.dart b/lib/pages/shared/settings_page/sections/backup_page.dart new file mode 100644 index 0000000..a4452f7 --- /dev/null +++ b/lib/pages/shared/settings_page/sections/backup_page.dart @@ -0,0 +1,115 @@ +import 'package:flutter/material.dart'; +import 'package:fluttertoast/fluttertoast.dart'; +import 'package:movielab/constants/colors.dart'; +import 'package:movielab/modules/preferences/backup.dart'; +import 'package:movielab/widgets/default_appbar.dart'; +import 'package:movielab/widgets/guide_modal.dart'; +import 'package:movielab/widgets/toast.dart'; + +import '../settings_page.dart'; + +class BackupPage extends StatefulWidget { + const BackupPage({Key? key}) : super(key: key); + + @override + State createState() => _BackupPageState(); +} + +class _BackupPageState extends State with TickerProviderStateMixin { + FToast fToast = FToast(); + + @override + Widget build(BuildContext context) { + fToast.init(context); + return Scaffold( + appBar: defaultAppBar(context, title: "Backup"), + body: SingleChildScrollView( + physics: const BouncingScrollPhysics(), + child: Column( + children: [ + settingSection( + icon: Icons.cloud_download_rounded, + iconSize: 30, + title: "Export backup file", + description: + "You can export the entire database for keeping a backup or to import it in a new device\nAfter exporting, it's recommended to upload this backup file to a cloud server like Google Drive", + onPressed: () async { + guideModalSheet(context, + vsync: this, + title: "Select File Location", + decription: + "Select the directory where you want to save this file.", + onTap: () async { + await createBackup().then((final bool success) { + if (success) { + fToast.removeQueuedCustomToasts(); + fToast.showToast( + child: ToastWidget( + mainText: "The backup file successfully created.", + buttonText: "Ok", + buttonColor: kAccentColor, + buttonOnTap: () async { + await Future.delayed( + const Duration(milliseconds: 200)); + fToast.removeCustomToast(); + }, + ), + gravity: ToastGravity.BOTTOM, + toastDuration: const Duration(seconds: 2), + ); + } else { + fToast.removeQueuedCustomToasts(); + fToast.showToast( + child: ToastWidget( + mainText: "Something went wrong!", + buttonText: "Ok", + buttonColor: kPrimaryColor, + buttonOnTap: () async { + await Future.delayed( + const Duration(milliseconds: 200)); + fToast.removeCustomToast(); + }, + ), + gravity: ToastGravity.BOTTOM, + toastDuration: const Duration(seconds: 2), + ); + } + }); + Navigator.pop(context); + }); + }), + settingSection( + icon: Icons.cloud_upload_rounded, + iconSize: 30, + title: "Import backup file", + description: + "Select the exported movielab-backup-date.db from file manager", + onPressed: () async { + await restoreBackup().then((final bool success) async { + fToast.removeQueuedCustomToasts(); + fToast.showToast( + child: ToastWidget( + mainText: success + ? "The backup file imported successfully." + : "There's a problem with the imported file!", + mainTextColor: success ? Colors.green : kPrimaryColor, + buttonText: "Ok", + fontSize: 13, + buttonColor: Colors.black, + buttonOnTap: () async { + await Future.delayed( + const Duration(milliseconds: 200)); + fToast.removeCustomToast(); + }, + ), + gravity: ToastGravity.BOTTOM, + toastDuration: const Duration(seconds: 2), + ); + }); + }), + ], + ), + ), + ); + } +} diff --git a/lib/pages/shared/settings_page/settings_page.dart b/lib/pages/shared/settings_page/settings_page.dart index 6ca4971..cd6e77c 100644 --- a/lib/pages/shared/settings_page/settings_page.dart +++ b/lib/pages/shared/settings_page/settings_page.dart @@ -1,8 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:fluttertoast/fluttertoast.dart'; -import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:movielab/constants/colors.dart'; +import 'package:movielab/modules/tools/navigate.dart'; +import 'package:movielab/pages/shared/settings_page/sections/backup_page.dart'; import 'package:movielab/widgets/default_appbar.dart'; import 'package:movielab/widgets/toast.dart'; @@ -19,7 +20,15 @@ class SettingsPage extends StatelessWidget { child: Column( children: [ settingSection( - icon: FontAwesomeIcons.xmark, + icon: Icons.backup_sharp, + title: "Offline import/export database", + description: + "Get a backup file of your personal data locally on your phone", + onPressed: () async { + Navigate.pushTo(context, BackupPage()); + }), + settingSection( + icon: Icons.cancel_sharp, title: "Clear media content cache", description: "Remove all cached content, but not your personal data", @@ -49,6 +58,7 @@ class SettingsPage extends StatelessWidget { Widget settingSection( {required final IconData icon, + double iconSize = 30, required final String title, required final String description, required final void Function() onPressed}) { @@ -56,6 +66,7 @@ Widget settingSection( leading: Icon( icon, color: Colors.white, + size: iconSize, ), title: Padding( padding: const EdgeInsets.only(bottom: 5), diff --git a/lib/pages/splash/get_user_data.dart b/lib/pages/splash/get_user_data.dart index 135d413..bf4d662 100644 --- a/lib/pages/splash/get_user_data.dart +++ b/lib/pages/splash/get_user_data.dart @@ -1,3 +1,4 @@ +import 'package:flutter/foundation.dart'; import 'package:get/get.dart'; import 'package:movielab/models/show_models/show_preview_model.dart'; import 'package:movielab/models/user_model/user_model.dart'; @@ -202,7 +203,9 @@ Future updateUserStats() async { sortedContentRatings: sortedContentRatings, contentRatingsLength: contentRatingsLength, contentRatingsOthers: contentRatingsOthers); - print("User stats updated"); + if (kDebugMode) { + print("User stats updated"); + } return true; } return false; diff --git a/lib/widgets/buttons/activeable_button.dart b/lib/widgets/buttons/activeable_button.dart index a7907fa..ab0c6fb 100644 --- a/lib/widgets/buttons/activeable_button.dart +++ b/lib/widgets/buttons/activeable_button.dart @@ -4,7 +4,7 @@ import 'package:flutter_spinkit/flutter_spinkit.dart'; class ActiveableButton extends StatelessWidget { final String text; final String? activeText; - final IconData icon; + final IconData? icon; final IconData? activeIcon; final VoidCallback onTap; final bool isActive; @@ -52,14 +52,19 @@ class ActiveableButton extends StatelessWidget { ), ), child: Row( + mainAxisAlignment: icon != null + ? MainAxisAlignment.start + : MainAxisAlignment.center, children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 25), - child: Icon( - isActive ? activeIcon ?? icon : icon, - size: 20, - ), - ), + icon != null + ? Padding( + padding: const EdgeInsets.symmetric(horizontal: 25), + child: Icon( + isActive ? activeIcon ?? icon : icon, + size: 20, + ), + ) + : const SizedBox.shrink(), Text( isActive ? activeText ?? text : text, style: const TextStyle( diff --git a/lib/widgets/guide_modal.dart b/lib/widgets/guide_modal.dart new file mode 100644 index 0000000..6337189 --- /dev/null +++ b/lib/widgets/guide_modal.dart @@ -0,0 +1,63 @@ +import 'package:flutter/material.dart'; +import 'package:movielab/constants/colors.dart'; +import 'package:movielab/widgets/buttons/activeable_button.dart'; + +Future guideModalSheet(BuildContext context, + {required dynamic vsync, + final Color backgroundColor = kBackgroundColor, + required final String title, + required final String decription, + required final VoidCallback onTap}) { + return showModalBottomSheet( + context: context, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: Radius.circular(30), + )), + clipBehavior: Clip.antiAliasWithSaveLayer, + transitionAnimationController: AnimationController( + duration: const Duration(milliseconds: 250), vsync: vsync), + builder: (context) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 50), + color: backgroundColor, + height: 250, + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + width: MediaQuery.of(context).size.width / 3, + margin: const EdgeInsets.only(top: 8.5, bottom: 32.5), + height: 3, + color: Colors.white.withOpacity(0.4), + ), + Text( + title, + style: + const TextStyle(fontWeight: FontWeight.w600, fontSize: 18.5), + ), + const SizedBox( + height: 30, + ), + Text( + decription, + textAlign: TextAlign.center, + style: + const TextStyle(fontWeight: FontWeight.w500, fontSize: 16.5), + ), + const SizedBox( + height: 30, + ), + ActiveableButton( + isActive: true, + text: "Select Folder", + icon: null, + onTap: onTap, + activeColor: kAccentColor, + ) + ], + ), + ); + }, + ); +} diff --git a/lib/widgets/toast.dart b/lib/widgets/toast.dart index 866284b..08da2bf 100644 --- a/lib/widgets/toast.dart +++ b/lib/widgets/toast.dart @@ -2,12 +2,16 @@ import 'package:flutter/material.dart'; class ToastWidget extends StatelessWidget { final String mainText; + final Color mainTextColor; final String buttonText; final Color buttonColor; final VoidCallback buttonOnTap; + final double fontSize; const ToastWidget( {Key? key, required this.mainText, + this.mainTextColor = Colors.black, + this.fontSize = 14, required this.buttonText, required this.buttonColor, required this.buttonOnTap}) @@ -29,10 +33,10 @@ class ToastWidget extends StatelessWidget { children: [ Text( mainText, - style: const TextStyle( - fontWeight: FontWeight.w600, - color: Colors.black, - ), + style: TextStyle( + fontWeight: FontWeight.w600, + color: mainTextColor, + fontSize: fontSize), ), TextButton( onPressed: buttonOnTap, @@ -40,8 +44,10 @@ class ToastWidget extends StatelessWidget { TextButton.styleFrom(primary: buttonColor.withOpacity(0.5)), child: Text( buttonText, - style: - TextStyle(fontWeight: FontWeight.w600, color: buttonColor), + style: TextStyle( + fontWeight: FontWeight.w600, + color: buttonColor, + fontSize: fontSize), )) ], ), diff --git a/pubspec.lock b/pubspec.lock index e495ea9..fcf6c7a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -238,7 +238,7 @@ packages: name: ffi url: "https://pub.dartlang.org" source: hosted - version: "1.1.2" + version: "2.0.1" file: dependency: transitive description: @@ -246,6 +246,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "6.1.2" + file_picker: + dependency: "direct main" + description: + name: file_picker + url: "https://pub.dartlang.org" + source: hosted + version: "5.0.1" fixnum: dependency: transitive description: @@ -286,6 +293,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.1" + flutter_phoenix: + dependency: "direct main" + description: + name: flutter_phoenix + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" flutter_plugin_android_lifecycle: dependency: transitive description: @@ -624,7 +638,7 @@ packages: name: path_provider_linux url: "https://pub.dartlang.org" source: hosted - version: "2.1.5" + version: "2.1.7" path_provider_macos: dependency: transitive description: @@ -645,7 +659,7 @@ packages: name: path_provider_windows url: "https://pub.dartlang.org" source: hosted - version: "2.0.5" + version: "2.1.2" pedantic: dependency: transitive description: @@ -653,6 +667,41 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.11.1" + permission_handler: + dependency: "direct main" + description: + name: permission_handler + url: "https://pub.dartlang.org" + source: hosted + version: "10.0.0" + permission_handler_android: + dependency: transitive + description: + name: permission_handler_android + url: "https://pub.dartlang.org" + source: hosted + version: "10.0.0" + permission_handler_apple: + dependency: transitive + description: + name: permission_handler_apple + url: "https://pub.dartlang.org" + source: hosted + version: "9.0.4" + permission_handler_platform_interface: + dependency: transitive + description: + name: permission_handler_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "3.7.0" + permission_handler_windows: + dependency: transitive + description: + name: permission_handler_windows + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.0" petitparser: dependency: transitive description: @@ -1098,7 +1147,7 @@ packages: name: win32 url: "https://pub.dartlang.org" source: hosted - version: "2.3.10" + version: "2.7.0" xdg_directories: dependency: transitive description: @@ -1121,5 +1170,5 @@ packages: source: hosted version: "3.1.0" sdks: - dart: ">=2.17.0-206.0.dev <3.0.0" + dart: ">=2.17.0 <3.0.0" flutter: ">=3.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index 69af281..dcc4e76 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -44,6 +44,9 @@ dependencies: flutter_cache_manager: ^3.3.0 gallery_saver: ^2.3.2 external_path: ^1.0.1 + permission_handler: ^10.0.0 + file_picker: ^5.0.1 + flutter_phoenix: ^1.1.0