Compare commits

..

No commits in common. "fc88d5c22f5e116f58acf852a16d91c8ac95e727" and "a65c7d92531e070b6c57339ab2fafd9102cb7105" have entirely different histories.

60 changed files with 295 additions and 453 deletions

View file

@ -4,7 +4,7 @@ name: default
steps: steps:
- name: build - name: build
image: cirrusci/flutter:3.3.10 image: cirrusci/flutter:3.3.9
commands: commands:
- flutter doctor - flutter doctor
- flutter pub get - flutter pub get

View file

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

View file

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

View file

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

View file

@ -18,10 +18,8 @@ import 'ui/shared/app_colors.dart';
import 'ui/views/startup_view.dart'; import 'ui/views/startup_view.dart';
class MyApp extends StatelessWidget { class MyApp extends StatelessWidget {
static final _defaultLightColorScheme = ColorScheme.fromSwatch( static final _defaultLightColorScheme = ColorScheme.fromSwatch(primarySwatch: myColor, brightness: Brightness.light);
primarySwatch: myColor, brightness: Brightness.light); static final _defaultDarkColorScheme = ColorScheme.fromSwatch(primarySwatch: myColor, brightness: Brightness.dark);
static final _defaultDarkColorScheme = ColorScheme.fromSwatch(
primarySwatch: myColor, brightness: Brightness.dark);
MyApp({super.key}) { MyApp({super.key}) {
initializeDateFormatting('en'); initializeDateFormatting('en');
@ -35,30 +33,23 @@ class MyApp extends StatelessWidget {
state: LocalizationProvider.of(context).state, state: LocalizationProvider.of(context).state,
child: StreamProvider<RefreshEvent?>( child: StreamProvider<RefreshEvent?>(
initialData: null, initialData: null,
create: (context) => create: (context) => locator<RefreshService>().refreshEventController.stream,
locator<RefreshService>().refreshEventController.stream,
child: StreamProvider<Session?>( child: StreamProvider<Session?>(
initialData: Session.initial(), initialData: Session.initial(),
create: (context) => create: (context) => locator<SessionService>().sessionController.stream,
locator<SessionService>().sessionController.stream, child: LifeCycleManager(child: DynamicColorBuilder(builder: (lightColorScheme, darkColorScheme) {
child: LifeCycleManager(child: DynamicColorBuilder(
builder: (lightColorScheme, darkColorScheme) {
return MaterialApp( return MaterialApp(
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
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) => MaterialPageRoute( onGenerateRoute: (settings) => MaterialPageRoute(builder: (context) => DialogManager(child: child)),
builder: (context) => DialogManager(child: child)),
), ),
theme: ThemeData( theme: ThemeData(
useMaterial3: true, useMaterial3: true,
brightness: Brightness.light, brightness: Brightness.light,
colorScheme: colorScheme: lightColorScheme ?? _defaultLightColorScheme),
lightColorScheme ?? _defaultLightColorScheme), darkTheme: ThemeData(useMaterial3: true, colorScheme: darkColorScheme ?? _defaultDarkColorScheme),
darkTheme: ThemeData(
useMaterial3: true,
colorScheme: darkColorScheme ?? _defaultDarkColorScheme),
onGenerateRoute: AppRouter.generateRoute, onGenerateRoute: AppRouter.generateRoute,
navigatorKey: locator<NavigationService>().navigationKey, navigatorKey: locator<NavigationService>().navigationKey,
home: const StartUpView(), home: const StartUpView(),

View file

@ -31,8 +31,7 @@ class _DialogManagerState extends State<DialogManager> {
void _showDialog(DialogRequest request) { void _showDialog(DialogRequest request) {
List<Widget> actions = <Widget>[]; List<Widget> actions = <Widget>[];
if (request.buttonTitleDeny != null && if (request.buttonTitleDeny != null && request.buttonTitleDeny!.isNotEmpty) {
request.buttonTitleDeny!.isNotEmpty) {
Widget denyBtn = TextButton( Widget denyBtn = TextButton(
child: Text(request.buttonTitleDeny!), child: Text(request.buttonTitleDeny!),
onPressed: () { onPressed: () {

View file

@ -17,14 +17,10 @@ class LifeCycleManager extends StatefulWidget {
_LifeCycleManagerState createState() => _LifeCycleManagerState(); _LifeCycleManagerState createState() => _LifeCycleManagerState();
} }
class _LifeCycleManagerState extends State<LifeCycleManager> class _LifeCycleManagerState extends State<LifeCycleManager> with WidgetsBindingObserver {
with WidgetsBindingObserver {
final Logger logger = getLogger(); final Logger logger = getLogger();
List<StoppableService> servicesToManage = [ List<StoppableService> servicesToManage = [locator<SessionService>(), locator<PermissionService>()];
locator<SessionService>(),
locator<PermissionService>()
];
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

View file

@ -15,11 +15,7 @@ class ApiKey {
final String? comment; final String? comment;
ApiKey( ApiKey({required this.key, required this.created, required this.accessLevel, this.comment});
{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

@ -12,8 +12,7 @@ class ApiKeys {
ApiKeys({required this.apikeys}); ApiKeys({required this.apikeys});
// JSON Init // JSON Init
factory ApiKeys.fromJson(Map<String, dynamic> json) => factory ApiKeys.fromJson(Map<String, dynamic> json) => _$ApiKeysFromJson(json);
_$ApiKeysFromJson(json);
// JSON Export // JSON Export
Map<String, dynamic> toJson() => _$ApiKeysToJson(this); Map<String, dynamic> toJson() => _$ApiKeysToJson(this);

View file

@ -15,8 +15,7 @@ class ApiKeysResponse {
ApiKeysResponse({required this.status, required this.data}); ApiKeysResponse({required this.status, required this.data});
// JSON Init // JSON Init
factory ApiKeysResponse.fromJson(Map<String, dynamic> json) => factory ApiKeysResponse.fromJson(Map<String, dynamic> json) => _$ApiKeysResponseFromJson(json);
_$ApiKeysResponseFromJson(json);
// JSON Export // JSON Export
Map<String, dynamic> toJson() => _$ApiKeysResponseToJson(this); Map<String, dynamic> toJson() => _$ApiKeysResponseToJson(this);

View file

@ -15,8 +15,7 @@ class ConfigResponse {
ConfigResponse({required this.status, required this.data}); ConfigResponse({required this.status, required this.data});
// JSON Init // JSON Init
factory ConfigResponse.fromJson(Map<String, dynamic> json) => factory ConfigResponse.fromJson(Map<String, dynamic> json) => _$ConfigResponseFromJson(json);
_$ConfigResponseFromJson(json);
// JSON Export // JSON Export
Map<String, dynamic> toJson() => _$ConfigResponseToJson(this); Map<String, dynamic> toJson() => _$ConfigResponseToJson(this);

View file

@ -13,8 +13,7 @@ class CreateApiKeyResponse {
CreateApiKeyResponse({required this.status, required this.data}); CreateApiKeyResponse({required this.status, required this.data});
// JSON Init // JSON Init
factory CreateApiKeyResponse.fromJson(Map<String, dynamic> json) => factory CreateApiKeyResponse.fromJson(Map<String, dynamic> json) => _$CreateApiKeyResponseFromJson(json);
_$CreateApiKeyResponseFromJson(json);
// JSON Export // JSON Export
Map<String, dynamic> toJson() => _$CreateApiKeyResponseToJson(this); Map<String, dynamic> toJson() => _$CreateApiKeyResponseToJson(this);

View file

@ -19,8 +19,7 @@ class History {
History({required this.items, required this.multipasteItems, this.totalSize}); History({required this.items, required this.multipasteItems, this.totalSize});
// JSON Init // JSON Init
factory History.fromJson(Map<String, dynamic> json) => factory History.fromJson(Map<String, dynamic> json) => _$HistoryFromJson(json);
_$HistoryFromJson(json);
// JSON Export // JSON Export
Map<String, dynamic> toJson() => _$HistoryToJson(this); Map<String, dynamic> toJson() => _$HistoryToJson(this);

View file

@ -23,8 +23,7 @@ class HistoryItem {
this.thumbnail}); this.thumbnail});
// JSON Init // JSON Init
factory HistoryItem.fromJson(Map<String, dynamic> json) => factory HistoryItem.fromJson(Map<String, dynamic> json) => _$HistoryItemFromJson(json);
_$HistoryItemFromJson(json);
// JSON Export // JSON Export
Map<String, dynamic> toJson() => _$HistoryItemToJson(this); Map<String, dynamic> toJson() => _$HistoryItemToJson(this);

View file

@ -15,8 +15,7 @@ class HistoryMultipasteItem {
HistoryMultipasteItem(this.items, {required this.date, required this.urlId}); HistoryMultipasteItem(this.items, {required this.date, required this.urlId});
// JSON Init // JSON Init
factory HistoryMultipasteItem.fromJson(Map<String, dynamic> json) => factory HistoryMultipasteItem.fromJson(Map<String, dynamic> json) => _$HistoryMultipasteItemFromJson(json);
_$HistoryMultipasteItemFromJson(json);
// JSON Export // JSON Export
Map<String, dynamic> toJson() => _$HistoryMultipasteItemToJson(this); Map<String, dynamic> toJson() => _$HistoryMultipasteItemToJson(this);

View file

@ -9,8 +9,7 @@ class HistoryMultipasteItemEntry {
HistoryMultipasteItemEntry({required this.id}); HistoryMultipasteItemEntry({required this.id});
// JSON Init // JSON Init
factory HistoryMultipasteItemEntry.fromJson(Map<String, dynamic> json) => factory HistoryMultipasteItemEntry.fromJson(Map<String, dynamic> json) => _$HistoryMultipasteItemEntryFromJson(json);
_$HistoryMultipasteItemEntryFromJson(json);
// JSON Export // JSON Export
Map<String, dynamic> toJson() => _$HistoryMultipasteItemEntryToJson(this); Map<String, dynamic> toJson() => _$HistoryMultipasteItemEntryToJson(this);

View file

@ -15,8 +15,7 @@ class HistoryResponse {
HistoryResponse({required this.status, required this.data}); HistoryResponse({required this.status, required this.data});
// JSON Init // JSON Init
factory HistoryResponse.fromJson(Map<String, dynamic> json) => factory HistoryResponse.fromJson(Map<String, dynamic> json) => _$HistoryResponseFromJson(json);
_$HistoryResponseFromJson(json);
// JSON Export // JSON Export
Map<String, dynamic> toJson() => _$HistoryResponseToJson(this); Map<String, dynamic> toJson() => _$HistoryResponseToJson(this);

View file

@ -15,8 +15,7 @@ class RestError {
required this.errorId, required this.errorId,
}); // JSON Init }); // JSON Init
factory RestError.fromJson(Map<String, dynamic> json) => factory RestError.fromJson(Map<String, dynamic> json) => _$RestErrorFromJson(json);
_$RestErrorFromJson(json);
// JSON Export // JSON Export
Map<String, dynamic> toJson() => _$RestErrorToJson(this); Map<String, dynamic> toJson() => _$RestErrorToJson(this);

View file

@ -13,8 +13,7 @@ class Uploaded {
Uploaded({required this.ids, required this.urls}); Uploaded({required this.ids, required this.urls});
// JSON Init // JSON Init
factory Uploaded.fromJson(Map<String, dynamic> json) => factory Uploaded.fromJson(Map<String, dynamic> json) => _$UploadedFromJson(json);
_$UploadedFromJson(json);
// JSON Export // JSON Export
Map<String, dynamic> toJson() => _$UploadedToJson(this); Map<String, dynamic> toJson() => _$UploadedToJson(this);

View file

@ -13,8 +13,7 @@ class UploadedMulti {
UploadedMulti({required this.url, required this.urlId}); UploadedMulti({required this.url, required this.urlId});
// JSON Init // JSON Init
factory UploadedMulti.fromJson(Map<String, dynamic> json) => factory UploadedMulti.fromJson(Map<String, dynamic> json) => _$UploadedMultiFromJson(json);
_$UploadedMultiFromJson(json);
// JSON Export // JSON Export
Map<String, dynamic> toJson() => _$UploadedMultiToJson(this); Map<String, dynamic> toJson() => _$UploadedMultiToJson(this);

View file

@ -15,8 +15,7 @@ class UploadedMultiResponse {
UploadedMultiResponse({required this.status, required this.data}); UploadedMultiResponse({required this.status, required this.data});
// JSON Init // JSON Init
factory UploadedMultiResponse.fromJson(Map<String, dynamic> json) => factory UploadedMultiResponse.fromJson(Map<String, dynamic> json) => _$UploadedMultiResponseFromJson(json);
_$UploadedMultiResponseFromJson(json);
// JSON Export // JSON Export
Map<String, dynamic> toJson() => _$UploadedMultiResponseToJson(this); Map<String, dynamic> toJson() => _$UploadedMultiResponseToJson(this);

View file

@ -15,8 +15,7 @@ class UploadedResponse {
UploadedResponse({required this.status, required this.data}); UploadedResponse({required this.status, required this.data});
// JSON Init // JSON Init
factory UploadedResponse.fromJson(Map<String, dynamic> json) => factory UploadedResponse.fromJson(Map<String, dynamic> json) => _$UploadedResponseFromJson(json);
_$UploadedResponseFromJson(json);
// JSON Export // JSON Export
Map<String, dynamic> toJson() => _$UploadedResponseToJson(this); Map<String, dynamic> toJson() => _$UploadedResponseToJson(this);

View file

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

View file

@ -26,8 +26,7 @@ class UploadedPaste {
this.items}); this.items});
// JSON Init // JSON Init
factory UploadedPaste.fromJson(Map<String, dynamic> json) => factory UploadedPaste.fromJson(Map<String, dynamic> json) => _$UploadedPasteFromJson(json);
_$UploadedPasteFromJson(json);
// JSON Export // JSON Export
Map<String, dynamic> toJson() => _$UploadedPasteToJson(this); Map<String, dynamic> toJson() => _$UploadedPasteToJson(this);

View file

@ -33,10 +33,8 @@ class FileRepository {
return response; return response;
} }
Future<UploadedResponse> postUpload( Future<UploadedResponse> postUpload(List<File>? files, Map<String, String>? additionalFiles) async {
List<File>? files, Map<String, String>? additionalFiles) async { var response = await _api.post('/file/upload', files: files, additionalFiles: additionalFiles);
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));
} }
@ -44,12 +42,10 @@ class FileRepository {
Map<String, String> multiPasteIds = {}; Map<String, String> multiPasteIds = {};
for (var element in ids) { for (var element in ids) {
multiPasteIds.putIfAbsent( multiPasteIds.putIfAbsent("ids[${ids.indexOf(element) + 1}]", () => element);
"ids[${ids.indexOf(element) + 1}]", () => element);
} }
var response = var response = await _api.post('/file/create_multipaste', fields: multiPasteIds);
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,8 +8,8 @@ import '../services/api.dart';
class UserRepository { class UserRepository {
final Api _api = locator<Api>(); final Api _api = locator<Api>();
Future<CreateApiKeyResponse> postApiKey(String url, String username, Future<CreateApiKeyResponse> postApiKey(
String password, String accessLevel, String comment) async { String url, String username, String password, String accessLevel, String comment) async {
_api.setUrl(url); _api.setUrl(url);
var fields = Map.fromEntries([ var fields = Map.fromEntries([

View file

@ -24,34 +24,25 @@ class Api implements ApiErrorConverter {
String _url = ""; String _url = "";
String _apiKey = ""; String _apiKey = "";
final Map<String, String> _headers = { final Map<String, String> _headers = {"Content-Type": _applicationJson, "Accept": _applicationJson};
"Content-Type": _applicationJson,
"Accept": _applicationJson
};
Duration _timeout = const Duration(seconds: Constants.apiRequestTimeoutLimit); Duration _timeout = const Duration(seconds: Constants.apiRequestTimeoutLimit);
Future<http.Response> fetch<T>(String route) async { Future<http.Response> fetch<T>(String route) async {
try { try {
_logger.d( _logger
"Requesting GET API endpoint '${_url + route}' with headers '$_headers' and maximum timeout '$_timeout'"); .d("Requesting GET API endpoint '${_url + route}' with headers '$_headers' and maximum timeout '$_timeout'");
var response = await http var response = await http.get(Uri.parse(_url + route), headers: _headers).timeout(_timeout);
.get(Uri.parse(_url + route), headers: _headers)
.timeout(_timeout);
handleRestErrors(response); handleRestErrors(response);
return response; return response;
} on TimeoutException { } on TimeoutException {
throw ServiceException( throw ServiceException(code: ErrorCode.socketTimeout, message: _errorTimeout);
code: ErrorCode.socketTimeout, message: _errorTimeout);
} on SocketException { } on SocketException {
throw ServiceException( throw ServiceException(code: ErrorCode.socketError, message: _errorNoConnection);
code: ErrorCode.socketError, message: _errorNoConnection);
} }
} }
Future<http.Response> post<T>(String route, Future<http.Response> post<T>(String route,
{Map<String, String?>? fields, {Map<String, String?>? fields, List<File>? files, Map<String, String>? additionalFiles}) async {
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)
@ -68,34 +59,27 @@ class Api implements ApiErrorConverter {
if (files != null && files.isNotEmpty) { if (files != null && files.isNotEmpty) {
for (var element in files) { for (var element in files) {
request.files.add(await http.MultipartFile.fromPath( request.files.add(await http.MultipartFile.fromPath('file[${files.indexOf(element) + 1}]', element.path));
'file[${files.indexOf(element) + 1}]', element.path));
} }
} }
if (additionalFiles != null && additionalFiles.isNotEmpty) { if (additionalFiles != null && additionalFiles.isNotEmpty) {
List<String> keys = additionalFiles.keys.toList(); List<String> keys = additionalFiles.keys.toList();
additionalFiles.forEach((key, value) { additionalFiles.forEach((key, value) {
var index = files != null var index = files != null ? files.length + keys.indexOf(key) + 1 : keys.indexOf(key) + 1;
? files.length + keys.indexOf(key) + 1 request.files.add(http.MultipartFile.fromString('file[$index]', value, filename: key));
: keys.indexOf(key) + 1;
request.files.add(http.MultipartFile.fromString('file[$index]', value,
filename: key));
}); });
} }
_logger.d( _logger.d("Requesting POST API endpoint '${uri.toString()}' and ${request.files.length} files");
"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( throw ServiceException(code: ErrorCode.socketTimeout, message: _errorTimeout);
code: ErrorCode.socketTimeout, message: _errorTimeout);
} on SocketException { } on SocketException {
throw ServiceException( throw ServiceException(code: ErrorCode.socketError, message: _errorNoConnection);
code: ErrorCode.socketError, message: _errorNoConnection);
} }
} }
@ -123,17 +107,14 @@ 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.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 responseContentType = ContentType.parse(response.headers[HttpHeaders.contentTypeHeader]!);
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 RestServiceException(response.statusCode, throw RestServiceException(response.statusCode, responseBody: parsedBody);
responseBody: parsedBody);
} }
} }

View file

@ -7,8 +7,7 @@ import '../datamodels/dialog_request.dart';
import '../datamodels/dialog_response.dart'; import '../datamodels/dialog_response.dart';
class DialogService { class DialogService {
final GlobalKey<NavigatorState> _dialogNavigationKey = final GlobalKey<NavigatorState> _dialogNavigationKey = GlobalKey<NavigatorState>();
GlobalKey<NavigatorState>();
late Function(DialogRequest) _showDialogListener; late Function(DialogRequest) _showDialogListener;
Completer<DialogResponse>? _dialogCompleter; Completer<DialogResponse>? _dialogCompleter;
@ -28,28 +27,20 @@ class DialogService {
title: title, title: title,
description: description, description: description,
buttonTitleAccept: buttonTitleAccept:
buttonTitleAccept == null || buttonTitleAccept.isEmpty buttonTitleAccept == null || buttonTitleAccept.isEmpty ? translate('dialog.confirm') : buttonTitleAccept));
? translate('dialog.confirm')
: buttonTitleAccept));
return _dialogCompleter!.future; return _dialogCompleter!.future;
} }
Future<DialogResponse> showConfirmationDialog( Future<DialogResponse> showConfirmationDialog(
{String? title, {String? title, String? description, String? buttonTitleAccept, String? buttonTitleDeny}) {
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 buttonTitleAccept == null || buttonTitleAccept.isEmpty ? translate('dialog.confirm') : buttonTitleAccept,
? translate('dialog.confirm') buttonTitleDeny:
: buttonTitleAccept, buttonTitleDeny == null || buttonTitleDeny.isEmpty ? translate('dialog.cancel') : buttonTitleDeny));
buttonTitleDeny: buttonTitleDeny == null || buttonTitleDeny.isEmpty
? translate('dialog.cancel')
: buttonTitleDeny));
return _dialogCompleter!.future; return _dialogCompleter!.future;
} }

View file

@ -23,8 +23,7 @@ class FileService {
return await _fileRepository.postDelete(id); return await _fileRepository.postDelete(id);
} }
Future<UploadedResponse> uploadPaste( Future<UploadedResponse> uploadPaste(List<File>? files, Map<String, String>? additionalFiles) async {
List<File>? files, Map<String, String>? additionalFiles) async {
return await _fileRepository.postUpload(files, additionalFiles); return await _fileRepository.postUpload(files, additionalFiles);
} }

View file

@ -19,8 +19,7 @@ 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: description: translate('link.dialog.description', args: {'link': link}));
translate('link.dialog.description', args: {'link': link}));
} }
} }
} }

View file

@ -17,13 +17,11 @@ class NavigationService {
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! return _navigationKey.currentState!.pushNamed(routeName, arguments: arguments);
.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! return _navigationKey.currentState!.pushReplacementNamed(routeName, arguments: arguments);
.pushReplacementNamed(routeName, arguments: arguments);
} }
} }

View file

@ -57,8 +57,7 @@ class PermissionService extends StoppableService {
return; return;
} }
var ignoredDialog = var ignoredDialog = await _storageService.hasStoragePermissionDialogIgnored();
await _storageService.hasStoragePermissionDialogIgnored();
if (ignoredDialog) { if (ignoredDialog) {
_logger.d('Permanently ignored permission request, skipping'); _logger.d('Permanently ignored permission request, skipping');
@ -105,9 +104,8 @@ class PermissionService extends StoppableService {
super.start(); super.start();
await checkEnabledAndPermission(); await checkEnabledAndPermission();
_serviceCheckTimer = Timer.periodic( _serviceCheckTimer =
const Duration(milliseconds: Constants.mediaPermissionCheckInterval), Timer.periodic(const Duration(milliseconds: Constants.mediaPermissionCheckInterval), (serviceTimer) async {
(serviceTimer) async {
if (!super.serviceStopped) { if (!super.serviceStopped) {
await checkEnabledAndPermission(); await checkEnabledAndPermission();
} else { } else {

View file

@ -3,8 +3,7 @@ import 'dart:async';
import '../enums/refresh_event.dart'; import '../enums/refresh_event.dart';
class RefreshService { class RefreshService {
StreamController<RefreshEvent> refreshEventController = StreamController<RefreshEvent> refreshEventController = StreamController<RefreshEvent>.broadcast();
StreamController<RefreshEvent>.broadcast();
void addEvent(RefreshEvent event) { void addEvent(RefreshEvent event) {
if (refreshEventController.hasListener) { if (refreshEventController.hasListener) {

View file

@ -7,8 +7,7 @@ import '../models/session.dart';
class StorageService { class StorageService {
static const _sessionKey = 'session'; static const _sessionKey = 'session';
static const _lastUrlKey = 'last_url'; static const _lastUrlKey = 'last_url';
static const _storagePermissionDialogIgnoredKey = static const _storagePermissionDialogIgnoredKey = 'storage_permission_ignored';
'storage_permission_ignored';
Future<bool> storeLastUrl(String url) { Future<bool> storeLastUrl(String url) {
return _store(_lastUrlKey, url); return _store(_lastUrlKey, url);

View file

@ -12,10 +12,9 @@ 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(String url, String username, Future<CreateApiKeyResponse> createApiKey(
String password, String accessLevel, String comment) async { String url, String username, String password, String accessLevel, String comment) async {
return await _userRepository.postApiKey( return await _userRepository.postApiKey(url, username, password, accessLevel, comment);
url, username, password, accessLevel, comment);
} }
Future<ApiKeysResponse> getApiKeys() async { Future<ApiKeysResponse> getApiKeys() async {

View file

@ -6,8 +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 return dateFormat.format(DateTime.fromMillisecondsSinceEpoch(millis as int));
.format(DateTime.fromMillisecondsSinceEpoch(millis as int));
} }
static String formatBytes(int bytes, int decimals) { static String formatBytes(int bytes, int decimals) {

View file

@ -12,10 +12,7 @@ class BaseModel extends ChangeNotifier {
bool _isDisposed = false; bool _isDisposed = false;
final Map<String, Object?> _stateMap = { final Map<String, Object?> _stateMap = {stateViewKey: ViewState.idle, stateMessageKey: null};
stateViewKey: ViewState.idle,
stateMessageKey: null
};
ViewState? get state => _stateMap[stateViewKey] as ViewState?; ViewState? get state => _stateMap[stateViewKey] as ViewState?;
@ -45,14 +42,11 @@ class BaseModel extends ChangeNotifier {
return null; return null;
} }
void setStateBoolValue(String key, bool stateValue) => void setStateBoolValue(String key, bool stateValue) => _setStateValue(key, stateValue);
_setStateValue(key, stateValue);
void setStateIntValue(String key, int? stateValue) => void setStateIntValue(String key, int? stateValue) => _setStateValue(key, stateValue);
_setStateValue(key, stateValue);
void setStateStringValue(String key, String? stateValue) => void setStateStringValue(String key, String? stateValue) => _setStateValue(key, stateValue);
_setStateValue(key, stateValue);
void _setStateValue(String key, Object? stateValue) { void _setStateValue(String key, Object? stateValue) {
if (_stateMap.containsKey(key)) { if (_stateMap.containsKey(key)) {
@ -63,8 +57,7 @@ class BaseModel extends ChangeNotifier {
if (!_isDisposed) { if (!_isDisposed) {
notifyListeners(); notifyListeners();
_logger _logger.d("Notified state value update '($key, ${stateValue.toString()})'");
.d("Notified state value update '($key, ${stateValue.toString()})'");
} }
} }
@ -87,7 +80,6 @@ class BaseModel extends ChangeNotifier {
@override @override
void dispose() { void dispose() {
_logger.d("Calling dispose");
super.dispose(); super.dispose();
_isDisposed = true; _isDisposed = true;
} }

View file

@ -34,8 +34,7 @@ class HistoryModel extends BaseModel {
String? errorMessage; String? errorMessage;
void init() { void init() {
_refreshTriggerSubscription = _refreshTriggerSubscription = _refreshService.refreshEventController.stream.listen((event) {
_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();
@ -55,8 +54,7 @@ class HistoryModel extends BaseModel {
pastes.add( pastes.add(
UploadedPaste( UploadedPaste(
id: key, id: key,
date: date: DateTime.fromMillisecondsSinceEpoch(millisecondsSinceEpoch),
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,
@ -92,11 +90,9 @@ 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', errorMessage = translate('api.bad_request', args: {'reason': e.responseBody.message});
args: {'reason': e.responseBody.message});
} else { } else {
errorMessage = translate('api.general_rest_error_payload', errorMessage = translate('api.general_rest_error_payload', args: {'message': e.responseBody.message});
args: {'message': e.responseBody.message});
} }
} else { } else {
errorMessage = translate('api.general_rest_error'); errorMessage = translate('api.general_rest_error');
@ -119,8 +115,7 @@ 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: description: translate('history.delete_dialog.description', args: {'id': id}),
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'));
@ -144,8 +139,7 @@ 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', errorMessage = translate('api.general_rest_error_payload', args: {'message': e.responseBody.message});
args: {'message': e.responseBody.message});
} else { } else {
errorMessage = translate('api.general_rest_error'); errorMessage = translate('api.general_rest_error');
} }

View file

@ -117,19 +117,13 @@ class LoginModel extends BaseModel {
try { try {
if (useCredentialsLogin) { if (useCredentialsLogin) {
CreateApiKeyResponse apiKeyResponse = await _userService.createApiKey( CreateApiKeyResponse apiKeyResponse = await _userService.createApiKey(
url, url, username, password, 'apikey', 'fbmobile-${DateTime.now().millisecondsSinceEpoch}');
username,
password,
'apikey',
'fbmobile-${DateTime.now().millisecondsSinceEpoch}');
var newKey = apiKeyResponse.data['new_key']; var newKey = apiKeyResponse.data['new_key'];
if (newKey != null) { if (newKey != null) {
success = await _sessionService.login(url, newKey); success = await _sessionService.login(url, newKey);
} else { } else {
throw ServiceException( throw ServiceException(code: ErrorCode.invalidApiKey, message: translate('login.errors.invalid_api_key'));
code: ErrorCode.invalidApiKey,
message: translate('login.errors.invalid_api_key'));
} }
} else { } else {
_sessionService.setApiConfig(url, apiKey); _sessionService.setApiConfig(url, apiKey);
@ -141,15 +135,13 @@ 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 && } else if (e.statusCode != HttpStatus.unauthorized && e.statusCode == HttpStatus.forbidden) {
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', errorMessage = translate('api.bad_request', args: {'reason': e.responseBody.message});
args: {'reason': e.responseBody.message});
} else { } else {
errorMessage = translate('api.general_rest_error'); errorMessage = translate('api.general_rest_error');
} }

View file

@ -31,8 +31,7 @@ class ProfileModel extends BaseModel {
Future logout() async { Future logout() async {
var dialogResult = await _dialogService.showConfirmationDialog( var dialogResult = await _dialogService.showConfirmationDialog(
title: translate('logout.title'), title: translate('logout.title'), description: translate('logout.confirm'));
description: translate('logout.confirm'));
if (dialogResult.confirmed!) { if (dialogResult.confirmed!) {
await _sessionService.logout(); await _sessionService.logout();
@ -42,8 +41,7 @@ class ProfileModel extends BaseModel {
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', description: translate('profile.revealed_api_key.description', args: {'apiKey': apiKey}));
args: {'apiKey': apiKey}));
} }
Future showConfig(String url) async { Future showConfig(String url) async {
@ -56,15 +54,13 @@ 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 && } else if (e.statusCode != HttpStatus.unauthorized && e.statusCode == HttpStatus.forbidden) {
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', errorMessage = translate('api.bad_request', args: {'reason': e.responseBody.message});
args: {'reason': e.responseBody.message});
} else { } else {
errorMessage = translate('api.general_rest_error'); errorMessage = translate('api.general_rest_error');
} }
@ -88,18 +84,15 @@ class ProfileModel extends BaseModel {
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': 'uploadMaxSize': FormatterUtil.formatBytes(config.uploadMaxSize as int, 2),
FormatterUtil.formatBytes(config.uploadMaxSize as int, 2),
'maxFilesPerRequest': config.maxFilesPerRequest, 'maxFilesPerRequest': config.maxFilesPerRequest,
'maxInputVars': config.maxInputVars, 'maxInputVars': config.maxInputVars,
'requestMaxSize': 'requestMaxSize': FormatterUtil.formatBytes(config.requestMaxSize as int, 2)
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', description: translate('profile.shown_config.error.description', args: {'message': errorMessage}));
args: {'message': errorMessage}));
} }
} }

View file

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

View file

@ -11,8 +11,7 @@ void main() async {
setupLogger(Level.info); setupLogger(Level.info);
setupLocator(); setupLocator();
var delegate = await LocalizationDelegate.create( var delegate = await LocalizationDelegate.create(fallbackLocale: 'en', supportedLocales: ['en', 'en_US']);
fallbackLocale: 'en', supportedLocales: ['en', 'en_US']);
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
runApp(LocalizedApp(delegate, MyApp())); runApp(LocalizedApp(delegate, MyApp()));

View file

@ -27,8 +27,7 @@ class AppRouter {
return MaterialPageRoute( return MaterialPageRoute(
builder: (_) => Scaffold( builder: (_) => Scaffold(
body: Center( body: Center(
child: Text(translate('dev.no_route', child: Text(translate('dev.no_route', args: {'route': settings.name})),
args: {'route': settings.name})),
), ),
)); ));
} }

View file

@ -38,8 +38,7 @@ class AboutView extends StatelessWidget {
padding: const EdgeInsets.all(0), padding: const EdgeInsets.all(0),
child: ListView( child: ListView(
shrinkWrap: true, shrinkWrap: true,
padding: const EdgeInsets.only( padding: const EdgeInsets.only(left: 10.0, right: 10.0, bottom: 10, top: 10),
left: 10.0, right: 10.0, bottom: 10, top: 10),
children: <Widget>[ children: <Widget>[
UIHelper.verticalSpaceMedium(), UIHelper.verticalSpaceMedium(),
Center(child: logo), Center(child: logo),

View file

@ -31,9 +31,7 @@ class _BaseViewState<T extends BaseModel> extends State<BaseView<T>> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ChangeNotifierProvider<T?>( return ChangeNotifierProvider<T?>(create: (context) => model, child: Consumer<T>(builder: widget.builder!));
create: (context) => model,
child: Consumer<T>(builder: widget.builder!));
} }
@override @override

View file

@ -27,9 +27,8 @@ class HistoryView extends StatelessWidget {
model.init(); model.init();
return model.getHistory(); return model.getHistory();
}, },
builder: (context, model, child) => Scaffold( builder: (context, model, child) =>
appBar: MyAppBar(title: Text(translate('titles.history'))), Scaffold(appBar: MyAppBar(title: Text(translate('titles.history'))), body: _render(model, context)),
body: _render(model, context)),
); );
} }
@ -42,8 +41,7 @@ class HistoryView extends StatelessWidget {
? Container( ? Container(
padding: const EdgeInsets.all(0), padding: const EdgeInsets.all(0),
child: RefreshIndicator( child: RefreshIndicator(
onRefresh: () async => await model.getHistory(), onRefresh: () async => await model.getHistory(), child: _renderItems(model, url, context)))
child: _renderItems(model, url, context)))
: Container( : Container(
padding: const EdgeInsets.all(25), padding: const EdgeInsets.all(25),
child: CenteredErrorRow( child: CenteredErrorRow(
@ -63,8 +61,7 @@ class HistoryView extends StatelessWidget {
var openInBrowserButton = _renderOpenInBrowser(model, fullPasteUrl); var openInBrowserButton = _renderOpenInBrowser(model, fullPasteUrl);
var dateWidget = ListTile( var dateWidget = ListTile(
title: Text( title: Text(FormatterUtil.formatEpoch(paste.date!.millisecondsSinceEpoch)),
FormatterUtil.formatEpoch(paste.date!.millisecondsSinceEpoch)),
subtitle: Text(translate('history.date')), subtitle: Text(translate('history.date')),
); );
@ -76,8 +73,7 @@ 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: const Icon(Icons.copy, icon: const Icon(Icons.copy, color: blueColor, textDirection: TextDirection.ltr),
color: blueColor, textDirection: TextDirection.ltr),
onPressed: () { onPressed: () {
FlutterClipboard.copy(fullPasteUrl).then((value) { FlutterClipboard.copy(fullPasteUrl).then((value) {
final snackBar = SnackBar( final snackBar = SnackBar(
@ -168,14 +164,12 @@ class HistoryView extends StatelessWidget {
trailing: Wrap(children: [ trailing: Wrap(children: [
openInBrowserButton, openInBrowserButton,
IconButton( IconButton(
icon: const Icon(Icons.share, icon: const Icon(Icons.share, color: blueColor, textDirection: TextDirection.ltr),
color: blueColor, textDirection: TextDirection.ltr),
onPressed: () async { onPressed: () async {
await Share.share(fullPasteUrl); await Share.share(fullPasteUrl);
}) })
]), ]),
subtitle: Text(!paste.isMulti! ? paste.filename! : '', subtitle: Text(!paste.isMulti! ? paste.filename! : '', style: const TextStyle(fontStyle: FontStyle.italic)),
style: const TextStyle(fontStyle: FontStyle.italic)),
), ),
)); ));
} }
@ -196,8 +190,7 @@ class HistoryView extends StatelessWidget {
Widget _renderOpenInBrowser(HistoryModel model, String url) { Widget _renderOpenInBrowser(HistoryModel model, String url) {
return IconButton( return IconButton(
icon: const Icon(Icons.open_in_new, icon: const Icon(Icons.open_in_new, color: blueColor, textDirection: TextDirection.ltr),
color: blueColor, textDirection: TextDirection.ltr),
onPressed: () { onPressed: () {
return model.openLink(url); return model.openLink(url);
}); });

View file

@ -16,9 +16,7 @@ class HomeView extends StatelessWidget {
return BaseView<HomeModel>( return BaseView<HomeModel>(
builder: (context, model, child) => Scaffold( builder: (context, model, child) => Scaffold(
appBar: MyAppBar(title: Text(translate('app.title'))), appBar: MyAppBar(title: Text(translate('app.title'))),
body: model.state == ViewState.busy body: model.state == ViewState.busy ? const Center(child: CircularProgressIndicator()) : Container()),
? const Center(child: CircularProgressIndicator())
: Container()),
); );
} }
} }

View file

@ -55,18 +55,13 @@ class LoginView extends StatelessWidget {
child: const Icon(Icons.help), child: const Icon(Icons.help),
onTap: () { onTap: () {
_dialogService.showDialog( _dialogService.showDialog(
title: translate( title: translate('login.compatibility_dialog.title'),
'login.compatibility_dialog.title'), description: translate('login.compatibility_dialog.body'));
description: translate(
'login.compatibility_dialog.body'));
}, },
), ),
InkWell( InkWell(
child: Icon( child:
model.useCredentialsLogin Icon(model.useCredentialsLogin ? Icons.person_outline : Icons.vpn_key, color: blueColor),
? Icons.person_outline
: Icons.vpn_key,
color: blueColor),
onTap: () { onTap: () {
model.toggleLoginMethod(); model.toggleLoginMethod();
}, },
@ -91,8 +86,7 @@ class LoginView extends StatelessWidget {
onPressed: () async { onPressed: () async {
var loginSuccess = await model.login(); var loginSuccess = await model.login();
if (loginSuccess) { if (loginSuccess) {
_navigationService _navigationService.navigateAndReplaceTo(HomeView.routeName);
.navigateAndReplaceTo(HomeView.routeName);
} }
}, },
) )

View file

@ -14,8 +14,7 @@ class AuthenticatedNavBarView extends StatefulWidget {
AuthenticatedNavBarState createState() => AuthenticatedNavBarState(); AuthenticatedNavBarState createState() => AuthenticatedNavBarState();
} }
class AuthenticatedNavBarState extends State<AuthenticatedNavBarView> class AuthenticatedNavBarState extends State<AuthenticatedNavBarView> with SingleTickerProviderStateMixin {
with SingleTickerProviderStateMixin {
final Logger _logger = getLogger(); final Logger _logger = getLogger();
int _currentTabIndex = 0; int _currentTabIndex = 0;

View file

@ -20,9 +20,8 @@ class ProfileView extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BaseView<ProfileModel>( return BaseView<ProfileModel>(
builder: (context, model, child) => Scaffold( builder: (context, model, child) =>
appBar: MyAppBar(title: Text(translate('titles.profile'))), Scaffold(appBar: MyAppBar(title: Text(translate('titles.profile'))), body: _render(model, context)));
body: _render(model, context)));
} }
Widget _render(ProfileModel model, BuildContext context) { Widget _render(ProfileModel model, BuildContext context) {

View file

@ -13,7 +13,7 @@ class StartUpView extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ViewModelBuilder<StartUpViewModel>.reactive( return ViewModelBuilder<StartUpViewModel>.reactive(
viewModelBuilder: () => StartUpViewModel(), viewModelBuilder: () => StartUpViewModel(),
onViewModelReady: (model) => model.handleStartUpLogic(), onModelReady: (model) => model.handleStartUpLogic(),
builder: (context, model, child) => Scaffold( builder: (context, model, child) => Scaffold(
body: model.state == ViewState.busy body: model.state == ViewState.busy
? Center( ? Center(
@ -22,9 +22,7 @@ class StartUpView extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
const CircularProgressIndicator(), const CircularProgressIndicator(),
(model.stateMessage!.isNotEmpty (model.stateMessage!.isNotEmpty ? Text(model.stateMessage!) : Container())
? Text(model.stateMessage!)
: Container())
])) ]))
: Container())); : Container()));
} }

View file

@ -11,8 +11,7 @@ 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 = bool isAuthenticated = currentSession != null ? currentSession.apiKey.isNotEmpty : false;
currentSession != null ? currentSession.apiKey.isNotEmpty : false;
if (isAuthenticated) { if (isAuthenticated) {
return const AuthenticatedNavBarView(); return const AuthenticatedNavBarView();

View file

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

View file

@ -20,10 +20,7 @@ class CenteredErrorRow extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[ children: <Widget>[
Expanded( Expanded(child: Center(child: Text(message!, style: const TextStyle(color: redColor)))),
child: Center(
child: Text(message!,
style: const TextStyle(color: redColor)))),
], ],
), ),
(retryCallback != null (retryCallback != null

View file

@ -11,19 +11,13 @@ class LoginApiKeyHeaders extends StatelessWidget {
final String? validationMessage; final String? validationMessage;
const LoginApiKeyHeaders( const LoginApiKeyHeaders(
{super.key, {super.key, required this.uriController, required this.apiKeyController, this.validationMessage});
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>[
validationMessage != null validationMessage != null ? Text(validationMessage!, style: const TextStyle(color: redColor)) : Container(),
? Text(validationMessage!, style: const TextStyle(color: redColor)) LoginTextField(uriController, translate('login.url_placeholder'), const Icon(Icons.link),
: Container(),
LoginTextField(uriController, translate('login.url_placeholder'),
const Icon(Icons.link),
keyboardType: TextInputType.url), keyboardType: TextInputType.url),
LoginTextField( LoginTextField(
apiKeyController, apiKeyController,

View file

@ -21,17 +21,12 @@ class LoginCredentialsHeaders extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Column(children: <Widget>[ return Column(children: <Widget>[
validationMessage != null validationMessage != null ? Text(validationMessage!, style: const TextStyle(color: redColor)) : Container(),
? Text(validationMessage!, style: const TextStyle(color: redColor)) LoginTextField(uriController, translate('login.url_placeholder'), const Icon(Icons.link),
: Container(),
LoginTextField(uriController, translate('login.url_placeholder'),
const Icon(Icons.link),
keyboardType: TextInputType.url), keyboardType: TextInputType.url),
LoginTextField(usernameController, LoginTextField(usernameController, translate('login.username_placeholder'), const Icon(Icons.person),
translate('login.username_placeholder'), const Icon(Icons.person),
keyboardType: TextInputType.name), keyboardType: TextInputType.name),
LoginTextField(passwordController, LoginTextField(passwordController, translate('login.password_placeholder'), const Icon(Icons.vpn_key),
translate('login.password_placeholder'), const Icon(Icons.vpn_key),
obscureText: true), obscureText: true),
]); ]);
} }

View file

@ -1,5 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../shared/app_colors.dart';
class LoginTextField extends StatelessWidget { class LoginTextField extends StatelessWidget {
final TextEditingController controller; final TextEditingController controller;
final String placeHolder; final String placeHolder;
@ -8,9 +10,7 @@ class LoginTextField extends StatelessWidget {
final Widget prefixIcon; final Widget prefixIcon;
const LoginTextField(this.controller, this.placeHolder, this.prefixIcon, const LoginTextField(this.controller, this.placeHolder, this.prefixIcon,
{super.key, {super.key, this.keyboardType = TextInputType.text, this.obscureText = false});
this.keyboardType = TextInputType.text,
this.obscureText = false});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -19,6 +19,7 @@ class LoginTextField extends StatelessWidget {
margin: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 10.0), margin: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 10.0),
height: 50.0, height: 50.0,
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
decoration: BoxDecoration(color: whiteColor, borderRadius: BorderRadius.circular(10.0)),
child: TextFormField( child: TextFormField(
keyboardType: keyboardType, keyboardType: keyboardType,
obscureText: obscureText, obscureText: obscureText,
@ -30,8 +31,7 @@ class LoginTextField extends StatelessWidget {
prefixIcon: prefixIcon, prefixIcon: prefixIcon,
hintText: placeHolder, hintText: placeHolder,
contentPadding: const EdgeInsets.fromLTRB(20.0, 10.0, 20.0, 10.0), contentPadding: const EdgeInsets.fromLTRB(20.0, 10.0, 20.0, 10.0),
border: border: OutlineInputBorder(borderRadius: BorderRadius.circular(32.0)),
OutlineInputBorder(borderRadius: BorderRadius.circular(32.0)),
), ),
controller: controller), controller: controller),
); );

View file

@ -6,18 +6,10 @@ 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( MyAppBar({Key? key, required Widget title, List<Widget>? actionWidgets, bool enableAbout = true})
{Key? key, : super(key: key, title: Row(children: <Widget>[title]), actions: _renderIconButtons(actionWidgets, enableAbout));
required Widget title,
List<Widget>? actionWidgets,
bool enableAbout = true})
: super(
key: key,
title: Row(children: <Widget>[title]),
actions: _renderIconButtons(actionWidgets, enableAbout));
static List<Widget> _renderIconButtons( static List<Widget> _renderIconButtons(List<Widget>? actionWidgets, bool aboutEnabled) {
List<Widget>? actionWidgets, bool aboutEnabled) {
actionWidgets ??= []; actionWidgets ??= [];
List<Widget> widgets = [...actionWidgets]; List<Widget> widgets = [...actionWidgets];

View file

@ -98,7 +98,7 @@ packages:
name: built_value_generator name: built_value_generator
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "8.4.3" version: "8.4.2"
characters: characters:
dependency: transitive dependency: transitive
description: description:
@ -217,7 +217,7 @@ packages:
name: file_picker name: file_picker
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "5.2.5" version: "5.2.4"
fixnum: fixnum:
dependency: transitive dependency: transitive
description: description:
@ -256,13 +256,6 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.7" version: "2.0.7"
flutter_sharing_intent:
dependency: "direct main"
description:
name: flutter_sharing_intent
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.5"
flutter_test: flutter_test:
dependency: "direct dev" dependency: "direct dev"
description: flutter description: flutter
@ -588,6 +581,20 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.2.1" version: "1.2.1"
quiver:
dependency: transitive
description:
name: quiver
url: "https://pub.dartlang.org"
source: hosted
version: "3.1.0"
receive_sharing_intent:
dependency: "direct main"
description:
name: receive_sharing_intent
url: "https://pub.dartlang.org"
source: hosted
version: "1.4.5"
share_plus: share_plus:
dependency: "direct main" dependency: "direct main"
description: description:
@ -608,7 +615,7 @@ packages:
name: shared_preferences name: shared_preferences
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.16" version: "2.0.15"
shared_preferences_android: shared_preferences_android:
dependency: transitive dependency: transitive
description: description:
@ -616,10 +623,10 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.14" version: "2.0.14"
shared_preferences_foundation: shared_preferences_ios:
dependency: transitive dependency: transitive
description: description:
name: shared_preferences_foundation name: shared_preferences_ios
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.1.1" version: "2.1.1"
@ -630,6 +637,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.1.2" version: "2.1.2"
shared_preferences_macos:
dependency: transitive
description:
name: shared_preferences_macos
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.4"
shared_preferences_platform_interface: shared_preferences_platform_interface:
dependency: transitive dependency: transitive
description: description:
@ -704,7 +718,7 @@ packages:
name: stacked name: stacked
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.1.0+1" version: "3.0.1"
stacked_core: stacked_core:
dependency: transitive dependency: transitive
description: description:

View file

@ -11,7 +11,7 @@ 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.6.0+18 version: 1.5.2+18
environment: environment:
sdk: '>=2.18.5 <3.0.0' sdk: '>=2.18.5 <3.0.0'
@ -24,31 +24,31 @@ dependencies:
sdk: flutter sdk: flutter
flutter_translate: 4.0.3 flutter_translate: 4.0.3
provider: 6.0.5 provider: 6.0.5
stacked: 3.1.0+1 stacked: 3.0.1
get_it: 7.2.0 get_it: 7.2.0
logger: 1.1.0 logger: 1.1.0
shared_preferences: 2.0.16 shared_preferences: 2.0.15
http: 0.13.5 http: 0.13.5
validators: 3.0.0 validators: 3.0.0
flutter_linkify: 5.0.2 flutter_linkify: 5.0.2
url_launcher: 6.1.7 url_launcher: 6.1.7
expandable: 5.0.1 expandable: 5.0.1
share_plus: 6.3.0 share_plus: 6.3.0
file_picker: 5.2.5 file_picker: 5.2.4
clipboard: 0.1.3 clipboard: 0.1.3
receive_sharing_intent: 1.4.5
permission_handler: 10.2.0 permission_handler: 10.2.0
package_info_plus: 3.0.2 package_info_plus: 3.0.2
json_annotation: 4.7.0 json_annotation: 4.7.0
dynamic_color: 1.5.4 dynamic_color: 1.5.4
intl: 0.17.0 intl: 0.17.0
path: 1.8.2 path: 1.8.2
flutter_sharing_intent: 1.0.5
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter
build_runner: 2.3.3 build_runner: 2.3.3
built_value_generator: 8.4.3 built_value_generator: 8.4.2
json_serializable: 6.5.4 json_serializable: 6.5.4
flutter_lints: 2.0.1 flutter_lints: 2.0.1