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 # CHANGELOG
## 1.3.3+13 - UNRELEASED ## 1.4.0+13 - UNRELEASED
* Increased target SDK to 30 * 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 ## 1.3.3+12
* Automatically switch to initial tab when coming from the share menu * Automatically switch to initial tab when coming from the share menu
@ -48,4 +51,4 @@
* Automatic refresh history if something has been uploaded * Automatic refresh history if something has been uploaded
## 1.0.0+1 ## 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 ### 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 ## 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 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 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( return LocalizationProvider(
state: LocalizationProvider.of(context).state, state: LocalizationProvider.of(context).state,
child: StreamProvider<SwipeEvent>( child: StreamProvider<SwipeEvent?>(
initialData: null, initialData: null,
create: (context) => locator<SwipeService>().swipeEventController.stream, create: (context) =>
child: StreamProvider<RefreshEvent>( locator<SwipeService>().swipeEventController.stream,
child: StreamProvider<RefreshEvent?>(
initialData: null, initialData: null,
create: (context) => locator<RefreshService>().refreshEventController.stream, create: (context) =>
child: StreamProvider<Session>( locator<RefreshService>().refreshEventController.stream,
child: StreamProvider<Session?>(
initialData: Session.initial(), initialData: Session.initial(),
create: (context) => locator<SessionService>().sessionController.stream, create: (context) =>
locator<SessionService>().sessionController.stream,
child: LifeCycleManager( child: LifeCycleManager(
child: MaterialApp( child: MaterialApp(
title: translate('app.title'), title: translate('app.title'),
builder: (context, child) => Navigator( builder: (context, child) => Navigator(
key: locator<DialogService>().dialogNavigationKey, key: locator<DialogService>().dialogNavigationKey,
onGenerateRoute: (settings) => onGenerateRoute: (settings) => MaterialPageRoute(
MaterialPageRoute(builder: (context) => DialogManager(child: child)), builder: (context) => DialogManager(child: child)),
), ),
theme: ThemeData( theme: ThemeData(
brightness: Brightness.light, brightness: Brightness.light,
primarySwatch: primaryAccentColor, primarySwatch: primaryAccentColor as MaterialColor?,
primaryColor: primaryAccentColor), primaryColor: primaryAccentColor),
onGenerateRoute: AppRouter.generateRoute, onGenerateRoute: AppRouter.generateRoute,
navigatorKey: locator<NavigationService>().navigationKey, navigatorKey: locator<NavigationService>().navigationKey,

View file

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

View file

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

View file

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

View file

@ -2,7 +2,7 @@ import '../enums/error_code.dart';
class ServiceException implements Exception { class ServiceException implements Exception {
final ErrorCode code; final ErrorCode code;
final String message; final String? message;
ServiceException({this.code = ErrorCode.GENERAL_ERROR, this.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'; import '../services/dialog_service.dart';
class DialogManager extends StatefulWidget { 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(); _DialogManagerState createState() => _DialogManagerState();
} }
@ -24,15 +24,16 @@ class _DialogManagerState extends State<DialogManager> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return widget.child; return widget.child!;
} }
void _showDialog(DialogRequest request) { void _showDialog(DialogRequest request) {
List<Widget> actions = <Widget>[]; List<Widget> actions = <Widget>[];
if (request.buttonTitleDeny != null && request.buttonTitleDeny.isNotEmpty) { if (request.buttonTitleDeny != null &&
request.buttonTitleDeny!.isNotEmpty) {
Widget denyBtn = TextButton( Widget denyBtn = TextButton(
child: Text(request.buttonTitleDeny), child: Text(request.buttonTitleDeny!),
onPressed: () { onPressed: () {
_dialogService.dialogComplete(DialogResponse(confirmed: false)); _dialogService.dialogComplete(DialogResponse(confirmed: false));
}, },
@ -41,7 +42,7 @@ class _DialogManagerState extends State<DialogManager> {
} }
Widget confirmBtn = TextButton( Widget confirmBtn = TextButton(
child: Text(request.buttonTitleAccept), child: Text(request.buttonTitleAccept!),
onPressed: () { onPressed: () {
_dialogService.dialogComplete(DialogResponse(confirmed: true)); _dialogService.dialogComplete(DialogResponse(confirmed: true));
}, },
@ -49,8 +50,8 @@ class _DialogManagerState extends State<DialogManager> {
actions.add(confirmBtn); actions.add(confirmBtn);
AlertDialog alert = AlertDialog( AlertDialog alert = AlertDialog(
title: Text(request.title), title: Text(request.title!),
content: Text(request.description), content: Text(request.description!),
actions: actions, actions: actions,
); );

View file

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

View file

@ -13,9 +13,13 @@ class ApiKey {
@JsonKey(required: true, name: 'access_level') @JsonKey(required: true, name: 'access_level')
final String accessLevel; 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 // JSON Init
factory ApiKey.fromJson(Map<String, dynamic> json) => _$ApiKeyFromJson(json); factory ApiKey.fromJson(Map<String, dynamic> json) => _$ApiKeyFromJson(json);

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -11,12 +11,20 @@ class HistoryItem {
final String filesize; final String filesize;
final String hash; final String hash;
final String mimetype; 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 // JSON Init
factory HistoryItem.fromJson(Map<String, dynamic> json) => _$HistoryItemFromJson(json); factory HistoryItem.fromJson(Map<String, dynamic> json) =>
_$HistoryItemFromJson(json);
// JSON Export // JSON Export
Map<String, dynamic> toJson() => _$HistoryItemToJson(this); Map<String, dynamic> toJson() => _$HistoryItemToJson(this);

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -27,12 +27,16 @@ class FileRepository {
return parsedResponse.data; return parsedResponse.data;
} }
Future<void> postDelete(String id) async { Future postDelete(String id) async {
await _api.post('/file/delete', fields: {'ids[1]': id}); 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 { Future<UploadedResponse> postUpload(
var response = await _api.post('/file/upload', files: files, additionalFiles: additionalFiles); 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)); return UploadedResponse.fromJson(json.decode(response.body));
} }
@ -40,10 +44,12 @@ class FileRepository {
Map<String, String> multiPasteIds = Map(); Map<String, String> multiPasteIds = Map();
ids.forEach((element) { 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)); return UploadedMultiResponse.fromJson(json.decode(response.body));
} }
} }

View file

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

View file

@ -24,25 +24,34 @@ class Api implements ApiErrorConverter {
String _url = ""; String _url = "";
String _apiKey = ""; 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); Duration _timeout = Duration(seconds: Constants.apiRequestTimeoutLimit);
Future<http.Response> fetch<T>(String route) async { Future<http.Response> fetch<T>(String route) async {
try { try {
_logger _logger.d(
.d("Requesting GET API endpoint '${_url + route}' with headers '$_headers' and maximum timeout '$_timeout'"); "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); var response = await http
.get(Uri.parse(_url + route), headers: _headers)
.timeout(_timeout);
handleRestErrors(response); handleRestErrors(response);
return response; return response;
} on TimeoutException { } on TimeoutException {
throw ServiceException(code: ErrorCode.SOCKET_TIMEOUT, message: _errorTimeout); throw ServiceException(
code: ErrorCode.SOCKET_TIMEOUT, message: _errorTimeout);
} on SocketException { } 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, 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 { try {
var uri = Uri.parse(_url + route); var uri = Uri.parse(_url + route);
var request = http.MultipartRequest('POST', uri) var request = http.MultipartRequest('POST', uri)
@ -54,32 +63,39 @@ class Api implements ApiErrorConverter {
} }
if (fields != null && fields.isNotEmpty) { if (fields != null && fields.isNotEmpty) {
request.fields.addAll(fields); request.fields.addAll(fields as Map<String, String>);
} }
if (files != null && files.isNotEmpty) { if (files != null && files.isNotEmpty) {
files.forEach((element) async { 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) { if (additionalFiles != null && additionalFiles.length > 0) {
List<String> keys = additionalFiles.keys.toList(); List<String> keys = additionalFiles.keys.toList();
additionalFiles.forEach((key, value) { additionalFiles.forEach((key, value) {
var index = files != null ? files.length + keys.indexOf(key) + 1 : keys.indexOf(key) + 1; var index = files != null
request.files.add(http.MultipartFile.fromString('file[$index]', value, filename: key)); ? 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 multiResponse = await request.send();
var response = await http.Response.fromStream(multiResponse); var response = await http.Response.fromStream(multiResponse);
handleRestErrors(response); handleRestErrors(response);
return response; return response;
} on TimeoutException { } on TimeoutException {
throw ServiceException(code: ErrorCode.SOCKET_TIMEOUT, message: _errorTimeout); throw ServiceException(
code: ErrorCode.SOCKET_TIMEOUT, message: _errorTimeout);
} on SocketException { } 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 /// have a json decoded object. Replace this with a custom
/// conversion method by overwriting the interface if needed /// conversion method by overwriting the interface if needed
void handleRestErrors(http.Response response) { void handleRestErrors(http.Response response) {
if (response != null) { if (response.statusCode != HttpStatus.ok &&
if (response.statusCode != HttpStatus.ok && response.statusCode != HttpStatus.noContent) { response.statusCode != HttpStatus.noContent) {
if (response.headers.containsKey(HttpHeaders.contentTypeHeader)) { if (response.headers.containsKey(HttpHeaders.contentTypeHeader)) {
ContentType responseContentType = ContentType.parse(response.headers[HttpHeaders.contentTypeHeader]); ContentType responseContentType =
ContentType.parse(response.headers[HttpHeaders.contentTypeHeader]!);
if (ContentType.json.primaryType == responseContentType.primaryType && if (ContentType.json.primaryType == responseContentType.primaryType &&
ContentType.json.subType == responseContentType.subType) { ContentType.json.subType == responseContentType.subType) {
var parsedBody = convert(response); var parsedBody = convert(response);
throw new RestServiceException(response.statusCode, responseBody: parsedBody); 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 { class DialogService {
GlobalKey<NavigatorState> _dialogNavigationKey = GlobalKey<NavigatorState>(); GlobalKey<NavigatorState> _dialogNavigationKey = GlobalKey<NavigatorState>();
Function(DialogRequest) _showDialogListener; late Function(DialogRequest) _showDialogListener;
Completer<DialogResponse> _dialogCompleter; Completer<DialogResponse>? _dialogCompleter;
GlobalKey<NavigatorState> get dialogNavigationKey => _dialogNavigationKey; GlobalKey<NavigatorState> get dialogNavigationKey => _dialogNavigationKey;
@ -18,35 +18,43 @@ class DialogService {
} }
Future<DialogResponse> showDialog({ Future<DialogResponse> showDialog({
String title, String? title,
String description, String? description,
String buttonTitleAccept, String? buttonTitleAccept,
}) { }) {
_dialogCompleter = Completer<DialogResponse>(); _dialogCompleter = Completer<DialogResponse>();
_showDialogListener(DialogRequest( _showDialogListener(DialogRequest(
title: title, title: title,
description: description, description: description,
buttonTitleAccept: buttonTitleAccept:
buttonTitleAccept == null || buttonTitleAccept.isEmpty ? translate('dialog.confirm') : buttonTitleAccept)); buttonTitleAccept == null || buttonTitleAccept.isEmpty
return _dialogCompleter.future; ? translate('dialog.confirm')
: buttonTitleAccept));
return _dialogCompleter!.future;
} }
Future<DialogResponse> showConfirmationDialog( Future<DialogResponse> showConfirmationDialog(
{String title, String description, String buttonTitleAccept, String buttonTitleDeny}) { {String? title,
String? description,
String? buttonTitleAccept,
String? buttonTitleDeny}) {
_dialogCompleter = Completer<DialogResponse>(); _dialogCompleter = Completer<DialogResponse>();
_showDialogListener(DialogRequest( _showDialogListener(DialogRequest(
title: title, title: title,
description: description, description: description,
buttonTitleAccept: buttonTitleAccept:
buttonTitleAccept == null || buttonTitleAccept.isEmpty ? translate('dialog.confirm') : buttonTitleAccept, buttonTitleAccept == null || buttonTitleAccept.isEmpty
buttonTitleDeny: ? translate('dialog.confirm')
buttonTitleDeny == null || buttonTitleDeny.isEmpty ? translate('dialog.cancel') : buttonTitleDeny)); : buttonTitleAccept,
return _dialogCompleter.future; buttonTitleDeny: buttonTitleDeny == null || buttonTitleDeny.isEmpty
? translate('dialog.cancel')
: buttonTitleDeny));
return _dialogCompleter!.future;
} }
void dialogComplete(DialogResponse response) { void dialogComplete(DialogResponse response) {
_dialogNavigationKey.currentState.pop(); _dialogNavigationKey.currentState!.pop();
_dialogCompleter.complete(response); _dialogCompleter!.complete(response);
_dialogCompleter = null; _dialogCompleter = null;
} }
} }

View file

@ -15,15 +15,16 @@ class FileService {
return await _fileRepository.getConfig(url); return await _fileRepository.getConfig(url);
} }
Future<History> getHistory() async { FutureOr<History> getHistory() async {
return await _fileRepository.getHistory(); return await _fileRepository.getHistory();
} }
Future<void> deletePaste(String id) async { Future deletePaste(String id) async {
return await _fileRepository.postDelete(id); 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); return await _fileRepository.postUpload(files, additionalFiles);
} }

View file

@ -17,7 +17,8 @@ class LinkService {
_logger.e('Could not launch link $link'); _logger.e('Could not launch link $link');
_dialogService.showDialog( _dialogService.showDialog(
title: translate('link.dialog.title'), 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() { void pop() {
logger.d('NavigationService: pop'); logger.d('NavigationService: pop');
_navigationKey.currentState.pop(); _navigationKey.currentState!.pop();
} }
Future<dynamic> navigateTo(String routeName, {dynamic arguments}) { Future<dynamic> navigateTo(String routeName, {dynamic arguments}) {
logger.d('NavigationService: navigateTo $routeName'); 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}) { Future<dynamic> navigateAndReplaceTo(String routeName, {dynamic arguments}) {
logger.d('NavigationService: navigateAndReplaceTo $routeName'); 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 DialogService _dialogService = locator<DialogService>();
final StorageService _storageService = locator<StorageService>(); final StorageService _storageService = locator<StorageService>();
Timer _serviceCheckTimer; Timer? _serviceCheckTimer;
PermissionStatus _permissionStatus; PermissionStatus? _permissionStatus;
bool _permanentlyIgnored = false; bool _permanentlyIgnored = false;
bool _devicePermissionDialogActive = false; bool _devicePermissionDialogActive = false;
@ -57,7 +57,8 @@ class PermissionService extends StoppableService {
return; return;
} }
var ignoredDialog = await _storageService.hasStoragePermissionDialogIgnored(); var ignoredDialog =
await _storageService.hasStoragePermissionDialogIgnored();
if (ignoredDialog) { if (ignoredDialog) {
_logger.d('Permanently ignored permission request, skipping'); _logger.d('Permanently ignored permission request, skipping');
@ -79,7 +80,7 @@ class PermissionService extends StoppableService {
buttonTitleAccept: translate('permission_service.dialog.grant'), buttonTitleAccept: translate('permission_service.dialog.grant'),
buttonTitleDeny: translate('permission_service.dialog.ignore')); buttonTitleDeny: translate('permission_service.dialog.ignore'));
if (!response.confirmed) { if (!response.confirmed!) {
await _storageService.storeStoragePermissionDialogIgnored(); await _storageService.storeStoragePermissionDialogIgnored();
} else { } else {
_devicePermissionDialogActive = true; _devicePermissionDialogActive = true;
@ -104,8 +105,9 @@ class PermissionService extends StoppableService {
super.start(); super.start();
await checkEnabledAndPermission(); await checkEnabledAndPermission();
_serviceCheckTimer = _serviceCheckTimer = Timer.periodic(
Timer.periodic(Duration(milliseconds: Constants.mediaPermissionCheckInterval), (_serviceTimer) async { Duration(milliseconds: Constants.mediaPermissionCheckInterval),
(_serviceTimer) async {
if (!super.serviceStopped) { if (!super.serviceStopped) {
await checkEnabledAndPermission(); await checkEnabledAndPermission();
} else { } else {
@ -124,7 +126,7 @@ class PermissionService extends StoppableService {
void _removeServiceCheckTimer() { void _removeServiceCheckTimer() {
if (_serviceCheckTimer != null) { if (_serviceCheckTimer != null) {
_serviceCheckTimer.cancel(); _serviceCheckTimer!.cancel();
_serviceCheckTimer = null; _serviceCheckTimer = null;
_logger.d('Removed service check timer'); _logger.d('Removed service check timer');
} }

View file

@ -14,9 +14,9 @@ class SessionService extends StoppableService {
final StorageService _storageService = locator<StorageService>(); final StorageService _storageService = locator<StorageService>();
final Api _api = locator<Api>(); 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'); _logger.d('Setting API config for session');
_api.setUrl(url); _api.setUrl(url);
_api.addApiKeyAuthorization(apiKey); _api.addApiKeyAuthorization(apiKey);

View file

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

View file

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

View file

@ -6,7 +6,7 @@ class FormatterUtil {
/// Format epoch timestamp /// Format epoch timestamp
static String formatEpoch(num millis) { static String formatEpoch(num millis) {
DateFormat dateFormat = DateFormat().add_yMEd().add_Hm(); 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) { 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 '../../core/services/link_service.dart';
import '../../locator.dart'; import '../../locator.dart';

View file

@ -12,13 +12,13 @@ class BaseModel extends ChangeNotifier {
bool _isDisposed = false; 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)) { if (_stateMap.containsKey(key)) {
_stateMap.update(key, (value) => stateValue); _stateMap.update(key, (value) => stateValue);
} else { } else {
@ -44,7 +44,7 @@ class BaseModel extends ChangeNotifier {
setStateValue(STATE_VIEW, stateView); setStateValue(STATE_VIEW, stateView);
} }
void setStateMessage(String stateMessage) { void setStateMessage(String? stateMessage) {
setStateValue(STATE_MESSAGE, stateMessage); setStateValue(STATE_MESSAGE, stateMessage);
} }

View file

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

View file

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

View file

@ -24,25 +24,28 @@ class ProfileModel extends BaseModel {
final FileService _fileService = locator<FileService>(); final FileService _fileService = locator<FileService>();
final Logger _logger = getLogger(); final Logger _logger = getLogger();
String errorMessage; String? errorMessage;
Future logout() async { Future logout() async {
var dialogResult = await _dialogService.showConfirmationDialog( 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(); await _sessionService.logout();
} }
} }
Future revealApiKey(String apiKey) async { Future revealApiKey(String? apiKey) async {
await _dialogService.showDialog( await _dialogService.showDialog(
title: translate('profile.revealed_api_key.title'), 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 { Future showConfig(String url) async {
Config config; setStateView(ViewState.Busy);
Config? config;
try { try {
config = await _fileService.getConfig(url); config = await _fileService.getConfig(url);
errorMessage = null; errorMessage = null;
@ -50,13 +53,15 @@ class ProfileModel extends BaseModel {
if (e is RestServiceException) { if (e is RestServiceException) {
if (e.statusCode == HttpStatus.unauthorized) { if (e.statusCode == HttpStatus.unauthorized) {
errorMessage = translate('login.errors.wrong_credentials'); 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'); errorMessage = translate('login.errors.forbidden');
} else if (e.statusCode == HttpStatus.notFound) { } else if (e.statusCode == HttpStatus.notFound) {
errorMessage = translate('api.incompatible_error_not_found'); errorMessage = translate('api.incompatible_error_not_found');
} }
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'); errorMessage = translate('api.general_rest_error');
} }
@ -66,6 +71,7 @@ class ProfileModel extends BaseModel {
errorMessage = translate('api.socket_timeout'); errorMessage = translate('api.socket_timeout');
} else { } else {
errorMessage = translate('app.unknown_error'); errorMessage = translate('app.unknown_error');
setStateView(ViewState.Idle);
_sessionService.logout(); _sessionService.logout();
setStateView(ViewState.Idle); setStateView(ViewState.Idle);
_logger.e('An unknown error occurred', e); _logger.e('An unknown error occurred', e);
@ -73,19 +79,24 @@ class ProfileModel extends BaseModel {
} }
} }
setStateView(ViewState.Idle);
if (config != null && errorMessage == null) { if (config != null && errorMessage == null) {
await _dialogService.showDialog( await _dialogService.showDialog(
title: translate('profile.shown_config.title'), title: translate('profile.shown_config.title'),
description: translate('profile.shown_config.description', args: { description: translate('profile.shown_config.description', args: {
'uploadMaxSize': FormatterUtil.formatBytes(config.uploadMaxSize, 2), 'uploadMaxSize':
FormatterUtil.formatBytes(config.uploadMaxSize as int, 2),
'maxFilesPerRequest': config.maxFilesPerRequest, 'maxFilesPerRequest': config.maxFilesPerRequest,
'maxInputVars': config.maxInputVars, 'maxInputVars': config.maxInputVars,
'requestMaxSize': FormatterUtil.formatBytes(config.requestMaxSize, 2) 'requestMaxSize':
FormatterUtil.formatBytes(config.requestMaxSize as int, 2)
})); }));
} else { } else {
await _dialogService.showDialog( await _dialogService.showDialog(
title: translate('profile.shown_config.error.title'), 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(); TextEditingController _pasteTextController = TextEditingController();
bool pasteTextTouched = false; bool pasteTextTouched = false;
StreamSubscription _intentDataStreamSubscription; late StreamSubscription _intentDataStreamSubscription;
bool createMulti = false; bool createMulti = false;
String fileName; String? fileName;
List<PlatformFile> paths; List<PlatformFile>? paths;
String _extension; String? _extension;
bool loadingPath = false; bool loadingPath = false;
String errorMessage; String? errorMessage;
TextEditingController get pasteTextController => _pasteTextController; 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 // For sharing images coming from outside the app while the app is in the memory
_intentDataStreamSubscription = ReceiveSharingIntent.getMediaStream().listen((List<SharedMediaFile> value) { _intentDataStreamSubscription = ReceiveSharingIntent.getMediaStream()
if (value != null && value.length > 0) { .listen((List<SharedMediaFile> value) {
if (value.length > 0) {
setStateView(ViewState.Busy); setStateView(ViewState.Busy);
paths = value.map((sharedFile) { paths = value.map((sharedFile) {
return PlatformFile.fromMap({ return PlatformFile.fromMap({
@ -67,7 +68,7 @@ class UploadModel extends BaseModel {
}); });
}).toList(); }).toList();
setStateView(ViewState.Idle); setStateView(ViewState.Idle);
if (paths.isNotEmpty && paths.length > 0) { if (paths!.isNotEmpty && paths!.length > 0) {
_swipeService.addEvent(SwipeEvent.Start); _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 // For sharing images coming from outside the app while the app is closed
ReceiveSharingIntent.getInitialMedia().then((List<SharedMediaFile> value) { ReceiveSharingIntent.getInitialMedia().then((List<SharedMediaFile> value) {
if (value != null && value.length > 0) { if (value.length > 0) {
setStateView(ViewState.Busy); setStateView(ViewState.Busy);
paths = value.map((sharedFile) { paths = value.map((sharedFile) {
return PlatformFile.fromMap({ return PlatformFile.fromMap({
@ -88,7 +89,7 @@ class UploadModel extends BaseModel {
}); });
}).toList(); }).toList();
setStateView(ViewState.Idle); setStateView(ViewState.Idle);
if (paths.isNotEmpty && paths.length > 0) { if (paths!.isNotEmpty && paths!.length > 0) {
_swipeService.addEvent(SwipeEvent.Start); _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 // For sharing or opening urls/text coming from outside the app while the app is in the memory
_intentDataStreamSubscription = ReceiveSharingIntent.getTextStream().listen((String value) { _intentDataStreamSubscription =
if (value != null && value.isNotEmpty) { ReceiveSharingIntent.getTextStream().listen((String value) {
if (value.isNotEmpty) {
setStateView(ViewState.Busy); setStateView(ViewState.Busy);
pasteTextController.text = value; pasteTextController.text = value;
setStateView(ViewState.Idle); 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 // 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) { if (value != null && value.isNotEmpty) {
setStateView(ViewState.Busy); setStateView(ViewState.Busy);
pasteTextController.text = value; pasteTextController.text = value;
setStateView(ViewState.Idle); setStateView(ViewState.Idle);
if (paths.isNotEmpty && paths.length > 0) { if (paths!.isNotEmpty && paths!.length > 0) {
_swipeService.addEvent(SwipeEvent.Start); _swipeService.addEvent(SwipeEvent.Start);
} }
} }
@ -132,7 +134,7 @@ class UploadModel extends BaseModel {
_swipeService.addEvent(SwipeEvent.Start); _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) { if (uploads != null && uploads.length > 0) {
var links = ''; var links = '';
@ -165,7 +167,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) {
@ -175,7 +179,7 @@ class UploadModel extends BaseModel {
} }
loadingPath = false; loadingPath = false;
fileName = paths != null ? paths.map((e) => e.name).toString() : '...'; fileName = paths != null ? paths!.map((e) => e.name).toString() : '...';
setStateMessage(null); setStateMessage(null);
setStateView(ViewState.Idle); setStateView(ViewState.Idle);
@ -190,31 +194,35 @@ class UploadModel extends BaseModel {
setStateView(ViewState.Idle); setStateView(ViewState.Idle);
} }
Future<Map<String, bool>> upload() async { Future<Map<String, bool>?> upload() async {
setStateView(ViewState.Busy); setStateView(ViewState.Busy);
setStateMessage(translate('upload.uploading_now')); setStateMessage(translate('upload.uploading_now'));
Map<String, bool> uploadedPasteIds = new Map(); Map<String, bool> uploadedPasteIds = new Map();
try { try {
List<File> files; List<File>? files;
Map<String, String> additionalFiles; Map<String, String>? additionalFiles;
if (pasteTextController.text != null && pasteTextController.text.isNotEmpty) { if (pasteTextController.text.isNotEmpty) {
additionalFiles = Map.from( additionalFiles = Map.from({
{'paste-${(new DateTime.now().millisecondsSinceEpoch / 1000).round()}.txt': pasteTextController.text}); 'paste-${(new DateTime.now().millisecondsSinceEpoch / 1000).round()}.txt':
pasteTextController.text
});
} }
if (paths != null && paths.length > 0) { if (paths != null && paths!.length > 0) {
files = paths.map((e) => new File(e.path)).toList(); 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) { response.data.ids.forEach((element) {
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);
} }
@ -234,9 +242,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');

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -26,7 +26,8 @@ class UploadView extends StatelessWidget {
} }
bool _isUploadButtonEnabled(UploadModel model) { 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) { Widget _render(UploadModel model, BuildContext context) {
@ -39,7 +40,9 @@ class UploadView extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
CircularProgressIndicator(), 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(
@ -58,12 +61,15 @@ class UploadView extends StatelessWidget {
color: buttonBackgroundColor, color: buttonBackgroundColor,
), ),
suffixIcon: IconButton( suffixIcon: IconButton(
onPressed: () => model.pasteTextController.clear(), onPressed: () =>
model.pasteTextController.clear(),
icon: Icon(Icons.clear), icon: Icon(Icons.clear),
), ),
hintText: translate('upload.text_to_be_pasted'), hintText: translate('upload.text_to_be_pasted'),
contentPadding: EdgeInsets.fromLTRB(20.0, 10.0, 20.0, 10.0), contentPadding:
border: OutlineInputBorder(borderRadius: BorderRadius.circular(32.0)), EdgeInsets.fromLTRB(20.0, 10.0, 20.0, 10.0),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(32.0)),
), ),
controller: model.pasteTextController)), controller: model.pasteTextController)),
Padding( Padding(
@ -79,20 +85,24 @@ class UploadView extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end,
children: [ children: [
ElevatedButton.icon( ElevatedButton.icon(
icon: Icon(Icons.file_copy_sharp, color: blueColor), icon: 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'),
style: TextStyle(color: buttonForegroundColor), style:
TextStyle(color: buttonForegroundColor),
)), )),
ElevatedButton.icon( ElevatedButton.icon(
icon: Icon(Icons.cancel, color: orangeColor), icon: Icon(Icons.cancel, color: orangeColor),
onPressed: model.paths != null && model.paths.length > 0 onPressed: model.paths != null &&
model.paths!.length > 0
? () => model.clearCachedFiles() ? () => model.clearCachedFiles()
: null, : null,
label: Text( label: Text(
translate('upload.clear_temporary_files'), translate('upload.clear_temporary_files'),
style: TextStyle(color: buttonForegroundColor), style:
TextStyle(color: buttonForegroundColor),
)), )),
], ],
)), )),
@ -118,35 +128,48 @@ 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(
'upload.uploaded')),
duration: Duration(seconds: 10), 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( label: Text(
translate('upload.upload'), 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(
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(
@ -158,16 +181,28 @@ class UploadView extends StatelessWidget {
: model.paths != null : model.paths != null
? Container( ? Container(
padding: const EdgeInsets.only(bottom: 30.0), 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( 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 path = model.paths.length > 0 final path = model.paths!.length > 0
? model.paths.map((e) => e.path).toList()[index].toString() ? model.paths!
.map((e) => e.path)
.toList()[index]
.toString()
: ''; : '';
return Card( return Card(
@ -178,7 +213,9 @@ class UploadView extends StatelessWidget {
subtitle: Text(path), subtitle: Text(path),
)); ));
}, },
separatorBuilder: (BuildContext context, int index) => const Divider(), separatorBuilder:
(BuildContext context, int index) =>
const Divider(),
), ),
) )
: Container(), : Container(),

View file

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

View file

@ -8,14 +8,14 @@ class LoginApiKeyHeaders extends StatelessWidget {
final TextEditingController uriController; final TextEditingController uriController;
final TextEditingController apiKeyController; 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Column(children: <Widget>[ 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), LoginTextField(uriController, translate('login.url_placeholder'), Icon(Icons.link),
keyboardType: TextInputType.url), keyboardType: TextInputType.url),
LoginTextField( LoginTextField(

View file

@ -9,18 +9,18 @@ class LoginCredentialsHeaders extends StatelessWidget {
final TextEditingController usernameController; final TextEditingController usernameController;
final TextEditingController passwordController; final TextEditingController passwordController;
final String validationMessage; final String? validationMessage;
LoginCredentialsHeaders( LoginCredentialsHeaders(
{@required this.uriController, {required this.uriController,
@required this.usernameController, required this.usernameController,
@required this.passwordController, required this.passwordController,
this.validationMessage}); this.validationMessage});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Column(children: <Widget>[ 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), LoginTextField(uriController, translate('login.url_placeholder'), Icon(Icons.link),
keyboardType: TextInputType.url), keyboardType: TextInputType.url),
LoginTextField(usernameController, translate('login.username_placeholder'), Icon(Icons.person), LoginTextField(usernameController, translate('login.username_placeholder'), Icon(Icons.person),

View file

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

View file

@ -7,11 +7,11 @@ import '../../locator.dart';
class SwipeNavigation extends StatefulWidget { class SwipeNavigation extends StatefulWidget {
/// Widget to be augmented with gesture detection. /// Widget to be augmented with gesture detection.
final Widget child; final Widget? child;
/// Creates a [SwipeNavigation] widget. /// Creates a [SwipeNavigation] widget.
const SwipeNavigation({ const SwipeNavigation({
Key key, Key? key,
this.child, this.child,
}) : super(key: key); }) : super(key: key);
@ -32,6 +32,7 @@ class _SwipeNavigationState extends State<SwipeNavigation> {
@override @override
Widget build(BuildContext context) { 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. # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
# Read more about iOS versioning at # Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 1.3.4+13 version: 1.4.0+13
environment: environment:
sdk: ">=2.10.5 <3.0.0" sdk: '>=2.14.4 <3.0.0'
dependencies: dependencies:
flutter: flutter:
sdk: flutter sdk: flutter
cupertino_icons: 1.0.3 cupertino_icons: 1.0.4
flutter_localizations: flutter_localizations:
sdk: flutter sdk: flutter
flutter_translate: 3.0.0 flutter_translate: 3.0.1
provider: 5.0.0 provider: 6.0.1
stacked: 2.1.1 stacked: 2.2.7+1
get_it: 6.1.1 # stacked requires ^6 get_it: 7.2.0
logger: 1.0.0 logger: 1.1.0
shared_preferences: 2.0.5 shared_preferences: 2.0.9
http: 0.13.3 http: 0.13.4
validators: 3.0.0 validators: 3.0.0
flutter_linkify: 5.0.2 flutter_linkify: 5.0.2
url_launcher: 6.0.3 url_launcher: 6.0.16
expandable: 5.0.1 expandable: 5.0.1
share: 2.0.1 share: 2.0.4
file_picker: 3.0.1 file_picker: 3.0.1
clipboard: 0.1.3 clipboard: 0.1.3
receive_sharing_intent: 1.4.5 receive_sharing_intent: 1.4.5
permission_handler: 7.1.0 permission_handler: 7.1.0
package_info: 2.0.0 package_info_plus: 1.3.0
simple_gesture_detector: 0.2.0 simple_gesture_detector: 0.2.0
json_annotation: 3.1.1 # requires null-safety update json_annotation: 4.3.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter
build_runner: 1.11.5 # requires null-safety update build_runner: 2.1.5
built_value_generator: 8.0.4 # requires null-safety update built_value_generator: 8.1.3
json_serializable: 3.5.1 # requires null-safety update json_serializable: 6.0.1
# For information on the generic Dart part of this file, see the # For information on the generic Dart part of this file, see the
# following page: https://www.dartlang.org/tools/pub/pubspec # following page: https://www.dartlang.org/tools/pub/pubspec