diff --git a/frontend/android/app/build.gradle.kts b/frontend/android/app/build.gradle.kts index 909eb6a..445199c 100644 --- a/frontend/android/app/build.gradle.kts +++ b/frontend/android/app/build.gradle.kts @@ -20,12 +20,11 @@ android { } defaultConfig { - // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId = "app.odweta.novehlasky" // You can update the following values to match your application needs. // For more information, see: https://flutter.dev/to/review-gradle-config. minSdk = flutter.minSdkVersion - targetSdk = flutter.targetSdkVersion + targetSdk = 33 versionCode = flutter.versionCode versionName = flutter.versionName } diff --git a/frontend/android/app/src/main/AndroidManifest.xml b/frontend/android/app/src/main/AndroidManifest.xml index 2c734fc..0aef1e2 100644 --- a/frontend/android/app/src/main/AndroidManifest.xml +++ b/frontend/android/app/src/main/AndroidManifest.xml @@ -1,6 +1,8 @@ + + createState() => _ListPageState(); +} + +class _ListPageState extends State { @override Widget build(BuildContext context) { final provider = context.watch(); @@ -112,7 +117,7 @@ class ListPage extends StatelessWidget { ], ); } - + void _showUpdateDialog(BuildContext context, QuoteProvider provider, Quote quote) { final bodyController = TextEditingController(text: quote.body); final authorController = TextEditingController(text: quote.author); @@ -273,12 +278,15 @@ class ListPage extends StatelessWidget { ListTile( title: Text(Strings.exportSelectedQuotes), trailing: Icon(Icons.import_export), - onTap: () { - provider.exportQuotesToFile(); + onTap: () async { Navigator.pop(context); - ScaffoldMessenger.of(context).showSnackBar( + final scaffoldMessenger = ScaffoldMessenger.of(context); + final success = await provider.exportWithSAF(); + // Make sure the context is still valid + if (!mounted) return; + scaffoldMessenger.showSnackBar( SnackBar( - content: Text(Strings.fileExportedSuccessfully), + content: Text(success ? Strings.fileExportedSuccessfully + provider.exportDir : "export [x]"), duration: const Duration(seconds: 3), ), ); diff --git a/frontend/lib/provider/quote_provider.dart b/frontend/lib/provider/quote_provider.dart index 35b3702..660752d 100644 --- a/frontend/lib/provider/quote_provider.dart +++ b/frontend/lib/provider/quote_provider.dart @@ -1,7 +1,14 @@ // gets all quotes from the database and returns them in a list -import 'package:go_router/go_router.dart'; +import 'package:file_saver/file_saver.dart'; + import 'package:path_provider/path_provider.dart'; +import 'package:flutter/foundation.dart'; // for kIsWeb + +import 'dart:convert'; +import 'dart:typed_data'; +import 'package:go_router/go_router.dart'; +import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:frontend/model/quote.dart'; @@ -67,6 +74,8 @@ class QuoteProvider extends ChangeNotifier { late Quote randomQuote; bool randomQuoteDeterminator = true; + + String exportDir = ""; String get randomQuoteText { if (randomQuoteDeterminator) { return randomQuote.body; @@ -170,30 +179,28 @@ class QuoteProvider extends ChangeNotifier { notifyListeners(); } + Future exportWithSAF() async { + try { + final datetime = DateTime.now().toUtc().toString() + .replaceAll(" ", "T") + .replaceAll(":", "-") + .replaceAll(RegExp(r"\..*"), ""); + final filename = 'hlasky_$datetime.txt'; + final content = filteredQuotes.map((q) => "${q.body} – ${q.author}").join('\n') + '\n'; + final bytes = Uint8List.fromList(utf8.encode(content)); - Future _getSaveDirectory() async { - if (Platform.isWindows || Platform.isLinux || Platform.isMacOS) { - final path = Platform.isWindows - ? Platform.environment['USERPROFILE'] - : Platform.environment['HOME']; + // Show folder picker + final folderPath = await FilePicker.platform.getDirectoryPath(); + if (folderPath == null) return false; // user cancelled + exportDir = folderPath; - return Directory(path!); + final file = File('$folderPath/$filename'); + await file.writeAsBytes(bytes); + + return true; // export successful + } catch (e) { + return false; // something went wrong } - - // Mobile fallback - return await getApplicationDocumentsDirectory(); - } - - Future exportQuotesToFile() async { - final dir = await _getSaveDirectory(); - - final datetime = DateTime.now().toUtc().toString() - .replaceAll(" ", "T") - .replaceAll(":", "-") - .replaceAll(RegExp(r"\..*"), ""); - - final file = File('${dir.path}/hlasky_$datetime.txt'); - await file.writeAsString("${filteredQuotes.join("\n")}\n"); } Color getLeaderboardEntryBgColor(BuildContext context, int index) { diff --git a/frontend/lib/values/strings.dart b/frontend/lib/values/strings.dart index c7d8193..4acdc4d 100644 --- a/frontend/lib/values/strings.dart +++ b/frontend/lib/values/strings.dart @@ -23,7 +23,7 @@ class Strings { static const String nextFlashcard = "Další flashkarta"; //static const String importQuotesFromFile = "Importovat hlášky ze souboru"; - static const String fileExportedSuccessfully = "Soubor byl úspěšně vyexportován."; + static const String fileExportedSuccessfully = "Soubor byl úspěšně vyexportován do: "; static const String noUndo = "Tuto akci nelze odčinit."; static const String filterQuery = "Filtrovací požadavek"; static const String allQuotesCount = "Celkový počet hlášek"; diff --git a/frontend/linux/flutter/generated_plugin_registrant.cc b/frontend/linux/flutter/generated_plugin_registrant.cc index e71a16d..05397a8 100644 --- a/frontend/linux/flutter/generated_plugin_registrant.cc +++ b/frontend/linux/flutter/generated_plugin_registrant.cc @@ -6,6 +6,10 @@ #include "generated_plugin_registrant.h" +#include void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) file_saver_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FileSaverPlugin"); + file_saver_plugin_register_with_registrar(file_saver_registrar); } diff --git a/frontend/linux/flutter/generated_plugins.cmake b/frontend/linux/flutter/generated_plugins.cmake index 2e1de87..9336355 100644 --- a/frontend/linux/flutter/generated_plugins.cmake +++ b/frontend/linux/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + file_saver ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/frontend/macos/Flutter/GeneratedPluginRegistrant.swift b/frontend/macos/Flutter/GeneratedPluginRegistrant.swift index 724bb2a..93f820f 100644 --- a/frontend/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/frontend/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,8 +5,12 @@ import FlutterMacOS import Foundation +import file_picker +import file_saver import shared_preferences_foundation func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin")) + FileSaverPlugin.register(with: registry.registrar(forPlugin: "FileSaverPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) } diff --git a/frontend/pubspec.lock b/frontend/pubspec.lock index ad637dc..7c4ffb7 100644 --- a/frontend/pubspec.lock +++ b/frontend/pubspec.lock @@ -1,6 +1,14 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" async: dependency: transitive description: @@ -49,6 +57,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.19.1" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: "28bb3ae56f117b5aec029d702a90f57d285cd975c3c5c281eaca38dbc47c5937" + url: "https://pub.dev" + source: hosted + version: "0.3.5+2" crypto: dependency: transitive description: @@ -65,6 +81,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.9" + dbus: + dependency: transitive + description: + name: dbus + sha256: d0c98dcd4f5169878b6cf8f6e0a52403a9dff371a3e2f019697accbf6f44a270 + url: "https://pub.dev" + source: hosted + version: "0.7.12" + dio: + dependency: transitive + description: + name: dio + sha256: aff32c08f92787a557dd5c0145ac91536481831a01b4648136373cddb0e64f8c + url: "https://pub.dev" + source: hosted + version: "5.9.2" + dio_web_adapter: + dependency: transitive + description: + name: dio_web_adapter + sha256: "2f9e64323a7c3c7ef69567d5c800424a11f8337b8b228bad02524c9fb3c1f340" + url: "https://pub.dev" + source: hosted + version: "2.1.2" fake_async: dependency: transitive description: @@ -89,6 +129,22 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.1" + file_picker: + dependency: "direct main" + description: + name: file_picker + sha256: "57d9a1dd5063f85fa3107fb42d1faffda52fdc948cefd5fe5ea85267a5fc7343" + url: "https://pub.dev" + source: hosted + version: "10.3.10" + file_saver: + dependency: "direct main" + description: + name: file_saver + sha256: "9d93db09bd4da9e43238f9dd485360fc51a5c138eea5ef5f407ec56e58079ac0" + url: "https://pub.dev" + source: hosted + version: "0.3.1" flutter: dependency: "direct main" description: flutter @@ -102,6 +158,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.0.0" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + sha256: "38d1c268de9097ff59cf0e844ac38759fc78f76836d37edad06fa21e182055a0" + url: "https://pub.dev" + source: hosted + version: "2.0.34" flutter_test: dependency: "direct dev" description: flutter @@ -216,6 +280,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.17.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev" + source: hosted + version: "2.0.0" native_toolchain_c: dependency: transitive description: @@ -296,6 +368,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.0" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675" + url: "https://pub.dev" + source: hosted + version: "7.0.2" platform: dependency: transitive description: @@ -469,6 +549,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" + win32: + dependency: transitive + description: + name: win32 + sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e + url: "https://pub.dev" + source: hosted + version: "5.15.0" xdg_directories: dependency: transitive description: @@ -477,6 +565,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" + xml: + dependency: transitive + description: + name: xml + sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" + url: "https://pub.dev" + source: hosted + version: "6.6.1" yaml: dependency: transitive description: diff --git a/frontend/pubspec.yaml b/frontend/pubspec.yaml index ece9de9..39dc833 100644 --- a/frontend/pubspec.yaml +++ b/frontend/pubspec.yaml @@ -39,6 +39,8 @@ dependencies: http: ^1.2.0 path_provider: shared_preferences: ^2.1.1 + file_picker: ^10.3.10 + file_saver: ^0.3.1 dev_dependencies: flutter_test: diff --git a/frontend/windows/flutter/generated_plugin_registrant.cc b/frontend/windows/flutter/generated_plugin_registrant.cc index 8b6d468..8cce40f 100644 --- a/frontend/windows/flutter/generated_plugin_registrant.cc +++ b/frontend/windows/flutter/generated_plugin_registrant.cc @@ -6,6 +6,9 @@ #include "generated_plugin_registrant.h" +#include void RegisterPlugins(flutter::PluginRegistry* registry) { + FileSaverPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FileSaverPlugin")); } diff --git a/frontend/windows/flutter/generated_plugins.cmake b/frontend/windows/flutter/generated_plugins.cmake index b93c4c3..cd9db6c 100644 --- a/frontend/windows/flutter/generated_plugins.cmake +++ b/frontend/windows/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + file_saver ) list(APPEND FLUTTER_FFI_PLUGIN_LIST