Initial upgrade to null-safety and latest dependency versions

This commit is contained in:
Varakh 2021-11-30 00:44:22 +01:00
parent 35e957a049
commit ccb780be50
60 changed files with 561 additions and 339 deletions

View file

@ -1,7 +1,10 @@
# CHANGELOG
## 1.3.3+13 - UNRELEASED
* Increased target SDK to 30
## 1.4.0+13 - UNRELEASED
* Increased target SDK to `30`
* Upgraded to Dart `2.14.4`
* Upgraded to use null-safety
* Upgraded internal dependencies to latest versions
## 1.3.3+12
* Automatically switch to initial tab when coming from the share menu
@ -48,4 +51,4 @@
* Automatic refresh history if something has been uploaded
## 1.0.0+1
* Initial release
* Initial release

View file

@ -32,7 +32,8 @@ Start by installing dependencies and generating entities!
### Working versions for SDK
```
[✓] Flutter (Channel stable, 2.0.6, on Linux, locale de_DE.UTF-8)
Flutter version 2.5.3
Dart version 2.14.4
```
## Dependencies
@ -152,4 +153,4 @@ This should not happen under normal circumstances, please file an issue if it do
Ensure to be on the version mentioned above which should be in the stable branch. If everything
breaks, start from fresh via `flutter clean` and maybe re-do all necessary steps to get the app
working in the first place.
working in the first place.

View file

