Added gesture detection for tab bar, disable upload when no files have been attached or upload text input is empty, added version information to about view, minor refactor regarding state management, release 1.3.1+10
This commit is contained in:
parent
230de7fe40
commit
06eb990eea
22 changed files with 490 additions and 320 deletions
|
@ -1,5 +1,11 @@
|
||||||
# CHANGELOG
|
# CHANGELOG
|
||||||
|
|
||||||
|
## 1.3.1+10
|
||||||
|
* Added gesture detection for tab bar
|
||||||
|
* Disable upload when no files have been attached or upload text input is empty
|
||||||
|
* Added version information to about view
|
||||||
|
* Minor refactor regarding state management
|
||||||
|
|
||||||
## 1.3.0+9
|
## 1.3.0+9
|
||||||
* Allow API key login
|
* Allow API key login
|
||||||
* Revamp profile view
|
* Revamp profile view
|
||||||
|
|
|
@ -94,6 +94,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"about": {
|
"about": {
|
||||||
|
"versions": "{appName} ({packageName}) {version}+{buildNumber}",
|
||||||
"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.",
|
"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_headline": "F.A.Q",
|
||||||
"faq": "- How do I login?\nInsert your instance URL and valid credentials you also use in the web interface of FileBin.\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.",
|
"faq": "- How do I login?\nInsert your instance URL and valid credentials you also use in the web interface of FileBin.\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.",
|
||||||
|
|
50
lib/app.dart
50
lib/app.dart
|
@ -5,6 +5,7 @@ import 'package:flutter_translate/localized_app.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
import 'core/enums/refresh_event.dart';
|
import 'core/enums/refresh_event.dart';
|
||||||
|
import 'core/enums/swipe_event.dart';
|
||||||
import 'core/manager/dialog_manager.dart';
|
import 'core/manager/dialog_manager.dart';
|
||||||
import 'core/manager/lifecycle_manager.dart';
|
import 'core/manager/lifecycle_manager.dart';
|
||||||
import 'core/models/session.dart';
|
import 'core/models/session.dart';
|
||||||
|
@ -12,6 +13,7 @@ import 'core/services/dialog_service.dart';
|
||||||
import 'core/services/navigation_service.dart';
|
import 'core/services/navigation_service.dart';
|
||||||
import 'core/services/refresh_service.dart';
|
import 'core/services/refresh_service.dart';
|
||||||
import 'core/services/session_service.dart';
|
import 'core/services/session_service.dart';
|
||||||
|
import 'core/services/swipe_service.dart';
|
||||||
import 'locator.dart';
|
import 'locator.dart';
|
||||||
import 'ui/app_router.dart';
|
import 'ui/app_router.dart';
|
||||||
import 'ui/shared/app_colors.dart';
|
import 'ui/shared/app_colors.dart';
|
||||||
|
@ -24,27 +26,33 @@ class MyApp extends StatelessWidget {
|
||||||
|
|
||||||
return LocalizationProvider(
|
return LocalizationProvider(
|
||||||
state: LocalizationProvider.of(context).state,
|
state: LocalizationProvider.of(context).state,
|
||||||
child: StreamProvider<RefreshEvent>(
|
child: StreamProvider<SwipeEvent>(
|
||||||
initialData: null,
|
initialData: null,
|
||||||
create: (context) => locator<RefreshService>().refreshHistoryController.stream,
|
create: (context) => locator<SwipeService>().swipeEventController.stream,
|
||||||
child: StreamProvider<Session>(
|
child: StreamProvider<RefreshEvent>(
|
||||||
initialData: Session.initial(),
|
initialData: null,
|
||||||
create: (context) => locator<SessionService>().sessionController.stream,
|
create: (context) => locator<RefreshService>().refreshEventController.stream,
|
||||||
child: LifeCycleManager(
|
child: StreamProvider<Session>(
|
||||||
child: MaterialApp(
|
initialData: Session.initial(),
|
||||||
title: translate('app.title'),
|
create: (context) => locator<SessionService>().sessionController.stream,
|
||||||
builder: (context, child) => Navigator(
|
child: LifeCycleManager(
|
||||||
key: locator<DialogService>().dialogNavigationKey,
|
child: MaterialApp(
|
||||||
onGenerateRoute: (settings) => MaterialPageRoute(builder: (context) => DialogManager(child: child)),
|
title: translate('app.title'),
|
||||||
),
|
builder: (context, child) => Navigator(
|
||||||
theme: ThemeData(
|
key: locator<DialogService>().dialogNavigationKey,
|
||||||
brightness: Brightness.light, primarySwatch: primaryAccentColor, primaryColor: primaryAccentColor),
|
onGenerateRoute: (settings) =>
|
||||||
onGenerateRoute: AppRouter.generateRoute,
|
MaterialPageRoute(builder: (context) => DialogManager(child: child)),
|
||||||
navigatorKey: locator<NavigationService>().navigationKey,
|
),
|
||||||
home: StartUpView(),
|
theme: ThemeData(
|
||||||
supportedLocales: localizationDelegate.supportedLocales,
|
brightness: Brightness.light,
|
||||||
locale: localizationDelegate.currentLocale,
|
primarySwatch: primaryAccentColor,
|
||||||
)),
|
primaryColor: primaryAccentColor),
|
||||||
)));
|
onGenerateRoute: AppRouter.generateRoute,
|
||||||
|
navigatorKey: locator<NavigationService>().navigationKey,
|
||||||
|
home: StartUpView(),
|
||||||
|
supportedLocales: localizationDelegate.supportedLocales,
|
||||||
|
locale: localizationDelegate.currentLocale,
|
||||||
|
)),
|
||||||
|
))));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
1
lib/core/enums/swipe_event.dart
Normal file
1
lib/core/enums/swipe_event.dart
Normal file
|
@ -0,0 +1 @@
|
||||||
|
enum SwipeEvent { Left, Right }
|
|
@ -3,11 +3,11 @@ import 'dart:async';
|
||||||
import '../enums/refresh_event.dart';
|
import '../enums/refresh_event.dart';
|
||||||
|
|
||||||
class RefreshService {
|
class RefreshService {
|
||||||
StreamController<RefreshEvent> refreshHistoryController = StreamController<RefreshEvent>.broadcast();
|
StreamController<RefreshEvent> refreshEventController = StreamController<RefreshEvent>.broadcast();
|
||||||
|
|
||||||
void addEvent(RefreshEvent event) {
|
void addEvent(RefreshEvent event) {
|
||||||
if (refreshHistoryController.hasListener) {
|
if (refreshEventController.hasListener) {
|
||||||
refreshHistoryController.add(event);
|
refreshEventController.add(event);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
13
lib/core/services/swipe_service.dart
Normal file
13
lib/core/services/swipe_service.dart
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import '../enums/swipe_event.dart';
|
||||||
|
|
||||||
|
class SwipeService {
|
||||||
|
StreamController<SwipeEvent> swipeEventController = StreamController<SwipeEvent>.broadcast();
|
||||||
|
|
||||||
|
void addEvent(SwipeEvent event) {
|
||||||
|
if (swipeEventController.hasListener) {
|
||||||
|
swipeEventController.add(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,10 +1,31 @@
|
||||||
|
import 'package:package_info/package_info.dart';
|
||||||
|
|
||||||
import '../../core/services/link_service.dart';
|
import '../../core/services/link_service.dart';
|
||||||
import '../../locator.dart';
|
import '../../locator.dart';
|
||||||
|
import '../enums/viewstate.dart';
|
||||||
import 'base_model.dart';
|
import 'base_model.dart';
|
||||||
|
|
||||||
class AboutModel extends BaseModel {
|
class AboutModel extends BaseModel {
|
||||||
final LinkService _linkService = locator<LinkService>();
|
final LinkService _linkService = locator<LinkService>();
|
||||||
|
|
||||||
|
PackageInfo packageInfo = PackageInfo(
|
||||||
|
appName: 'Unknown',
|
||||||
|
packageName: 'Unknown',
|
||||||
|
version: 'Unknown',
|
||||||
|
buildNumber: 'Unknown',
|
||||||
|
);
|
||||||
|
|
||||||
|
void init() async {
|
||||||
|
await _initPackageInfo();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _initPackageInfo() async {
|
||||||
|
setStateView(ViewState.Busy);
|
||||||
|
final PackageInfo info = await PackageInfo.fromPlatform();
|
||||||
|
packageInfo = info;
|
||||||
|
setStateView(ViewState.Idle);
|
||||||
|
}
|
||||||
|
|
||||||
void openLink(String link) {
|
void openLink(String link) {
|
||||||
_linkService.open(link);
|
_linkService.open(link);
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,33 +5,48 @@ import '../../core/util/logger.dart';
|
||||||
import '../enums/viewstate.dart';
|
import '../enums/viewstate.dart';
|
||||||
|
|
||||||
class BaseModel extends ChangeNotifier {
|
class BaseModel extends ChangeNotifier {
|
||||||
|
static const String STATE_VIEW = 'viewState';
|
||||||
|
static const String STATE_MESSAGE = 'viewMessage';
|
||||||
|
|
||||||
final Logger _logger = getLogger();
|
final Logger _logger = getLogger();
|
||||||
|
|
||||||
bool _isDisposed = false;
|
bool _isDisposed = false;
|
||||||
|
|
||||||
ViewState _state = ViewState.Idle;
|
Map<String, Object> _stateMap = {STATE_VIEW: ViewState.Idle, STATE_MESSAGE: null};
|
||||||
String _stateMessage;
|
|
||||||
|
|
||||||
ViewState get state => _state;
|
ViewState get state => _stateMap[STATE_VIEW];
|
||||||
|
String get stateMessage => _stateMap[STATE_MESSAGE];
|
||||||
|
|
||||||
String get stateMessage => _stateMessage;
|
void setStateValue(String key, Object stateValue) {
|
||||||
|
if (_stateMap.containsKey(key)) {
|
||||||
|
_stateMap.update(key, (value) => stateValue);
|
||||||
|
} else {
|
||||||
|
_stateMap.putIfAbsent(key, () => stateValue);
|
||||||
|
}
|
||||||
|
|
||||||
void setState(ViewState viewState) {
|
|
||||||
_state = viewState;
|
|
||||||
if (!_isDisposed) {
|
if (!_isDisposed) {
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
_logger.d("Notified state change '${viewState.toString()}'");
|
_logger.d("Notified state value update '($key, ${stateValue.toString()})'");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void setStateMessage(String stateMessage) {
|
void removeStateValue(String key) {
|
||||||
_stateMessage = stateMessage;
|
_stateMap.remove(key);
|
||||||
|
|
||||||
if (!_isDisposed) {
|
if (!_isDisposed) {
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
_logger.d("Notified state message change '$stateMessage'");
|
_logger.d("Notified state removal of '$key'");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void setStateView(ViewState stateView) {
|
||||||
|
setStateValue(STATE_VIEW, stateView);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setStateMessage(String stateMessage) {
|
||||||
|
setStateValue(STATE_MESSAGE, stateMessage);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
super.dispose();
|
super.dispose();
|
||||||
|
|
|
@ -34,7 +34,7 @@ class HistoryModel extends BaseModel {
|
||||||
String errorMessage;
|
String errorMessage;
|
||||||
|
|
||||||
void init() {
|
void init() {
|
||||||
_refreshTriggerSubscription = _refreshService.refreshHistoryController.stream.listen((event) {
|
_refreshTriggerSubscription = _refreshService.refreshEventController.stream.listen((event) {
|
||||||
if (event == RefreshEvent.RefreshHistory) {
|
if (event == RefreshEvent.RefreshHistory) {
|
||||||
_logger.d('History needs a refresh');
|
_logger.d('History needs a refresh');
|
||||||
getHistory();
|
getHistory();
|
||||||
|
@ -43,7 +43,7 @@ class HistoryModel extends BaseModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future getHistory() async {
|
Future getHistory() async {
|
||||||
setState(ViewState.Busy);
|
setStateView(ViewState.Busy);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
pastes.clear();
|
pastes.clear();
|
||||||
|
@ -103,13 +103,13 @@ class HistoryModel extends BaseModel {
|
||||||
errorMessage = translate('api.socket_timeout');
|
errorMessage = translate('api.socket_timeout');
|
||||||
} else {
|
} else {
|
||||||
errorMessage = translate('app.unknown_error');
|
errorMessage = translate('app.unknown_error');
|
||||||
setState(ViewState.Idle);
|
setStateView(ViewState.Idle);
|
||||||
_logger.e('An unknown error occurred', e);
|
_logger.e('An unknown error occurred', e);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setState(ViewState.Idle);
|
setStateView(ViewState.Idle);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future deletePaste(String id) async {
|
Future deletePaste(String id) async {
|
||||||
|
@ -123,7 +123,7 @@ class HistoryModel extends BaseModel {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setState(ViewState.Busy);
|
setStateView(ViewState.Busy);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await _fileService.deletePaste(id);
|
await _fileService.deletePaste(id);
|
||||||
|
@ -149,13 +149,13 @@ class HistoryModel extends BaseModel {
|
||||||
errorMessage = translate('api.socket_timeout');
|
errorMessage = translate('api.socket_timeout');
|
||||||
} else {
|
} else {
|
||||||
errorMessage = translate('app.unknown_error');
|
errorMessage = translate('app.unknown_error');
|
||||||
setState(ViewState.Idle);
|
setStateView(ViewState.Idle);
|
||||||
_logger.e('An unknown error occurred', e);
|
_logger.e('An unknown error occurred', e);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setState(ViewState.Idle);
|
setStateView(ViewState.Idle);
|
||||||
}
|
}
|
||||||
|
|
||||||
void openLink(String link) {
|
void openLink(String link) {
|
||||||
|
|
|
@ -43,23 +43,23 @@ class LoginModel extends BaseModel {
|
||||||
String errorMessage;
|
String errorMessage;
|
||||||
|
|
||||||
void toggleLoginMethod() {
|
void toggleLoginMethod() {
|
||||||
setState(ViewState.Busy);
|
setStateView(ViewState.Busy);
|
||||||
useCredentialsLogin = !useCredentialsLogin;
|
useCredentialsLogin = !useCredentialsLogin;
|
||||||
setState(ViewState.Idle);
|
setStateView(ViewState.Idle);
|
||||||
}
|
}
|
||||||
|
|
||||||
void init() async {
|
void init() async {
|
||||||
bool hasLastUrl = await _storageService.hasLastUrl();
|
bool hasLastUrl = await _storageService.hasLastUrl();
|
||||||
|
|
||||||
if (hasLastUrl) {
|
if (hasLastUrl) {
|
||||||
setState(ViewState.Busy);
|
setStateView(ViewState.Busy);
|
||||||
var s = await _storageService.retrieveLastUrl();
|
var s = await _storageService.retrieveLastUrl();
|
||||||
|
|
||||||
if (s.isNotEmpty) {
|
if (s.isNotEmpty) {
|
||||||
_uriController = new TextEditingController(text: s);
|
_uriController = new TextEditingController(text: s);
|
||||||
}
|
}
|
||||||
|
|
||||||
setState(ViewState.Idle);
|
setStateView(ViewState.Idle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -69,45 +69,45 @@ class LoginModel extends BaseModel {
|
||||||
var password = passwordController.text;
|
var password = passwordController.text;
|
||||||
var apiKey = apiKeyController.text;
|
var apiKey = apiKeyController.text;
|
||||||
|
|
||||||
setState(ViewState.Busy);
|
setStateView(ViewState.Busy);
|
||||||
url = trim(url);
|
url = trim(url);
|
||||||
username = trim(username);
|
username = trim(username);
|
||||||
|
|
||||||
if (url.isEmpty) {
|
if (url.isEmpty) {
|
||||||
errorMessage = translate('login.errors.empty_url');
|
errorMessage = translate('login.errors.empty_url');
|
||||||
setState(ViewState.Idle);
|
setStateView(ViewState.Idle);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!url.contains("https://") && !url.contains("http://")) {
|
if (!url.contains("https://") && !url.contains("http://")) {
|
||||||
errorMessage = translate('login.errors.no_protocol');
|
errorMessage = translate('login.errors.no_protocol');
|
||||||
setState(ViewState.Idle);
|
setStateView(ViewState.Idle);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool validUri = Uri.parse(url).isAbsolute;
|
bool validUri = Uri.parse(url).isAbsolute;
|
||||||
if (!validUri || !isURL(url)) {
|
if (!validUri || !isURL(url)) {
|
||||||
errorMessage = translate('login.errors.invalid_url');
|
errorMessage = translate('login.errors.invalid_url');
|
||||||
setState(ViewState.Idle);
|
setStateView(ViewState.Idle);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (useCredentialsLogin) {
|
if (useCredentialsLogin) {
|
||||||
if (username.isEmpty) {
|
if (username.isEmpty) {
|
||||||
errorMessage = translate('login.errors.empty_username');
|
errorMessage = translate('login.errors.empty_username');
|
||||||
setState(ViewState.Idle);
|
setStateView(ViewState.Idle);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (password.isEmpty) {
|
if (password.isEmpty) {
|
||||||
errorMessage = translate('login.errors.empty_password');
|
errorMessage = translate('login.errors.empty_password');
|
||||||
setState(ViewState.Idle);
|
setStateView(ViewState.Idle);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (apiKey.isEmpty) {
|
if (apiKey.isEmpty) {
|
||||||
errorMessage = translate('login.errors.empty_apikey');
|
errorMessage = translate('login.errors.empty_apikey');
|
||||||
setState(ViewState.Idle);
|
setStateView(ViewState.Idle);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -147,7 +147,7 @@ class LoginModel extends BaseModel {
|
||||||
} else {
|
} else {
|
||||||
errorMessage = translate('app.unknown_error');
|
errorMessage = translate('app.unknown_error');
|
||||||
_sessionService.logout();
|
_sessionService.logout();
|
||||||
setState(ViewState.Idle);
|
setStateView(ViewState.Idle);
|
||||||
_logger.e('An unknown error occurred', e);
|
_logger.e('An unknown error occurred', e);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
@ -156,7 +156,7 @@ class LoginModel extends BaseModel {
|
||||||
_sessionService.logout();
|
_sessionService.logout();
|
||||||
}
|
}
|
||||||
|
|
||||||
setState(ViewState.Idle);
|
setStateView(ViewState.Idle);
|
||||||
return success;
|
return success;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -67,7 +67,7 @@ class ProfileModel extends BaseModel {
|
||||||
} else {
|
} else {
|
||||||
errorMessage = translate('app.unknown_error');
|
errorMessage = translate('app.unknown_error');
|
||||||
_sessionService.logout();
|
_sessionService.logout();
|
||||||
setState(ViewState.Idle);
|
setStateView(ViewState.Idle);
|
||||||
_logger.e('An unknown error occurred', e);
|
_logger.e('An unknown error occurred', e);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,7 @@ class StartUpViewModel extends BaseModel {
|
||||||
final NavigationService _navigationService = locator<NavigationService>();
|
final NavigationService _navigationService = locator<NavigationService>();
|
||||||
|
|
||||||
Future handleStartUpLogic() async {
|
Future handleStartUpLogic() async {
|
||||||
setState(ViewState.Busy);
|
setStateView(ViewState.Busy);
|
||||||
setStateMessage(translate('startup.init'));
|
setStateMessage(translate('startup.init'));
|
||||||
await Future.delayed(Duration(milliseconds: 150));
|
await Future.delayed(Duration(milliseconds: 150));
|
||||||
|
|
||||||
|
@ -22,6 +22,6 @@ class StartUpViewModel extends BaseModel {
|
||||||
|
|
||||||
_navigationService.navigateAndReplaceTo(HomeView.routeName);
|
_navigationService.navigateAndReplaceTo(HomeView.routeName);
|
||||||
|
|
||||||
setState(ViewState.Idle);
|
setStateView(ViewState.Idle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,6 +31,8 @@ class UploadModel extends BaseModel {
|
||||||
final RefreshService _refreshService = locator<RefreshService>();
|
final RefreshService _refreshService = locator<RefreshService>();
|
||||||
|
|
||||||
TextEditingController _pasteTextController = TextEditingController();
|
TextEditingController _pasteTextController = TextEditingController();
|
||||||
|
bool pasteTextTouched = false;
|
||||||
|
|
||||||
StreamSubscription _intentDataStreamSubscription;
|
StreamSubscription _intentDataStreamSubscription;
|
||||||
|
|
||||||
bool createMulti = false;
|
bool createMulti = false;
|
||||||
|
@ -43,10 +45,15 @@ class UploadModel extends BaseModel {
|
||||||
TextEditingController get pasteTextController => _pasteTextController;
|
TextEditingController get pasteTextController => _pasteTextController;
|
||||||
|
|
||||||
void init() {
|
void init() {
|
||||||
|
_pasteTextController.addListener(() {
|
||||||
|
pasteTextTouched = pasteTextController.text.isNotEmpty;
|
||||||
|
setStateValue("PASTE_TEXT_TOUCHED", pasteTextTouched);
|
||||||
|
});
|
||||||
|
|
||||||
// For sharing images coming from outside the app while the app is in the memory
|
// For sharing images coming from outside the app while the app is in the memory
|
||||||
_intentDataStreamSubscription = ReceiveSharingIntent.getMediaStream().listen((List<SharedMediaFile> value) {
|
_intentDataStreamSubscription = ReceiveSharingIntent.getMediaStream().listen((List<SharedMediaFile> value) {
|
||||||
if (value != null && value.length > 0) {
|
if (value != null && value.length > 0) {
|
||||||
setState(ViewState.Busy);
|
setStateView(ViewState.Busy);
|
||||||
paths = value.map((sharedFile) {
|
paths = value.map((sharedFile) {
|
||||||
return PlatformFile.fromMap({
|
return PlatformFile.fromMap({
|
||||||
'path': sharedFile.path,
|
'path': sharedFile.path,
|
||||||
|
@ -55,19 +62,19 @@ class UploadModel extends BaseModel {
|
||||||
'bytes': null
|
'bytes': null
|
||||||
});
|
});
|
||||||
}).toList();
|
}).toList();
|
||||||
setState(ViewState.Idle);
|
setStateView(ViewState.Idle);
|
||||||
}
|
}
|
||||||
}, onError: (err) {
|
}, onError: (err) {
|
||||||
setState(ViewState.Busy);
|
setStateView(ViewState.Busy);
|
||||||
errorMessage = translate('upload.retrieval_intent');
|
errorMessage = translate('upload.retrieval_intent');
|
||||||
_logger.e('Error while retrieving shared data: $err');
|
_logger.e('Error while retrieving shared data: $err');
|
||||||
setState(ViewState.Idle);
|
setStateView(ViewState.Idle);
|
||||||
});
|
});
|
||||||
|
|
||||||
// For sharing images coming from outside the app while the app is closed
|
// For sharing images coming from outside the app while the app is closed
|
||||||
ReceiveSharingIntent.getInitialMedia().then((List<SharedMediaFile> value) {
|
ReceiveSharingIntent.getInitialMedia().then((List<SharedMediaFile> value) {
|
||||||
if (value != null && value.length > 0) {
|
if (value != null && value.length > 0) {
|
||||||
setState(ViewState.Busy);
|
setStateView(ViewState.Busy);
|
||||||
paths = value.map((sharedFile) {
|
paths = value.map((sharedFile) {
|
||||||
return PlatformFile.fromMap({
|
return PlatformFile.fromMap({
|
||||||
'path': sharedFile.path,
|
'path': sharedFile.path,
|
||||||
|
@ -76,42 +83,42 @@ class UploadModel extends BaseModel {
|
||||||
'bytes': null
|
'bytes': null
|
||||||
});
|
});
|
||||||
}).toList();
|
}).toList();
|
||||||
setState(ViewState.Idle);
|
setStateView(ViewState.Idle);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// For sharing or opening urls/text coming from outside the app while the app is in the memory
|
// For sharing or opening urls/text coming from outside the app while the app is in the memory
|
||||||
_intentDataStreamSubscription = ReceiveSharingIntent.getTextStream().listen((String value) {
|
_intentDataStreamSubscription = ReceiveSharingIntent.getTextStream().listen((String value) {
|
||||||
if (value != null && value.isNotEmpty) {
|
if (value != null && value.isNotEmpty) {
|
||||||
setState(ViewState.Busy);
|
setStateView(ViewState.Busy);
|
||||||
pasteTextController.text = value;
|
pasteTextController.text = value;
|
||||||
setState(ViewState.Idle);
|
setStateView(ViewState.Idle);
|
||||||
}
|
}
|
||||||
}, onError: (err) {
|
}, onError: (err) {
|
||||||
setState(ViewState.Busy);
|
setStateView(ViewState.Busy);
|
||||||
errorMessage = translate('upload.retrieval_intent');
|
errorMessage = translate('upload.retrieval_intent');
|
||||||
_logger.e('Error while retrieving shared data: $err');
|
_logger.e('Error while retrieving shared data: $err');
|
||||||
setState(ViewState.Idle);
|
setStateView(ViewState.Idle);
|
||||||
});
|
});
|
||||||
|
|
||||||
// For sharing or opening urls/text coming from outside the app while the app is closed
|
// For sharing or opening urls/text coming from outside the app while the app is closed
|
||||||
ReceiveSharingIntent.getInitialText().then((String value) {
|
ReceiveSharingIntent.getInitialText().then((String value) {
|
||||||
if (value != null && value.isNotEmpty) {
|
if (value != null && value.isNotEmpty) {
|
||||||
setState(ViewState.Busy);
|
setStateView(ViewState.Busy);
|
||||||
pasteTextController.text = value;
|
pasteTextController.text = value;
|
||||||
setState(ViewState.Idle);
|
setStateView(ViewState.Idle);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void toggleCreateMulti() {
|
void toggleCreateMulti() {
|
||||||
setState(ViewState.Busy);
|
setStateView(ViewState.Busy);
|
||||||
createMulti = !createMulti;
|
createMulti = !createMulti;
|
||||||
setState(ViewState.Idle);
|
setStateView(ViewState.Idle);
|
||||||
}
|
}
|
||||||
|
|
||||||
void openFileExplorer() async {
|
void openFileExplorer() async {
|
||||||
setState(ViewState.Busy);
|
setStateView(ViewState.Busy);
|
||||||
setStateMessage(translate('upload.file_explorer_open'));
|
setStateMessage(translate('upload.file_explorer_open'));
|
||||||
loadingPath = true;
|
loadingPath = true;
|
||||||
|
|
||||||
|
@ -134,20 +141,20 @@ class UploadModel extends BaseModel {
|
||||||
fileName = paths != null ? paths.map((e) => e.name).toString() : '...';
|
fileName = paths != null ? paths.map((e) => e.name).toString() : '...';
|
||||||
|
|
||||||
setStateMessage(null);
|
setStateMessage(null);
|
||||||
setState(ViewState.Idle);
|
setStateView(ViewState.Idle);
|
||||||
}
|
}
|
||||||
|
|
||||||
void clearCachedFiles() async {
|
void clearCachedFiles() async {
|
||||||
setState(ViewState.Busy);
|
setStateView(ViewState.Busy);
|
||||||
await FilePicker.platform.clearTemporaryFiles();
|
await FilePicker.platform.clearTemporaryFiles();
|
||||||
paths = null;
|
paths = null;
|
||||||
fileName = null;
|
fileName = null;
|
||||||
errorMessage = null;
|
errorMessage = null;
|
||||||
setState(ViewState.Idle);
|
setStateView(ViewState.Idle);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Map<String, bool>> upload() async {
|
Future<Map<String, bool>> upload() async {
|
||||||
setState(ViewState.Busy);
|
setStateView(ViewState.Busy);
|
||||||
setStateMessage(translate('upload.uploading_now'));
|
setStateMessage(translate('upload.uploading_now'));
|
||||||
|
|
||||||
Map<String, bool> uploadedPasteIds = new Map();
|
Map<String, bool> uploadedPasteIds = new Map();
|
||||||
|
@ -204,14 +211,14 @@ class UploadModel extends BaseModel {
|
||||||
} else {
|
} else {
|
||||||
errorMessage = translate('app.unknown_error');
|
errorMessage = translate('app.unknown_error');
|
||||||
setStateMessage(null);
|
setStateMessage(null);
|
||||||
setState(ViewState.Idle);
|
setStateView(ViewState.Idle);
|
||||||
_logger.e('An unknown error occurred', e);
|
_logger.e('An unknown error occurred', e);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setStateMessage(null);
|
setStateMessage(null);
|
||||||
setState(ViewState.Idle);
|
setStateView(ViewState.Idle);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,7 @@ import 'core/services/permission_service.dart';
|
||||||
import 'core/services/refresh_service.dart';
|
import 'core/services/refresh_service.dart';
|
||||||
import 'core/services/session_service.dart';
|
import 'core/services/session_service.dart';
|
||||||
import 'core/services/storage_service.dart';
|
import 'core/services/storage_service.dart';
|
||||||
|
import 'core/services/swipe_service.dart';
|
||||||
import 'core/services/user_service.dart';
|
import 'core/services/user_service.dart';
|
||||||
import 'core/viewmodels/about_model.dart';
|
import 'core/viewmodels/about_model.dart';
|
||||||
import 'core/viewmodels/history_model.dart';
|
import 'core/viewmodels/history_model.dart';
|
||||||
|
@ -41,6 +42,7 @@ void setupLocator() {
|
||||||
locator.registerLazySingleton(() => LinkService());
|
locator.registerLazySingleton(() => LinkService());
|
||||||
locator.registerLazySingleton(() => PermissionService());
|
locator.registerLazySingleton(() => PermissionService());
|
||||||
locator.registerLazySingleton(() => RefreshService());
|
locator.registerLazySingleton(() => RefreshService());
|
||||||
|
locator.registerLazySingleton(() => SwipeService());
|
||||||
|
|
||||||
/// view models
|
/// view models
|
||||||
locator.registerFactory(() => StartUpViewModel());
|
locator.registerFactory(() => StartUpViewModel());
|
||||||
|
|
|
@ -25,6 +25,7 @@ class AboutView extends StatelessWidget {
|
||||||
);
|
);
|
||||||
|
|
||||||
return BaseView<AboutModel>(
|
return BaseView<AboutModel>(
|
||||||
|
onModelReady: (model) => model.init(),
|
||||||
builder: (context, model, child) => Scaffold(
|
builder: (context, model, child) => Scaffold(
|
||||||
appBar: MyAppBar(
|
appBar: MyAppBar(
|
||||||
title: Text(translate('titles.about')),
|
title: Text(translate('titles.about')),
|
||||||
|
@ -44,12 +45,22 @@ class AboutView extends StatelessWidget {
|
||||||
UIHelper.verticalSpaceMedium(),
|
UIHelper.verticalSpaceMedium(),
|
||||||
Center(
|
Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
translate(('about.description')),
|
translate('about.versions', args: {
|
||||||
|
'appName': model.packageInfo.appName,
|
||||||
|
'packageName': model.packageInfo.packageName,
|
||||||
|
'version': model.packageInfo.version,
|
||||||
|
'buildNumber': model.packageInfo.buildNumber
|
||||||
|
}),
|
||||||
)),
|
)),
|
||||||
UIHelper.verticalSpaceMedium(),
|
UIHelper.verticalSpaceMedium(),
|
||||||
Center(
|
Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
translate(('about.faq_headline')),
|
translate('about.description'),
|
||||||
|
)),
|
||||||
|
UIHelper.verticalSpaceMedium(),
|
||||||
|
Center(
|
||||||
|
child: Text(
|
||||||
|
translate('about.faq_headline'),
|
||||||
style: subHeaderStyle,
|
style: subHeaderStyle,
|
||||||
)),
|
)),
|
||||||
Center(
|
Center(
|
||||||
|
@ -59,7 +70,7 @@ class AboutView extends StatelessWidget {
|
||||||
UIHelper.verticalSpaceMedium(),
|
UIHelper.verticalSpaceMedium(),
|
||||||
Center(
|
Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
translate(('about.contact_us')),
|
translate('about.contact_us'),
|
||||||
style: subHeaderStyle,
|
style: subHeaderStyle,
|
||||||
)),
|
)),
|
||||||
UIHelper.verticalSpaceSmall(),
|
UIHelper.verticalSpaceSmall(),
|
||||||
|
|
|
@ -12,6 +12,7 @@ import '../../core/viewmodels/history_model.dart';
|
||||||
import '../../ui/widgets/centered_error_row.dart';
|
import '../../ui/widgets/centered_error_row.dart';
|
||||||
import '../shared/app_colors.dart';
|
import '../shared/app_colors.dart';
|
||||||
import '../widgets/my_appbar.dart';
|
import '../widgets/my_appbar.dart';
|
||||||
|
import '../widgets/swipe_navigation.dart';
|
||||||
import 'base_view.dart';
|
import 'base_view.dart';
|
||||||
|
|
||||||
class HistoryView extends StatelessWidget {
|
class HistoryView extends StatelessWidget {
|
||||||
|
@ -19,8 +20,6 @@ class HistoryView extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
var url = Provider.of<Session>(context).url;
|
|
||||||
|
|
||||||
return BaseView<HistoryModel>(
|
return BaseView<HistoryModel>(
|
||||||
onModelReady: (model) {
|
onModelReady: (model) {
|
||||||
model.init();
|
model.init();
|
||||||
|
@ -29,22 +28,28 @@ class HistoryView extends StatelessWidget {
|
||||||
builder: (context, model, child) => Scaffold(
|
builder: (context, model, child) => Scaffold(
|
||||||
appBar: MyAppBar(title: Text(translate('titles.history'))),
|
appBar: MyAppBar(title: Text(translate('titles.history'))),
|
||||||
backgroundColor: backgroundColor,
|
backgroundColor: backgroundColor,
|
||||||
body: model.state == ViewState.Busy
|
body: SwipeNavigation(child: _render(model, context))),
|
||||||
? Center(child: CircularProgressIndicator())
|
|
||||||
: (model.errorMessage == null
|
|
||||||
? Container(
|
|
||||||
padding: EdgeInsets.all(0),
|
|
||||||
child: RefreshIndicator(onRefresh: () => model.getHistory(), child: _render(model, url, context)))
|
|
||||||
: Container(
|
|
||||||
padding: EdgeInsets.all(25),
|
|
||||||
child: CenteredErrorRow(
|
|
||||||
model.errorMessage,
|
|
||||||
retryCallback: () => model.getHistory(),
|
|
||||||
)))),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _render(HistoryModel model, String url, BuildContext context) {
|
Widget _render(HistoryModel model, BuildContext context) {
|
||||||
|
var url = Provider.of<Session>(context).url;
|
||||||
|
|
||||||
|
return model.state == ViewState.Busy
|
||||||
|
? Center(child: CircularProgressIndicator())
|
||||||
|
: (model.errorMessage == null
|
||||||
|
? Container(
|
||||||
|
padding: EdgeInsets.all(0),
|
||||||
|
child: RefreshIndicator(onRefresh: () => model.getHistory(), child: _renderItems(model, url, context)))
|
||||||
|
: Container(
|
||||||
|
padding: EdgeInsets.all(25),
|
||||||
|
child: CenteredErrorRow(
|
||||||
|
model.errorMessage,
|
||||||
|
retryCallback: () => model.getHistory(),
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _renderItems(HistoryModel model, String url, BuildContext context) {
|
||||||
List<Widget> cards = [];
|
List<Widget> cards = [];
|
||||||
|
|
||||||
if (model.pastes.length > 0) {
|
if (model.pastes.length > 0) {
|
||||||
|
|
|
@ -10,6 +10,7 @@ import '../shared/app_colors.dart';
|
||||||
import '../shared/text_styles.dart';
|
import '../shared/text_styles.dart';
|
||||||
import '../shared/ui_helpers.dart';
|
import '../shared/ui_helpers.dart';
|
||||||
import '../widgets/my_appbar.dart';
|
import '../widgets/my_appbar.dart';
|
||||||
|
import '../widgets/swipe_navigation.dart';
|
||||||
import 'base_view.dart';
|
import 'base_view.dart';
|
||||||
|
|
||||||
class ProfileView extends StatelessWidget {
|
class ProfileView extends StatelessWidget {
|
||||||
|
@ -17,71 +18,75 @@ class ProfileView extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
var url = Provider.of<Session>(context).url;
|
|
||||||
var apiKey = Provider.of<Session>(context).apiKey;
|
|
||||||
|
|
||||||
return BaseView<ProfileModel>(
|
return BaseView<ProfileModel>(
|
||||||
builder: (context, model, child) => Scaffold(
|
builder: (context, model, child) => Scaffold(
|
||||||
appBar: MyAppBar(title: Text(translate('titles.profile'))),
|
appBar: MyAppBar(title: Text(translate('titles.profile'))),
|
||||||
backgroundColor: backgroundColor,
|
backgroundColor: backgroundColor,
|
||||||
body: model.state == ViewState.Busy
|
body: SwipeNavigation(child: _render(model, context))));
|
||||||
? Center(child: CircularProgressIndicator())
|
}
|
||||||
: ListView(
|
|
||||||
children: <Widget>[
|
Widget _render(ProfileModel model, BuildContext context) {
|
||||||
UIHelper.verticalSpaceMedium(),
|
var url = Provider.of<Session>(context).url;
|
||||||
Padding(
|
var apiKey = Provider.of<Session>(context).apiKey;
|
||||||
padding: const EdgeInsets.only(left: 25.0),
|
|
||||||
child: Center(
|
return model.state == ViewState.Busy
|
||||||
child: Text(
|
? Center(child: CircularProgressIndicator())
|
||||||
translate('profile.instance'),
|
: ListView(
|
||||||
style: subHeaderStyle,
|
children: <Widget>[
|
||||||
))),
|
UIHelper.verticalSpaceMedium(),
|
||||||
UIHelper.verticalSpaceMedium(),
|
Padding(
|
||||||
Padding(
|
padding: const EdgeInsets.only(left: 25.0),
|
||||||
padding: const EdgeInsets.only(left: 25.0),
|
child: Center(
|
||||||
child: Center(
|
child: Text(
|
||||||
child: Linkify(
|
translate('profile.instance'),
|
||||||
onOpen: (link) => model.openLink(link.url),
|
style: subHeaderStyle,
|
||||||
text: translate('profile.connection', args: {'url': url}),
|
))),
|
||||||
options: LinkifyOptions(humanize: false),
|
UIHelper.verticalSpaceMedium(),
|
||||||
))),
|
Padding(
|
||||||
UIHelper.verticalSpaceMedium(),
|
padding: const EdgeInsets.only(left: 25.0),
|
||||||
Padding(
|
child: Center(
|
||||||
padding: const EdgeInsets.only(left: 25.0, right: 25.0),
|
child: Linkify(
|
||||||
child: ElevatedButton.icon(
|
onOpen: (link) => model.openLink(link.url),
|
||||||
icon: Icon(Icons.settings, color: blueColor),
|
text: translate('profile.connection', args: {'url': url}),
|
||||||
label: Text(
|
options: LinkifyOptions(humanize: false),
|
||||||
translate('profile.show_config'),
|
))),
|
||||||
style: TextStyle(color: buttonForegroundColor),
|
UIHelper.verticalSpaceMedium(),
|
||||||
),
|
Padding(
|
||||||
onPressed: () {
|
padding: const EdgeInsets.only(left: 25.0, right: 25.0),
|
||||||
return model.showConfig(url);
|
child: ElevatedButton.icon(
|
||||||
})),
|
icon: Icon(Icons.settings, color: blueColor),
|
||||||
UIHelper.verticalSpaceMedium(),
|
label: Text(
|
||||||
Padding(
|
translate('profile.show_config'),
|
||||||
padding: const EdgeInsets.only(left: 25.0, right: 25.0),
|
style: TextStyle(color: buttonForegroundColor),
|
||||||
child: ElevatedButton.icon(
|
),
|
||||||
icon: Icon(Icons.lock, color: orangeColor),
|
onPressed: () {
|
||||||
label: Text(
|
return model.showConfig(url);
|
||||||
translate('profile.reveal_api_key'),
|
})),
|
||||||
style: TextStyle(color: buttonForegroundColor),
|
UIHelper.verticalSpaceMedium(),
|
||||||
),
|
Padding(
|
||||||
onPressed: () {
|
padding: const EdgeInsets.only(left: 25.0, right: 25.0),
|
||||||
return model.revealApiKey(apiKey);
|
child: ElevatedButton.icon(
|
||||||
})),
|
icon: Icon(Icons.lock, color: orangeColor),
|
||||||
UIHelper.verticalSpaceMedium(),
|
label: Text(
|
||||||
Padding(
|
translate('profile.reveal_api_key'),
|
||||||
padding: const EdgeInsets.only(left: 25.0, right: 25.0),
|
style: TextStyle(color: buttonForegroundColor),
|
||||||
child: ElevatedButton.icon(
|
),
|
||||||
icon: Icon(Icons.exit_to_app, color: redColor),
|
onPressed: () {
|
||||||
label: Text(
|
return model.revealApiKey(apiKey);
|
||||||
translate('profile.logout'),
|
})),
|
||||||
style: TextStyle(color: buttonForegroundColor),
|
UIHelper.verticalSpaceMedium(),
|
||||||
),
|
Padding(
|
||||||
onPressed: () {
|
padding: const EdgeInsets.only(left: 25.0, right: 25.0),
|
||||||
return model.logout();
|
child: ElevatedButton.icon(
|
||||||
})),
|
icon: Icon(Icons.exit_to_app, color: redColor),
|
||||||
],
|
label: Text(
|
||||||
)));
|
translate('profile.logout'),
|
||||||
|
style: TextStyle(color: buttonForegroundColor),
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
return model.logout();
|
||||||
|
})),
|
||||||
|
],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,15 @@
|
||||||
|
import 'dart:async';
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:flutter_translate/flutter_translate.dart';
|
import 'package:flutter_translate/flutter_translate.dart';
|
||||||
|
import 'package:logger/logger.dart';
|
||||||
|
|
||||||
|
import '../../core/enums/swipe_event.dart';
|
||||||
|
import '../../core/services/swipe_service.dart';
|
||||||
|
import '../../core/util/logger.dart';
|
||||||
|
import '../../locator.dart';
|
||||||
import '../shared/app_colors.dart';
|
import '../shared/app_colors.dart';
|
||||||
import 'history_view.dart';
|
import 'history_view.dart';
|
||||||
import 'profile_view.dart';
|
import 'profile_view.dart';
|
||||||
|
@ -13,6 +21,10 @@ class AuthenticatedTabBarView extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class AuthenticatedTabBarState extends State<AuthenticatedTabBarView> with SingleTickerProviderStateMixin {
|
class AuthenticatedTabBarState extends State<AuthenticatedTabBarView> with SingleTickerProviderStateMixin {
|
||||||
|
final Logger _logger = getLogger();
|
||||||
|
final SwipeService _swipeService = locator<SwipeService>();
|
||||||
|
|
||||||
|
StreamSubscription _swipeEventSubscription;
|
||||||
TabController _tabController;
|
TabController _tabController;
|
||||||
int _currentTabIndex = 0;
|
int _currentTabIndex = 0;
|
||||||
|
|
||||||
|
@ -38,11 +50,28 @@ class AuthenticatedTabBarState extends State<AuthenticatedTabBarView> with Singl
|
||||||
setState(() => _currentTabIndex = selectedIndex);
|
setState(() => _currentTabIndex = selectedIndex);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
_swipeEventSubscription = _swipeService.swipeEventController.stream.listen((SwipeEvent event) {
|
||||||
|
_logger.d('Received an swipe event for the authenticated tab bar: $event');
|
||||||
|
|
||||||
|
int targetIndex = _currentTabIndex;
|
||||||
|
if (SwipeEvent.Left == event) {
|
||||||
|
targetIndex = min(_currentTabIndex + 1, _realPages.length - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (SwipeEvent.Right == event) {
|
||||||
|
targetIndex = max(_currentTabIndex - 1, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.d("Changing to tab '$targetIndex' because of a swipe event");
|
||||||
|
_tabController.animateTo(targetIndex);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_tabController.dispose();
|
_tabController.dispose();
|
||||||
|
_swipeEventSubscription.cancel();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,7 @@ import '../../core/viewmodels/upload_model.dart';
|
||||||
import '../shared/app_colors.dart';
|
import '../shared/app_colors.dart';
|
||||||
import '../widgets/centered_error_row.dart';
|
import '../widgets/centered_error_row.dart';
|
||||||
import '../widgets/my_appbar.dart';
|
import '../widgets/my_appbar.dart';
|
||||||
|
import '../widgets/swipe_navigation.dart';
|
||||||
import 'base_view.dart';
|
import 'base_view.dart';
|
||||||
|
|
||||||
class UploadView extends StatelessWidget {
|
class UploadView extends StatelessWidget {
|
||||||
|
@ -16,173 +17,180 @@ class UploadView extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
var url = Provider.of<Session>(context).url;
|
|
||||||
|
|
||||||
return BaseView<UploadModel>(
|
return BaseView<UploadModel>(
|
||||||
onModelReady: (model) => model.init(),
|
onModelReady: (model) => model.init(),
|
||||||
builder: (context, model, child) => Scaffold(
|
builder: (context, model, child) => Scaffold(
|
||||||
appBar: MyAppBar(title: Text(translate('titles.upload'))),
|
appBar: MyAppBar(title: Text(translate('titles.upload'))),
|
||||||
backgroundColor: backgroundColor,
|
backgroundColor: backgroundColor,
|
||||||
body: model.state == ViewState.Busy
|
body: SwipeNavigation(child: _render(model, context))));
|
||||||
? Center(
|
}
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
bool _isUploadButtonEnabled(UploadModel model) {
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
return model.pasteTextTouched || (model.paths != null && model.paths.length > 0);
|
||||||
children: [
|
}
|
||||||
CircularProgressIndicator(),
|
|
||||||
(model.stateMessage != null && model.stateMessage.isNotEmpty
|
Widget _render(UploadModel model, BuildContext context) {
|
||||||
? Text(model.stateMessage)
|
var url = Provider.of<Session>(context).url;
|
||||||
: Container())
|
|
||||||
]))
|
return model.state == ViewState.Busy
|
||||||
: ListView(children: <Widget>[
|
? Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
CircularProgressIndicator(),
|
||||||
|
(model.stateMessage != null && model.stateMessage.isNotEmpty ? Text(model.stateMessage) : Container())
|
||||||
|
]))
|
||||||
|
: ListView(children: <Widget>[
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 25.0, right: 25.0),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(left: 25.0, right: 25.0),
|
padding: const EdgeInsets.only(top: 10.0, bottom: 10.0),
|
||||||
child: Column(
|
child: TextFormField(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
minLines: 1,
|
||||||
children: <Widget>[
|
maxLines: 7,
|
||||||
Padding(
|
decoration: InputDecoration(
|
||||||
padding: const EdgeInsets.only(top: 10.0, bottom: 10.0),
|
prefixIcon: Icon(
|
||||||
child: TextFormField(
|
Icons.text_snippet,
|
||||||
minLines: 1,
|
color: buttonBackgroundColor,
|
||||||
maxLines: 7,
|
),
|
||||||
decoration: InputDecoration(
|
suffixIcon: IconButton(
|
||||||
prefixIcon: Icon(
|
onPressed: () => model.pasteTextController.clear(),
|
||||||
Icons.text_snippet,
|
icon: Icon(Icons.clear),
|
||||||
color: buttonBackgroundColor,
|
),
|
||||||
),
|
hintText: translate('upload.text_to_be_pasted'),
|
||||||
suffixIcon: IconButton(
|
contentPadding: EdgeInsets.fromLTRB(20.0, 10.0, 20.0, 10.0),
|
||||||
onPressed: () => model.pasteTextController.clear(),
|
border: OutlineInputBorder(borderRadius: BorderRadius.circular(32.0)),
|
||||||
icon: Icon(Icons.clear),
|
|
||||||
),
|
|
||||||
hintText: translate('upload.text_to_be_pasted'),
|
|
||||||
contentPadding: EdgeInsets.fromLTRB(20.0, 10.0, 20.0, 10.0),
|
|
||||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(32.0)),
|
|
||||||
),
|
|
||||||
controller: model.pasteTextController)),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(top: 10.0, bottom: 10.0),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
|
||||||
children: [Text(translate('upload.and_or'))])),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(top: 10.0, bottom: 10.0),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
|
||||||
children: [
|
|
||||||
ElevatedButton.icon(
|
|
||||||
icon: Icon(Icons.file_copy_sharp, color: blueColor),
|
|
||||||
onPressed: () => model.openFileExplorer(),
|
|
||||||
label: Text(
|
|
||||||
translate('upload.open_file_explorer'),
|
|
||||||
style: TextStyle(color: buttonForegroundColor),
|
|
||||||
)),
|
|
||||||
ElevatedButton.icon(
|
|
||||||
icon: Icon(Icons.cancel, color: orangeColor),
|
|
||||||
onPressed: model.paths != null && model.paths.length > 0
|
|
||||||
? () => model.clearCachedFiles()
|
|
||||||
: null,
|
|
||||||
label: Text(
|
|
||||||
translate('upload.clear_temporary_files'),
|
|
||||||
style: TextStyle(color: buttonForegroundColor),
|
|
||||||
)),
|
|
||||||
],
|
|
||||||
)),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(top: 10.0, bottom: 10.0),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Checkbox(
|
|
||||||
value: model.createMulti,
|
|
||||||
onChanged: (v) => model.toggleCreateMulti(),
|
|
||||||
),
|
|
||||||
Text(translate('upload.multipaste')),
|
|
||||||
])),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(top: 10.0, bottom: 10.0),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
|
||||||
children: [
|
|
||||||
ElevatedButton.icon(
|
|
||||||
onPressed: () async {
|
|
||||||
Map<String, bool> items = await model.upload();
|
|
||||||
|
|
||||||
if (items != null) {
|
|
||||||
var clipboardContent = '';
|
|
||||||
items.forEach((id, isMulti) {
|
|
||||||
if (isMulti && model.createMulti || !isMulti && !model.createMulti) {
|
|
||||||
clipboardContent += '$url/$id\n';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
FlutterClipboard.copy(clipboardContent).then((value) {
|
|
||||||
final snackBar = SnackBar(
|
|
||||||
action: SnackBarAction(
|
|
||||||
label: translate('upload.dismiss'),
|
|
||||||
textColor: blueColor,
|
|
||||||
onPressed: () {
|
|
||||||
ScaffoldMessenger.of(context).hideCurrentSnackBar();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
content: Text(translate('upload.uploaded')),
|
|
||||||
duration: Duration(seconds: 10),
|
|
||||||
);
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(snackBar);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
icon: Icon(Icons.upload_rounded, color: greenColor),
|
|
||||||
label: Text(
|
|
||||||
translate('upload.upload'),
|
|
||||||
style: TextStyle(color: buttonForegroundColor),
|
|
||||||
)),
|
|
||||||
])),
|
|
||||||
model.errorMessage != null && model.errorMessage.isNotEmpty
|
|
||||||
? (Padding(
|
|
||||||
padding: const EdgeInsets.only(top: 10.0, bottom: 10.0),
|
|
||||||
child: CenteredErrorRow(model.errorMessage)))
|
|
||||||
: Container(),
|
|
||||||
Builder(
|
|
||||||
builder: (BuildContext context) => model.loadingPath
|
|
||||||
? Padding(
|
|
||||||
padding: const EdgeInsets.only(bottom: 10.0),
|
|
||||||
child: const CircularProgressIndicator(),
|
|
||||||
)
|
|
||||||
: model.paths != null
|
|
||||||
? Container(
|
|
||||||
padding: const EdgeInsets.only(bottom: 30.0),
|
|
||||||
height: MediaQuery.of(context).size.height * 0.50,
|
|
||||||
child: ListView.separated(
|
|
||||||
itemCount:
|
|
||||||
model.paths != null && model.paths.isNotEmpty ? model.paths.length : 1,
|
|
||||||
itemBuilder: (BuildContext context, int index) {
|
|
||||||
final bool isMultiPath = model.paths != null && model.paths.isNotEmpty;
|
|
||||||
final String name = (isMultiPath
|
|
||||||
? model.paths.map((e) => e.name).toList()[index]
|
|
||||||
: model.fileName ?? '...');
|
|
||||||
final path = model.paths.length > 0
|
|
||||||
? model.paths.map((e) => e.path).toList()[index].toString()
|
|
||||||
: '';
|
|
||||||
|
|
||||||
return Card(
|
|
||||||
child: ListTile(
|
|
||||||
title: Text(
|
|
||||||
name,
|
|
||||||
),
|
|
||||||
subtitle: Text(path),
|
|
||||||
));
|
|
||||||
},
|
|
||||||
separatorBuilder: (BuildContext context, int index) => const Divider(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: Container(),
|
|
||||||
),
|
),
|
||||||
|
controller: model.pasteTextController)),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 10.0, bottom: 10.0),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
children: [Text(translate('upload.and_or'))])),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 10.0, bottom: 10.0),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
ElevatedButton.icon(
|
||||||
|
icon: Icon(Icons.file_copy_sharp, color: blueColor),
|
||||||
|
onPressed: () => model.openFileExplorer(),
|
||||||
|
label: Text(
|
||||||
|
translate('upload.open_file_explorer'),
|
||||||
|
style: TextStyle(color: buttonForegroundColor),
|
||||||
|
)),
|
||||||
|
ElevatedButton.icon(
|
||||||
|
icon: Icon(Icons.cancel, color: orangeColor),
|
||||||
|
onPressed: model.paths != null && model.paths.length > 0
|
||||||
|
? () => model.clearCachedFiles()
|
||||||
|
: null,
|
||||||
|
label: Text(
|
||||||
|
translate('upload.clear_temporary_files'),
|
||||||
|
style: TextStyle(color: buttonForegroundColor),
|
||||||
|
)),
|
||||||
],
|
],
|
||||||
))
|
)),
|
||||||
])));
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 10.0, bottom: 10.0),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Checkbox(
|
||||||
|
value: model.createMulti,
|
||||||
|
onChanged: (v) => model.toggleCreateMulti(),
|
||||||
|
),
|
||||||
|
Text(translate('upload.multipaste')),
|
||||||
|
])),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 10.0, bottom: 10.0),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
ElevatedButton.icon(
|
||||||
|
onPressed: !_isUploadButtonEnabled(model)
|
||||||
|
? null
|
||||||
|
: () async {
|
||||||
|
Map<String, bool> items = await model.upload();
|
||||||
|
|
||||||
|
if (items != null) {
|
||||||
|
var clipboardContent = '';
|
||||||
|
items.forEach((id, isMulti) {
|
||||||
|
if (isMulti && model.createMulti || !isMulti && !model.createMulti) {
|
||||||
|
clipboardContent += '$url/$id\n';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
FlutterClipboard.copy(clipboardContent).then((value) {
|
||||||
|
final snackBar = SnackBar(
|
||||||
|
action: SnackBarAction(
|
||||||
|
label: translate('upload.dismiss'),
|
||||||
|
textColor: blueColor,
|
||||||
|
onPressed: () {
|
||||||
|
ScaffoldMessenger.of(context).hideCurrentSnackBar();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
content: Text(translate('upload.uploaded')),
|
||||||
|
duration: Duration(seconds: 10),
|
||||||
|
);
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(snackBar);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
icon: Icon(Icons.upload_rounded, color: greenColor),
|
||||||
|
label: Text(
|
||||||
|
translate('upload.upload'),
|
||||||
|
style: TextStyle(color: buttonForegroundColor),
|
||||||
|
)),
|
||||||
|
])),
|
||||||
|
model.errorMessage != null && model.errorMessage.isNotEmpty
|
||||||
|
? (Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 10.0, bottom: 10.0),
|
||||||
|
child: CenteredErrorRow(model.errorMessage)))
|
||||||
|
: Container(),
|
||||||
|
Builder(
|
||||||
|
builder: (BuildContext context) => model.loadingPath
|
||||||
|
? Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 10.0),
|
||||||
|
child: const CircularProgressIndicator(),
|
||||||
|
)
|
||||||
|
: model.paths != null
|
||||||
|
? Container(
|
||||||
|
padding: const EdgeInsets.only(bottom: 30.0),
|
||||||
|
height: MediaQuery.of(context).size.height * 0.50,
|
||||||
|
child: ListView.separated(
|
||||||
|
itemCount: model.paths != null && model.paths.isNotEmpty ? model.paths.length : 1,
|
||||||
|
itemBuilder: (BuildContext context, int index) {
|
||||||
|
final bool isMultiPath = model.paths != null && model.paths.isNotEmpty;
|
||||||
|
final String name = (isMultiPath
|
||||||
|
? model.paths.map((e) => e.name).toList()[index]
|
||||||
|
: model.fileName ?? '...');
|
||||||
|
final path = model.paths.length > 0
|
||||||
|
? model.paths.map((e) => e.path).toList()[index].toString()
|
||||||
|
: '';
|
||||||
|
|
||||||
|
return Card(
|
||||||
|
child: ListTile(
|
||||||
|
title: Text(
|
||||||
|
name,
|
||||||
|
),
|
||||||
|
subtitle: Text(path),
|
||||||
|
));
|
||||||
|
},
|
||||||
|
separatorBuilder: (BuildContext context, int index) => const Divider(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Container(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
))
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import 'package:flutter/cupertino.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import '../shared/app_colors.dart';
|
import '../shared/app_colors.dart';
|
||||||
|
|
37
lib/ui/widgets/swipe_navigation.dart
Normal file
37
lib/ui/widgets/swipe_navigation.dart
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:simple_gesture_detector/simple_gesture_detector.dart';
|
||||||
|
|
||||||
|
import '../../core/enums/swipe_event.dart';
|
||||||
|
import '../../core/services/swipe_service.dart';
|
||||||
|
import '../../locator.dart';
|
||||||
|
|
||||||
|
class SwipeNavigation extends StatefulWidget {
|
||||||
|
/// Widget to be augmented with gesture detection.
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
/// Creates a [SwipeNavigation] widget.
|
||||||
|
const SwipeNavigation({
|
||||||
|
Key key,
|
||||||
|
this.child,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_SwipeNavigationState createState() => _SwipeNavigationState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SwipeNavigationState extends State<SwipeNavigation> {
|
||||||
|
final SwipeService _swipeService = locator<SwipeService>();
|
||||||
|
|
||||||
|
void _onHorizontalSwipe(SwipeDirection direction) {
|
||||||
|
if (direction == SwipeDirection.left) {
|
||||||
|
_swipeService.addEvent(SwipeEvent.Left);
|
||||||
|
} else {
|
||||||
|
_swipeService.addEvent(SwipeEvent.Right);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SimpleGestureDetector(onHorizontalSwipe: _onHorizontalSwipe, child: widget.child);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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.3.0+9
|
version: 1.3.1+10
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=2.7.0 <3.0.0"
|
sdk: ">=2.7.0 <3.0.0"
|
||||||
|
@ -39,6 +39,8 @@ dependencies:
|
||||||
clipboard: 0.1.3
|
clipboard: 0.1.3
|
||||||
receive_sharing_intent: 1.4.5
|
receive_sharing_intent: 1.4.5
|
||||||
permission_handler: 5.1.0+2
|
permission_handler: 5.1.0+2
|
||||||
|
package_info: 2.0.0
|
||||||
|
simple_gesture_detector: 0.2.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|
Loading…
Reference in a new issue