10: Fixed input colors in login view when using dark theme; Added removal of individual files selected for upload; Added size for individual files selected for upload; Replaced intent sharing library with flutter_sharing_intent

This commit is contained in:
Varakh 2023-01-16 01:43:37 +01:00
parent a65c7d9253
commit a5dab51765
9 changed files with 158 additions and 140 deletions

View file

@ -1,6 +1,10 @@
# CHANGELOG # CHANGELOG
## 1.5.2+18 ## 1.5.2+18
* Fixed input colors in login view when using dark theme
* Added removal of individual files selected for upload
* Added size for individual files selected for upload
* Replaced intent sharing library with `flutter_sharing_intent`
* Added proper linting to project * Added proper linting to project
## 1.5.1+17 ## 1.5.1+17

View file

@ -21,46 +21,6 @@
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/*" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/*" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND_MULTIPLE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/*" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="video/*" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND_MULTIPLE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="video/*" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="*/*" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND_MULTIPLE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="*/*" />
</intent-filter>
</activity> </activity>
<!-- Don't delete the meta-data below. <!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java --> This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->

View file

@ -60,7 +60,7 @@
</dict> </dict>
<dict/> <dict/>
</array> </array>
// TODO follow steps 2) on create share extension (https://pub.dev/packages/receive_sharing_intent) // TODO follow steps on create share extension (https://pub.dev/packages/flutter_sharing_intent)
<key>NSPhotoLibraryUsageDescription</key> <key>NSPhotoLibraryUsageDescription</key>
<string>Allow to select photos and upload them via the app</string> <string>Allow to select photos and upload them via the app</string>
<key>LSApplicationQueriesSchemes</key> <key>LSApplicationQueriesSchemes</key>

View file

@ -80,6 +80,7 @@ class BaseModel extends ChangeNotifier {
@override @override
void dispose() { void dispose() {
_logger.d("Calling dispose");
super.dispose(); super.dispose();
_isDisposed = true; _isDisposed = true;
} }

View file

@ -4,10 +4,11 @@ import 'dart:io';
import 'package:file_picker/file_picker.dart'; import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.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:flutter_translate/flutter_translate.dart';
import 'package:logger/logger.dart'; import 'package:logger/logger.dart';
import 'package:path/path.dart'; import 'package:path/path.dart';
import 'package:receive_sharing_intent/receive_sharing_intent.dart';
import '../../locator.dart'; import '../../locator.dart';
import '../enums/error_code.dart'; import '../enums/error_code.dart';
@ -45,6 +46,41 @@ class UploadModel extends BaseModel {
TextEditingController get pasteTextController => _pasteTextController; TextEditingController get pasteTextController => _pasteTextController;
void _parseIntentFiles(List<SharedFile> 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() { void init() {
_pasteTextController.addListener(() { _pasteTextController.addListener(() {
pasteTextTouched = pasteTextController.text.isNotEmpty; pasteTextTouched = pasteTextController.text.isNotEmpty;
@ -52,61 +88,21 @@ class UploadModel extends BaseModel {
}); });
// For sharing images coming from outside the app while the app is in the memory // For sharing images coming from outside the app while the app is in the memory
_intentDataStreamSubscription = ReceiveSharingIntent.getMediaStream().listen((List<SharedMediaFile> value) { _intentDataStreamSubscription = FlutterSharingIntent.instance
if (value.isNotEmpty) { .getMediaStream()
setStateView(ViewState.busy); .listen((List<SharedFile> value) {
paths = value.map((sharedFile) { _logger.d("Retrieved ${value.length} files from intent");
return PlatformFile.fromMap({ _parseIntentFiles(value);
'path': sharedFile.path,
'name': basename(sharedFile.path),
'size': File(sharedFile.path).lengthSync(),
'bytes': null
});
}).toList();
setStateView(ViewState.idle);
}
}, onError: (err) { }, onError: (err) {
_errorIntentHandle(err); _errorIntentHandle(err);
}); });
// For sharing images coming from outside the app while the app is closed // For sharing images coming from outside the app while the app is closed
ReceiveSharingIntent.getInitialMedia().then((List<SharedMediaFile> value) { FlutterSharingIntent.instance
if (value.isNotEmpty) { .getInitialSharing()
setStateView(ViewState.busy); .then((List<SharedFile> value) {
paths = value.map((sharedFile) { _logger.d("Retrieved ${value.length} files from inactive intent");
return PlatformFile.fromMap({ _parseIntentFiles(value);
'path': sharedFile.path,
'name': basename(sharedFile.path),
'size': File(sharedFile.path).lengthSync(),
'bytes': null
});
}).toList();
setStateView(ViewState.idle);
}
}, onError: (err) {
_errorIntentHandle(err);
});
// For sharing or opening urls/text coming from outside the app while the app is in the memory
_intentDataStreamSubscription = ReceiveSharingIntent.getTextStream().listen((String value) {
if (value.isNotEmpty) {
setStateView(ViewState.busy);
pasteTextController.text = value;
setStateView(ViewState.idle);
}
}, onError: (err) {
_errorIntentHandle(err);
});
// For sharing or opening urls/text coming from outside the app while the app is closed
ReceiveSharingIntent.getInitialText().then((String? value) {
if (value != null && value.isNotEmpty) {
setStateView(ViewState.busy);
pasteTextController.text = value;
setStateView(ViewState.idle);
}
}, onError: (err) {
_errorIntentHandle(err);
}); });
} }
@ -150,7 +146,9 @@ class UploadModel extends BaseModel {
allowMultiple: true, allowMultiple: true,
withData: false, withData: false,
withReadStream: true, withReadStream: true,
allowedExtensions: (_extension?.isNotEmpty ?? false) ? _extension?.replaceAll(' ', '').split(',') : null, allowedExtensions: (_extension?.isNotEmpty ?? false)
? _extension?.replaceAll(' ', '').split(',')
: null,
)) ))
?.files; ?.files;
} on PlatformException catch (e) { } on PlatformException catch (e) {
@ -185,21 +183,25 @@ class UploadModel extends BaseModel {
Map<String, String>? additionalFiles; Map<String, String>? additionalFiles;
if (pasteTextController.text.isNotEmpty) { if (pasteTextController.text.isNotEmpty) {
additionalFiles = additionalFiles = Map.from({
Map.from({'paste-${(DateTime.now().millisecondsSinceEpoch / 1000).round()}.txt': pasteTextController.text}); 'paste-${(DateTime.now().millisecondsSinceEpoch / 1000).round()}.txt':
pasteTextController.text
});
} }
if (paths != null && paths!.isNotEmpty) { if (paths != null && paths!.isNotEmpty) {
files = paths!.map((e) => File(e.path!)).toList(); files = paths!.map((e) => File(e.path!)).toList();
} }
UploadedResponse response = await _fileService.uploadPaste(files, additionalFiles); UploadedResponse response =
await _fileService.uploadPaste(files, additionalFiles);
for (var element in response.data.ids) { for (var element in response.data.ids) {
uploadedPasteIds.putIfAbsent(element, () => false); uploadedPasteIds.putIfAbsent(element, () => false);
} }
if (createMulti && response.data.ids.length > 1) { if (createMulti && response.data.ids.length > 1) {
UploadedMultiResponse multiResponse = await _fileService.uploadMultiPaste(response.data.ids); UploadedMultiResponse multiResponse =
await _fileService.uploadMultiPaste(response.data.ids);
uploadedPasteIds.putIfAbsent(multiResponse.data.urlId, () => true); uploadedPasteIds.putIfAbsent(multiResponse.data.urlId, () => true);
} }
@ -219,9 +221,11 @@ class UploadModel extends BaseModel {
e.responseBody is RestError && e.responseBody is RestError &&
e.responseBody.message != null) { e.responseBody.message != null) {
if (e.statusCode == HttpStatus.badRequest) { if (e.statusCode == HttpStatus.badRequest) {
errorMessage = translate('api.bad_request', args: {'reason': e.responseBody.message}); errorMessage = translate('api.bad_request',
args: {'reason': e.responseBody.message});
} else { } else {
errorMessage = translate('api.general_rest_error_payload', args: {'message': e.responseBody.message}); errorMessage = translate('api.general_rest_error_payload',
args: {'message': e.responseBody.message});
} }
} else { } else {
errorMessage = translate('api.general_rest_error'); errorMessage = translate('api.general_rest_error');
@ -252,6 +256,8 @@ class UploadModel extends BaseModel {
void dispose() { void dispose() {
_pasteTextController.dispose(); _pasteTextController.dispose();
_intentDataStreamSubscription.cancel(); _intentDataStreamSubscription.cancel();
FlutterSharingIntent.instance.reset();
paths = null;
super.dispose(); super.dispose();
} }
} }

View file

@ -1,4 +1,5 @@
import 'package:clipboard/clipboard.dart'; import 'package:clipboard/clipboard.dart';
import 'package:fbmobile/core/util/formatter_util.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_translate/flutter_translate.dart'; import 'package:flutter_translate/flutter_translate.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -20,12 +21,14 @@ class UploadView extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BaseView<UploadModel>( return BaseView<UploadModel>(
onModelReady: (model) => model.init(), onModelReady: (model) => model.init(),
builder: (context, model, child) => builder: (context, model, child) => Scaffold(
Scaffold(appBar: MyAppBar(title: Text(translate('titles.upload'))), body: _render(model, context))); appBar: MyAppBar(title: Text(translate('titles.upload'))),
body: _render(model, context)));
} }
bool _isUploadButtonEnabled(UploadModel model) { bool _isUploadButtonEnabled(UploadModel model) {
return model.pasteTextTouched || (model.paths != null && model.paths!.isNotEmpty); return model.pasteTextTouched ||
(model.paths != null && model.paths!.isNotEmpty);
} }
Widget _render(UploadModel model, BuildContext context) { Widget _render(UploadModel model, BuildContext context) {
@ -38,7 +41,9 @@ class UploadView extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
const CircularProgressIndicator(), const CircularProgressIndicator(),
(model.stateMessage != null && model.stateMessage!.isNotEmpty ? Text(model.stateMessage!) : Container()) (model.stateMessage != null && model.stateMessage!.isNotEmpty
? Text(model.stateMessage!)
: Container())
])) ]))
: ListView(children: <Widget>[ : ListView(children: <Widget>[
Padding( Padding(
@ -56,12 +61,15 @@ class UploadView extends StatelessWidget {
Icons.text_snippet, Icons.text_snippet,
), ),
suffixIcon: IconButton( suffixIcon: IconButton(
onPressed: () => model.pasteTextController.clear(), onPressed: () =>
model.pasteTextController.clear(),
icon: const Icon(Icons.clear), icon: const Icon(Icons.clear),
), ),
hintText: translate('upload.text_to_be_pasted'), hintText: translate('upload.text_to_be_pasted'),
contentPadding: const EdgeInsets.fromLTRB(20.0, 10.0, 20.0, 10.0), contentPadding: const EdgeInsets.fromLTRB(
border: OutlineInputBorder(borderRadius: BorderRadius.circular(32.0)), 20.0, 10.0, 20.0, 10.0),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(32.0)),
), ),
controller: model.pasteTextController)), controller: model.pasteTextController)),
Padding( Padding(
@ -77,14 +85,17 @@ class UploadView extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end,
children: [ children: [
ElevatedButton.icon( ElevatedButton.icon(
icon: const Icon(Icons.file_copy_sharp, color: blueColor), icon: const Icon(Icons.file_copy_sharp,
color: blueColor),
onPressed: () => model.openFileExplorer(), onPressed: () => model.openFileExplorer(),
label: Text( label: Text(
translate('upload.open_file_explorer'), translate('upload.open_file_explorer'),
)), )),
ElevatedButton.icon( ElevatedButton.icon(
icon: const Icon(Icons.cancel, color: orangeColor), icon: const Icon(Icons.cancel,
onPressed: model.paths != null && model.paths!.isNotEmpty color: orangeColor),
onPressed: model.paths != null &&
model.paths!.isNotEmpty
? () => model.clearCachedFiles() ? () => model.clearCachedFiles()
: null, : null,
label: Text( label: Text(
@ -114,34 +125,47 @@ class UploadView extends StatelessWidget {
onPressed: !_isUploadButtonEnabled(model) onPressed: !_isUploadButtonEnabled(model)
? null ? null
: () async { : () async {
Map<String, bool>? items = await model.upload(); Map<String, bool>? items =
String? clipboardContent = model.generatePasteLinks(items, url); await model.upload();
String? clipboardContent = model
.generatePasteLinks(items, url);
if (clipboardContent != null && clipboardContent.isNotEmpty) { if (clipboardContent != null &&
FlutterClipboard.copy(clipboardContent).then((value) { clipboardContent.isNotEmpty) {
FlutterClipboard.copy(
clipboardContent)
.then((value) {
final snackBar = SnackBar( final snackBar = SnackBar(
action: SnackBarAction( action: SnackBarAction(
label: translate('upload.dismiss'), label: translate(
'upload.dismiss'),
textColor: blueColor, textColor: blueColor,
onPressed: () { onPressed: () {
ScaffoldMessenger.of(context).hideCurrentSnackBar(); ScaffoldMessenger.of(
context)
.hideCurrentSnackBar();
}, },
), ),
content: Text(translate('upload.uploaded')), content: Text(translate(
duration: const Duration(seconds: 10), 'upload.uploaded')),
duration:
const Duration(seconds: 10),
); );
ScaffoldMessenger.of(context).showSnackBar(snackBar); ScaffoldMessenger.of(context)
.showSnackBar(snackBar);
}); });
} }
}, },
icon: const Icon(Icons.upload_rounded, color: greenColor), icon: const Icon(Icons.upload_rounded,
color: greenColor),
label: Text( label: Text(
translate('upload.upload'), translate('upload.upload'),
)), )),
])), ])),
model.errorMessage != null && model.errorMessage!.isNotEmpty model.errorMessage != null && model.errorMessage!.isNotEmpty
? (Padding( ? (Padding(
padding: const EdgeInsets.only(top: 10.0, bottom: 10.0), padding:
const EdgeInsets.only(top: 10.0, bottom: 10.0),
child: CenteredErrorRow(model.errorMessage))) child: CenteredErrorRow(model.errorMessage)))
: Container(), : Container(),
Builder( Builder(
@ -152,28 +176,54 @@ class UploadView extends StatelessWidget {
) )
: model.paths != null : model.paths != null
? Container( ? Container(
padding: const EdgeInsets.only(bottom: 30.0), padding: const EdgeInsets.only(bottom: 20.0),
height: MediaQuery.of(context).size.height * 0.50, height:
MediaQuery.of(context).size.height * 0.50,
child: ListView.separated( child: ListView.separated(
itemCount: model.paths != null && model.paths!.isNotEmpty ? model.paths!.length : 1, itemCount: model.paths != null &&
itemBuilder: (BuildContext context, int index) { model.paths!.isNotEmpty
final bool isMultiPath = model.paths != null && model.paths!.isNotEmpty; ? model.paths!.length
: 1,
itemBuilder:
(BuildContext context, int index) {
final bool isMultiPath =
model.paths != null &&
model.paths!.isNotEmpty;
final String name = (isMultiPath final String name = (isMultiPath
? model.paths!.map((e) => e.name).toList()[index] ? model.paths!
.map((e) => e.name)
.toList()[index]
: model.fileName ?? '...'); : model.fileName ?? '...');
final size = model.paths!.isNotEmpty
? model.paths!
.map((e) => e.size)
.toList()[index]
.toString()
: '';
final path = model.paths!.isNotEmpty final path = model.paths!.isNotEmpty
? model.paths!.map((e) => e.path).toList()[index].toString() ? model.paths!
.map((e) => e.path)
.toList()[index]
.toString()
: ''; : '';
return Card( return Card(
child: ListTile( child: ListTile(
trailing: IconButton(
icon: const Icon(Icons.clear,
color: orangeColor),
onPressed: () {
model.deleteIntentFile(path);
}),
title: Text( title: Text(
name, "$name (${FormatterUtil.formatBytes(int.parse(size), 2)})",
), ),
subtitle: Text(path), subtitle: Text(path),
)); ));
}, },
separatorBuilder: (BuildContext context, int index) => const Divider(), separatorBuilder:
(BuildContext context, int index) =>
const Divider(),
), ),
) )
: Container(), : Container(),

View file

@ -1,7 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../shared/app_colors.dart';
class LoginTextField extends StatelessWidget { class LoginTextField extends StatelessWidget {
final TextEditingController controller; final TextEditingController controller;
final String placeHolder; final String placeHolder;
@ -19,7 +17,6 @@ class LoginTextField extends StatelessWidget {
margin: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 10.0), margin: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 10.0),
height: 50.0, height: 50.0,
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
decoration: BoxDecoration(color: whiteColor, borderRadius: BorderRadius.circular(10.0)),
child: TextFormField( child: TextFormField(
keyboardType: keyboardType, keyboardType: keyboardType,
obscureText: obscureText, obscureText: obscureText,

View file

@ -256,6 +256,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.7" version: "2.0.7"
flutter_sharing_intent:
dependency: "direct main"
description:
name: flutter_sharing_intent
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.5"
flutter_test: flutter_test:
dependency: "direct dev" dependency: "direct dev"
description: flutter description: flutter
@ -588,13 +595,6 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.1.0" version: "3.1.0"
receive_sharing_intent:
dependency: "direct main"
description:
name: receive_sharing_intent
url: "https://pub.dartlang.org"
source: hosted
version: "1.4.5"
share_plus: share_plus:
dependency: "direct main" dependency: "direct main"
description: description:

View file

@ -36,13 +36,13 @@ dependencies:
share_plus: 6.3.0 share_plus: 6.3.0
file_picker: 5.2.4 file_picker: 5.2.4
clipboard: 0.1.3 clipboard: 0.1.3
receive_sharing_intent: 1.4.5
permission_handler: 10.2.0 permission_handler: 10.2.0
package_info_plus: 3.0.2 package_info_plus: 3.0.2
json_annotation: 4.7.0 json_annotation: 4.7.0
dynamic_color: 1.5.4 dynamic_color: 1.5.4
intl: 0.17.0 intl: 0.17.0
path: 1.8.2 path: 1.8.2
flutter_sharing_intent: 1.0.5
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: