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

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

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