import 'dart:async'; import 'dart:io'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_sharing_intent/flutter_sharing_intent.dart'; import 'package:flutter_sharing_intent/model/sharing_file.dart'; import 'package:flutter_translate/flutter_translate.dart'; import 'package:logger/logger.dart'; import 'package:path/path.dart'; import '../../locator.dart'; import '../enums/error_code.dart'; import '../enums/refresh_event.dart'; import '../enums/viewstate.dart'; import '../error/rest_service_exception.dart'; import '../error/service_exception.dart'; import '../models/rest/rest_error.dart'; import '../models/rest/uploaded_multi_response.dart'; import '../models/rest/uploaded_response.dart'; import '../services/file_service.dart'; import '../services/link_service.dart'; import '../services/refresh_service.dart'; import '../util/logger.dart'; import '../util/paste_util.dart'; import 'base_model.dart'; class UploadModel extends BaseModel { final Logger _logger = getLogger(); final FileService _fileService = locator(); final LinkService _linkService = locator(); final RefreshService _refreshService = locator(); final TextEditingController _pasteTextController = TextEditingController(); bool pasteTextTouched = false; late StreamSubscription _intentDataStreamSubscription; bool createMulti = false; String? fileName; List? paths; String? _extension; bool loadingPath = false; String? errorMessage; TextEditingController get pasteTextController => _pasteTextController; void _parseIntentFiles(List files) { if (files.isNotEmpty) { setStateView(ViewState.busy); paths = files.map((sharedFile) { _logger.d("Shared file name: ${basename(sharedFile.value ?? '')}"); _logger.d("Shared file path: ${sharedFile.value}"); _logger.d( "Shared file size: ${File(sharedFile.value ?? '').lengthSync()}"); _logger.d("Shared file type: ${sharedFile.type}"); return PlatformFile.fromMap({ 'path': sharedFile.value, 'name': basename(sharedFile.value!), 'size': File(sharedFile.value!).lengthSync(), 'bytes': null }); }).toList(); setStateView(ViewState.idle); } } void deleteIntentFile(String path) { setStateView(ViewState.busy); _logger.d("Removing path '$path' from $paths"); paths?.removeWhere((element) => element.path == path); int length = paths!.length; if (length == 0) { paths = null; } setStateView(ViewState.idle); } void init() { _pasteTextController.addListener(() { pasteTextTouched = pasteTextController.text.isNotEmpty; setStateBoolValue("PASTE_TEXT_TOUCHED", pasteTextTouched); }); // For sharing images coming from outside the app while the app is in the memory _intentDataStreamSubscription = FlutterSharingIntent.instance .getMediaStream() .listen((List value) { _logger.d("Retrieved ${value.length} files from intent"); _parseIntentFiles(value); }, onError: (err) { _errorIntentHandle(err); }); // For sharing images coming from outside the app while the app is closed FlutterSharingIntent.instance .getInitialSharing() .then((List value) { _logger.d("Retrieved ${value.length} files from inactive intent"); _parseIntentFiles(value); }); } void _errorIntentHandle(err) { setStateView(ViewState.busy); errorMessage = translate('upload.retrieval_intent'); _logger.e('Error while retrieving shared data: $err'); setStateView(ViewState.idle); } String? generatePasteLinks(Map? uploads, String url) { if (uploads != null && uploads.isNotEmpty) { var links = ''; uploads.forEach((id, isMulti) { if (isMulti && createMulti || !isMulti && !createMulti) { links += '${PasteUtil.generateLink(url, id)}\n'; } }); return links; } return null; } void toggleCreateMulti() { setStateView(ViewState.busy); createMulti = !createMulti; setStateView(ViewState.idle); } void openFileExplorer() async { setStateView(ViewState.busy); setStateMessage(translate('upload.file_explorer_open')); loadingPath = true; try { paths = (await FilePicker.platform.pickFiles( type: FileType.any, allowMultiple: true, withData: false, withReadStream: true, allowedExtensions: (_extension?.isNotEmpty ?? false) ? _extension?.replaceAll(' ', '').split(',') : null, )) ?.files; } on PlatformException catch (e) { _logger.e('Unsupported operation', error: e); } catch (ex) { _logger.e('An unknown error occurred', error: ex); } loadingPath = false; fileName = paths != null ? paths!.map((e) => e.name).toString() : '...'; setStateMessage(null); setStateView(ViewState.idle); } void clearCachedFiles() async { setStateView(ViewState.busy); await FilePicker.platform.clearTemporaryFiles(); paths = null; fileName = null; errorMessage = null; setStateView(ViewState.idle); } Future?> upload() async { setStateView(ViewState.busy); setStateMessage(translate('upload.uploading_now')); Map uploadedPasteIds = {}; try { List? files; Map? additionalFiles; if (pasteTextController.text.isNotEmpty) { additionalFiles = Map.from({ 'paste-${(DateTime.now().millisecondsSinceEpoch / 1000).round()}.txt': pasteTextController.text }); } if (paths != null && paths!.isNotEmpty) { files = paths!.map((e) => File(e.path!)).toList(); } UploadedResponse response = await _fileService.uploadPaste(files, additionalFiles); for (var element in response.data.ids) { uploadedPasteIds.putIfAbsent(element, () => false); } if (createMulti && response.data.ids.length > 1) { UploadedMultiResponse multiResponse = await _fileService.uploadMultiPaste(response.data.ids); uploadedPasteIds.putIfAbsent(multiResponse.data.urlId, () => true); } clearCachedFiles(); _pasteTextController.clear(); _refreshService.addEvent(RefreshEvent.refreshHistory); errorMessage = null; return uploadedPasteIds; } catch (e) { if (e is RestServiceException) { if (e.statusCode == HttpStatus.notFound) { errorMessage = translate('upload.errors.not_found'); } else if (e.statusCode == HttpStatus.forbidden) { errorMessage = translate('api.forbidden'); } else if (e.statusCode != HttpStatus.notFound && e.statusCode != HttpStatus.forbidden && e.responseBody is RestError && e.responseBody.message != null) { if (e.statusCode == HttpStatus.badRequest) { errorMessage = translate('api.bad_request', args: {'reason': e.responseBody.message}); } else { errorMessage = translate('api.general_rest_error_payload', args: {'message': e.responseBody.message}); } } else { errorMessage = translate('api.general_rest_error'); } } else if (e is ServiceException && e.code == ErrorCode.socketError) { errorMessage = translate('api.socket_error'); } else if (e is ServiceException && e.code == ErrorCode.socketTimeout) { errorMessage = translate('api.socket_timeout'); } else { errorMessage = translate('app.unknown_error'); setStateMessage(null); setStateView(ViewState.idle); _logger.e('An unknown error occurred', error: e); rethrow; } } setStateMessage(null); setStateView(ViewState.idle); return null; } void openLink(String link) { _linkService.open(link); } @override void dispose() { _pasteTextController.dispose(); _intentDataStreamSubscription.cancel(); FlutterSharingIntent.instance.reset(); paths = null; super.dispose(); } }