@ -24,26 +24,29 @@ class MyApp extends StatelessWidget {
return LocalizationProvider(
state: LocalizationProvider.of(context).state,
child: StreamProvider<SwipeEvent>(
child: StreamProvider<SwipeEvent?>(
initialData: null,
create: (context) => locator<SwipeService>().swipeEventController.stream,
child: StreamProvider<RefreshEvent>(
create: (context) =>
locator<SwipeService>().swipeEventController.stream,
child: StreamProvider<RefreshEvent?>(
initialData: null,
create: (context) => locator<RefreshService>().refreshEventController.stream,
child: StreamProvider<Session>(
create: (context) =>
locator<RefreshService>().refreshEventController.stream,
child: StreamProvider<Session?>(
initialData: Session.initial(),
create: (context) => locator<SessionService>().sessionController.stream,
create: (context) =>
locator<SessionService>().sessionController.stream,
child: LifeCycleManager(
child: MaterialApp(
title: translate('app.title'),
builder: (context, child) => Navigator(
key: locator<DialogService>().dialogNavigationKey,
onGenerateRoute: (settings) =>
MaterialPageRoute(builder: (context) => DialogManager(child: child)),
onGenerateRoute: (settings) => MaterialPageRoute(
builder: (context) => DialogManager(child: child)),
),
theme: ThemeData(
brightness: Brightness.light,
primarySwatch: primaryAccentColor,
primarySwatch: primaryAccentColor as MaterialColor?,
primaryColor: primaryAccentColor),
onGenerateRoute: AppRouter.generateRoute,
navigatorKey: locator<NavigationService>().navigationKey,

View file

@ -1,8 +1,8 @@
class DialogRequest {
final String title;
final String description;
final String buttonTitleAccept;
final String buttonTitleDeny;
final String? title;
final String? description;
final String? buttonTitleAccept;
final String? buttonTitleDeny;
DialogRequest({
this.title,

View file

@ -1,5 +1,5 @@
class DialogResponse {
final bool confirmed;
final bool? confirmed;
DialogResponse({
this.confirmed,

View file

@ -5,7 +5,7 @@ class RestServiceException extends ServiceException {
final int statusCode;
final dynamic responseBody;
RestServiceException(this.statusCode, {this.responseBody, String message})
RestServiceException(this.statusCode, {this.responseBody, String? message})
: super(code: ErrorCode.REST_ERROR, message: message);
String toString() {

View file

@ -2,7 +2,7 @@ import '../enums/error_code.dart';
class ServiceException implements Exception {
final ErrorCode code;
final String message;
final String? message;
ServiceException({this.code = ErrorCode.GENERAL_ERROR, this.message = ''});

View file

@ -6,9 +6,9 @@ import '../datamodels/dialog_response.dart';
import '../services/dialog_service.dart';
class DialogManager extends StatefulWidget {
final Widget child;
final Widget? child;
DialogManager({Key key, this.child}) : super(key: key);
DialogManager({Key? key, this.child}) : super(key: key);
_DialogManagerState createState() => _DialogManagerState();
}
@ -24,15 +24,16 @@ class _DialogManagerState extends State<DialogManager> {
@override
Widget build(BuildContext context) {
return widget.child;
return widget.child!;
}
void _showDialog(DialogRequest request) {
List<Widget> actions = <Widget>[];
if (request.buttonTitleDeny != null && request.buttonTitleDeny.isNotEmpty) {
if (request.buttonTitleDeny != null &&
request.buttonTitleDeny!.isNotEmpty) {
Widget denyBtn = TextButton(
child: Text(request.buttonTitleDeny),
child: Text(request.buttonTitleDeny!),
onPressed: () {
_dialogService.dialogComplete(DialogResponse(confirmed: false));
},
@ -41,7 +42,7 @@ class _DialogManagerState extends State<DialogManager> {
}
Widget confirmBtn = TextButton(
child: Text(request.buttonTitleAccept),
child: Text(request.buttonTitleAccept!),
onPressed: () {
_dialogService.dialogComplete(DialogResponse(confirmed: true));
},
@ -49,8 +50,8 @@ class _DialogManagerState extends State<DialogManager> {
actions.add(confirmBtn);
AlertDialog alert = AlertDialog(
title: Text(request.title),
content: Text(request.description),
title: Text(request.title!),
content: Text(request.description!),
actions: actions,
);

View file

@ -9,33 +9,37 @@ import '../util/logger.dart';
/// Stop and start long running services
class LifeCycleManager extends StatefulWidget {
final Widget child;
final Widget? child;
LifeCycleManager({Key key, this.child}) : super(key: key);
LifeCycleManager({Key? key, this.child}) : super(key: key);
_LifeCycleManagerState createState() => _LifeCycleManagerState();
}
class _LifeCycleManagerState extends State<LifeCycleManager> with WidgetsBindingObserver {
class _LifeCycleManagerState extends State<LifeCycleManager>
with WidgetsBindingObserver {
final Logger logger = getLogger();
List<StoppableService> servicesToManage = [locator<SessionService>(), locator<PermissionService>()];
List<StoppableService> servicesToManage = [
locator<SessionService>(),
locator<PermissionService>()
];
@override
Widget build(BuildContext context) {
return widget.child;
return widget.child!;
}
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
WidgetsBinding.instance!.addObserver(this);
}
@override
void dispose() {
super.dispose();
WidgetsBinding.instance.removeObserver(this);
WidgetsBinding.instance!.removeObserver(this);
}
@override

View file

@ -13,9 +13,13 @@ class ApiKey {
@JsonKey(required: true, name: 'access_level')
final String accessLevel;
final String comment;
final String? comment;
ApiKey({this.key, this.created, this.accessLevel, this.comment});
ApiKey(
{required this.key,
required this.created,
required this.accessLevel,
this.comment});
// JSON Init
factory ApiKey.fromJson(Map<String, dynamic> json) => _$ApiKeyFromJson(json);

View file

@ -6,13 +6,14 @@ part 'apikeys.g.dart';
@JsonSerializable()
class ApiKeys {
@JsonKey(name: "items")
@JsonKey(name: "items", required: true)
final Map<String, ApiKey> apikeys;
ApiKeys({this.apikeys});
ApiKeys({required this.apikeys});
// JSON Init
factory ApiKeys.fromJson(Map<String, dynamic> json) => _$ApiKeysFromJson(json);
factory ApiKeys.fromJson(Map<String, dynamic> json) =>
_$ApiKeysFromJson(json);
// JSON Export
Map<String, dynamic> toJson() => _$ApiKeysToJson(this);

View file

@ -12,10 +12,11 @@ class ApiKeysResponse {
@JsonKey(required: true)
final ApiKeys data;
ApiKeysResponse({this.status, this.data});
ApiKeysResponse({required this.status, required this.data});
// JSON Init
factory ApiKeysResponse.fromJson(Map<String, dynamic> json) => _$ApiKeysResponseFromJson(json);
factory ApiKeysResponse.fromJson(Map<String, dynamic> json) =>
_$ApiKeysResponseFromJson(json);
// JSON Export
Map<String, dynamic> toJson() => _$ApiKeysResponseToJson(this);

View file

@ -16,7 +16,11 @@ class Config {
@JsonKey(name: "request_max_size", required: true)
final num requestMaxSize;
Config({this.uploadMaxSize, this.maxFilesPerRequest, this.maxInputVars, this.requestMaxSize});
Config(
{required this.uploadMaxSize,
required this.maxFilesPerRequest,
required this.maxInputVars,
required this.requestMaxSize});
// JSON Init
factory Config.fromJson(Map<String, dynamic> json) => _$ConfigFromJson(json);

View file

@ -12,10 +12,11 @@ class ConfigResponse {
@JsonKey(required: true)
final Config data;
ConfigResponse({this.status, this.data});
ConfigResponse({required this.status, required this.data});
// JSON Init
factory ConfigResponse.fromJson(Map<String, dynamic> json) => _$ConfigResponseFromJson(json);
factory ConfigResponse.fromJson(Map<String, dynamic> json) =>
_$ConfigResponseFromJson(json);
// JSON Export
Map<String, dynamic> toJson() => _$ConfigResponseToJson(this);

View file

@ -10,10 +10,11 @@ class CreateApiKeyResponse {
@JsonKey(required: true)
final Map<String, String> data;
CreateApiKeyResponse({this.status, this.data});
CreateApiKeyResponse({required this.status, required this.data});
// JSON Init
factory CreateApiKeyResponse.fromJson(Map<String, dynamic> json) => _$CreateApiKeyResponseFromJson(json);
factory CreateApiKeyResponse.fromJson(Map<String, dynamic> json) =>
_$CreateApiKeyResponseFromJson(json);
// JSON Export
Map<String, dynamic> toJson() => _$CreateApiKeyResponseToJson(this);

View file

@ -14,12 +14,13 @@ class History {
final Map<String, HistoryMultipasteItem> multipasteItems;
@JsonKey(name: "total_size")
final String totalSize;
final String? totalSize;
History({this.items, this.multipasteItems, this.totalSize});
History({required this.items, required this.multipasteItems, this.totalSize});
// JSON Init
factory History.fromJson(Map<String, dynamic> json) => _$HistoryFromJson(json);
factory History.fromJson(Map<String, dynamic> json) =>
_$HistoryFromJson(json);
// JSON Export
Map<String, dynamic> toJson() => _$HistoryToJson(this);

View file

@ -11,12 +11,20 @@ class HistoryItem {
final String filesize;
final String hash;
final String mimetype;
final String thumbnail;
final String? thumbnail;
HistoryItem({this.date, this.filename, this.filesize, this.hash, this.id, this.mimetype, this.thumbnail});
HistoryItem(
{required this.date,
required this.filename,
required this.filesize,
required this.hash,
required this.id,
required this.mimetype,
this.thumbnail});
// JSON Init
factory HistoryItem.fromJson(Map<String, dynamic> json) => _$HistoryItemFromJson(json);
factory HistoryItem.fromJson(Map<String, dynamic> json) =>
_$HistoryItemFromJson(json);
// JSON Export
Map<String, dynamic> toJson() => _$HistoryItemToJson(this);

View file

@ -1,21 +1,22 @@
import 'package:json_annotation/json_annotation.dart';
import 'history_item.dart';
import 'history_multipaste_item_entry.dart';
part 'history_multipaste_item.g.dart';
@JsonSerializable()
class HistoryMultipasteItem {
final String date;
final Map<String, HistoryItem> items;
final Map<String, HistoryMultipasteItemEntry> items;
@JsonKey(name: "url_id")
final String urlId;
HistoryMultipasteItem({this.date, this.items, this.urlId});
HistoryMultipasteItem(this.items, {required this.date, required this.urlId});
// JSON Init
factory HistoryMultipasteItem.fromJson(Map<String, dynamic> json) => _$HistoryMultipasteItemFromJson(json);
factory HistoryMultipasteItem.fromJson(Map<String, dynamic> json) =>
_$HistoryMultipasteItemFromJson(json);
// JSON Export
Map<String, dynamic> toJson() => _$HistoryMultipasteItemToJson(this);

View file

@ -0,0 +1,17 @@
import 'package:json_annotation/json_annotation.dart';
part 'history_multipaste_item_entry.g.dart';
@JsonSerializable()
class HistoryMultipasteItemEntry {
final String id;
HistoryMultipasteItemEntry({required this.id});
// JSON Init
factory HistoryMultipasteItemEntry.fromJson(Map<String, dynamic> json) =>
_$HistoryMultipasteItemEntryFromJson(json);
// JSON Export
Map<String, dynamic> toJson() => _$HistoryMultipasteItemEntryToJson(this);
}

View file

@ -12,10 +12,11 @@ class HistoryResponse {
@JsonKey(required: true)
final History data;
HistoryResponse({this.status, this.data});
HistoryResponse({required this.status, required this.data});
// JSON Init
factory HistoryResponse.fromJson(Map<String, dynamic> json) => _$HistoryResponseFromJson(json);
factory HistoryResponse.fromJson(Map<String, dynamic> json) =>
_$HistoryResponseFromJson(json);
// JSON Export
Map<String, dynamic> toJson() => _$HistoryResponseToJson(this);

View file

@ -10,12 +10,13 @@ class RestError {
final String errorId;
RestError({
this.status,
this.message,
this.errorId,
required this.status,
required this.message,
required this.errorId,
}); // JSON Init
factory RestError.fromJson(Map<String, dynamic> json) => _$RestErrorFromJson(json);
factory RestError.fromJson(Map<String, dynamic> json) =>
_$RestErrorFromJson(json);
// JSON Export
Map<String, dynamic> toJson() => _$RestErrorToJson(this);

View file

@ -10,10 +10,11 @@ class Uploaded {
@JsonKey(required: true)
final List<String> urls;
Uploaded({this.ids, this.urls});
Uploaded({required this.ids, required this.urls});
// JSON Init
factory Uploaded.fromJson(Map<String, dynamic> json) => _$UploadedFromJson(json);
factory Uploaded.fromJson(Map<String, dynamic> json) =>
_$UploadedFromJson(json);
// JSON Export
Map<String, dynamic> toJson() => _$UploadedToJson(this);

View file

@ -10,10 +10,11 @@ class UploadedMulti {
@JsonKey(required: true, name: "url_id")
final String urlId;
UploadedMulti({this.url, this.urlId});
UploadedMulti({required this.url, required this.urlId});
// JSON Init
factory UploadedMulti.fromJson(Map<String, dynamic> json) => _$UploadedMultiFromJson(json);
factory UploadedMulti.fromJson(Map<String, dynamic> json) =>
_$UploadedMultiFromJson(json);
// JSON Export
Map<String, dynamic> toJson() => _$UploadedMultiToJson(this);

View file

@ -12,10 +12,11 @@ class UploadedMultiResponse {
@JsonKey(required: true)
final UploadedMulti data;
UploadedMultiResponse({this.status, this.data});
UploadedMultiResponse({required this.status, required this.data});
// JSON Init
factory UploadedMultiResponse.fromJson(Map<String, dynamic> json) => _$UploadedMultiResponseFromJson(json);
factory UploadedMultiResponse.fromJson(Map<String, dynamic> json) =>
_$UploadedMultiResponseFromJson(json);
// JSON Export
Map<String, dynamic> toJson() => _$UploadedMultiResponseToJson(this);

View file

@ -12,10 +12,11 @@ class UploadedResponse {
@JsonKey(required: true)
final Uploaded data;
UploadedResponse({this.status, this.data});
UploadedResponse({required this.status, required this.data});
// JSON Init
factory UploadedResponse.fromJson(Map<String, dynamic> json) => _$UploadedResponseFromJson(json);
factory UploadedResponse.fromJson(Map<String, dynamic> json) =>
_$UploadedResponseFromJson(json);
// JSON Export
Map<String, dynamic> toJson() => _$UploadedResponseToJson(this);

View file

@ -7,13 +7,14 @@ class Session {
final String url;
final String apiKey;
Session({this.url, this.apiKey});
Session({required this.url, required this.apiKey});
Session.initial()
: url = '',
apiKey = '';
factory Session.fromJson(Map<String, dynamic> json) => _$SessionFromJson(json);
factory Session.fromJson(Map<String, dynamic> json) =>
_$SessionFromJson(json);
Map<String, dynamic> toJson() => _$SessionToJson(this);
}

View file

@ -4,29 +4,30 @@ part 'uploaded_paste.g.dart';
@JsonSerializable()
class UploadedPaste {
final DateTime date;
final String filename;
final num filesize;
final String hash;
final DateTime? date;
final String? filename;
final num? filesize;
final String? hash;
final String id;
final String mimetype;
final String thumbnail;
final bool isMulti;
final List<String> items;
final String? mimetype;
final String? thumbnail;
final bool? isMulti;
final List<String?>? items;
UploadedPaste(
{this.date,
this.filename,
this.filesize,
this.hash,
this.id,
required this.id,
this.mimetype,
this.thumbnail,
this.isMulti,
this.items});
// JSON Init
factory UploadedPaste.fromJson(Map<String, dynamic> json) => _$UploadedPasteFromJson(json);
factory UploadedPaste.fromJson(Map<String, dynamic> json) =>
_$UploadedPasteFromJson(json);
// JSON Export
Map<String, dynamic> toJson() => _$UploadedPasteToJson(this);

View file

@ -27,12 +27,16 @@ class FileRepository {
return parsedResponse.data;
}
Future<void> postDelete(String id) async {
await _api.post('/file/delete', fields: {'ids[1]': id});
Future postDelete(String id) async {
var fields = Map.fromEntries([MapEntry("ids[1]", id)]);
var response = await _api.post('/file/delete', fields: fields);
return response;
}
Future<UploadedResponse> postUpload(List<File> files, Map<String, String> additionalFiles) async {
var response = await _api.post('/file/upload', files: files, additionalFiles: additionalFiles);
Future<UploadedResponse> postUpload(
List<File>? files, Map<String, String>? additionalFiles) async {
var response = await _api.post('/file/upload',
files: files, additionalFiles: additionalFiles);
return UploadedResponse.fromJson(json.decode(response.body));
}
@ -40,10 +44,12 @@ class FileRepository {
Map<String, String> multiPasteIds = Map();
ids.forEach((element) {
multiPasteIds.putIfAbsent("ids[${ids.indexOf(element) + 1}]", () => element);
multiPasteIds.putIfAbsent(
"ids[${ids.indexOf(element) + 1}]", () => element);
});
var response = await _api.post('/file/create_multipaste', fields: multiPasteIds);
var response =
await _api.post('/file/create_multipaste', fields: multiPasteIds);
return UploadedMultiResponse.fromJson(json.decode(response.body));
}
}

View file

@ -8,12 +8,17 @@ import '../services/api.dart';
class UserRepository {
Api _api = locator<Api>();
Future<CreateApiKeyResponse> postApiKey(
String url, String username, String password, String accessLevel, String comment) async {
Future<CreateApiKeyResponse> postApiKey(String url, String username,
String password, String accessLevel, String comment) async {
_api.setUrl(url);
var response = await _api.post('/user/create_apikey',
fields: {'username': username, 'password': password, 'access_level': accessLevel, 'comment': comment});
var fields = Map.fromEntries([
MapEntry("username", username),
MapEntry("password", password),
MapEntry("access_level", accessLevel),
MapEntry("comment", comment),
]);
var response = await _api.post('/user/create_apikey', fields: fields);
return CreateApiKeyResponse.fromJson(json.decode(response.body));
}

View file

@ -24,25 +24,34 @@ class Api implements ApiErrorConverter {
String _url = "";
String _apiKey = "";
Map<String, String> _headers = {"Content-Type": _applicationJson, "Accept": _applicationJson};
Map<String, String> _headers = {
"Content-Type": _applicationJson,
"Accept": _applicationJson
};
Duration _timeout = Duration(seconds: Constants.apiRequestTimeoutLimit);
Future<http.Response> fetch<T>(String route) async {
try {
_logger
.d("Requesting GET API endpoint '${_url + route}' with headers '$_headers' and maximum timeout '$_timeout'");
var response = await http.get(Uri.parse(_url + route), headers: _headers).timeout(_timeout);
_logger.d(
"Requesting GET API endpoint '${_url + route}' with headers '$_headers' and maximum timeout '$_timeout'");
var response = await http
.get(Uri.parse(_url + route), headers: _headers)
.timeout(_timeout);
handleRestErrors(response);
return response;
} on TimeoutException {
throw ServiceException(code: ErrorCode.SOCKET_TIMEOUT, message: _errorTimeout);
throw ServiceException(
code: ErrorCode.SOCKET_TIMEOUT, message: _errorTimeout);
} on SocketException {
throw ServiceException(code: ErrorCode.SOCKET_ERROR, message: _errorNoConnection);
throw ServiceException(
code: ErrorCode.SOCKET_ERROR, message: _errorNoConnection);
}
}
Future<http.Response> post<T>(String route,
{Map<String, String> fields, List<File> files, Map<String, String> additionalFiles}) async {
{Map<String, String?>? fields,
List<File>? files,
Map<String, String>? additionalFiles}) async {
try {
var uri = Uri.parse(_url + route);
var request = http.MultipartRequest('POST', uri)
@ -54,32 +63,39 @@ class Api implements ApiErrorConverter {
}
if (fields != null && fields.isNotEmpty) {
request.fields.addAll(fields);
request.fields.addAll(fields as Map<String, String>);
}
if (files != null && files.isNotEmpty) {
files.forEach((element) async {
request.files.add(await http.MultipartFile.fromPath('file[${files.indexOf(element) + 1}]', element.path));
request.files.add(await http.MultipartFile.fromPath(
'file[${files.indexOf(element) + 1}]', element.path));
});
}
if (additionalFiles != null && additionalFiles.length > 0) {
List<String> keys = additionalFiles.keys.toList();
additionalFiles.forEach((key, value) {
var index = files != null ? files.length + keys.indexOf(key) + 1 : keys.indexOf(key) + 1;
request.files.add(http.MultipartFile.fromString('file[$index]', value, filename: key));
var index = files != null
? files.length + keys.indexOf(key) + 1
: keys.indexOf(key) + 1;
request.files.add(http.MultipartFile.fromString('file[$index]', value,
filename: key));
});
}
_logger.d("Requesting POST API endpoint '${uri.toString()}' and ${request.files.length} files");
_logger.d(
"Requesting POST API endpoint '${uri.toString()}' and ${request.files.length} files");
var multiResponse = await request.send();
var response = await http.Response.fromStream(multiResponse);
handleRestErrors(response);
return response;
} on TimeoutException {
throw ServiceException(code: ErrorCode.SOCKET_TIMEOUT, message: _errorTimeout);
throw ServiceException(
code: ErrorCode.SOCKET_TIMEOUT, message: _errorTimeout);
} on SocketException {
throw ServiceException(code: ErrorCode.SOCKET_ERROR, message: _errorNoConnection);
throw ServiceException(
code: ErrorCode.SOCKET_ERROR, message: _errorNoConnection);
}
}
@ -107,20 +123,21 @@ class Api implements ApiErrorConverter {
/// have a json decoded object. Replace this with a custom
/// conversion method by overwriting the interface if needed
void handleRestErrors(http.Response response) {
if (response != null) {
if (response.statusCode != HttpStatus.ok && response.statusCode != HttpStatus.noContent) {
if (response.headers.containsKey(HttpHeaders.contentTypeHeader)) {
ContentType responseContentType = ContentType.parse(response.headers[HttpHeaders.contentTypeHeader]);
if (response.statusCode != HttpStatus.ok &&
response.statusCode != HttpStatus.noContent) {
if (response.headers.containsKey(HttpHeaders.contentTypeHeader)) {
ContentType responseContentType =
ContentType.parse(response.headers[HttpHeaders.contentTypeHeader]!);
if (ContentType.json.primaryType == responseContentType.primaryType &&
ContentType.json.subType == responseContentType.subType) {
var parsedBody = convert(response);
throw new RestServiceException(response.statusCode, responseBody: parsedBody);
}
if (ContentType.json.primaryType == responseContentType.primaryType &&
ContentType.json.subType == responseContentType.subType) {
var parsedBody = convert(response);
throw new RestServiceException(response.statusCode,
responseBody: parsedBody);
}
throw new RestServiceException(response.statusCode);
}
throw new RestServiceException(response.statusCode);
}
}

View file

@ -8,8 +8,8 @@ import '../datamodels/dialog_response.dart';
class DialogService {
GlobalKey<NavigatorState> _dialogNavigationKey = GlobalKey<NavigatorState>();
Function(DialogRequest) _showDialogListener;
Completer<DialogResponse> _dialogCompleter;
late Function(DialogRequest) _showDialogListener;
Completer<DialogResponse>? _dialogCompleter;
GlobalKey<NavigatorState> get dialogNavigationKey => _dialogNavigationKey;
@ -18,35 +18,43 @@ class DialogService {
}
Future<DialogResponse> showDialog({
String title,
String description,
String buttonTitleAccept,
String? title,
String? description,
String? buttonTitleAccept,
}) {
_dialogCompleter = Completer<DialogResponse>();
_showDialogListener(DialogRequest(
title: title,
description: description,
buttonTitleAccept:
buttonTitleAccept == null || buttonTitleAccept.isEmpty ? translate('dialog.confirm') : buttonTitleAccept));
return _dialogCompleter.future;
buttonTitleAccept == null || buttonTitleAccept.isEmpty
? translate('dialog.confirm')
: buttonTitleAccept));
return _dialogCompleter!.future;
}
Future<DialogResponse> showConfirmationDialog(
{String title, String description, String buttonTitleAccept, String buttonTitleDeny}) {
{String? title,
String? description,
String? buttonTitleAccept,
String? buttonTitleDeny}) {
_dialogCompleter = Completer<DialogResponse>();
_showDialogListener(DialogRequest(
title: title,
description: description,
buttonTitleAccept:
buttonTitleAccept == null || buttonTitleAccept.isEmpty ? translate('dialog.confirm') : buttonTitleAccept,
buttonTitleDeny:
buttonTitleDeny == null || buttonTitleDeny.isEmpty ? translate('dialog.cancel') : buttonTitleDeny));
return _dialogCompleter.future;
buttonTitleAccept == null || buttonTitleAccept.isEmpty
? translate('dialog.confirm')
: buttonTitleAccept,
buttonTitleDeny: buttonTitleDeny == null || buttonTitleDeny.isEmpty
? translate('dialog.cancel')
: buttonTitleDeny));
return _dialogCompleter!.future;
}
void dialogComplete(DialogResponse response) {
_dialogNavigationKey.currentState.pop();
_dialogCompleter.complete(response);
_dialogNavigationKey.currentState!.pop();
_dialogCompleter!.complete(response);
_dialogCompleter = null;
}
}

View file

@ -15,15 +15,16 @@ class FileService {
return await _fileRepository.getConfig(url);
}
Future<History> getHistory() async {
FutureOr<History> getHistory() async {
return await _fileRepository.getHistory();
}
Future<void> deletePaste(String id) async {
Future deletePaste(String id) async {
return await _fileRepository.postDelete(id);
}
Future<UploadedResponse> uploadPaste(List<File> files, Map<String, String> additionalFiles) async {
Future<UploadedResponse> uploadPaste(
List<File>? files, Map<String, String>? additionalFiles) async {
return await _fileRepository.postUpload(files, additionalFiles);
}

View file

@ -17,7 +17,8 @@ class LinkService {
_logger.e('Could not launch link $link');
_dialogService.showDialog(
title: translate('link.dialog.title'),
description: translate('link.dialog.description', args: {'link': link}));
description:
translate('link.dialog.description', args: {'link': link}));
}
}
}

View file

@ -12,16 +12,16 @@ class NavigationService {
void pop() {
logger.d('NavigationService: pop');
_navigationKey.currentState.pop();
_navigationKey.currentState!.pop();
}
Future<dynamic> navigateTo(String routeName, {dynamic arguments}) {
logger.d('NavigationService: navigateTo $routeName');
return _navigationKey.currentState.pushNamed(routeName, arguments: arguments);
return _navigationKey.currentState!.pushNamed(routeName, arguments: arguments);
}
Future<dynamic> navigateAndReplaceTo(String routeName, {dynamic arguments}) {
logger.d('NavigationService: navigateAndReplaceTo $routeName');
return _navigationKey.currentState.pushReplacementNamed(routeName, arguments: arguments);
return _navigationKey.currentState!.pushReplacementNamed(routeName, arguments: arguments);
}
}

View file

@ -17,9 +17,9 @@ class PermissionService extends StoppableService {
final DialogService _dialogService = locator<DialogService>();
final StorageService _storageService = locator<StorageService>();
Timer _serviceCheckTimer;
Timer? _serviceCheckTimer;
PermissionStatus _permissionStatus;
PermissionStatus? _permissionStatus;
bool _permanentlyIgnored = false;
bool _devicePermissionDialogActive = false;
@ -57,7 +57,8 @@ class PermissionService extends StoppableService {
return;
}
var ignoredDialog = await _storageService.hasStoragePermissionDialogIgnored();
var ignoredDialog =
await _storageService.hasStoragePermissionDialogIgnored();
if (ignoredDialog) {
_logger.d('Permanently ignored permission request, skipping');
@ -79,7 +80,7 @@ class PermissionService extends StoppableService {
buttonTitleAccept: translate('permission_service.dialog.grant'),
buttonTitleDeny: translate('permission_service.dialog.ignore'));
if (!response.confirmed) {
if (!response.confirmed!) {
await _storageService.storeStoragePermissionDialogIgnored();
} else {
_devicePermissionDialogActive = true;
@ -104,8 +105,9 @@ class PermissionService extends StoppableService {
super.start();
await checkEnabledAndPermission();
_serviceCheckTimer =
Timer.periodic(Duration(milliseconds: Constants.mediaPermissionCheckInterval), (_serviceTimer) async {
_serviceCheckTimer = Timer.periodic(
Duration(milliseconds: Constants.mediaPermissionCheckInterval),
(_serviceTimer) async {
if (!super.serviceStopped) {
await checkEnabledAndPermission();
} else {
@ -124,7 +126,7 @@ class PermissionService extends StoppableService {
void _removeServiceCheckTimer() {
if (_serviceCheckTimer != null) {
_serviceCheckTimer.cancel();
_serviceCheckTimer!.cancel();
_serviceCheckTimer = null;
_logger.d('Removed service check timer');
}

View file

@ -14,9 +14,9 @@ class SessionService extends StoppableService {
final StorageService _storageService = locator<StorageService>();
final Api _api = locator<Api>();
StreamController<Session> sessionController = StreamController<Session>();
StreamController<Session?> sessionController = StreamController<Session?>();
void setApiConfig(String url, String apiKey) {
void setApiConfig(String url, String? apiKey) {
_logger.d('Setting API config for session');
_api.setUrl(url);
_api.addApiKeyAuthorization(apiKey);

View file

@ -13,7 +13,7 @@ class StorageService {
return _store(_LAST_URL_KEY, url);
}
Future<String> retrieveLastUrl() async {
Future<String?> retrieveLastUrl() async {
return await _retrieve(_LAST_URL_KEY);
}
@ -27,7 +27,7 @@ class StorageService {
Future<Session> retrieveSession() async {
var retrieve = await _retrieve(_SESSION_KEY);
return Session.fromJson(json.decode(retrieve));
return Session.fromJson(json.decode(retrieve!));
}
Future<bool> hasSession() {
@ -56,7 +56,7 @@ class StorageService {
return prefs.remove(key);
}
Future<String> _retrieve(String key) async {
Future<String?> _retrieve(String key) async {
final SharedPreferences prefs = await SharedPreferences.getInstance();
return prefs.getString(key);
}

View file

@ -12,9 +12,10 @@ class UserService {
final FileService _fileService = locator<FileService>();
final UserRepository _userRepository = locator<UserRepository>();
Future<CreateApiKeyResponse> createApiKey(
String url, String username, String password, String accessLevel, String comment) async {
return await _userRepository.postApiKey(url, username, password, accessLevel, comment);
Future<CreateApiKeyResponse> createApiKey(String url, String username,
String password, String accessLevel, String comment) async {
return await _userRepository.postApiKey(
url, username, password, accessLevel, comment);
}
Future<ApiKeysResponse> getApiKeys() async {
@ -25,8 +26,9 @@ class UserService {
Future<void> checkAccessLevelIsAtLeastApiKey() async {
try {
await _fileService.getHistory();
} catch (e) {
throw new ServiceException(code: ErrorCode.INVALID_API_KEY, message: e.message);
} on ServiceException catch (e) {
throw new ServiceException(
code: ErrorCode.INVALID_API_KEY, message: e.message);
}
}
}

View file

@ -6,7 +6,7 @@ class FormatterUtil {
/// Format epoch timestamp
static String formatEpoch(num millis) {
DateFormat dateFormat = DateFormat().add_yMEd().add_Hm();
return dateFormat.format(DateTime.fromMillisecondsSinceEpoch(millis));
return dateFormat.format(DateTime.fromMillisecondsSinceEpoch(millis as int));
}
static String formatBytes(int bytes, int decimals) {

View file

@ -1,4 +1,4 @@
import 'package:package_info/package_info.dart';
import 'package:package_info_plus/package_info_plus.dart';
import '../../core/services/link_service.dart';
import '../../locator.dart';

View file

@ -12,13 +12,13 @@ class BaseModel extends ChangeNotifier {
bool _isDisposed = false;
Map<String, Object> _stateMap = {STATE_VIEW: ViewState.Idle, STATE_MESSAGE: null};
Map<String, Object?> _stateMap = {STATE_VIEW: ViewState.Idle, STATE_MESSAGE: null};
ViewState get state => _stateMap[STATE_VIEW];
ViewState? get state => _stateMap[STATE_VIEW] as ViewState?;
String get stateMessage => _stateMap[STATE_MESSAGE];
String? get stateMessage => _stateMap[STATE_MESSAGE] as String?;
void setStateValue(String key, Object stateValue) {
void setStateValue(String key, Object? stateValue) {
if (_stateMap.containsKey(key)) {
_stateMap.update(key, (value) => stateValue);
} else {
@ -44,7 +44,7 @@ class BaseModel extends ChangeNotifier {
setStateValue(STATE_VIEW, stateView);
}
void setStateMessage(String stateMessage) {
void setStateMessage(String? stateMessage) {
setStateValue(STATE_MESSAGE, stateMessage);
}

View file

@ -28,13 +28,14 @@ class HistoryModel extends BaseModel {
final LinkService _linkService = locator<LinkService>();
final DialogService _dialogService = locator<DialogService>();
StreamSubscription _refreshTriggerSubscription;
late StreamSubscription _refreshTriggerSubscription;
List<UploadedPaste> pastes = [];
String errorMessage;
String? errorMessage;
void init() {
_refreshTriggerSubscription = _refreshService.refreshEventController.stream.listen((event) {
_refreshTriggerSubscription =
_refreshService.refreshEventController.stream.listen((event) {
if (event == RefreshEvent.RefreshHistory) {
_logger.d('History needs a refresh');
getHistory();
@ -48,13 +49,14 @@ class HistoryModel extends BaseModel {
try {
pastes.clear();
History _history = await _fileService.getHistory();
if (_history.items != null) {
if (_history.items.isNotEmpty) {
_history.items.forEach((key, value) {
var millisecondsSinceEpoch = int.parse(value.date) * 1000;
pastes.add(
UploadedPaste(
id: key,
date: DateTime.fromMillisecondsSinceEpoch(millisecondsSinceEpoch),
date:
DateTime.fromMillisecondsSinceEpoch(millisecondsSinceEpoch),
filename: value.filename,
filesize: int.parse(value.filesize),
hash: value.hash,
@ -66,7 +68,7 @@ class HistoryModel extends BaseModel {
});
}
if (_history.multipasteItems != null) {
if (_history.multipasteItems.isNotEmpty) {
_history.multipasteItems.forEach((key, multiPaste) {
var millisecondsSinceEpoch = int.parse(multiPaste.date) * 1000;
pastes.add(UploadedPaste(
@ -77,7 +79,7 @@ class HistoryModel extends BaseModel {
});
}
pastes.sort((a, b) => a.date.compareTo(b.date));
pastes.sort((a, b) => a.date!.compareTo(b.date!));
errorMessage = null;
} catch (e) {
if (e is RestServiceException) {
@ -90,9 +92,11 @@ class HistoryModel extends BaseModel {
e.responseBody is RestError &&
e.responseBody.message != null) {
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 {
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 {
errorMessage = translate('api.general_rest_error');
@ -115,11 +119,12 @@ class HistoryModel extends BaseModel {
Future deletePaste(String id) async {
DialogResponse res = await _dialogService.showConfirmationDialog(
title: translate('history.delete_dialog.title'),
description: translate('history.delete_dialog.description', args: {'id': id}),
description:
translate('history.delete_dialog.description', args: {'id': id}),
buttonTitleAccept: translate('history.delete_dialog.accept'),
buttonTitleDeny: translate('history.delete_dialog.deny'));
if (!res.confirmed) {
if (!res.confirmed!) {
return;
}
@ -139,7 +144,8 @@ class HistoryModel extends BaseModel {
e.statusCode != HttpStatus.forbidden &&
e.responseBody is RestError &&
e.responseBody.message != null) {
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 {
errorMessage = translate('api.general_rest_error');
}

View file

@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart';
@ -40,7 +41,7 @@ class LoginModel extends BaseModel {
final Logger _logger = getLogger();
bool useCredentialsLogin = true;
String errorMessage;
String? errorMessage;
void toggleLoginMethod() {
setStateView(ViewState.Busy);
@ -53,7 +54,7 @@ class LoginModel extends BaseModel {
if (hasLastUrl) {
setStateView(ViewState.Busy);
var s = await _storageService.retrieveLastUrl();
var s = await (_storageService.retrieveLastUrl() as FutureOr<String>);
if (s.isNotEmpty) {
_uriController = new TextEditingController(text: s);
@ -116,8 +117,20 @@ class LoginModel extends BaseModel {
try {
if (useCredentialsLogin) {
CreateApiKeyResponse apiKeyResponse = await _userService.createApiKey(
url, username, password, 'apikey', 'fbmobile-${new DateTime.now().millisecondsSinceEpoch}');
success = await _sessionService.login(url, apiKeyResponse.data['new_key']);
url,
username,
password,
'apikey',
'fbmobile-${new DateTime.now().millisecondsSinceEpoch}');
var newKey = apiKeyResponse.data['new_key'];
if (newKey != null) {
success = await _sessionService.login(url, newKey);
} else {
throw new ServiceException(
code: ErrorCode.INVALID_API_KEY,
message: translate('login.errors.invalid_api_key'));
}
} else {
_sessionService.setApiConfig(url, apiKey);
await _userService.checkAccessLevelIsAtLeastApiKey();
@ -128,13 +141,15 @@ class LoginModel extends BaseModel {
if (e is RestServiceException) {
if (e.statusCode == HttpStatus.unauthorized) {
errorMessage = translate('login.errors.wrong_credentials');
} else if (e.statusCode != HttpStatus.unauthorized && e.statusCode == HttpStatus.forbidden) {
} else if (e.statusCode != HttpStatus.unauthorized &&
e.statusCode == HttpStatus.forbidden) {
errorMessage = translate('login.errors.forbidden');
} else if (e.statusCode == HttpStatus.notFound) {
errorMessage = translate('api.incompatible_error_not_found');
}
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 {
errorMessage = translate('api.general_rest_error');
}
@ -152,7 +167,7 @@ class LoginModel extends BaseModel {
throw e;
}
if (errorMessage.isNotEmpty) {
if (errorMessage!.isNotEmpty) {
_sessionService.logout();
}

View file

@ -24,25 +24,28 @@ class ProfileModel extends BaseModel {
final FileService _fileService = locator<FileService>();
final Logger _logger = getLogger();
String errorMessage;
String? errorMessage;
Future logout() async {
var dialogResult = await _dialogService.showConfirmationDialog(
title: translate('logout.title'), description: translate('logout.confirm'));
title: translate('logout.title'),
description: translate('logout.confirm'));
if (dialogResult.confirmed) {
if (dialogResult.confirmed!) {
await _sessionService.logout();
}
}
Future revealApiKey(String apiKey) async {
Future revealApiKey(String? apiKey) async {
await _dialogService.showDialog(
title: translate('profile.revealed_api_key.title'),
description: translate('profile.revealed_api_key.description', args: {'apiKey': apiKey}));
description: translate('profile.revealed_api_key.description',
args: {'apiKey': apiKey}));
}
Future showConfig(String url) async {
Config config;
setStateView(ViewState.Busy);
Config? config;
try {
config = await _fileService.getConfig(url);
errorMessage = null;
@ -50,13 +53,15 @@ class ProfileModel extends BaseModel {
if (e is RestServiceException) {
if (e.statusCode == HttpStatus.unauthorized) {
errorMessage = translate('login.errors.wrong_credentials');
} else if (e.statusCode != HttpStatus.unauthorized && e.statusCode == HttpStatus.forbidden) {
} else if (e.statusCode != HttpStatus.unauthorized &&
e.statusCode == HttpStatus.forbidden) {
errorMessage = translate('login.errors.forbidden');
} else if (e.statusCode == HttpStatus.notFound) {
errorMessage = translate('api.incompatible_error_not_found');
}
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 {
errorMessage = translate('api.general_rest_error');
}
@ -66,6 +71,7 @@ class ProfileModel extends BaseModel {
errorMessage = translate('api.socket_timeout');
} else {
errorMessage = translate('app.unknown_error');
setStateView(ViewState.Idle);
_sessionService.logout();
setStateView(ViewState.Idle);
_logger.e('An unknown error occurred', e);
@ -73,19 +79,24 @@ class ProfileModel extends BaseModel {
}
}
setStateView(ViewState.Idle);
if (config != null && errorMessage == null) {
await _dialogService.showDialog(
title: translate('profile.shown_config.title'),
description: translate('profile.shown_config.description', args: {
'uploadMaxSize': FormatterUtil.formatBytes(config.uploadMaxSize, 2),
'uploadMaxSize':
FormatterUtil.formatBytes(config.uploadMaxSize as int, 2),
'maxFilesPerRequest': config.maxFilesPerRequest,
'maxInputVars': config.maxInputVars,
'requestMaxSize': FormatterUtil.formatBytes(config.requestMaxSize, 2)
'requestMaxSize':
FormatterUtil.formatBytes(config.requestMaxSize as int, 2)
}));
} else {
await _dialogService.showDialog(
title: translate('profile.shown_config.error.title'),
description: translate('profile.shown_config.error.description', args: {'message': errorMessage}));
description: translate('profile.shown_config.error.description',
args: {'message': errorMessage}));
}
}

View file

@ -37,14 +37,14 @@ class UploadModel extends BaseModel {
TextEditingController _pasteTextController = TextEditingController();
bool pasteTextTouched = false;
StreamSubscription _intentDataStreamSubscription;
late StreamSubscription _intentDataStreamSubscription;
bool createMulti = false;
String fileName;
List<PlatformFile> paths;
String _extension;
String? fileName;
List<PlatformFile>? paths;
String? _extension;
bool loadingPath = false;
String errorMessage;
String? errorMessage;
TextEditingController get pasteTextController => _pasteTextController;
@ -55,8 +55,9 @@ class UploadModel extends BaseModel {
});
// For sharing images coming from outside the app while the app is in the memory
_intentDataStreamSubscription = ReceiveSharingIntent.getMediaStream().listen((List<SharedMediaFile> value) {
if (value != null && value.length > 0) {
_intentDataStreamSubscription = ReceiveSharingIntent.getMediaStream()
.listen((List<SharedMediaFile> value) {
if (value.length > 0) {
setStateView(ViewState.Busy);
paths = value.map((sharedFile) {
return PlatformFile.fromMap({
@ -67,7 +68,7 @@ class UploadModel extends BaseModel {
});
}).toList();
setStateView(ViewState.Idle);
if (paths.isNotEmpty && paths.length > 0) {
if (paths!.isNotEmpty && paths!.length > 0) {
_swipeService.addEvent(SwipeEvent.Start);
}
}
@ -77,7 +78,7 @@ class UploadModel extends BaseModel {
// For sharing images coming from outside the app while the app is closed
ReceiveSharingIntent.getInitialMedia().then((List<SharedMediaFile> value) {
if (value != null && value.length > 0) {
if (value.length > 0) {
setStateView(ViewState.Busy);
paths = value.map((sharedFile) {
return PlatformFile.fromMap({
@ -88,7 +89,7 @@ class UploadModel extends BaseModel {
});
}).toList();
setStateView(ViewState.Idle);
if (paths.isNotEmpty && paths.length > 0) {
if (paths!.isNotEmpty && paths!.length > 0) {
_swipeService.addEvent(SwipeEvent.Start);
}
}
@ -97,8 +98,9 @@ class UploadModel extends BaseModel {
});
// 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 != null && value.isNotEmpty) {
_intentDataStreamSubscription =
ReceiveSharingIntent.getTextStream().listen((String value) {
if (value.isNotEmpty) {
setStateView(ViewState.Busy);
pasteTextController.text = value;
setStateView(ViewState.Idle);
@ -109,12 +111,12 @@ class UploadModel extends BaseModel {
});
// For sharing or opening urls/text coming from outside the app while the app is closed
ReceiveSharingIntent.getInitialText().then((String value) {
ReceiveSharingIntent.getInitialText().then((String? value) {
if (value != null && value.isNotEmpty) {
setStateView(ViewState.Busy);
pasteTextController.text = value;
setStateView(ViewState.Idle);
if (paths.isNotEmpty && paths.length > 0) {
if (paths!.isNotEmpty && paths!.length > 0) {
_swipeService.addEvent(SwipeEvent.Start);
}
}
@ -132,7 +134,7 @@ class UploadModel extends BaseModel {
_swipeService.addEvent(SwipeEvent.Start);
}
String generatePasteLinks(Map<String, bool> uploads, String url) {
String? generatePasteLinks(Map<String, bool>? uploads, String url) {
if (uploads != null && uploads.length > 0) {
var links = '';
@ -165,7 +167,9 @@ class UploadModel extends BaseModel {
allowMultiple: true,
withData: false,
withReadStream: true,
allowedExtensions: (_extension?.isNotEmpty ?? false) ? _extension?.replaceAll(' ', '')?.split(',') : null,
allowedExtensions: (_extension?.isNotEmpty ?? false)
? _extension?.replaceAll(' ', '').split(',')
: null,
))
?.files;
} on PlatformException catch (e) {
@ -175,7 +179,7 @@ class UploadModel extends BaseModel {
}
loadingPath = false;
fileName = paths != null ? paths.map((e) => e.name).toString() : '...';
fileName = paths != null ? paths!.map((e) => e.name).toString() : '...';
setStateMessage(null);
setStateView(ViewState.Idle);
@ -190,31 +194,35 @@ class UploadModel extends BaseModel {
setStateView(ViewState.Idle);
}
Future<Map<String, bool>> upload() async {
Future<Map<String, bool>?> upload() async {
setStateView(ViewState.Busy);
setStateMessage(translate('upload.uploading_now'));
Map<String, bool> uploadedPasteIds = new Map();
try {
List<File> files;
Map<String, String> additionalFiles;
List<File>? files;
Map<String, String>? additionalFiles;
if (pasteTextController.text != null && pasteTextController.text.isNotEmpty) {
additionalFiles = Map.from(
{'paste-${(new DateTime.now().millisecondsSinceEpoch / 1000).round()}.txt': pasteTextController.text});
if (pasteTextController.text.isNotEmpty) {
additionalFiles = Map.from({
'paste-${(new DateTime.now().millisecondsSinceEpoch / 1000).round()}.txt':
pasteTextController.text
});
}
if (paths != null && paths.length > 0) {
files = paths.map((e) => new File(e.path)).toList();
if (paths != null && paths!.length > 0) {
files = paths!.map((e) => new File(e.path!)).toList();
}
UploadedResponse response = await _fileService.uploadPaste(files, additionalFiles);
UploadedResponse response =
await _fileService.uploadPaste(files, additionalFiles);
response.data.ids.forEach((element) {
uploadedPasteIds.putIfAbsent(element, () => false);
});
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);
}
@ -234,9 +242,11 @@ class UploadModel extends BaseModel {
e.responseBody is RestError &&
e.responseBody.message != null) {
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 {
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 {
errorMessage = translate('api.general_rest_error');

View file

@ -7,8 +7,8 @@ import '../../core/viewmodels/base_model.dart';
import '../../locator.dart';
class BaseView<T extends BaseModel> extends StatefulWidget {
final Widget Function(BuildContext context, T model, Widget child) builder;
final Function(T) onModelReady;
final Widget Function(BuildContext context, T model, Widget? child)? builder;
final Function(T)? onModelReady;
BaseView({this.builder, this.onModelReady});
@ -24,14 +24,16 @@ class _BaseViewState<T extends BaseModel> extends State<BaseView<T>> {
@override
void initState() {
if (widget.onModelReady != null) {
widget.onModelReady(model);
widget.onModelReady!(model);
}
super.initState();
}
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider<T>(create: (context) => model, child: Consumer<T>(builder: widget.builder));
return ChangeNotifierProvider<T?>(
create: (context) => model,
child: Consumer<T>(builder: widget.builder!));
}
@override

View file

@ -41,7 +41,9 @@ class HistoryView extends StatelessWidget {
: (model.errorMessage == null
? Container(
padding: EdgeInsets.all(0),
child: RefreshIndicator(onRefresh: () => model.getHistory(), child: _renderItems(model, url, context)))
child: RefreshIndicator(
onRefresh: () async => await model.getHistory(),
child: _renderItems(model, url, context)))
: Container(
padding: EdgeInsets.all(25),
child: CenteredErrorRow(
@ -61,7 +63,8 @@ class HistoryView extends StatelessWidget {
var openInBrowserButton = _renderOpenInBrowser(model, fullPasteUrl);
var dateWidget = ListTile(
title: Text(FormatterUtil.formatEpoch(paste.date.millisecondsSinceEpoch)),
title: Text(
FormatterUtil.formatEpoch(paste.date!.millisecondsSinceEpoch)),
subtitle: Text(translate('history.date')),
);
@ -73,7 +76,8 @@ class HistoryView extends StatelessWidget {
var copyWidget = ListTile(
title: Text(translate('history.copy_link.description')),
trailing: IconButton(
icon: Icon(Icons.copy, color: blueColor, textDirection: TextDirection.ltr),
icon: Icon(Icons.copy,
color: blueColor, textDirection: TextDirection.ltr),
onPressed: () {
FlutterClipboard.copy(fullPasteUrl).then((value) {
final snackBar = SnackBar(
@ -96,16 +100,16 @@ class HistoryView extends StatelessWidget {
trailing: IconButton(
icon: Icon(Icons.delete, color: redColor),
onPressed: () {
return model.deletePaste(paste.id);
model.deletePaste(paste.id);
}));
if (!paste.isMulti) {
if (!paste.isMulti!) {
var titleWidget = ListTile(
title: Text(paste.filename ?? paste.id),
subtitle: Text(translate('history.filename')),
);
var fileSizeWidget = ListTile(
title: Text(FormatterUtil.formatBytes(paste.filesize, 2)),
title: Text(FormatterUtil.formatBytes(paste.filesize as int, 2)),
subtitle: Text(translate('history.filesize')),
);
var idWidget = ListTile(
@ -113,7 +117,7 @@ class HistoryView extends StatelessWidget {
subtitle: Text(translate('history.id')),
);
var mimeTypeWidget = ListTile(
title: Text(paste.mimetype),
title: Text(paste.mimetype!),
subtitle: Text(translate('history.mimetype')),
);
@ -122,9 +126,9 @@ class HistoryView extends StatelessWidget {
widgets.add(fileSizeWidget);
widgets.add(mimeTypeWidget);
} else {
paste.items.forEach((element) {
paste.items!.forEach((element) {
widgets.add(ListTile(
title: Text(element),
title: Text(element!),
subtitle: Text(translate('history.multipaste_element')),
trailing: _renderOpenInBrowser(model, '$url/$element'),
));
@ -164,12 +168,14 @@ class HistoryView extends StatelessWidget {
trailing: Wrap(children: [
openInBrowserButton,
IconButton(
icon: Icon(Icons.share, color: blueColor, textDirection: TextDirection.ltr),
onPressed: () {
return Share.share(fullPasteUrl);
icon: Icon(Icons.share,
color: blueColor, textDirection: TextDirection.ltr),
onPressed: () async {
await Share.share(fullPasteUrl);
})
]),
subtitle: Text(!paste.isMulti ? paste.filename : '', style: TextStyle(fontStyle: FontStyle.italic)),
subtitle: Text(!paste.isMulti! ? paste.filename! : '',
style: TextStyle(fontStyle: FontStyle.italic)),
),
));
});
@ -190,7 +196,8 @@ class HistoryView extends StatelessWidget {
Widget _renderOpenInBrowser(HistoryModel model, String url) {
return IconButton(
icon: Icon(Icons.open_in_new, color: blueColor, textDirection: TextDirection.ltr),
icon: Icon(Icons.open_in_new,
color: blueColor, textDirection: TextDirection.ltr),
onPressed: () {
return model.openLink(url);
});

View file

@ -59,13 +59,18 @@ class LoginView extends StatelessWidget {
child: Icon(Icons.help, color: buttonBackgroundColor),
onTap: () {
_dialogService.showDialog(
title: translate('login.compatibility_dialog.title'),
description: translate('login.compatibility_dialog.body'));
title: translate(
'login.compatibility_dialog.title'),
description: translate(
'login.compatibility_dialog.body'));
},
),
InkWell(
child:
Icon(model.useCredentialsLogin ? Icons.person_outline : Icons.vpn_key, color: blueColor),
child: Icon(
model.useCredentialsLogin
? Icons.person_outline
: Icons.vpn_key,
color: blueColor),
onTap: () {
model.toggleLoginMethod();
},
@ -85,11 +90,13 @@ class LoginView extends StatelessWidget {
apiKeyController: model.apiKeyController),
UIHelper.verticalSpaceMedium(),
ElevatedButton(
child: Text(translate('login.button'), style: TextStyle(color: buttonForegroundColor)),
child: Text(translate('login.button'),
style: TextStyle(color: buttonForegroundColor)),
onPressed: () async {
var loginSuccess = await model.login();
if (loginSuccess) {
_navigationService.navigateAndReplaceTo(HomeView.routeName);
_navigationService
.navigateAndReplaceTo(HomeView.routeName);
}
},
)

View file

@ -59,8 +59,8 @@ class ProfileView extends StatelessWidget {
translate('profile.show_config'),
style: TextStyle(color: buttonForegroundColor),
),
onPressed: () {
return model.showConfig(url);
onPressed: () async {
await model.showConfig(url);
})),
UIHelper.verticalSpaceMedium(),
Padding(
@ -72,7 +72,7 @@ class ProfileView extends StatelessWidget {
style: TextStyle(color: buttonForegroundColor),
),
onPressed: () {
return model.revealApiKey(apiKey);
model.revealApiKey(apiKey);
})),
UIHelper.verticalSpaceMedium(),
Padding(
@ -83,8 +83,8 @@ class ProfileView extends StatelessWidget {
translate('profile.logout'),
style: TextStyle(color: buttonForegroundColor),
),
onPressed: () {
return model.logout();
onPressed: () async {
await model.logout();
})),
],
);

View file

@ -22,7 +22,7 @@ class StartUpView extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.center,
children: [
CircularProgressIndicator(),
(model.stateMessage.isNotEmpty ? Text(model.stateMessage) : Container())
(model.stateMessage!.isNotEmpty ? Text(model.stateMessage!) : Container())
]))
: Container()));
}

View file

@ -11,7 +11,7 @@ class AnonymousTabBarView extends StatefulWidget {
}
class AnonymousTabBarState extends State<AnonymousTabBarView> with SingleTickerProviderStateMixin {
TabController _tabController;
TabController? _tabController;
int _currentTabIndex = 0;
List<Widget> _realPages = [LoginView()];
@ -33,7 +33,7 @@ class AnonymousTabBarState extends State<AnonymousTabBarView> with SingleTickerP
super.initState();
_tabController = TabController(length: _realPages.length, vsync: this)
..addListener(() {
int selectedIndex = _tabController.index;
int selectedIndex = _tabController!.index;
if (_currentTabIndex != selectedIndex) {
if (!_hasInit[selectedIndex]) {
_tabPages[selectedIndex] = _realPages[selectedIndex];
@ -46,7 +46,7 @@ class AnonymousTabBarState extends State<AnonymousTabBarView> with SingleTickerP
@override
void dispose() {
_tabController.dispose();
_tabController!.dispose();
super.dispose();
}

View file

@ -20,12 +20,13 @@ class AuthenticatedTabBarView extends StatefulWidget {
AuthenticatedTabBarState createState() => AuthenticatedTabBarState();
}
class AuthenticatedTabBarState extends State<AuthenticatedTabBarView> with SingleTickerProviderStateMixin {
class AuthenticatedTabBarState extends State<AuthenticatedTabBarView>
with SingleTickerProviderStateMixin {
final Logger _logger = getLogger();
final SwipeService _swipeService = locator<SwipeService>();
StreamSubscription _swipeEventSubscription;
TabController _tabController;
late StreamSubscription _swipeEventSubscription;
TabController? _tabController;
int _currentTabIndex = 0;
List<Widget> _realPages = [UploadView(), HistoryView(), ProfileView()];
@ -41,7 +42,7 @@ class AuthenticatedTabBarState extends State<AuthenticatedTabBarView> with Singl
super.initState();
_tabController = TabController(length: _realPages.length, vsync: this)
..addListener(() {
int selectedIndex = _tabController.index;
int selectedIndex = _tabController!.index;
if (_currentTabIndex != selectedIndex) {
if (!_hasInit[selectedIndex]) {
_tabPages[selectedIndex] = _realPages[selectedIndex];
@ -51,7 +52,8 @@ class AuthenticatedTabBarState extends State<AuthenticatedTabBarView> with Singl
}
});
_swipeEventSubscription = _swipeService.swipeEventController.stream.listen((SwipeEvent event) {
_swipeEventSubscription =
_swipeService.swipeEventController.stream.listen((SwipeEvent event) {
_logger.d('Received a swipe event for the authenticated tab bar: $event');
int targetIndex = _currentTabIndex;
@ -71,14 +73,15 @@ class AuthenticatedTabBarState extends State<AuthenticatedTabBarView> with Singl
targetIndex = _tabPages.length - 1;
}
_logger.d("Changing to tab '$targetIndex' because of a swipe event '$event'");
_tabController.animateTo(targetIndex);
_logger.d(
"Changing to tab '$targetIndex' because of a swipe event '$event'");
_tabController!.animateTo(targetIndex);
});
}
@override
void dispose() {
_tabController.dispose();
_tabController!.dispose();
_swipeEventSubscription.cancel();
super.dispose();
}
@ -89,9 +92,12 @@ class AuthenticatedTabBarState extends State<AuthenticatedTabBarView> with Singl
double yourWidth = width / 3;
double yourHeight = 55;
Color colorTabItem0 = _currentTabIndex == 0 ? blueColor : primaryAccentColor;
Color colorTabItem1 = _currentTabIndex == 1 ? blueColor : primaryAccentColor;
Color colorTabItem2 = _currentTabIndex == 2 ? blueColor : primaryAccentColor;
Color colorTabItem0 =
_currentTabIndex == 0 ? blueColor : primaryAccentColor;
Color colorTabItem1 =
_currentTabIndex == 1 ? blueColor : primaryAccentColor;
Color colorTabItem2 =
_currentTabIndex == 2 ? blueColor : primaryAccentColor;
List<Widget> _tabsButton = [
Container(
@ -100,10 +106,13 @@ class AuthenticatedTabBarState extends State<AuthenticatedTabBarView> with Singl
alignment: Alignment.center,
child: Tab(
icon: Icon(
_currentTabIndex == 0 ? Icons.upload_outlined : Icons.upload_rounded,
_currentTabIndex == 0
? Icons.upload_outlined
: Icons.upload_rounded,
color: colorTabItem0,
),
child: Text(translate('tabs.upload'), style: TextStyle(color: colorTabItem0)),
child: Text(translate('tabs.upload'),
style: TextStyle(color: colorTabItem0)),
),
),
Container(
@ -112,10 +121,13 @@ class AuthenticatedTabBarState extends State<AuthenticatedTabBarView> with Singl
alignment: Alignment.center,
child: Tab(
icon: Icon(
_currentTabIndex == 1 ? Icons.history_outlined : Icons.history_rounded,
_currentTabIndex == 1
? Icons.history_outlined
: Icons.history_rounded,
color: colorTabItem1,
),
child: Text(translate('tabs.history'), style: TextStyle(color: colorTabItem1)),
child: Text(translate('tabs.history'),
style: TextStyle(color: colorTabItem1)),
),
),
Container(
@ -124,10 +136,13 @@ class AuthenticatedTabBarState extends State<AuthenticatedTabBarView> with Singl
alignment: Alignment.center,
child: Tab(
icon: Icon(
_currentTabIndex == 2 ? Icons.person_outlined : Icons.person_rounded,
_currentTabIndex == 2
? Icons.person_outlined
: Icons.person_rounded,
color: colorTabItem2,
),
child: Text(translate('tabs.profile'), style: TextStyle(color: colorTabItem2)),
child: Text(translate('tabs.profile'),
style: TextStyle(color: colorTabItem2)),
),
),
];

View file

@ -8,8 +8,9 @@ import 'tabbar_authenticated.dart';
class TabBarContainerView extends StatelessWidget {
@override
Widget build(BuildContext context) {
Session currentSession = Provider.of<Session>(context);
bool isAuthenticated = currentSession != null && currentSession.apiKey.isNotEmpty;
Session? currentSession = Provider.of<Session?>(context);
bool isAuthenticated =
currentSession != null ? currentSession.apiKey.isNotEmpty : false;
if (isAuthenticated) {
return AuthenticatedTabBarView();

View file

@ -26,7 +26,8 @@ class UploadView extends StatelessWidget {
}
bool _isUploadButtonEnabled(UploadModel model) {
return model.pasteTextTouched || (model.paths != null && model.paths.length > 0);
return model.pasteTextTouched ||
(model.paths != null && model.paths!.length > 0);
}
Widget _render(UploadModel model, BuildContext context) {
@ -39,7 +40,9 @@ class UploadView extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.center,
children: [
CircularProgressIndicator(),
(model.stateMessage != null && model.stateMessage.isNotEmpty ? Text(model.stateMessage) : Container())
(model.stateMessage != null && model.stateMessage!.isNotEmpty
? Text(model.stateMessage!)
: Container())
]))
: ListView(children: <Widget>[
Padding(
@ -58,12 +61,15 @@ class UploadView extends StatelessWidget {
color: buttonBackgroundColor,
),
suffixIcon: IconButton(
onPressed: () => model.pasteTextController.clear(),
onPressed: () =>
model.pasteTextController.clear(),
icon: Icon(Icons.clear),
),
hintText: translate('upload.text_to_be_pasted'),
contentPadding: EdgeInsets.fromLTRB(20.0, 10.0, 20.0, 10.0),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(32.0)),
contentPadding:
EdgeInsets.fromLTRB(20.0, 10.0, 20.0, 10.0),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(32.0)),
),
controller: model.pasteTextController)),
Padding(
@ -79,20 +85,24 @@ class UploadView extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.end,
children: [
ElevatedButton.icon(
icon: Icon(Icons.file_copy_sharp, color: blueColor),
icon: Icon(Icons.file_copy_sharp,
color: blueColor),
onPressed: () => model.openFileExplorer(),
label: Text(
translate('upload.open_file_explorer'),
style: TextStyle(color: buttonForegroundColor),
style:
TextStyle(color: buttonForegroundColor),
)),
ElevatedButton.icon(
icon: Icon(Icons.cancel, color: orangeColor),
onPressed: model.paths != null && model.paths.length > 0
onPressed: model.paths != null &&
model.paths!.length > 0
? () => model.clearCachedFiles()
: null,
label: Text(
translate('upload.clear_temporary_files'),
style: TextStyle(color: buttonForegroundColor),
style:
TextStyle(color: buttonForegroundColor),
)),
],
)),
@ -118,35 +128,48 @@ class UploadView extends StatelessWidget {
onPressed: !_isUploadButtonEnabled(model)
? null
: () async {
Map<String, bool> items = await model.upload();
String clipboardContent = model.generatePasteLinks(items, url);
Map<String, bool>? items =
await model.upload();
String? clipboardContent = model
.generatePasteLinks(items, url);
if (clipboardContent != null && clipboardContent.isNotEmpty) {
FlutterClipboard.copy(clipboardContent).then((value) {
if (clipboardContent != null &&
clipboardContent.isNotEmpty) {
FlutterClipboard.copy(
clipboardContent)
.then((value) {
final snackBar = SnackBar(
action: SnackBarAction(
label: translate('upload.dismiss'),
label: translate(
'upload.dismiss'),
textColor: blueColor,
onPressed: () {
ScaffoldMessenger.of(context).hideCurrentSnackBar();
ScaffoldMessenger.of(
context)
.hideCurrentSnackBar();
},
),
content: Text(translate('upload.uploaded')),
content: Text(translate(
'upload.uploaded')),
duration: Duration(seconds: 10),
);
ScaffoldMessenger.of(context).showSnackBar(snackBar);
ScaffoldMessenger.of(context)
.showSnackBar(snackBar);
});
}
},
icon: Icon(Icons.upload_rounded, color: greenColor),
icon: Icon(Icons.upload_rounded,
color: greenColor),
label: Text(
translate('upload.upload'),
style: TextStyle(color: buttonForegroundColor),
style:
TextStyle(color: buttonForegroundColor),
)),
])),
model.errorMessage != null && model.errorMessage.isNotEmpty
model.errorMessage != null && model.errorMessage!.isNotEmpty
? (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)))
: Container(),
Builder(
@ -158,16 +181,28 @@ class UploadView extends StatelessWidget {
: model.paths != null
? Container(
padding: const EdgeInsets.only(bottom: 30.0),
height: MediaQuery.of(context).size.height * 0.50,
height:
MediaQuery.of(context).size.height * 0.50,
child: ListView.separated(
itemCount: model.paths != null && model.paths.isNotEmpty ? model.paths.length : 1,
itemBuilder: (BuildContext context, int index) {
final bool isMultiPath = model.paths != null && model.paths.isNotEmpty;
itemCount: 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
? model.paths.map((e) => e.name).toList()[index]
? model.paths!
.map((e) => e.name)
.toList()[index]!
: model.fileName ?? '...');
final path = model.paths.length > 0
? model.paths.map((e) => e.path).toList()[index].toString()
final path = model.paths!.length > 0
? model.paths!
.map((e) => e.path)
.toList()[index]
.toString()
: '';
return Card(
@ -178,7 +213,9 @@ class UploadView extends StatelessWidget {
subtitle: Text(path),
));
},
separatorBuilder: (BuildContext context, int index) => const Divider(),
separatorBuilder:
(BuildContext context, int index) =>
const Divider(),
),
)
: Container(),

View file

@ -3,8 +3,8 @@ import 'package:flutter/material.dart';
import '../shared/app_colors.dart';
class CenteredErrorRow extends StatelessWidget {
final Function retryCallback;
final String message;
final Function? retryCallback;
final String? message;
CenteredErrorRow(this.message, {this.retryCallback});
@ -20,7 +20,7 @@ class CenteredErrorRow extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Expanded(child: Center(child: Text(message, style: TextStyle(color: redColor)))),
Expanded(child: Center(child: Text(message!, style: TextStyle(color: redColor)))),
],
),
(retryCallback != null
@ -33,7 +33,7 @@ class CenteredErrorRow extends StatelessWidget {
icon: Icon(Icons.refresh),
color: primaryAccentColor,
onPressed: () {
retryCallback();
retryCallback!();
},
))
])

View file

@ -8,14 +8,14 @@ class LoginApiKeyHeaders extends StatelessWidget {
final TextEditingController uriController;
final TextEditingController apiKeyController;
final String validationMessage;
final String? validationMessage;
LoginApiKeyHeaders({@required this.uriController, @required this.apiKeyController, this.validationMessage});
LoginApiKeyHeaders({required this.uriController, required this.apiKeyController, this.validationMessage});
@override
Widget build(BuildContext context) {
return Column(children: <Widget>[
this.validationMessage != null ? Text(validationMessage, style: TextStyle(color: redColor)) : Container(),
this.validationMessage != null ? Text(validationMessage!, style: TextStyle(color: redColor)) : Container(),
LoginTextField(uriController, translate('login.url_placeholder'), Icon(Icons.link),
keyboardType: TextInputType.url),
LoginTextField(

View file

@ -9,18 +9,18 @@ class LoginCredentialsHeaders extends StatelessWidget {
final TextEditingController usernameController;
final TextEditingController passwordController;
final String validationMessage;
final String? validationMessage;
LoginCredentialsHeaders(
{@required this.uriController,
@required this.usernameController,
@required this.passwordController,
{required this.uriController,
required this.usernameController,
required this.passwordController,
this.validationMessage});
@override
Widget build(BuildContext context) {
return Column(children: <Widget>[
this.validationMessage != null ? Text(validationMessage, style: TextStyle(color: redColor)) : Container(),
this.validationMessage != null ? Text(validationMessage!, style: TextStyle(color: redColor)) : Container(),
LoginTextField(uriController, translate('login.url_placeholder'), Icon(Icons.link),
keyboardType: TextInputType.url),
LoginTextField(usernameController, translate('login.username_placeholder'), Icon(Icons.person),

View file

@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../shared/app_colors.dart';
import '../widgets/about_iconbutton.dart';
@ -7,15 +8,23 @@ class MyAppBar extends AppBar {
static final List<Widget> aboutEnabledWidgets = [AboutIconButton()];
static final List<Widget> aboutDisabledWidgets = [];
MyAppBar({Key key, Widget title, List<Widget> actionWidgets, bool enableAbout = true})
MyAppBar(
{Key? key,
required Widget title,
List<Widget>? actionWidgets,
bool enableAbout = true})
: super(
key: key,
title: Row(children: <Widget>[title]),
actions: _renderIconButtons(actionWidgets, enableAbout),
brightness: appBarBrightness,
systemOverlayStyle: SystemUiOverlayStyle(
systemNavigationBarColor: primaryAccentColor, // Navigation bar
statusBarColor: primaryAccentColor, // Status bar
),
backgroundColor: primaryAccentColor);
static List<Widget> _renderIconButtons(List<Widget> actionWidgets, bool aboutEnabled) {
static List<Widget> _renderIconButtons(
List<Widget>? actionWidgets, bool aboutEnabled) {
if (actionWidgets == null) {
actionWidgets = [];
}

View file

@ -7,11 +7,11 @@ import '../../locator.dart';
class SwipeNavigation extends StatefulWidget {
/// Widget to be augmented with gesture detection.
final Widget child;
final Widget? child;
/// Creates a [SwipeNavigation] widget.
const SwipeNavigation({
Key key,
Key? key,
this.child,
}) : super(key: key);
@ -32,6 +32,7 @@ class _SwipeNavigationState extends State<SwipeNavigation> {
@override
Widget build(BuildContext context) {
return SimpleGestureDetector(onHorizontalSwipe: _onHorizontalSwipe, child: widget.child);
return SimpleGestureDetector(
onHorizontalSwipe: _onHorizontalSwipe, child: widget.child!);
}
}

View file

@ -11,43 +11,43 @@ description: A mobile client for FileBin.
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 1.3.4+13
version: 1.4.0+13
environment:
sdk: ">=2.10.5 <3.0.0"
sdk: '>=2.14.4 <3.0.0'
dependencies:
flutter:
sdk: flutter
cupertino_icons: 1.0.3
cupertino_icons: 1.0.4
flutter_localizations:
sdk: flutter
flutter_translate: 3.0.0
provider: 5.0.0
stacked: 2.1.1
get_it: 6.1.1 # stacked requires ^6
logger: 1.0.0
shared_preferences: 2.0.5
http: 0.13.3
flutter_translate: 3.0.1
provider: 6.0.1
stacked: 2.2.7+1
get_it: 7.2.0
logger: 1.1.0
shared_preferences: 2.0.9
http: 0.13.4
validators: 3.0.0
flutter_linkify: 5.0.2
url_launcher: 6.0.3
url_launcher: 6.0.16
expandable: 5.0.1
share: 2.0.1
share: 2.0.4
file_picker: 3.0.1
clipboard: 0.1.3
receive_sharing_intent: 1.4.5
permission_handler: 7.1.0
package_info: 2.0.0
package_info_plus: 1.3.0
simple_gesture_detector: 0.2.0
json_annotation: 3.1.1 # requires null-safety update
json_annotation: 4.3.0
dev_dependencies:
flutter_test:
sdk: flutter
build_runner: 1.11.5 # requires null-safety update
built_value_generator: 8.0.4 # requires null-safety update
json_serializable: 3.5.1 # requires null-safety update
build_runner: 2.1.5
built_value_generator: 8.1.3
json_serializable: 6.0.1
# For information on the generic Dart part of this file, see the
# following page: https://www.dartlang.org/tools/pub/pubspec