Add share to app, add requesting permission, add FAQ to about view, minor UI improvements to history list
This commit is contained in:
parent
0c5384441a
commit
9f082f768a
16 changed files with 339 additions and 29 deletions
|
@ -4,4 +4,5 @@
|
|||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||
</manifest>
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
android:icon="@mipmap/ic_launcher">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:launchMode="singleTop"
|
||||
android:launchMode="singleTask"
|
||||
android:theme="@style/LaunchTheme"
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||
android:hardwareAccelerated="true"
|
||||
|
@ -20,6 +20,46 @@
|
|||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</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>
|
||||
<!-- Don't delete the meta-data below.
|
||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||
|
@ -28,4 +68,5 @@
|
|||
android:value="2" />
|
||||
</application>
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||
</manifest>
|
||||
|
|
|
@ -4,4 +4,5 @@
|
|||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||
</manifest>
|
||||
|
|
|
@ -35,7 +35,8 @@
|
|||
"dismiss": "Dismiss",
|
||||
"multipaste": "multipaste",
|
||||
"errors": {
|
||||
"not_found": "Not found"
|
||||
"not_found": "Not found",
|
||||
"retrieval_intent": "An error occurred while retrieving shared data"
|
||||
}
|
||||
},
|
||||
"startup": {
|
||||
|
@ -67,7 +68,7 @@
|
|||
"filesize": "Filesize",
|
||||
"link": "Link",
|
||||
"date": "Date",
|
||||
"open_link": "Open in browser",
|
||||
"mimetype": "Mimetype",
|
||||
"delete": "Delete permanently",
|
||||
"multipaste_element": "Included as multipaste item",
|
||||
"errors": {
|
||||
|
@ -82,7 +83,9 @@
|
|||
},
|
||||
"about": {
|
||||
"headline": "Welcome to FileBin mobile!",
|
||||
"description": "This application is a mobile client for FileBin and it's open source. It helps you to manage your pastes.\n\nIn order to use the application, you need access to a FileBin instance and an API key to sign in. It's recommended that the API key has at least 'apikey' access-level.",
|
||||
"description": "This application is a mobile client for FileBin and it's open source. It helps you to manage your pastes.\n\nIn order to use the application, you need access to a FileBin instance.",
|
||||
"faq_headline": "F.A.Q",
|
||||
"faq": "- How do I login?\nInsert your instance URL and an API key which you can generate in the web interface of FileBin. You should use 'apikey' access-level in order to show history.\n\n- Why is storage permission required?\nIt's not required, but highly advised to grant it. Otherwise sharing files with the app won't work correctly and you might think that sharing has no effect.\n\n- When I am logged out, sharing files via share with the app won't list all files I selected after I login.\nPlease login before you start using the app. Account information are persisted. You only need to do it once.",
|
||||
"contact_us": "Feedback? Issues?",
|
||||
"website": "Main application: https://github.com/Bluewind/filebin\n\nMobile: https://github.com/v4rakh/fbmobile"
|
||||
},
|
||||
|
@ -108,6 +111,14 @@
|
|||
"description": "Could not open '{link}'. Please ensure that you have an application installed which handles opening such link types."
|
||||
}
|
||||
},
|
||||
"permission_service": {
|
||||
"dialog": {
|
||||
"title": "Storage permission",
|
||||
"description": "Storage permission should be granted to the app so that it can work properly. Do you want to grant permission or ignore this message permanently in the future?",
|
||||
"grant": "Grant",
|
||||
"ignore": "Ignore"
|
||||
}
|
||||
},
|
||||
"dialog": {
|
||||
"confirm": "OK",
|
||||
"cancel": "Cancel"
|
||||
|
|
|
@ -48,6 +48,19 @@
|
|||
</array>
|
||||
<key>NSAppleMusicUsageDescription</key>
|
||||
<string>Allow to select music files and upload them via the app</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>ShareMedia</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict/>
|
||||
</array>
|
||||
// TODO follow steps 2) on create share extension (https://pub.dev/packages/receive_sharing_intent)
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>Allow to select photos and upload them via the app</string>
|
||||
</dict>
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
class Constants {
|
||||
static const int apiRequestTimeoutLimit = 8;
|
||||
static const String apiUrlSuffix = '/api/v2.2.0';
|
||||
|
||||
static const int mediaPermissionCheckInterval = 120 * 1000;
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ import 'package:logger/logger.dart';
|
|||
|
||||
import '../../core/services/session_service.dart';
|
||||
import '../../locator.dart';
|
||||
import '../services/permission_service.dart';
|
||||
import '../services/stoppable_service.dart';
|
||||
import '../util/logger.dart';
|
||||
|
||||
|
@ -18,7 +19,7 @@ class LifeCycleManager extends StatefulWidget {
|
|||
class _LifeCycleManagerState extends State<LifeCycleManager> with WidgetsBindingObserver {
|
||||
final Logger logger = getLogger();
|
||||
|
||||
List<StoppableService> servicesToManage = [locator<SessionService>()];
|
||||
List<StoppableService> servicesToManage = [locator<SessionService>(), locator<PermissionService>()];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
|
132
lib/core/services/permission_service.dart
Normal file
132
lib/core/services/permission_service.dart
Normal file
|
@ -0,0 +1,132 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter_translate/flutter_translate.dart';
|
||||
import 'package:logger/logger.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
|
||||
import '../../constants.dart';
|
||||
import '../../core/datamodels/dialog_response.dart';
|
||||
import '../../core/services/dialog_service.dart';
|
||||
import '../../core/services/stoppable_service.dart';
|
||||
import '../../core/util/logger.dart';
|
||||
import '../../locator.dart';
|
||||
import 'storage_service.dart';
|
||||
|
||||
class PermissionService extends StoppableService {
|
||||
final Logger _logger = getLogger();
|
||||
final DialogService _dialogService = locator<DialogService>();
|
||||
final StorageService _storageService = locator<StorageService>();
|
||||
|
||||
Timer _serviceCheckTimer;
|
||||
|
||||
PermissionStatus _permissionStatus;
|
||||
|
||||
bool _permanentlyIgnored = false;
|
||||
bool _devicePermissionDialogActive = false;
|
||||
bool _ownPermissionDialogActive = false;
|
||||
|
||||
PermissionService() {
|
||||
_devicePermissionDialogActive = true;
|
||||
|
||||
Permission.storage.request().then((status) {
|
||||
_permissionStatus = status;
|
||||
if (PermissionStatus.permanentlyDenied == status) {
|
||||
_permanentlyIgnored = true;
|
||||
}
|
||||
}).whenComplete(() {
|
||||
_logger.d('Initial device request permission finished');
|
||||
_devicePermissionDialogActive = false;
|
||||
});
|
||||
}
|
||||
|
||||
Future checkEnabledAndPermission() async {
|
||||
if (_permanentlyIgnored) {
|
||||
await _storageService.storeStoragePermissionDialogIgnored();
|
||||
_permanentlyIgnored = false;
|
||||
_logger.d('Set permanently ignored permission request');
|
||||
stop();
|
||||
}
|
||||
|
||||
if (_devicePermissionDialogActive) {
|
||||
_logger.d('Device permission dialog active, skipping');
|
||||
return;
|
||||
}
|
||||
|
||||
if (_ownPermissionDialogActive) {
|
||||
_logger.d('Own permission dialog already active, skipping');
|
||||
return;
|
||||
}
|
||||
|
||||
var ignoredDialog = await _storageService.hasStoragePermissionDialogIgnored();
|
||||
|
||||
if (ignoredDialog) {
|
||||
_logger.d('Permanently ignored permission request, skipping');
|
||||
stop();
|
||||
return;
|
||||
}
|
||||
|
||||
_permissionStatus = await Permission.storage.status;
|
||||
if (_permissionStatus != PermissionStatus.granted) {
|
||||
if (_permissionStatus == PermissionStatus.permanentlyDenied) {
|
||||
await _storageService.storeStoragePermissionDialogIgnored();
|
||||
return;
|
||||
}
|
||||
|
||||
_ownPermissionDialogActive = true;
|
||||
DialogResponse response = await _dialogService.showConfirmationDialog(
|
||||
title: translate('permission_service.dialog.title'),
|
||||
description: translate('permission_service.dialog.description'),
|
||||
buttonTitleAccept: translate('permission_service.dialog.grant'),
|
||||
buttonTitleDeny: translate('permission_service.dialog.ignore'));
|
||||
|
||||
if (!response.confirmed) {
|
||||
await _storageService.storeStoragePermissionDialogIgnored();
|
||||
} else {
|
||||
_devicePermissionDialogActive = true;
|
||||
Permission.storage.request().then((status) async {
|
||||
if (PermissionStatus.permanentlyDenied == status) {
|
||||
await _storageService.storeStoragePermissionDialogIgnored();
|
||||
}
|
||||
}).whenComplete(() {
|
||||
_logger.d('Device request permission finished');
|
||||
_devicePermissionDialogActive = false;
|
||||
});
|
||||
}
|
||||
|
||||
_ownPermissionDialogActive = false;
|
||||
} else {
|
||||
await _storageService.storeStoragePermissionDialogIgnored();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future start() async {
|
||||
super.start();
|
||||
await checkEnabledAndPermission();
|
||||
|
||||
_serviceCheckTimer =
|
||||
Timer.periodic(Duration(milliseconds: Constants.mediaPermissionCheckInterval), (_serviceTimer) async {
|
||||
if (!super.serviceStopped) {
|
||||
await checkEnabledAndPermission();
|
||||
} else {
|
||||
_serviceTimer.cancel();
|
||||
}
|
||||
});
|
||||
_logger.d('PermissionService started');
|
||||
}
|
||||
|
||||
@override
|
||||
void stop() {
|
||||
_removeServiceCheckTimer();
|
||||
super.stop();
|
||||
_logger.d('PermissionService stopped');
|
||||
}
|
||||
|
||||
void _removeServiceCheckTimer() {
|
||||
if (_serviceCheckTimer != null) {
|
||||
_serviceCheckTimer.cancel();
|
||||
_serviceCheckTimer = null;
|
||||
_logger.d('Removed service check timer');
|
||||
}
|
||||
}
|
||||
}
|
|
@ -7,6 +7,7 @@ import '../models/session.dart';
|
|||
class StorageService {
|
||||
static const _SESSION_KEY = 'session';
|
||||
static const _LAST_URL_KEY = 'last_url';
|
||||
static const _STORAGE_PERMISSION_DIALOG_IGNORED = 'storage_permission_ignored';
|
||||
|
||||
Future<bool> storeLastUrl(String url) {
|
||||
return _store(_LAST_URL_KEY, url);
|
||||
|
@ -37,6 +38,14 @@ class StorageService {
|
|||
return _remove(_SESSION_KEY);
|
||||
}
|
||||
|
||||
Future<bool> storeStoragePermissionDialogIgnored() {
|
||||
return _store(_STORAGE_PERMISSION_DIALOG_IGNORED, true.toString());
|
||||
}
|
||||
|
||||
Future<bool> hasStoragePermissionDialogIgnored() {
|
||||
return _exists(_STORAGE_PERMISSION_DIALOG_IGNORED);
|
||||
}
|
||||
|
||||
Future<bool> _exists(String key) async {
|
||||
final SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
return prefs.containsKey(key);
|
||||
|
|
|
@ -14,11 +14,11 @@ class StartUpViewModel extends BaseModel {
|
|||
Future handleStartUpLogic() async {
|
||||
setState(ViewState.Busy);
|
||||
setStateMessage(translate('startup.init'));
|
||||
await Future.delayed(Duration(milliseconds: 500));
|
||||
await Future.delayed(Duration(milliseconds: 150));
|
||||
|
||||
setStateMessage(translate('startup.start_services'));
|
||||
await _sessionService.start();
|
||||
await Future.delayed(Duration(milliseconds: 500));
|
||||
await Future.delayed(Duration(milliseconds: 150));
|
||||
|
||||
_navigationService.navigateAndReplaceTo(HomeView.routeName);
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
|
@ -5,6 +6,8 @@ import 'package:flutter/material.dart';
|
|||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_translate/flutter_translate.dart';
|
||||
import 'package:logger/logger.dart';
|
||||
import 'package:path/path.dart';
|
||||
import 'package:receive_sharing_intent/receive_sharing_intent.dart';
|
||||
|
||||
import '../../locator.dart';
|
||||
import '../enums/error_code.dart';
|
||||
|
@ -15,15 +18,19 @@ import '../models/rest/rest_error.dart';
|
|||
import '../models/rest/uploaded_multi_response.dart';
|
||||
import '../models/rest/uploaded_response.dart';
|
||||
import '../services/file_service.dart';
|
||||
import '../services/link_service.dart';
|
||||
import '../util/logger.dart';
|
||||
import 'base_model.dart';
|
||||
|
||||
class UploadModel extends BaseModel {
|
||||
final Logger _logger = getLogger();
|
||||
final FileService _fileService = locator<FileService>();
|
||||
TextEditingController _pasteTextController = TextEditingController();
|
||||
bool createMulti = false;
|
||||
final LinkService _linkService = locator<LinkService>();
|
||||
|
||||
TextEditingController _pasteTextController = TextEditingController();
|
||||
StreamSubscription _intentDataStreamSubscription;
|
||||
|
||||
bool createMulti = false;
|
||||
String fileName;
|
||||
List<PlatformFile> paths;
|
||||
String _extension;
|
||||
|
@ -32,6 +39,68 @@ class UploadModel extends BaseModel {
|
|||
|
||||
TextEditingController get pasteTextController => _pasteTextController;
|
||||
|
||||
void init() {
|
||||
// For sharing images coming from outside the app while the app is in the memory
|
||||
_intentDataStreamSubscription = ReceiveSharingIntent.getMediaStream().listen((List<SharedMediaFile> value) {
|
||||
if (value != null && value.length > 0) {
|
||||
setState(ViewState.Busy);
|
||||
paths = value.map((sharedFile) {
|
||||
return PlatformFile.fromMap({
|
||||
'path': sharedFile.path,
|
||||
'name': basename(sharedFile.path),
|
||||
'size': File(sharedFile.path).lengthSync(),
|
||||
'bytes': null
|
||||
});
|
||||
}).toList();
|
||||
setState(ViewState.Idle);
|
||||
}
|
||||
}, onError: (err) {
|
||||
setState(ViewState.Busy);
|
||||
errorMessage = translate('upload.retrieval_intent');
|
||||
_logger.e('Error while retrieving shared data: $err');
|
||||
setState(ViewState.Idle);
|
||||
});
|
||||
|
||||
// For sharing images coming from outside the app while the app is closed
|
||||
ReceiveSharingIntent.getInitialMedia().then((List<SharedMediaFile> value) {
|
||||
if (value != null && value.length > 0) {
|
||||
setState(ViewState.Busy);
|
||||
paths = value.map((sharedFile) {
|
||||
return PlatformFile.fromMap({
|
||||
'path': sharedFile.path,
|
||||
'name': basename(sharedFile.path),
|
||||
'size': File(sharedFile.path).lengthSync(),
|
||||
'bytes': null
|
||||
});
|
||||
}).toList();
|
||||
setState(ViewState.Idle);
|
||||
}
|
||||
});
|
||||
|
||||
// For sharing or opening urls/text coming from outside the app while the app is in the memory
|
||||
_intentDataStreamSubscription = ReceiveSharingIntent.getTextStream().listen((String value) {
|
||||
if (value != null && value.isNotEmpty) {
|
||||
setState(ViewState.Busy);
|
||||
pasteTextController.text = value;
|
||||
setState(ViewState.Idle);
|
||||
}
|
||||
}, onError: (err) {
|
||||
setState(ViewState.Busy);
|
||||
errorMessage = translate('upload.retrieval_intent');
|
||||
_logger.e('Error while retrieving shared data: $err');
|
||||
setState(ViewState.Idle);
|
||||
});
|
||||
|
||||
// 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) {
|
||||
setState(ViewState.Busy);
|
||||
pasteTextController.text = value;
|
||||
setState(ViewState.Idle);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void toggleCreateMulti() {
|
||||
setState(ViewState.Busy);
|
||||
createMulti = !createMulti;
|
||||
|
@ -140,9 +209,14 @@ class UploadModel extends BaseModel {
|
|||
return null;
|
||||
}
|
||||
|
||||
void openLink(String link) {
|
||||
_linkService.open(link);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_pasteTextController.dispose();
|
||||
_intentDataStreamSubscription.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import 'core/services/dialog_service.dart';
|
|||
import 'core/services/file_service.dart';
|
||||
import 'core/services/link_service.dart';
|
||||
import 'core/services/navigation_service.dart';
|
||||
import 'core/services/permission_service.dart';
|
||||
import 'core/services/session_service.dart';
|
||||
import 'core/services/storage_service.dart';
|
||||
import 'core/viewmodels/about_model.dart';
|
||||
|
@ -33,6 +34,7 @@ void setupLocator() {
|
|||
locator.registerLazySingleton(() => SessionService());
|
||||
locator.registerLazySingleton(() => FileService());
|
||||
locator.registerLazySingleton(() => LinkService());
|
||||
locator.registerLazySingleton(() => PermissionService());
|
||||
|
||||
/// view models
|
||||
locator.registerFactory(() => StartUpViewModel());
|
||||
|
|
|
@ -19,7 +19,7 @@ class AboutView extends StatelessWidget {
|
|||
tag: 'hero',
|
||||
child: CircleAvatar(
|
||||
backgroundColor: Colors.transparent,
|
||||
radius: 96.0,
|
||||
radius: 30.0,
|
||||
child: Image.asset('assets/logo_caption.png'),
|
||||
),
|
||||
);
|
||||
|
@ -39,12 +39,24 @@ class AboutView extends StatelessWidget {
|
|||
shrinkWrap: true,
|
||||
padding: EdgeInsets.only(left: 24.0, right: 24.0),
|
||||
children: <Widget>[
|
||||
UIHelper.verticalSpaceMedium(),
|
||||
Center(child: logo),
|
||||
UIHelper.verticalSpaceMedium(),
|
||||
Center(
|
||||
child: Text(
|
||||
translate(('about.description')),
|
||||
)),
|
||||
UIHelper.verticalSpaceMedium(),
|
||||
Center(
|
||||
child: Text(
|
||||
translate(('about.faq_headline')),
|
||||
style: subHeaderStyle,
|
||||
)),
|
||||
Center(
|
||||
child: Text(
|
||||
translate(('about.faq')),
|
||||
)),
|
||||
UIHelper.verticalSpaceMedium(),
|
||||
Center(
|
||||
child: Text(
|
||||
translate(('about.contact_us')),
|
||||
|
|
|
@ -89,11 +89,15 @@ class HistoryView extends StatelessWidget {
|
|||
title: Text(paste.id),
|
||||
subtitle: Text(translate('history.id')),
|
||||
);
|
||||
var mimeTypeWidget = ListTile(
|
||||
title: Text(paste.mimetype),
|
||||
subtitle: Text(translate('history.mimetype')),
|
||||
);
|
||||
|
||||
widgets.add(titleWidget);
|
||||
|
||||
widgets.add(fileSizeWidget);
|
||||
widgets.add(idWidget);
|
||||
widgets.add(fileSizeWidget);
|
||||
widgets.add(mimeTypeWidget);
|
||||
} else {
|
||||
paste.items.forEach((element) {
|
||||
widgets.add(ListTile(
|
||||
|
@ -110,18 +114,19 @@ class HistoryView extends StatelessWidget {
|
|||
|
||||
var expandable = ExpandableTheme(
|
||||
data: ExpandableThemeData(
|
||||
iconColor: Colors.blue,
|
||||
tapHeaderToExpand: true,
|
||||
iconPlacement: ExpandablePanelIconPlacement.right,
|
||||
headerAlignment: ExpandablePanelHeaderAlignment.center,
|
||||
hasIcon: true,
|
||||
),
|
||||
iconColor: Colors.blue,
|
||||
tapHeaderToExpand: true),
|
||||
child: ExpandablePanel(
|
||||
header: Text(
|
||||
header: InkWell(
|
||||
onLongPress: () => model.deletePaste(paste.id),
|
||||
child: Text(
|
||||
paste.id,
|
||||
style: TextStyle(color: Colors.blue),
|
||||
textAlign: TextAlign.left,
|
||||
style: TextStyle(color: primaryAccentColor),
|
||||
),
|
||||
)),
|
||||
expanded: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: widgets,
|
||||
|
@ -132,12 +137,15 @@ class HistoryView extends StatelessWidget {
|
|||
cards.add(Card(
|
||||
child: ListTile(
|
||||
title: expandable,
|
||||
trailing: IconButton(
|
||||
trailing: Wrap(children: [
|
||||
openInBrowserButton,
|
||||
IconButton(
|
||||
icon: Icon(Icons.share, color: Colors.blue, textDirection: TextDirection.ltr),
|
||||
onPressed: () {
|
||||
return Share.share(fullPasteUrl);
|
||||
}),
|
||||
subtitle: Text(!paste.isMulti ? paste.filename : ''),
|
||||
})
|
||||
]),
|
||||
subtitle: Text(!paste.isMulti ? paste.filename : '', style: TextStyle(fontStyle: FontStyle.italic)),
|
||||
),
|
||||
));
|
||||
});
|
||||
|
|
|
@ -19,6 +19,7 @@ class UploadView extends StatelessWidget {
|
|||
var url = Provider.of<Session>(context).url;
|
||||
|
||||
return BaseView<UploadModel>(
|
||||
onModelReady: (model) => model.init(),
|
||||
builder: (context, model, child) => Scaffold(
|
||||
appBar: MyAppBar(title: Text(translate('titles.upload'))),
|
||||
backgroundColor: backgroundColor,
|
||||
|
|
|
@ -37,6 +37,8 @@ dependencies:
|
|||
share: 0.6.5+4
|
||||
file_picker: 2.1.6
|
||||
clipboard: 0.1.2+8
|
||||
receive_sharing_intent: 1.4.3
|
||||
permission_handler: 5.0.1+1
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
|
Loading…
Reference in a new issue