diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index ef8426e..b4076c6 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -3,7 +3,8 @@ + android:icon="@mipmap/ic_launcher" + android:requestLegacyExternalStorage="true"> :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/ios/Podfile.lock b/ios/Podfile.lock new file mode 100644 index 0000000..4ca8d62 --- /dev/null +++ b/ios/Podfile.lock @@ -0,0 +1,35 @@ +PODS: + - Flutter (1.0.0) + - flutter_pdfview (1.0.2): + - Flutter + - open_filex (0.0.2): + - Flutter + - path_provider_foundation (0.0.1): + - Flutter + - FlutterMacOS + +DEPENDENCIES: + - Flutter (from `Flutter`) + - flutter_pdfview (from `.symlinks/plugins/flutter_pdfview/ios`) + - open_filex (from `.symlinks/plugins/open_filex/ios`) + - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/ios`) + +EXTERNAL SOURCES: + Flutter: + :path: Flutter + flutter_pdfview: + :path: ".symlinks/plugins/flutter_pdfview/ios" + open_filex: + :path: ".symlinks/plugins/open_filex/ios" + path_provider_foundation: + :path: ".symlinks/plugins/path_provider_foundation/ios" + +SPEC CHECKSUMS: + Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 + flutter_pdfview: 25f53dd6097661e6395b17de506e6060585946bd + open_filex: 6e26e659846ec990262224a12ef1c528bb4edbe4 + path_provider_foundation: c68054786f1b4f3343858c1e1d0caaded73f0be9 + +PODFILE CHECKSUM: ef19549a9bc3046e7bb7d2fab4d021637c0c58a3 + +COCOAPODS: 1.12.0 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index b9ca151..e62d97f 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -10,6 +10,7 @@ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 887AC5A23721A1F877B7F356 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 22F4EF952F13499F3F7E0767 /* Pods_Runner.framework */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; @@ -31,10 +32,13 @@ /* Begin PBXFileReference section */ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 22F4EF952F13499F3F7E0767 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 6DF3AEE2F28EB639A4DAC27D /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 7C05F0E4FAD32985FF3457E6 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -42,6 +46,7 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 987D10A8EF2F80B25A5E7A3F /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -49,12 +54,21 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 887AC5A23721A1F877B7F356 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 04349F47C2218790A23C371A /* Frameworks */ = { + isa = PBXGroup; + children = ( + 22F4EF952F13499F3F7E0767 /* Pods_Runner.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( @@ -72,6 +86,8 @@ 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, + CA24959D5C511E1D91F51A51 /* Pods */, + 04349F47C2218790A23C371A /* Frameworks */, ); sourceTree = ""; }; @@ -98,6 +114,17 @@ path = Runner; sourceTree = ""; }; + CA24959D5C511E1D91F51A51 /* Pods */ = { + isa = PBXGroup; + children = ( + 7C05F0E4FAD32985FF3457E6 /* Pods-Runner.debug.xcconfig */, + 6DF3AEE2F28EB639A4DAC27D /* Pods-Runner.release.xcconfig */, + 987D10A8EF2F80B25A5E7A3F /* Pods-Runner.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -105,12 +132,14 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( + 0658BB80335F7C9530B062AC /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + 43594F3159E5854627626A1B /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -169,6 +198,28 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ + 0658BB80335F7C9530B062AC /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -184,6 +235,23 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; + 43594F3159E5854627626A1B /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; diff --git a/ios/Runner.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcworkspace/contents.xcworkspacedata index 1d526a1..21a3cc1 100644 --- a/ios/Runner.xcworkspace/contents.xcworkspacedata +++ b/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -4,4 +4,7 @@ + + diff --git a/lib/controllers/ticket_controller.dart b/lib/controllers/ticket_controller.dart new file mode 100644 index 0000000..f6f6454 --- /dev/null +++ b/lib/controllers/ticket_controller.dart @@ -0,0 +1,125 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:surf_flutter_study_jam_2023/models/ticket.dart'; +import 'package:surf_flutter_study_jam_2023/views/ticket_create_view.dart'; +import 'package:flutter/services.dart'; + +import '../services/fileDownloader.dart'; + +class TicketController { + final downloader = FileDownloader(); + + // todo: Попробовать сделать через ValueNotifier + // ValueNotifier _myModel = ValueNotifier('initial data'); + + final StreamController> _ticketStreamController = + StreamController>.broadcast(); + + Stream> get tasksStream => _ticketStreamController.stream; + + List _tickets = []; + + Future downloadAllTicket( + BuildContext context, List? tickets, Function() callback) async { + for (var ticket in tickets!) { + this.downloadTicket(context, ticket, () => callback); + } + callback(); + } + + Future downloadTicket( + BuildContext context, Ticket ticket, Function() callback) async { + ticket.isLoading = true; + callback(); + + if (ticket.url != null && ticket.url!.isNotEmpty) { + try { + final file = await downloader.downloadFile( + ticket.url!, + "${ticket.id}.pdf", + (received, total) { + if (total != -1) { + print('${(received / total * 100).toStringAsFixed(0)}%'); + ticket.persentLoaded = (received / total * 100).toInt(); + callback(); + } + }, + ); + + print('Файл загружен и сохранен: ${file.path}'); + ticket.filePath = file.path; + ticket.isLoaded = true; + callback(); + } catch (e) { + print('Ошибка загрузки файла: $e'); + } + } + ticket.isLoading = false; + callback(); + } + + void addTicket(Ticket task) { + task.id = DateTime.now().millisecondsSinceEpoch; + task.fileName = task.url?.split("/").last; + print(task.fileName); + _tickets.add(task); + _dispatch(); + } + + void deleteTicket(Ticket task) { + // todo: Добавить возможность удаления билетов + } + + bool validateUrl(String url) { + bool isURLValid = false; + try { + isURLValid = Uri.parse(url).host.isNotEmpty; + } catch (e) {} + + if (isURLValid) { + if (url.endsWith(".pdf")) { + return true; + } + } + return false; + } + + Future handleAddTicketButtonPressed(BuildContext context) async { + ClipboardData? cdata = await Clipboard.getData(Clipboard.kTextPlain); + String? copiedtext = cdata?.text; + String copiedUrl = ""; + if (copiedtext != null && copiedtext.isNotEmpty) { + if (this.validateUrl(copiedtext!)) { + copiedUrl = copiedtext; + } + } + + // ignore: use_build_context_synchronously + showModalBottomSheet( + context: context, + isScrollControlled: true, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: Radius.circular(30), + )), + builder: (context) => DraggableScrollableSheet( + initialChildSize: 0.5, + maxChildSize: 0.5, + minChildSize: 0.5, + expand: false, + snap: true, + builder: (context, scrollController) { + return TicketCreateView(this, copiedUrl); + }), + ); + } + + void _dispatch() { + _ticketStreamController.sink.add(_tickets); + } + + void dispose() { + _ticketStreamController.close(); + } +} diff --git a/lib/features/ticket_storage/ticket_storage_page.dart b/lib/features/ticket_storage/ticket_storage_page.dart deleted file mode 100644 index 1ab6dd5..0000000 --- a/lib/features/ticket_storage/ticket_storage_page.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'package:flutter/material.dart'; - -/// Экран “Хранения билетов”. -class TicketStoragePage extends StatelessWidget { - const TicketStoragePage({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return const FlutterLogo(); - } -} diff --git a/lib/main.dart b/lib/main.dart index c1be491..99a8a42 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,21 +1,43 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; -import 'package:surf_flutter_study_jam_2023/features/ticket_storage/ticket_storage_page.dart'; +import 'package:surf_flutter_study_jam_2023/controllers/ticket_controller.dart'; +import 'package:surf_flutter_study_jam_2023/views/ticket_list_view.dart'; +import 'package:flutter/services.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:surf_flutter_study_jam_2023/views/ticket_open_view.dart'; + +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); + Directory appDocDir = await getApplicationDocumentsDirectory(); -void main() { - runApp(const MyApp()); + // await PathProvider.registerWith(Registrar()); + runApp(MyApp()); } class MyApp extends StatelessWidget { - const MyApp({Key? key}) : super(key: key); + // const MyApp({super.key}); + + final TicketController _ticketController = TicketController(); + // This widget is the root of your application. @override Widget build(BuildContext context) { return MaterialApp( - title: 'Flutter Demo', - theme: ThemeData( - primarySwatch: Colors.blue, - ), - home: const TicketStoragePage(), + title: 'Ticket List', + initialRoute: '/', + routes: { + TicketListView.routeName: (context) => + TicketListView(ticketController: _ticketController), + TicketOpenView.routeName: (context) => const TicketOpenView(), + }, ); } + + @override + void dispose() { + _ticketController.dispose(); + // super.d(); + } } diff --git a/lib/models/ticket.dart b/lib/models/ticket.dart new file mode 100644 index 0000000..515f356 --- /dev/null +++ b/lib/models/ticket.dart @@ -0,0 +1,23 @@ +import 'dart:ffi'; + +import '../services/observable.dart'; + +class Ticket extends Observable { + int? id; + bool isLoaded = false; + bool isLoading = false; + int persentLoaded = 0; + String? url; + String? fileName; + String? _filePath; + + String? get filePath => _filePath; + + set filePath(String? value) { + _filePath = value; + // todo: попробовать реализовать изменение через слушателя + notifyObservers(); + } + + Ticket({required this.url}); +} diff --git a/lib/services/fileDownloader.dart b/lib/services/fileDownloader.dart new file mode 100644 index 0000000..e51e7d6 --- /dev/null +++ b/lib/services/fileDownloader.dart @@ -0,0 +1,28 @@ +import 'dart:io'; +import 'package:dio/dio.dart'; +import 'package:path_provider/path_provider.dart'; + +class FileDownloader { + final Dio _dio = Dio(); + + Future downloadFile(String url, String filename, + Function(int, int)? onReceiveProgress) async { + try { + final dir = await getApplicationDocumentsDirectory(); + print(dir); + print(dir.path); + final file = File('${dir.path}/$filename'); + + await _dio.download( + url, + file.path, + onReceiveProgress: onReceiveProgress, + ); + + return file; + } catch (e) { + print('Ошибка загрузки файла: $e'); + rethrow; + } + } +} diff --git a/lib/services/observable.dart b/lib/services/observable.dart new file mode 100644 index 0000000..2ac8b69 --- /dev/null +++ b/lib/services/observable.dart @@ -0,0 +1,19 @@ +import 'observer.dart'; + +abstract class Observable { + List _observers = []; + + void addObserver(Observer observer) { + _observers.add(observer); + } + + void removeObserver(Observer observer) { + _observers.remove(observer); + } + + void notifyObservers() { + for (Observer observer in _observers) { + observer.update(); + } + } +} diff --git a/lib/services/observer.dart b/lib/services/observer.dart new file mode 100644 index 0000000..20e4c7f --- /dev/null +++ b/lib/services/observer.dart @@ -0,0 +1,3 @@ +abstract class Observer { + void update(); +} diff --git a/lib/views/ticket_create_view.dart b/lib/views/ticket_create_view.dart new file mode 100644 index 0000000..f0fff5d --- /dev/null +++ b/lib/views/ticket_create_view.dart @@ -0,0 +1,109 @@ +import 'package:flutter/material.dart'; +import 'package:surf_flutter_study_jam_2023/models/ticket.dart'; + +import '../controllers/ticket_controller.dart'; + +class TicketCreateView extends StatefulWidget { + final TicketController _ticketController; + final String? _url; + + TicketCreateView(this._ticketController, this._url); + + @override + State createState() => _TicketCreateViewState(); +} + +class _TicketCreateViewState extends State { + final _urlController = TextEditingController(); + final _formKey = GlobalKey(); + + @override + void initState() { + super.initState(); + if (widget._url != null) { + _urlController.text = widget._url!; + } + } + + @override + Widget build(BuildContext context) { + return Stack( + alignment: AlignmentDirectional.topCenter, + clipBehavior: Clip.none, + children: [ + Positioned( + top: 15, + child: Container( + width: 60, + height: 7, + margin: const EdgeInsets.only(bottom: 20), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(5), + color: Colors.black26, + ), + ), + ), + Padding( + padding: EdgeInsets.only(top: 40), + child: Form( + key: _formKey, + child: Column( + children: [ + Row( + children: [ + Expanded( + child: Padding( + padding: EdgeInsets.all(25), + child: TextFormField( + controller: _urlController, + decoration: InputDecoration( + labelText: 'URL', + border: OutlineInputBorder(), + ), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Введите корректный Url'; + } + + if (!widget._ticketController + .validateUrl(value)) { + return 'Введите корректный Url (адрес долен оканчиваться на .pdf)'; + } + return null; + }, + ), + ), + ), + ], + ), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: Padding( + padding: EdgeInsets.all(100), + child: ElevatedButton( + onPressed: () { + if (_formKey.currentState!.validate()) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Файл добавлен')), + ); + widget._ticketController.addTicket( + Ticket(url: _urlController.text)); + + Navigator.pop(context); + } + }, + child: Text('Добавить'), + ), + ), + ), + ], + ) + ], + ), + )), + ]); + } +} diff --git a/lib/views/ticket_list_view.dart b/lib/views/ticket_list_view.dart new file mode 100644 index 0000000..c78fca1 --- /dev/null +++ b/lib/views/ticket_list_view.dart @@ -0,0 +1,116 @@ +import 'package:flutter/material.dart'; +import 'package:surf_flutter_study_jam_2023/controllers/ticket_controller.dart'; +import 'package:surf_flutter_study_jam_2023/models/ticket.dart'; +import 'package:surf_flutter_study_jam_2023/views/ticket_open_view.dart'; +import 'package:surf_flutter_study_jam_2023/widgets/file_status_trailing_widget.dart'; +import 'package:surf_flutter_study_jam_2023/widgets/file_status_widget.dart'; + +class TicketListView extends StatefulWidget { + final TicketController ticketController; + + TicketListView({required this.ticketController}); + + static const routeName = '/'; + + @override + _TicketListViewState createState() => _TicketListViewState(); +} + +class _TicketListViewState extends State { + void _showModalBottomSheet(BuildContext context) {} + + @override + Widget build(BuildContext context) { + return StreamBuilder>( + stream: widget.ticketController.tasksStream, + builder: (BuildContext context, AsyncSnapshot> snapshot) { + if (snapshot.hasData) { + return Scaffold( + appBar: AppBar( + title: const Text('Хранение билетов'), + ), + body: Center( + child: ListView.builder( + itemCount: snapshot.data!.length, + itemBuilder: (BuildContext context, int index) { + Ticket ticket = snapshot.data![index]; + return Card( + child: ListTile( + leading: Icon(Icons.file_present), + title: Text( + ticket.fileName ?? ticket.id.toString(), + style: TextStyle(fontSize: 20), + ), + dense: true, + subtitle: FileStatusWidget( + isLoaded: ticket.isLoaded, + isLoading: ticket.isLoading, + percentLoaded: ticket.persentLoaded, + ), + isThreeLine: true, + contentPadding: + EdgeInsets.only(left: 25, right: 25, top: 20), + trailing: FileStatusTraillingWidget( + isLoaded: ticket.isLoaded, + isLoading: ticket.isLoading, + ), + onTap: () { + if (ticket.isLoaded) { + Navigator.pushNamed( + context, + TicketOpenView.routeName!, + arguments: ScreenArguments(ticket), + ); + } else { + if (!ticket.isLoaded && !ticket.isLoading) { + widget.ticketController + .downloadTicket(context, ticket, () { + // Обновляем View после pагрузки элемента + print("чёто именилось!"); + setState(() {}); + }); + } + } + }, + ), + ); + }, + ), + ), + floatingActionButton: + Column(mainAxisAlignment: MainAxisAlignment.end, children: [ + ElevatedButton( + onPressed: () { + widget.ticketController.handleAddTicketButtonPressed(context); + }, + child: Text('Добавить'), + ), + ElevatedButton( + onPressed: () { + widget.ticketController + .downloadAllTicket(context, snapshot.data, () { + // Обновляем View после pагрузки элемента + setState(() {}); + }); + }, + child: Text('Загрузить все'), + ) + ]), + ); + } else { + return Scaffold( + appBar: AppBar( + title: const Text('Хранение билетов'), + ), + floatingActionButton: ElevatedButton( + onPressed: () { + widget.ticketController.handleAddTicketButtonPressed(context); + }, + child: Text('Добавить'), + ), + ); + } + }, + ); + } +} diff --git a/lib/views/ticket_open_view.dart b/lib/views/ticket_open_view.dart new file mode 100644 index 0000000..c71f6e2 --- /dev/null +++ b/lib/views/ticket_open_view.dart @@ -0,0 +1,136 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_pdfview/flutter_pdfview.dart'; +import 'package:path/path.dart'; +// import 'package:open_filex/open_filex.dart'; +import 'package:surf_flutter_study_jam_2023/models/ticket.dart'; + +class ScreenArguments { + final Ticket ticket; + + ScreenArguments(this.ticket); +} + +class TicketOpenView extends StatefulWidget { + const TicketOpenView({super.key}); + + static const routeName = '/view'; + + @override + State createState() => _TicketOpenViewState(); +} + +class _TicketOpenViewState extends State { + int? pages = 0; + + int? currentPage = 0; + + bool isReady = false; + + String errorMessage = ''; + + final Completer _controller = + Completer(); + + // @override + // void initState() { + // } + // Пытаслся сделать через OpenFilex + // Future openFile(filePath) async { + // final result = await OpenFilex.open(filePath); + // var _openResult = 'Unknown'; + + // setState(() { + // _openResult = "type=${result.type} message=${result.message}"; + // }); + // } + + @override + Widget build(BuildContext context) { + final args = ModalRoute.of(context)!.settings.arguments as ScreenArguments; + + try { + final File _file = File(args.ticket.filePath!); + final _filename = basename(_file.path); + final _extension = extension(_file.path); + final _nameWithoutExtension = basenameWithoutExtension(_file.path); + print('Filename: $_filename'); + print('Filename without extension: $_nameWithoutExtension'); + print('Extension: $_extension'); + print(_file.lengthSync()); + // OpenFile + // OpenFilex.open(args.filePath); + // openFile(args.filePath); + // Пытался сделать через OpenFilex + } catch (e) { + return Scaffold( + appBar: AppBar( + title: Text("error file"), + ), + ); + } + + return Scaffold( + appBar: AppBar( + title: Text(args.ticket.fileName!), + ), + body: Stack( + children: [ + PDFView( + filePath: args.ticket.filePath, + enableSwipe: true, + swipeHorizontal: true, + autoSpacing: false, + pageFling: true, + pageSnap: true, + defaultPage: currentPage!, + fitPolicy: FitPolicy.BOTH, + preventLinkNavigation: false, + onRender: (_pages) { + setState(() { + pages = _pages; + isReady = true; + }); + }, + onError: (error) { + setState(() { + errorMessage = error.toString(); + }); + print(error.toString()); + }, + onPageError: (page, error) { + setState(() { + errorMessage = '$page: ${error.toString()}'; + }); + print('$page: ${error.toString()}'); + }, + onViewCreated: (PDFViewController pdfViewController) { + _controller.complete(pdfViewController); + }, + onLinkHandler: (String? uri) { + print('goto uri: $uri'); + }, + onPageChanged: (int? page, int? total) { + print('page change: $page/$total'); + setState(() { + currentPage = page; + }); + }, + ), + errorMessage.isEmpty + ? !isReady + ? Center( + child: CircularProgressIndicator(), + ) + : Container() + : Center( + child: Text(errorMessage), + ) + ], + ), + ); + } +} diff --git a/lib/widgets/file_status_trailing_widget.dart b/lib/widgets/file_status_trailing_widget.dart new file mode 100644 index 0000000..772a07c --- /dev/null +++ b/lib/widgets/file_status_trailing_widget.dart @@ -0,0 +1,30 @@ +import 'package:flutter/material.dart'; + +class FileStatusTraillingWidget extends StatefulWidget { + final bool isLoading; + final bool isLoaded; + + FileStatusTraillingWidget({required this.isLoading, required this.isLoaded}); + + @override + _FileStatusTraillingWidgetState createState() => + _FileStatusTraillingWidgetState(); +} + +class _FileStatusTraillingWidgetState extends State { + @override + void initState() {} + + @override + Widget build(BuildContext context) { + if (widget.isLoaded) { + return Icon(Icons.cloud_done_outlined); + } else { + if (widget.isLoading) { + return Icon(Icons.pause_circle_outline); + } else { + return Icon(Icons.cloud_download_outlined); + } + } + } +} diff --git a/lib/widgets/file_status_widget.dart b/lib/widgets/file_status_widget.dart new file mode 100644 index 0000000..ca0fc83 --- /dev/null +++ b/lib/widgets/file_status_widget.dart @@ -0,0 +1,68 @@ +import 'package:flutter/material.dart'; + +class FileStatusWidget extends StatefulWidget { + final bool isLoading; + final bool isLoaded; + final int percentLoaded; + + FileStatusWidget( + {required this.isLoaded, + required this.isLoading, + required this.percentLoaded}); + + @override + _FileStatusWidgetState createState() => _FileStatusWidgetState(); +} + +class _FileStatusWidgetState extends State { + @override + void initState() {} + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Row( + children: [ + !widget.isLoaded + ? SizedBox( + width: 200, + child: Padding( + padding: EdgeInsets.only(top: 10), + child: LinearProgressIndicator( + minHeight: 5, + value: widget.percentLoaded / 100, + ), + ), + ) + : Container() + // Expanded( + // child: Container( + // alignment: Alignment.centerLeft, + // child: RotatedBox( + // quarterTurns: 1, + // child: LinearProgressIndicator( + // minHeight: 30, + // ), + // ), + // )) + // LinearProgressIndicator( + // value: 50.0, + // ) + ], + ), + Row( + children: [ + Padding( + padding: EdgeInsets.only(top: 10), + child: !widget.isLoaded + ? (widget.isLoading) + ? Text("Файл загружается") + : Text("Нажмите, чтобы загрузить файл") + : Text("Файл загружен")) + ], + ), + ], + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index b2cb69d..f39f960 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -49,6 +49,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.5" + dio: + dependency: "direct main" + description: + name: dio + sha256: "0894a098594263fe1caaba3520e3016d8a855caeb010a882273189cca10f11e9" + url: "https://pub.dev" + source: hosted + version: "5.1.1" fake_async: dependency: transitive description: @@ -57,6 +65,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.1" + ffi: + dependency: transitive + description: + name: ffi + sha256: a38574032c5f1dd06c4aee541789906c12ccaab8ba01446e800d9c5b79c4a978 + url: "https://pub.dev" + source: hosted + version: "2.0.1" + file: + dependency: transitive + description: + name: file + sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" + url: "https://pub.dev" + source: hosted + version: "6.1.4" flutter: dependency: "direct main" description: flutter @@ -70,11 +94,27 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.1" + flutter_pdfview: + dependency: "direct main" + description: + name: flutter_pdfview + sha256: "97c26f92d32fa09ed2302156bdc234befe78478353ba5541b88e538589bcbeab" + url: "https://pub.dev" + source: hosted + version: "1.3.0" flutter_test: dependency: "direct dev" description: flutter source: sdk version: "0.0.0" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + url: "https://pub.dev" + source: hosted + version: "4.0.2" js: dependency: transitive description: @@ -115,6 +155,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.8.0" + open_filex: + dependency: "direct main" + description: + name: open_filex + sha256: "854aefd72dfd74219dc8c8d1767c34ec1eae64b8399a5be317bddb1ec2108915" + url: "https://pub.dev" + source: hosted + version: "4.3.2" path: dependency: transitive description: @@ -123,6 +171,78 @@ packages: url: "https://pub.dev" source: hosted version: "1.8.2" + path_provider: + dependency: "direct main" + description: + name: path_provider + sha256: c7edf82217d4b2952b2129a61d3ad60f1075b9299e629e149a8d2e39c2e6aad4 + url: "https://pub.dev" + source: hosted + version: "2.0.14" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: "019f18c9c10ae370b08dce1f3e3b73bc9f58e7f087bb5e921f06529438ac0ae7" + url: "https://pub.dev" + source: hosted + version: "2.0.24" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "818b2dc38b0f178e0ea3f7cf3b28146faab11375985d815942a68eee11c2d0f7" + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: "2ae08f2216225427e64ad224a24354221c2c7907e448e6e0e8b57b1eb9f10ad1" + url: "https://pub.dev" + source: hosted + version: "2.1.10" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "57585299a729335f1298b43245842678cb9f43a6310351b18fb577d6e33165ec" + url: "https://pub.dev" + source: hosted + version: "2.0.6" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: f53720498d5a543f9607db4b0e997c4b5438884de25b0f73098cc2671a51b130 + url: "https://pub.dev" + source: hosted + version: "2.1.5" + platform: + dependency: transitive + description: + name: platform + sha256: "4a451831508d7d6ca779f7ac6e212b4023dd5a7d08a27a63da33756410e32b76" + url: "https://pub.dev" + source: hosted + version: "3.1.0" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "6a2128648c854906c53fa8e33986fc0247a1116122f9534dd20e3ab9e16a32bc" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + process: + dependency: transitive + description: + name: process + sha256: "53fd8db9cec1d37b0574e12f07520d582019cb6c44abf5479a01505099a34a09" + url: "https://pub.dev" + source: hosted + version: "4.2.4" sky_engine: dependency: transitive description: flutter @@ -176,6 +296,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.4.16" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: "26f87ade979c47a150c9eaab93ccd2bebe70a27dc0b4b29517f2904f04eb11a5" + url: "https://pub.dev" + source: hosted + version: "1.3.1" vector_math: dependency: transitive description: @@ -184,5 +312,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + win32: + dependency: transitive + description: + name: win32 + sha256: a6f0236dbda0f63aa9a25ad1ff9a9d8a4eaaa5012da0dc59d21afdb1dc361ca4 + url: "https://pub.dev" + source: hosted + version: "3.1.4" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: ee1505df1426458f7f60aac270645098d318a8b4766d85fde75f76f2e21807d1 + url: "https://pub.dev" + source: hosted + version: "1.0.0" sdks: dart: ">=2.19.6 <3.0.0" + flutter: ">=3.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index 5abeaf8..64c60f0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -35,6 +35,10 @@ dependencies: # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.2 + dio: ^5.1.1 + path_provider: ^2.0.14 + flutter_pdfview: ^1.3.0 + open_filex: ^4.3.2 dev_dependencies: flutter_test: