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
|
||||
|
||||
## 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
|
||||
* Allow API key login
|
||||
* Revamp profile view
|
||||
|
|
|
@ -94,6 +94,7 @@
|
|||
}
|
||||
},
|
||||
"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.",
|
||||
"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.",
|
||||
|
|
16
lib/app.dart
16
lib/app.dart
|
@ -5,6 +5,7 @@ import 'package:flutter_translate/localized_app.dart';
|
|||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'core/enums/refresh_event.dart';
|
||||
import 'core/enums/swipe_event.dart';
|
||||
import 'core/manager/dialog_manager.dart';
|
||||
import 'core/manager/lifecycle_manager.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/refresh_service.dart';
|
||||
import 'core/services/session_service.dart';
|
||||
import 'core/services/swipe_service.dart';
|
||||
import 'locator.dart';
|
||||
import 'ui/app_router.dart';
|
||||
import 'ui/shared/app_colors.dart';
|
||||
|
@ -24,9 +26,12 @@ class MyApp extends StatelessWidget {
|
|||
|
||||
return LocalizationProvider(
|
||||
state: LocalizationProvider.of(context).state,
|
||||
child: StreamProvider<SwipeEvent>(
|
||||
initialData: null,
|
||||
create: (context) => locator<SwipeService>().swipeEventController.stream,
|
||||
child: StreamProvider<RefreshEvent>(
|
||||
initialData: null,
|
||||
create: (context) => locator<RefreshService>().refreshHistoryController.stream,
|
||||
create: (context) => locator<RefreshService>().refreshEventController.stream,
|
||||
child: StreamProvider<Session>(
|
||||
initialData: Session.initial(),
|
||||
create: (context) => locator<SessionService>().sessionController.stream,
|
||||
|
@ -35,16 +40,19 @@ class MyApp extends StatelessWidget {
|
|||
title: translate('app.title'),
|
||||
builder: (context, child) => Navigator(
|
||||
key: locator<DialogService>().dialogNavigationKey,
|
||||
onGenerateRoute: (settings) => MaterialPageRoute(builder: (context) => DialogManager(child: child)),
|
||||
onGenerateRoute: (settings) =>
|
||||
MaterialPageRoute(builder: (context) => DialogManager(child: child)),
|
||||
),
|
||||
theme: ThemeData(
|
||||
brightness: Brightness.light, primarySwatch: primaryAccentColor, primaryColor: primaryAccentColor),
|
||||
brightness: Brightness.light,
|
||||
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';
|
||||
|
||||
class RefreshService {
|
||||
StreamController<RefreshEvent> refreshHistoryController = StreamController<RefreshEvent>.broadcast();
|
||||
StreamController<RefreshEvent> refreshEventController = StreamController<RefreshEvent>.broadcast();
|
||||
|
||||
void addEvent(RefreshEvent event) {
|
||||
if (refreshHistoryController.hasListener) {
|
||||
refreshHistoryController.add(event);
|
||||
if (refreshEventController.hasListener) {
|
||||
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 '../../locator.dart';
|
||||
import '../enums/viewstate.dart';
|
||||
import 'base_model.dart';
|
||||
|
||||
class AboutModel extends BaseModel {
|
||||
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) {
|
||||
_linkService.open(link);
|
||||
}
|
||||
|
|
|
@ -5,31 +5,46 @@ import '../../core/util/logger.dart';
|
|||
import '../enums/viewstate.dart';
|
||||
|
||||
class BaseModel extends ChangeNotifier {
|
||||
static const String STATE_VIEW = 'viewState';
|
||||
static const String STATE_MESSAGE = 'viewMessage';
|
||||
|
||||
final Logger _logger = getLogger();
|
||||
|
||||
bool _isDisposed = false;
|
||||
|
||||
ViewState _state = ViewState.Idle;
|
||||
String _stateMessage;
|
||||
Map<String, Object> _stateMap = {STATE_VIEW: ViewState.Idle, STATE_MESSAGE: null};
|
||||
|
||||
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) {
|
||||
notifyListeners();
|
||||
_logger.d("Notified state change '${viewState.toString()}'");
|
||||
_logger.d("Notified state value update '($key, ${stateValue.toString()})'");
|
||||
}
|
||||
}
|
||||
|
||||
void removeStateValue(String key) {
|
||||
_stateMap.remove(key);
|
||||
|
||||
if (!_isDisposed) {
|
||||
notifyListeners();
|
||||
_logger.d("Notified state removal of '$key'");
|
||||
}
|
||||
}
|
||||
|
||||
void setStateView(ViewState stateView) {
|
||||
setStateValue(STATE_VIEW, stateView);
|
||||
}
|
||||
|
||||
void setStateMessage(String stateMessage) {
|
||||
_stateMessage = stateMessage;
|
||||
if (!_isDisposed) {
|
||||
notifyListeners();
|
||||
_logger.d("Notified state message change '$stateMessage'");
|
||||
}
|
||||
setStateValue(STATE_MESSAGE, stateMessage);
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
|
@ -34,7 +34,7 @@ class HistoryModel extends BaseModel {
|
|||
String errorMessage;
|
||||
|
||||
void init() {
|
||||
_refreshTriggerSubscription = _refreshService.refreshHistoryController.stream.listen((event) {
|
||||
_refreshTriggerSubscription = _refreshService.refreshEventController.stream.listen((event) {
|
||||
if (event == RefreshEvent.RefreshHistory) {
|
||||
_logger.d('History needs a refresh');
|
||||
getHistory();
|
||||
|
@ -43,7 +43,7 @@ class HistoryModel extends BaseModel {
|
|||
}
|
||||
|
||||
Future getHistory() async {
|
||||
setState(ViewState.Busy);
|
||||
setStateView(ViewState.Busy);
|
||||
|
||||
try {
|
||||
pastes.clear();
|
||||
|
@ -103,13 +103,13 @@ class HistoryModel extends BaseModel {
|
|||
errorMessage = translate('api.socket_timeout');
|
||||
} else {
|
||||
errorMessage = translate('app.unknown_error');
|
||||
setState(ViewState.Idle);
|
||||
setStateView(ViewState.Idle);
|
||||
_logger.e('An unknown error occurred', e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
setState(ViewState.Idle);
|
||||
setStateView(ViewState.Idle);
|
||||
}
|
||||
|
||||
Future deletePaste(String id) async {
|
||||
|
@ -123,7 +123,7 @@ class HistoryModel extends BaseModel {
|
|||
return;
|
||||
}
|
||||
|
||||
setState(ViewState.Busy);
|
||||
setStateView(ViewState.Busy);
|
||||
|
||||
try {
|
||||
await _fileService.deletePaste(id);
|
||||
|
@ -149,13 +149,13 @@ class HistoryModel extends BaseModel {
|
|||
errorMessage = translate('api.socket_timeout');
|
||||
} else {
|
||||
errorMessage = translate('app.unknown_error');
|
||||
setState(ViewState.Idle);
|
||||
setStateView(ViewState.Idle);
|
||||
_logger.e('An unknown error occurred', e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
setState(ViewState.Idle);
|
||||
setStateView(ViewState.Idle);
|
||||
}
|
||||
|
||||
void openLink(String link) {
|
||||
|
|
|
@ -43,23 +43,23 @@ class LoginModel extends BaseModel {
|
|||
String errorMessage;
|
||||
|
||||
void toggleLoginMethod() {
|
||||
setState(ViewState.Busy);
|
||||
setStateView(ViewState.Busy);
|
||||
useCredentialsLogin = !useCredentialsLogin;
|
||||
setState(ViewState.Idle);
|
||||
setStateView(ViewState.Idle);
|
||||
}
|
||||
|
||||
void init() async {
|
||||
bool hasLastUrl = await _storageService.hasLastUrl();
|
||||
|
||||
if (hasLastUrl) {
|
||||
setState(ViewState.Busy);
|
||||
setStateView(ViewState.Busy);
|
||||
var s = await _storageService.retrieveLastUrl();
|
||||
|
||||
if (s.isNotEmpty) {
|
||||
_uriController = new TextEditingController(text: s);
|
||||
}
|
||||
|
||||
setState(ViewState.Idle);
|
||||
setStateView(ViewState.Idle);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -69,45 +69,45 @@ class LoginModel extends BaseModel {
|
|||
var password = passwordController.text;
|
||||
var apiKey = apiKeyController.text;
|
||||
|
||||
setState(ViewState.Busy);
|
||||
setStateView(ViewState.Busy);
|
||||
url = trim(url);
|
||||
username = trim(username);
|
||||
|
||||
if (url.isEmpty) {
|
||||
errorMessage = translate('login.errors.empty_url');
|
||||
setState(ViewState.Idle);
|
||||
setStateView(ViewState.Idle);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!url.contains("https://") && !url.contains("http://")) {
|
||||
errorMessage = translate('login.errors.no_protocol');
|
||||
setState(ViewState.Idle);
|
||||
setStateView(ViewState.Idle);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool validUri = Uri.parse(url).isAbsolute;
|
||||
if (!validUri || !isURL(url)) {
|
||||
errorMessage = translate('login.errors.invalid_url');
|
||||
setState(ViewState.Idle);
|
||||
setStateView(ViewState.Idle);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (useCredentialsLogin) {
|
||||
if (username.isEmpty) {
|
||||
errorMessage = translate('login.errors.empty_username');
|
||||
setState(ViewState.Idle);
|
||||
setStateView(ViewState.Idle);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (password.isEmpty) {
|
||||
errorMessage = translate('login.errors.empty_password');
|
||||
setState(ViewState.Idle);
|
||||
setStateView(ViewState.Idle);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if (apiKey.isEmpty) {
|
||||
errorMessage = translate('login.errors.empty_apikey');
|
||||
setState(ViewState.Idle);
|
||||
setStateView(ViewState.Idle);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -147,7 +147,7 @@ class LoginModel extends BaseModel {
|
|||
} else {
|
||||
errorMessage = translate('app.unknown_error');
|
||||
_sessionService.logout();
|
||||
setState(ViewState.Idle);
|
||||
setStateView(ViewState.Idle);
|
||||
_logger.e('An unknown error occurred', e);
|
||||
throw e;
|
||||
}
|
||||
|
@ -156,7 +156,7 @@ class LoginModel extends BaseModel {
|
|||
_sessionService.logout();
|
||||
}
|
||||
|
||||
setState(ViewState.Idle);
|
||||
setStateView(ViewState.Idle);
|
||||
return success;
|
||||
}
|
||||
|
||||
|
|
|
@ -67,7 +67,7 @@ class ProfileModel extends BaseModel {
|
|||
} else {
|
||||
errorMessage = translate('app.unknown_error');
|
||||
_sessionService.logout();
|
||||
setState(ViewState.Idle);
|
||||
setStateView(ViewState.Idle);
|
||||
_logger.e('An unknown error occurred', e);
|
||||
throw e;
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ class StartUpViewModel extends BaseModel {
|
|||
final NavigationService _navigationService = locator<NavigationService>();
|
||||
|
||||
Future handleStartUpLogic() async {
|
||||
setState(ViewState.Busy);
|
||||
setStateView(ViewState.Busy);
|
||||
setStateMessage(translate('startup.init'));
|
||||
await Future.delayed(Duration(milliseconds: 150));
|
||||
|
||||
|
@ -22,6 +22,6 @@ class StartUpViewModel extends BaseModel {
|
|||
|
||||
_navigationService.navigateAndReplaceTo(HomeView.routeName);
|
||||
|
||||
setState(ViewState.Idle);
|
||||
setStateView(ViewState.Idle);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,6 +31,8 @@ class UploadModel extends BaseModel {
|
|||
final RefreshService _refreshService = locator<RefreshService>();
|
||||
|
||||
TextEditingController _pasteTextController = TextEditingController();
|
||||
bool pasteTextTouched = false;
|
||||
|
||||
StreamSubscription _intentDataStreamSubscription;
|
||||
|
||||
bool createMulti = false;
|
||||
|
@ -43,10 +45,15 @@ class UploadModel extends BaseModel {
|
|||
TextEditingController get pasteTextController => _pasteTextController;
|
||||
|
||||
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
|
||||
_intentDataStreamSubscription = ReceiveSharingIntent.getMediaStream().listen((List<SharedMediaFile> value) {
|
||||
if (value != null && value.length > 0) {
|
||||
setState(ViewState.Busy);
|
||||
setStateView(ViewState.Busy);
|
||||
paths = value.map((sharedFile) {
|
||||
return PlatformFile.fromMap({
|
||||
'path': sharedFile.path,
|
||||
|
@ -55,19 +62,19 @@ class UploadModel extends BaseModel {
|
|||
'bytes': null
|
||||
});
|
||||
}).toList();
|
||||
setState(ViewState.Idle);
|
||||
setStateView(ViewState.Idle);
|
||||
}
|
||||
}, onError: (err) {
|
||||
setState(ViewState.Busy);
|
||||
setStateView(ViewState.Busy);
|
||||
errorMessage = translate('upload.retrieval_intent');
|
||||
_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
|
||||
ReceiveSharingIntent.getInitialMedia().then((List<SharedMediaFile> value) {
|
||||
if (value != null && value.length > 0) {
|
||||
setState(ViewState.Busy);
|
||||
setStateView(ViewState.Busy);
|
||||
paths = value.map((sharedFile) {
|
||||
return PlatformFile.fromMap({
|
||||
'path': sharedFile.path,
|
||||
|
@ -76,42 +83,42 @@ class UploadModel extends BaseModel {
|
|||
'bytes': null
|
||||
});
|
||||
}).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
|
||||
_intentDataStreamSubscription = ReceiveSharingIntent.getTextStream().listen((String value) {
|
||||
if (value != null && value.isNotEmpty) {
|
||||
setState(ViewState.Busy);
|
||||
setStateView(ViewState.Busy);
|
||||
pasteTextController.text = value;
|
||||
setState(ViewState.Idle);
|
||||
setStateView(ViewState.Idle);
|
||||
}
|
||||
}, onError: (err) {
|
||||
setState(ViewState.Busy);
|
||||
setStateView(ViewState.Busy);
|
||||
errorMessage = translate('upload.retrieval_intent');
|
||||
_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
|
||||
ReceiveSharingIntent.getInitialText().then((String value) {
|
||||
if (value != null && value.isNotEmpty) {
|
||||
setState(ViewState.Busy);
|
||||
setStateView(ViewState.Busy);
|
||||
pasteTextController.text = value;
|
||||
setState(ViewState.Idle);
|
||||
setStateView(ViewState.Idle);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void toggleCreateMulti() {
|
||||
setState(ViewState.Busy);
|
||||
setStateView(ViewState.Busy);
|
||||
createMulti = !createMulti;
|
||||
setState(ViewState.Idle);
|
||||
setStateView(ViewState.Idle);
|
||||
}
|
||||
|
||||
void openFileExplorer() async {
|
||||
setState(ViewState.Busy);
|
||||
setStateView(ViewState.Busy);
|
||||
setStateMessage(translate('upload.file_explorer_open'));
|
||||
loadingPath = true;
|
||||
|
||||
|
@ -134,20 +141,20 @@ class UploadModel extends BaseModel {
|
|||
fileName = paths != null ? paths.map((e) => e.name).toString() : '...';
|
||||
|
||||
setStateMessage(null);
|
||||
setState(ViewState.Idle);
|
||||
setStateView(ViewState.Idle);
|
||||
}
|
||||
|
||||
void clearCachedFiles() async {
|
||||
setState(ViewState.Busy);
|
||||
setStateView(ViewState.Busy);
|
||||
await FilePicker.platform.clearTemporaryFiles();
|
||||
paths = null;
|
||||
fileName = null;
|
||||
errorMessage = null;
|
||||
setState(ViewState.Idle);
|
||||
setStateView(ViewState.Idle);
|
||||
}
|
||||
|
||||
Future<Map<String, bool>> upload() async {
|
||||
setState(ViewState.Busy);
|
||||
setStateView(ViewState.Busy);
|
||||
setStateMessage(translate('upload.uploading_now'));
|
||||
|
||||
Map<String, bool> uploadedPasteIds = new Map();
|
||||
|
@ -204,14 +211,14 @@ class UploadModel extends BaseModel {
|
|||
} else {
|
||||
errorMessage = translate('app.unknown_error');
|
||||
setStateMessage(null);
|
||||
setState(ViewState.Idle);
|
||||
setStateView(ViewState.Idle);
|
||||
_logger.e('An unknown error occurred', e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
setStateMessage(null);
|
||||
setState(ViewState.Idle);
|
||||
setStateView(ViewState.Idle);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ import 'core/services/permission_service.dart';
|
|||
import 'core/services/refresh_service.dart';
|
||||
import 'core/services/session_service.dart';
|
||||
import 'core/services/storage_service.dart';
|
||||
import 'core/services/swipe_service.dart';
|
||||
import 'core/services/user_service.dart';
|
||||
import 'core/viewmodels/about_model.dart';
|
||||
import 'core/viewmodels/history_model.dart';
|
||||
|
@ -41,6 +42,7 @@ void setupLocator() {
|
|||
locator.registerLazySingleton(() => LinkService());
|
||||
locator.registerLazySingleton(() => PermissionService());
|
||||
locator.registerLazySingleton(() => RefreshService());
|
||||
locator.registerLazySingleton(() => SwipeService());
|
||||
|
||||
/// view models
|
||||
locator.registerFactory(() => StartUpViewModel());
|
||||
|
|
|
@ -25,6 +25,7 @@ class AboutView extends StatelessWidget {
|
|||
);
|
||||
|
||||
return BaseView<AboutModel>(
|
||||
onModelReady: (model) => model.init(),
|
||||
builder: (context, model, child) => Scaffold(
|
||||
appBar: MyAppBar(
|
||||
title: Text(translate('titles.about')),
|
||||
|
@ -44,12 +45,22 @@ class AboutView extends StatelessWidget {
|
|||
UIHelper.verticalSpaceMedium(),
|
||||
Center(
|
||||
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(),
|
||||
Center(
|
||||
child: Text(
|
||||
translate(('about.faq_headline')),
|
||||
translate('about.description'),
|
||||
)),
|
||||
UIHelper.verticalSpaceMedium(),
|
||||
Center(
|
||||
child: Text(
|
||||
translate('about.faq_headline'),
|
||||
style: subHeaderStyle,
|
||||
)),
|
||||
Center(
|
||||
|
@ -59,7 +70,7 @@ class AboutView extends StatelessWidget {
|
|||
UIHelper.verticalSpaceMedium(),
|
||||
Center(
|
||||
child: Text(
|
||||
translate(('about.contact_us')),
|
||||
translate('about.contact_us'),
|
||||
style: subHeaderStyle,
|
||||
)),
|
||||
UIHelper.verticalSpaceSmall(),
|
||||
|
|
|
@ -12,6 +12,7 @@ import '../../core/viewmodels/history_model.dart';
|
|||
import '../../ui/widgets/centered_error_row.dart';
|
||||
import '../shared/app_colors.dart';
|
||||
import '../widgets/my_appbar.dart';
|
||||
import '../widgets/swipe_navigation.dart';
|
||||
import 'base_view.dart';
|
||||
|
||||
class HistoryView extends StatelessWidget {
|
||||
|
@ -19,8 +20,6 @@ class HistoryView extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var url = Provider.of<Session>(context).url;
|
||||
|
||||
return BaseView<HistoryModel>(
|
||||
onModelReady: (model) {
|
||||
model.init();
|
||||
|
@ -29,22 +28,28 @@ class HistoryView extends StatelessWidget {
|
|||
builder: (context, model, child) => Scaffold(
|
||||
appBar: MyAppBar(title: Text(translate('titles.history'))),
|
||||
backgroundColor: backgroundColor,
|
||||
body: model.state == ViewState.Busy
|
||||
body: SwipeNavigation(child: _render(model, 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: _render(model, url, context)))
|
||||
child: RefreshIndicator(onRefresh: () => model.getHistory(), child: _renderItems(model, url, context)))
|
||||
: Container(
|
||||
padding: EdgeInsets.all(25),
|
||||
child: CenteredErrorRow(
|
||||
model.errorMessage,
|
||||
retryCallback: () => model.getHistory(),
|
||||
)))),
|
||||
);
|
||||
)));
|
||||
}
|
||||
|
||||
Widget _render(HistoryModel model, String url, BuildContext context) {
|
||||
Widget _renderItems(HistoryModel model, String url, BuildContext context) {
|
||||
List<Widget> cards = [];
|
||||
|
||||
if (model.pastes.length > 0) {
|
||||
|
|
|
@ -10,6 +10,7 @@ import '../shared/app_colors.dart';
|
|||
import '../shared/text_styles.dart';
|
||||
import '../shared/ui_helpers.dart';
|
||||
import '../widgets/my_appbar.dart';
|
||||
import '../widgets/swipe_navigation.dart';
|
||||
import 'base_view.dart';
|
||||
|
||||
class ProfileView extends StatelessWidget {
|
||||
|
@ -17,14 +18,18 @@ class ProfileView extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var url = Provider.of<Session>(context).url;
|
||||
var apiKey = Provider.of<Session>(context).apiKey;
|
||||
|
||||
return BaseView<ProfileModel>(
|
||||
builder: (context, model, child) => Scaffold(
|
||||
appBar: MyAppBar(title: Text(translate('titles.profile'))),
|
||||
backgroundColor: backgroundColor,
|
||||
body: model.state == ViewState.Busy
|
||||
body: SwipeNavigation(child: _render(model, context))));
|
||||
}
|
||||
|
||||
Widget _render(ProfileModel model, BuildContext context) {
|
||||
var url = Provider.of<Session>(context).url;
|
||||
var apiKey = Provider.of<Session>(context).apiKey;
|
||||
|
||||
return model.state == ViewState.Busy
|
||||
? Center(child: CircularProgressIndicator())
|
||||
: ListView(
|
||||
children: <Widget>[
|
||||
|
@ -82,6 +87,6 @@ class ProfileView extends StatelessWidget {
|
|||
return model.logout();
|
||||
})),
|
||||
],
|
||||
)));
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,15 @@
|
|||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.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 'history_view.dart';
|
||||
import 'profile_view.dart';
|
||||
|
@ -13,6 +21,10 @@ class AuthenticatedTabBarView extends StatefulWidget {
|
|||
}
|
||||
|
||||
class AuthenticatedTabBarState extends State<AuthenticatedTabBarView> with SingleTickerProviderStateMixin {
|
||||
final Logger _logger = getLogger();
|
||||
final SwipeService _swipeService = locator<SwipeService>();
|
||||
|
||||
StreamSubscription _swipeEventSubscription;
|
||||
TabController _tabController;
|
||||
int _currentTabIndex = 0;
|
||||
|
||||
|
@ -38,11 +50,28 @@ class AuthenticatedTabBarState extends State<AuthenticatedTabBarView> with Singl
|
|||
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
|
||||
void dispose() {
|
||||
_tabController.dispose();
|
||||
_swipeEventSubscription.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ import '../../core/viewmodels/upload_model.dart';
|
|||
import '../shared/app_colors.dart';
|
||||
import '../widgets/centered_error_row.dart';
|
||||
import '../widgets/my_appbar.dart';
|
||||
import '../widgets/swipe_navigation.dart';
|
||||
import 'base_view.dart';
|
||||
|
||||
class UploadView extends StatelessWidget {
|
||||
|
@ -16,23 +17,29 @@ class UploadView extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
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,
|
||||
body: model.state == ViewState.Busy
|
||||
body: SwipeNavigation(child: _render(model, context))));
|
||||
}
|
||||
|
||||
bool _isUploadButtonEnabled(UploadModel model) {
|
||||
return model.pasteTextTouched || (model.paths != null && model.paths.length > 0);
|
||||
}
|
||||
|
||||
Widget _render(UploadModel model, BuildContext context) {
|
||||
var url = Provider.of<Session>(context).url;
|
||||
|
||||
return model.state == ViewState.Busy
|
||||
? Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
CircularProgressIndicator(),
|
||||
(model.stateMessage != null && model.stateMessage.isNotEmpty
|
||||
? Text(model.stateMessage)
|
||||
: Container())
|
||||
(model.stateMessage != null && model.stateMessage.isNotEmpty ? Text(model.stateMessage) : Container())
|
||||
]))
|
||||
: ListView(children: <Widget>[
|
||||
Padding(
|
||||
|
@ -108,7 +115,9 @@ class UploadView extends StatelessWidget {
|
|||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
ElevatedButton.icon(
|
||||
onPressed: () async {
|
||||
onPressed: !_isUploadButtonEnabled(model)
|
||||
? null
|
||||
: () async {
|
||||
Map<String, bool> items = await model.upload();
|
||||
|
||||
if (items != null) {
|
||||
|
@ -157,8 +166,7 @@ class UploadView extends StatelessWidget {
|
|||
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,
|
||||
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
|
||||
|
@ -183,6 +191,6 @@ class UploadView extends StatelessWidget {
|
|||
),
|
||||
],
|
||||
))
|
||||
])));
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.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.
|
||||
# Read more about iOS versioning at
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
version: 1.3.0+9
|
||||
version: 1.3.1+10
|
||||
|
||||
environment:
|
||||
sdk: ">=2.7.0 <3.0.0"
|
||||
|
@ -39,6 +39,8 @@ dependencies:
|
|||
clipboard: 0.1.3
|
||||
receive_sharing_intent: 1.4.5
|
||||
permission_handler: 5.1.0+2
|
||||
package_info: 2.0.0
|
||||
simple_gesture_detector: 0.2.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
|
Loading…
Reference in a new issue