From 230de7fe40388681e0d0486d9066e8fefd44d86c Mon Sep 17 00:00:00 2001 From: Varakh Date: Mon, 5 Apr 2021 22:06:54 +0200 Subject: [PATCH] Allow API key login, revamp profile view, adapt text color in tab bar, fix already listened in history view, minor refactor, release 1.3.0+9 --- .gitignore | 1 + CHANGELOG.md | 8 ++ assets/i18n/en.json | 27 ++-- lib/core/enums/error_code.dart | 3 + lib/core/models/rest/apikey.dart | 25 ++++ lib/core/models/rest/apikeys.dart | 19 +++ lib/core/models/rest/apikeys_response.dart | 22 ++++ lib/core/models/session.dart | 8 +- lib/core/repositories/user_repository.dart | 6 + lib/core/services/refresh_service.dart | 2 +- lib/core/services/session_service.dart | 20 ++- lib/core/services/user_service.dart | 17 +++ lib/core/viewmodels/history_model.dart | 2 +- lib/core/viewmodels/login_model.dart | 66 +++++++--- lib/core/viewmodels/profile_model.dart | 63 +++++++++ lib/main.dart | 2 +- lib/ui/shared/app_colors.dart | 12 +- lib/ui/views/about_view.dart | 2 +- lib/ui/views/history_view.dart | 14 +- lib/ui/views/login_view.dart | 46 ++++--- lib/ui/views/profile_view.dart | 121 +++++++++--------- lib/ui/views/startup_view.dart | 3 +- lib/ui/views/tabbar_anonymous.dart | 12 +- lib/ui/views/tabbar_authenticated.dart | 66 ++++++---- lib/ui/views/upload_view.dart | 8 +- lib/ui/widgets/about_iconbutton.dart | 3 +- lib/ui/widgets/centered_error_row.dart | 2 +- lib/ui/widgets/login_header_apikey.dart | 29 +++++ lib/ui/widgets/login_header_credentials.dart | 32 +++++ ...ogin_header.dart => login_text_field.dart} | 30 +---- pubspec.yaml | 2 +- 31 files changed, 478 insertions(+), 195 deletions(-) create mode 100644 lib/core/models/rest/apikey.dart create mode 100644 lib/core/models/rest/apikeys.dart create mode 100644 lib/core/models/rest/apikeys_response.dart create mode 100644 lib/ui/widgets/login_header_apikey.dart create mode 100644 lib/ui/widgets/login_header_credentials.dart rename lib/ui/widgets/{login_header.dart => login_text_field.dart} (50%) diff --git a/.gitignore b/.gitignore index fe3227f..113dc73 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ *.iws .idea/ **/out/** +.run/ # Visual Studio Code related .vscode/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c4fa3c..6ff8eed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # CHANGELOG +## 1.3.0+9 +* Allow API key login +* Revamp profile view +* Adapt color of tab bar text and use outlined icons when active +* Suffix the API key comment with UNIX timestamp when credential login is used +* Fixed an error when logging out and logging back in again in the history view +* Minor code refactor + ## 1.2.2+8 * Adapt status bar color to match app's theme diff --git a/assets/i18n/en.json b/assets/i18n/en.json index acaef08..20307ed 100644 --- a/assets/i18n/en.json +++ b/assets/i18n/en.json @@ -48,9 +48,10 @@ "help": "Login", "compatibility_dialog": { "title": "How to login?", - "body": "A FileBin instance >= 3.5.0 and valid credentials for this instance are required." + "body": "A FileBin instance >= 3.5.0 is required. Enter valid user and password or switch to API key login by clicking on the icons right next to this help icon." }, "url_placeholder": "https://paste.domain.tld", + "apikey_placeholder": "API Key", "username_placeholder": "Username", "password_placeholder": "Password", "button": "Login", @@ -60,8 +61,10 @@ "invalid_url": "Please provide a valid FileBin URL", "empty_username": "Please provide a username", "empty_password": "Please provide a password", + "empty_apikey": "Please provide an API key", "wrong_credentials": "Credentials are invalid", - "forbidden": "You're not allowed to access this instance" + "forbidden": "You're not allowed to access this instance", + "invalid_api_key": "You're not allowed to use this API key. Please verify that it's valid and at least has access level 'apikey'." } }, "history": { @@ -91,22 +94,30 @@ } }, "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.", "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.", "contact_us": "Feedback? Issues?", - "website": "Main application: https://github.com/Bluewind/filebin\n\nMobile: https://github.com/v4rakh/fbmobile" + "website": "https://github.com/Bluewind/filebin and https://github.com/v4rakh/fbmobile" }, "profile": { - "welcome": "Hi!", - "connection": "You're currently connected to:\n\nURL: {url}", - "config": "Instance configuration:\n\nUpload max size: {uploadMaxSize}\n\nMax files per request: {maxFilesPerRequest}\n\nMax inputs vars: {maxInputVars}\n\nRequest max size: {requestMaxSize}", + "instance": "Instance", + "connection": "{url}", + "show_config": "Show configuration", + "shown_config": { + "title": "Configuration", + "description": "Upload max size: {uploadMaxSize}\n\nMax files per request: {maxFilesPerRequest}\n\nMax inputs vars: {maxInputVars}\n\nRequest max size: {requestMaxSize}", + "error": { + "title": "Error", + "description": "An error occurred while loading the configuration values. Reason: {message}" + } + }, "reveal_api_key": "Reveal API key", "revealed_api_key": { "title": "API key", "description": "{apiKey}" - } + }, + "logout": "Logout" }, "logout": { "title": "Logout", diff --git a/lib/core/enums/error_code.dart b/lib/core/enums/error_code.dart index d490724..9ebe1bb 100644 --- a/lib/core/enums/error_code.dart +++ b/lib/core/enums/error_code.dart @@ -9,4 +9,7 @@ enum ErrorCode { /// A REST error (response code wasn't 200 or 204) REST_ERROR, + + /// Custom errors + INVALID_API_KEY } diff --git a/lib/core/models/rest/apikey.dart b/lib/core/models/rest/apikey.dart new file mode 100644 index 0000000..2cdcacf --- /dev/null +++ b/lib/core/models/rest/apikey.dart @@ -0,0 +1,25 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'apikey.g.dart'; + +@JsonSerializable() +class ApiKey { + @JsonKey(required: true) + final String key; + + @JsonKey(required: true) + final String created; + + @JsonKey(required: true, name: 'access_level') + final String accessLevel; + + final String comment; + + ApiKey({this.key, this.created, this.accessLevel, this.comment}); + + // JSON Init + factory ApiKey.fromJson(Map json) => _$ApiKeyFromJson(json); + + // JSON Export + Map toJson() => _$ApiKeyToJson(this); +} diff --git a/lib/core/models/rest/apikeys.dart b/lib/core/models/rest/apikeys.dart new file mode 100644 index 0000000..5519931 --- /dev/null +++ b/lib/core/models/rest/apikeys.dart @@ -0,0 +1,19 @@ +import 'package:json_annotation/json_annotation.dart'; + +import 'apikey.dart'; + +part 'apikeys.g.dart'; + +@JsonSerializable() +class ApiKeys { + @JsonKey(name: "items") + final Map apikeys; + + ApiKeys({this.apikeys}); + + // JSON Init + factory ApiKeys.fromJson(Map json) => _$ApiKeysFromJson(json); + + // JSON Export + Map toJson() => _$ApiKeysToJson(this); +} diff --git a/lib/core/models/rest/apikeys_response.dart b/lib/core/models/rest/apikeys_response.dart new file mode 100644 index 0000000..ba5440c --- /dev/null +++ b/lib/core/models/rest/apikeys_response.dart @@ -0,0 +1,22 @@ +import 'package:json_annotation/json_annotation.dart'; + +import 'apikeys.dart'; + +part 'apikeys_response.g.dart'; + +@JsonSerializable() +class ApiKeysResponse { + @JsonKey(required: true) + final String status; + + @JsonKey(required: true) + final ApiKeys data; + + ApiKeysResponse({this.status, this.data}); + + // JSON Init + factory ApiKeysResponse.fromJson(Map json) => _$ApiKeysResponseFromJson(json); + + // JSON Export + Map toJson() => _$ApiKeysResponseToJson(this); +} diff --git a/lib/core/models/session.dart b/lib/core/models/session.dart index 8227b93..9909015 100644 --- a/lib/core/models/session.dart +++ b/lib/core/models/session.dart @@ -1,21 +1,17 @@ import 'package:json_annotation/json_annotation.dart'; -import 'rest/config.dart'; - part 'session.g.dart'; @JsonSerializable() class Session { final String url; final String apiKey; - final Config config; - Session({this.url, this.apiKey, this.config}); + Session({this.url, this.apiKey}); Session.initial() : url = '', - apiKey = '', - config = null; + apiKey = ''; factory Session.fromJson(Map json) => _$SessionFromJson(json); diff --git a/lib/core/repositories/user_repository.dart b/lib/core/repositories/user_repository.dart index f423816..ffc289e 100644 --- a/lib/core/repositories/user_repository.dart +++ b/lib/core/repositories/user_repository.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import '../../locator.dart'; +import '../models/rest/apikeys_response.dart'; import '../models/rest/create_apikey_response.dart'; import '../services/api.dart'; @@ -15,4 +16,9 @@ class UserRepository { fields: {'username': username, 'password': password, 'access_level': accessLevel, 'comment': comment}); return CreateApiKeyResponse.fromJson(json.decode(response.body)); } + + Future getApiKeys() async { + var response = await _api.post('/user/apikeys'); + return ApiKeysResponse.fromJson(json.decode(response.body)); + } } diff --git a/lib/core/services/refresh_service.dart b/lib/core/services/refresh_service.dart index 13abb15..f3c260c 100644 --- a/lib/core/services/refresh_service.dart +++ b/lib/core/services/refresh_service.dart @@ -3,7 +3,7 @@ import 'dart:async'; import '../enums/refresh_event.dart'; class RefreshService { - StreamController refreshHistoryController = StreamController(); + StreamController refreshHistoryController = StreamController.broadcast(); void addEvent(RefreshEvent event) { if (refreshHistoryController.hasListener) { diff --git a/lib/core/services/session_service.dart b/lib/core/services/session_service.dart index 760b364..aeeeb3e 100644 --- a/lib/core/services/session_service.dart +++ b/lib/core/services/session_service.dart @@ -4,7 +4,6 @@ import 'package:logger/logger.dart'; import '../../core/services/stoppable_service.dart'; import '../../locator.dart'; -import '../models/rest/config.dart'; import '../models/session.dart'; import '../services/storage_service.dart'; import '../util/logger.dart'; @@ -17,11 +16,22 @@ class SessionService extends StoppableService { StreamController sessionController = StreamController(); - Future login(String url, String apiKey, Config config) async { + void setApiConfig(String url, String apiKey) { + _logger.d('Setting API config for session'); _api.setUrl(url); _api.addApiKeyAuthorization(apiKey); + } - var session = new Session(url: url, apiKey: apiKey, config: config); + void unsetApiConfig() { + _logger.d('Removing API config'); + _api.removeApiKeyAuthorization(); + _api.removeUrl(); + } + + Future login(String url, String apiKey) async { + setApiConfig(url, apiKey); + + var session = new Session(url: url, apiKey: apiKey); sessionController.add(session); await _storageService.storeSession(session); _logger.d('Session created'); @@ -29,9 +39,7 @@ class SessionService extends StoppableService { } Future logout() async { - _api.removeApiKeyAuthorization(); - _api.removeUrl(); - + unsetApiConfig(); sessionController.add(null); _logger.d('Session destroyed'); return await _storageService.removeSession(); diff --git a/lib/core/services/user_service.dart b/lib/core/services/user_service.dart index f9e4914..30eaecc 100644 --- a/lib/core/services/user_service.dart +++ b/lib/core/services/user_service.dart @@ -1,12 +1,29 @@ import 'dart:async'; import '../../locator.dart'; +import '../enums/error_code.dart'; +import '../error/service_exception.dart'; import '../repositories/user_repository.dart'; +import 'file_service.dart'; class UserService { + final FileService _fileService = locator(); final UserRepository _userRepository = locator(); Future createApiKey(String url, String username, String password, String accessLevel, String comment) async { return await _userRepository.createApiKey(url, username, password, accessLevel, comment); } + + Future getApiKeys() async { + return await _userRepository.getApiKeys(); + } + + /// Use 'getHistory' to check currently used API key to require 'apikey' access level + Future checkAccessLevelIsAtLeastApiKey() async { + try { + await _fileService.getHistory(); + } catch (e) { + throw new ServiceException(code: ErrorCode.INVALID_API_KEY, message: e.message); + } + } } diff --git a/lib/core/viewmodels/history_model.dart b/lib/core/viewmodels/history_model.dart index 27f553a..c2c04fb 100644 --- a/lib/core/viewmodels/history_model.dart +++ b/lib/core/viewmodels/history_model.dart @@ -34,7 +34,7 @@ class HistoryModel extends BaseModel { String errorMessage; void init() { - this._refreshTriggerSubscription = _refreshService.refreshHistoryController.stream.listen((event) { + _refreshTriggerSubscription = _refreshService.refreshHistoryController.stream.listen((event) { if (event == RefreshEvent.RefreshHistory) { _logger.d('History needs a refresh'); getHistory(); diff --git a/lib/core/viewmodels/login_model.dart b/lib/core/viewmodels/login_model.dart index 0d16ec1..a07085e 100644 --- a/lib/core/viewmodels/login_model.dart +++ b/lib/core/viewmodels/login_model.dart @@ -1,12 +1,11 @@ import 'dart:io'; -import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_translate/flutter_translate.dart'; import 'package:logger/logger.dart'; import 'package:validators/sanitizers.dart'; import 'package:validators/validators.dart'; -import '../../core/services/file_service.dart'; import '../../core/services/session_service.dart'; import '../../core/services/storage_service.dart'; import '../../locator.dart'; @@ -14,7 +13,6 @@ import '../enums/error_code.dart'; import '../enums/viewstate.dart'; import '../error/rest_service_exception.dart'; import '../error/service_exception.dart'; -import '../models/rest/config.dart'; import '../models/rest/create_apikey_response.dart'; import '../services/user_service.dart'; import '../util/logger.dart'; @@ -22,23 +20,34 @@ import 'base_model.dart'; class LoginModel extends BaseModel { TextEditingController _uriController = new TextEditingController(); + final TextEditingController _userNameController = new TextEditingController(); final TextEditingController _passwordController = new TextEditingController(); + final TextEditingController _apiKeyController = new TextEditingController(); + TextEditingController get uriController => _uriController; TextEditingController get userNameController => _userNameController; TextEditingController get passwordController => _passwordController; + TextEditingController get apiKeyController => _apiKeyController; + final SessionService _sessionService = locator(); final StorageService _storageService = locator(); final UserService _userService = locator(); - final FileService _fileService = locator(); final Logger _logger = getLogger(); + bool useCredentialsLogin = true; String errorMessage; + void toggleLoginMethod() { + setState(ViewState.Busy); + useCredentialsLogin = !useCredentialsLogin; + setState(ViewState.Idle); + } + void init() async { bool hasLastUrl = await _storageService.hasLastUrl(); @@ -54,9 +63,13 @@ class LoginModel extends BaseModel { } } - Future login(String url, String username, String password) async { - setState(ViewState.Busy); + Future login() async { + var url = uriController.text; + var username = userNameController.text; + var password = passwordController.text; + var apiKey = apiKeyController.text; + setState(ViewState.Busy); url = trim(url); username = trim(username); @@ -79,24 +92,37 @@ class LoginModel extends BaseModel { return false; } - if (username.isEmpty) { - errorMessage = translate('login.errors.empty_username'); - setState(ViewState.Idle); - return false; - } + if (useCredentialsLogin) { + if (username.isEmpty) { + errorMessage = translate('login.errors.empty_username'); + setState(ViewState.Idle); + return false; + } - if (password.isEmpty) { - errorMessage = translate('login.errors.empty_password'); - setState(ViewState.Idle); - return false; + if (password.isEmpty) { + errorMessage = translate('login.errors.empty_password'); + setState(ViewState.Idle); + return false; + } + } else { + if (apiKey.isEmpty) { + errorMessage = translate('login.errors.empty_apikey'); + setState(ViewState.Idle); + return false; + } } var success = false; try { - Config config = await _fileService.getConfig(url); - CreateApiKeyResponse apiKeyResponse = - await _userService.createApiKey(url, username, password, 'apikey', 'fbmobile'); - success = await _sessionService.login(url, apiKeyResponse.data['new_key'], config); + if (useCredentialsLogin) { + CreateApiKeyResponse apiKeyResponse = await _userService.createApiKey( + url, username, password, 'apikey', 'fbmobile-${new DateTime.now().millisecondsSinceEpoch}'); + success = await _sessionService.login(url, apiKeyResponse.data['new_key']); + } else { + _sessionService.setApiConfig(url, apiKey); + success = await _userService.checkAccessLevelIsAtLeastApiKey(); + success = await _sessionService.login(url, apiKey); + } errorMessage = null; } catch (e) { if (e is RestServiceException) { @@ -112,6 +138,8 @@ class LoginModel extends BaseModel { } else { errorMessage = translate('api.general_rest_error'); } + } else if (e is ServiceException && e.code == ErrorCode.INVALID_API_KEY) { + errorMessage = translate('login.errors.invalid_api_key'); } else if (e is ServiceException && e.code == ErrorCode.SOCKET_ERROR) { errorMessage = translate('api.socket_error'); } else if (e is ServiceException && e.code == ErrorCode.SOCKET_TIMEOUT) { diff --git a/lib/core/viewmodels/profile_model.dart b/lib/core/viewmodels/profile_model.dart index 0868160..976242f 100644 --- a/lib/core/viewmodels/profile_model.dart +++ b/lib/core/viewmodels/profile_model.dart @@ -1,15 +1,30 @@ +import 'dart:io'; + import 'package:flutter_translate/flutter_translate.dart'; +import 'package:logger/logger.dart'; import '../../core/services/session_service.dart'; import '../../locator.dart'; +import '../enums/error_code.dart'; +import '../enums/viewstate.dart'; +import '../error/rest_service_exception.dart'; +import '../error/service_exception.dart'; +import '../models/rest/config.dart'; import '../services/dialog_service.dart'; +import '../services/file_service.dart'; import '../services/link_service.dart'; +import '../util/formatter_util.dart'; +import '../util/logger.dart'; import 'base_model.dart'; class ProfileModel extends BaseModel { final SessionService _sessionService = locator(); final DialogService _dialogService = locator(); final LinkService _linkService = locator(); + final FileService _fileService = locator(); + final Logger _logger = getLogger(); + + String errorMessage; Future logout() async { var dialogResult = await _dialogService.showConfirmationDialog( @@ -26,6 +41,54 @@ class ProfileModel extends BaseModel { description: translate('profile.revealed_api_key.description', args: {'apiKey': apiKey})); } + Future showConfig(String url) async { + Config config; + try { + config = await _fileService.getConfig(url); + errorMessage = null; + } catch (e) { + if (e is RestServiceException) { + if (e.statusCode == HttpStatus.unauthorized) { + errorMessage = translate('login.errors.wrong_credentials'); + } else if (e.statusCode != HttpStatus.unauthorized && e.statusCode == HttpStatus.forbidden) { + errorMessage = translate('login.errors.forbidden'); + } else if (e.statusCode == HttpStatus.notFound) { + errorMessage = translate('api.incompatible_error_not_found'); + } + if (e.statusCode == HttpStatus.badRequest) { + errorMessage = translate('api.bad_request', args: {'reason': e.responseBody.message}); + } else { + errorMessage = translate('api.general_rest_error'); + } + } else if (e is ServiceException && e.code == ErrorCode.SOCKET_ERROR) { + errorMessage = translate('api.socket_error'); + } else if (e is ServiceException && e.code == ErrorCode.SOCKET_TIMEOUT) { + errorMessage = translate('api.socket_timeout'); + } else { + errorMessage = translate('app.unknown_error'); + _sessionService.logout(); + setState(ViewState.Idle); + _logger.e('An unknown error occurred', e); + throw e; + } + } + + if (config != null && errorMessage == null) { + await _dialogService.showDialog( + title: translate('profile.shown_config.title'), + description: translate('profile.shown_config.description', args: { + 'uploadMaxSize': FormatterUtil.formatBytes(config.uploadMaxSize, 2), + 'maxFilesPerRequest': config.maxFilesPerRequest, + 'maxInputVars': config.maxInputVars, + 'requestMaxSize': FormatterUtil.formatBytes(config.requestMaxSize, 2) + })); + } else { + await _dialogService.showDialog( + title: translate('profile.shown_config.error.title'), + description: translate('profile.shown_config.error.description', args: {'message': errorMessage})); + } + } + void openLink(String link) { _linkService.open(link); } diff --git a/lib/main.dart b/lib/main.dart index 9ab230d..4d4cdb6 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -9,7 +9,7 @@ import 'locator.dart'; /// main entry point used to configure log level, locales, ... void main() async { setupLogger(Level.info); -// setupLogger(Level.debug); + // setupLogger(Level.debug); setupLocator(); var delegate = await LocalizationDelegate.create(fallbackLocale: 'en', supportedLocales: ['en']); diff --git a/lib/ui/shared/app_colors.dart b/lib/ui/shared/app_colors.dart index f390fe8..4f22032 100644 --- a/lib/ui/shared/app_colors.dart +++ b/lib/ui/shared/app_colors.dart @@ -1,9 +1,9 @@ import 'package:flutter/material.dart'; -const Color backgroundColor = Colors.white; +const Color backgroundColor = whiteColor; /// Colors -const Color primaryBackgroundColor = Colors.white; +const Color primaryBackgroundColor = whiteColor; const Map colors = { 50: Color.fromRGBO(63, 69, 75, .1), @@ -20,4 +20,10 @@ const Map colors = { const MaterialColor myColor = MaterialColor(0xFF3F454B, colors); const Color primaryAccentColor = myColor; const Color buttonBackgroundColor = primaryAccentColor; -const Color buttonForegroundColor = Colors.white; +const Color buttonForegroundColor = whiteColor; + +const Color blueColor = Colors.blue; +const Color whiteColor = Colors.white; +const Color redColor = Colors.red; +const Color orangeColor = Colors.orange; +const Color greenColor = Colors.green; diff --git a/lib/ui/views/about_view.dart b/lib/ui/views/about_view.dart index bffb3b3..617bb09 100644 --- a/lib/ui/views/about_view.dart +++ b/lib/ui/views/about_view.dart @@ -37,7 +37,7 @@ class AboutView extends StatelessWidget { padding: EdgeInsets.all(0), child: ListView( shrinkWrap: true, - padding: EdgeInsets.only(left: 24.0, right: 24.0), + padding: EdgeInsets.only(left: 10.0, right: 10.0, bottom: 10, top: 10), children: [ UIHelper.verticalSpaceMedium(), Center(child: logo), diff --git a/lib/ui/views/history_view.dart b/lib/ui/views/history_view.dart index 4be42df..24e129a 100644 --- a/lib/ui/views/history_view.dart +++ b/lib/ui/views/history_view.dart @@ -67,13 +67,13 @@ class HistoryView extends StatelessWidget { var copyWidget = ListTile( title: Text(translate('history.copy_link.description')), trailing: IconButton( - icon: Icon(Icons.copy, color: Colors.blue, textDirection: TextDirection.ltr), + icon: Icon(Icons.copy, color: blueColor, textDirection: TextDirection.ltr), onPressed: () { FlutterClipboard.copy(fullPasteUrl).then((value) { final snackBar = SnackBar( action: SnackBarAction( label: translate('history.copy_link.dismiss'), - textColor: Colors.blue, + textColor: blueColor, onPressed: () { ScaffoldMessenger.of(context).hideCurrentSnackBar(); }, @@ -88,7 +88,7 @@ class HistoryView extends StatelessWidget { var deleteWidget = ListTile( title: Text(translate('history.delete')), trailing: IconButton( - icon: Icon(Icons.delete, color: Colors.red), + icon: Icon(Icons.delete, color: redColor), onPressed: () { return model.deletePaste(paste.id); })); @@ -135,14 +135,14 @@ class HistoryView extends StatelessWidget { iconPlacement: ExpandablePanelIconPlacement.right, headerAlignment: ExpandablePanelHeaderAlignment.center, hasIcon: true, - iconColor: Colors.blue, + iconColor: blueColor, tapHeaderToExpand: true), child: ExpandablePanel( header: InkWell( onLongPress: () => model.deletePaste(paste.id), child: Text( paste.id, - style: TextStyle(color: Colors.blue), + style: TextStyle(color: blueColor), textAlign: TextAlign.left, )), expanded: Column( @@ -158,7 +158,7 @@ class HistoryView extends StatelessWidget { trailing: Wrap(children: [ openInBrowserButton, IconButton( - icon: Icon(Icons.share, color: Colors.blue, textDirection: TextDirection.ltr), + icon: Icon(Icons.share, color: blueColor, textDirection: TextDirection.ltr), onPressed: () { return Share.share(fullPasteUrl); }) @@ -184,7 +184,7 @@ class HistoryView extends StatelessWidget { Widget _renderOpenInBrowser(HistoryModel model, String url) { return IconButton( - icon: Icon(Icons.open_in_new, color: Colors.blue, textDirection: TextDirection.ltr), + icon: Icon(Icons.open_in_new, color: blueColor, textDirection: TextDirection.ltr), onPressed: () { return model.openLink(url); }); diff --git a/lib/ui/views/login_view.dart b/lib/ui/views/login_view.dart index c932114..b075a56 100644 --- a/lib/ui/views/login_view.dart +++ b/lib/ui/views/login_view.dart @@ -10,17 +10,14 @@ import '../../ui/shared/text_styles.dart'; import '../../ui/views/home_view.dart'; import '../../ui/widgets/my_appbar.dart'; import '../shared/app_colors.dart'; -import '../widgets/login_header.dart'; +import '../shared/ui_helpers.dart'; +import '../widgets/login_header_apikey.dart'; +import '../widgets/login_header_credentials.dart'; import 'base_view.dart'; -class LoginView extends StatefulWidget { +class LoginView extends StatelessWidget { static const routeName = '/login'; - @override - _LoginViewState createState() => _LoginViewState(); -} - -class _LoginViewState extends State { final NavigationService _navigationService = locator(); final DialogService _dialogService = locator(); @@ -30,7 +27,7 @@ class _LoginViewState extends State { tag: 'hero', child: CircleAvatar( backgroundColor: Colors.transparent, - radius: 96.0, + radius: 36.0, child: Image.asset('assets/logo_caption.png'), ), ); @@ -44,9 +41,11 @@ class _LoginViewState extends State { ? Center(child: CircularProgressIndicator()) : ListView( shrinkWrap: true, - padding: EdgeInsets.only(left: 24.0, right: 24.0), + padding: EdgeInsets.only(left: 10.0, right: 10.0), children: [ + UIHelper.verticalSpaceMedium(), Center(child: logo), + UIHelper.verticalSpaceMedium(), Center( child: Wrap( crossAxisAlignment: WrapCrossAlignment.center, @@ -63,19 +62,32 @@ class _LoginViewState extends State { title: translate('login.compatibility_dialog.title'), description: translate('login.compatibility_dialog.body')); }, + ), + InkWell( + child: + Icon(model.useCredentialsLogin ? Icons.person_outline : Icons.vpn_key, color: blueColor), + onTap: () { + model.toggleLoginMethod(); + }, ) ])), - LoginHeaders( - validationMessage: model.errorMessage, - uriController: model.uriController, - usernameController: model.userNameController, - passwordController: model.passwordController, - ), + UIHelper.verticalSpaceMedium(), + model.useCredentialsLogin + ? LoginCredentialsHeaders( + validationMessage: model.errorMessage, + uriController: model.uriController, + usernameController: model.userNameController, + passwordController: model.passwordController, + ) + : LoginApiKeyHeaders( + validationMessage: model.errorMessage, + uriController: model.uriController, + apiKeyController: model.apiKeyController), + UIHelper.verticalSpaceMedium(), ElevatedButton( child: Text(translate('login.button'), style: TextStyle(color: buttonForegroundColor)), onPressed: () async { - var loginSuccess = await model.login( - model.uriController.text, model.userNameController.text, model.passwordController.text); + var loginSuccess = await model.login(); if (loginSuccess) { _navigationService.navigateAndReplaceTo(HomeView.routeName); } diff --git a/lib/ui/views/profile_view.dart b/lib/ui/views/profile_view.dart index 82020b3..8b62a15 100644 --- a/lib/ui/views/profile_view.dart +++ b/lib/ui/views/profile_view.dart @@ -5,7 +5,6 @@ import 'package:provider/provider.dart'; import '../../core/enums/viewstate.dart'; import '../../core/models/session.dart'; -import '../../core/util/formatter_util.dart'; import '../../core/viewmodels/profile_model.dart'; import '../shared/app_colors.dart'; import '../shared/text_styles.dart'; @@ -20,65 +19,69 @@ class ProfileView extends StatelessWidget { Widget build(BuildContext context) { var url = Provider.of(context).url; var apiKey = Provider.of(context).apiKey; - var config = Provider.of(context).config; return BaseView( - builder: (context, model, child) => Scaffold( - appBar: MyAppBar(title: Text(translate('titles.profile'))), - floatingActionButton: FloatingActionButton( - heroTag: "logoutButton", - child: Icon(Icons.exit_to_app), - backgroundColor: primaryAccentColor, - onPressed: () { - model.logout(); - }, - ), - backgroundColor: backgroundColor, - body: model.state == ViewState.Busy - ? Center(child: CircularProgressIndicator()) - : ListView( - children: [ - UIHelper.verticalSpaceMedium(), - Padding( - padding: const EdgeInsets.only(left: 25.0), - child: Text( - translate('profile.welcome'), - style: headerStyle, - ), - ), - UIHelper.verticalSpaceMedium(), - Padding( - padding: const EdgeInsets.only(left: 25.0), - child: Linkify( - onOpen: (link) => model.openLink(link.url), - text: translate('profile.connection', args: {'url': url}), - options: LinkifyOptions(humanize: false), - )), - UIHelper.verticalSpaceMedium(), - Padding( - padding: const EdgeInsets.only(left: 25.0, right: 25.0), - child: ElevatedButton.icon( - icon: Icon(Icons.remove_red_eye, color: Colors.blue), - label: Text( - translate('profile.reveal_api_key'), - style: TextStyle(color: buttonForegroundColor), - ), - onPressed: () { - return model.revealApiKey(apiKey); - })), - UIHelper.verticalSpaceMedium(), - Padding( - padding: const EdgeInsets.only(left: 25.0), - child: Text( - translate('profile.config', args: { - 'uploadMaxSize': FormatterUtil.formatBytes(config.uploadMaxSize, 2), - 'maxFilesPerRequest': config.maxFilesPerRequest, - 'maxInputVars': config.maxInputVars, - 'requestMaxSize': FormatterUtil.formatBytes(config.requestMaxSize, 2) - }), - )), - ], - )), - ); + builder: (context, model, child) => Scaffold( + appBar: MyAppBar(title: Text(translate('titles.profile'))), + backgroundColor: backgroundColor, + body: model.state == ViewState.Busy + ? Center(child: CircularProgressIndicator()) + : ListView( + children: [ + UIHelper.verticalSpaceMedium(), + Padding( + padding: const EdgeInsets.only(left: 25.0), + child: Center( + child: Text( + translate('profile.instance'), + style: subHeaderStyle, + ))), + UIHelper.verticalSpaceMedium(), + Padding( + padding: const EdgeInsets.only(left: 25.0), + child: Center( + child: Linkify( + onOpen: (link) => model.openLink(link.url), + text: translate('profile.connection', args: {'url': url}), + options: LinkifyOptions(humanize: false), + ))), + UIHelper.verticalSpaceMedium(), + Padding( + padding: const EdgeInsets.only(left: 25.0, right: 25.0), + child: ElevatedButton.icon( + icon: Icon(Icons.settings, color: blueColor), + label: Text( + translate('profile.show_config'), + style: TextStyle(color: buttonForegroundColor), + ), + onPressed: () { + return model.showConfig(url); + })), + UIHelper.verticalSpaceMedium(), + Padding( + padding: const EdgeInsets.only(left: 25.0, right: 25.0), + child: ElevatedButton.icon( + icon: Icon(Icons.lock, color: orangeColor), + label: Text( + translate('profile.reveal_api_key'), + style: TextStyle(color: buttonForegroundColor), + ), + onPressed: () { + return model.revealApiKey(apiKey); + })), + UIHelper.verticalSpaceMedium(), + Padding( + padding: const EdgeInsets.only(left: 25.0, right: 25.0), + child: ElevatedButton.icon( + icon: Icon(Icons.exit_to_app, color: redColor), + label: Text( + translate('profile.logout'), + style: TextStyle(color: buttonForegroundColor), + ), + onPressed: () { + return model.logout(); + })), + ], + ))); } } diff --git a/lib/ui/views/startup_view.dart b/lib/ui/views/startup_view.dart index a5e2e64..b087ad9 100644 --- a/lib/ui/views/startup_view.dart +++ b/lib/ui/views/startup_view.dart @@ -3,6 +3,7 @@ import 'package:provider_architecture/provider_architecture.dart'; import '../../core/enums/viewstate.dart'; import '../../core/viewmodels/startup_model.dart'; +import '../shared/app_colors.dart'; class StartUpView extends StatelessWidget { static const routeName = '/'; @@ -13,7 +14,7 @@ class StartUpView extends StatelessWidget { viewModelBuilder: () => StartUpViewModel(), onModelReady: (model) => model.handleStartUpLogic(), builder: (context, model, child) => Scaffold( - backgroundColor: Colors.white, + backgroundColor: whiteColor, body: model.state == ViewState.Busy ? Center( child: Column( diff --git a/lib/ui/views/tabbar_anonymous.dart b/lib/ui/views/tabbar_anonymous.dart index 584a13a..0e2800a 100644 --- a/lib/ui/views/tabbar_anonymous.dart +++ b/lib/ui/views/tabbar_anonymous.dart @@ -18,7 +18,15 @@ class AnonymousTabBarState extends State with SingleTickerP List _tabPages = [LoginView()]; List _hasInit = [true]; - List _tabsButton = [Tab(icon: Icon(Icons.person_outline, color: Colors.blue), text: translate('tabs.login'))]; + List _tabsButton = [ + Tab( + icon: Icon(Icons.person_outline, color: blueColor), + child: Text( + translate('tabs.login'), + style: TextStyle(color: blueColor), + ), + ) + ]; @override void initState() { @@ -49,7 +57,7 @@ class AnonymousTabBarState extends State with SingleTickerP bottomNavigationBar: BottomAppBar( child: TabBar( labelColor: primaryAccentColor, - indicatorColor: Colors.blue, + indicatorColor: blueColor, indicatorWeight: 3.0, tabs: _tabsButton, controller: _tabController, diff --git a/lib/ui/views/tabbar_authenticated.dart b/lib/ui/views/tabbar_authenticated.dart index 3c7e366..2ad8437 100644 --- a/lib/ui/views/tabbar_authenticated.dart +++ b/lib/ui/views/tabbar_authenticated.dart @@ -52,37 +52,47 @@ class AuthenticatedTabBarState extends State with Singl double yourWidth = width / 3; double yourHeight = 55; + Color colorTabItem0 = _currentTabIndex == 0 ? blueColor : primaryAccentColor; + Color colorTabItem1 = _currentTabIndex == 1 ? blueColor : primaryAccentColor; + Color colorTabItem2 = _currentTabIndex == 2 ? blueColor : primaryAccentColor; + List _tabsButton = [ Container( - width: yourWidth, - height: yourHeight, - alignment: Alignment.center, - child: Tab( - icon: Icon( - Icons.upload_file, - color: _currentTabIndex == 0 ? Colors.blue : primaryAccentColor, - ), - text: translate('tabs.upload'))), + width: yourWidth, + height: yourHeight, + alignment: Alignment.center, + child: Tab( + icon: Icon( + _currentTabIndex == 0 ? Icons.upload_outlined : Icons.upload_rounded, + color: colorTabItem0, + ), + child: Text(translate('tabs.upload'), style: TextStyle(color: colorTabItem0)), + ), + ), Container( - width: yourWidth, - height: yourHeight, - alignment: Alignment.center, - child: Tab( - icon: Icon( - Icons.history, - color: _currentTabIndex == 1 ? Colors.blue : primaryAccentColor, - ), - text: translate('tabs.history'))), + width: yourWidth, + height: yourHeight, + alignment: Alignment.center, + child: Tab( + icon: Icon( + _currentTabIndex == 1 ? Icons.history_outlined : Icons.history_rounded, + color: colorTabItem1, + ), + child: Text(translate('tabs.history'), style: TextStyle(color: colorTabItem1)), + ), + ), Container( - width: yourWidth, - height: yourHeight, - alignment: Alignment.center, - child: Tab( - icon: Icon( - Icons.person, - color: _currentTabIndex == 2 ? Colors.blue : primaryAccentColor, - ), - text: translate('tabs.profile'))), + width: yourWidth, + height: yourHeight, + alignment: Alignment.center, + child: Tab( + icon: Icon( + _currentTabIndex == 2 ? Icons.person_outlined : Icons.person_rounded, + color: colorTabItem2, + ), + child: Text(translate('tabs.profile'), style: TextStyle(color: colorTabItem2)), + ), + ), ]; return Scaffold( @@ -91,7 +101,7 @@ class AuthenticatedTabBarState extends State with Singl child: TabBar( indicatorSize: TabBarIndicatorSize.label, labelColor: primaryAccentColor, - indicatorColor: Colors.blue, + indicatorColor: blueColor, indicatorWeight: 3.0, labelPadding: EdgeInsets.all(0), tabs: _tabsButton, diff --git a/lib/ui/views/upload_view.dart b/lib/ui/views/upload_view.dart index 28cec3c..e7f7a00 100644 --- a/lib/ui/views/upload_view.dart +++ b/lib/ui/views/upload_view.dart @@ -72,14 +72,14 @@ class UploadView extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.end, children: [ ElevatedButton.icon( - icon: Icon(Icons.file_copy_sharp, color: Colors.blue), + 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: Colors.orange), + icon: Icon(Icons.cancel, color: orangeColor), onPressed: model.paths != null && model.paths.length > 0 ? () => model.clearCachedFiles() : null, @@ -123,7 +123,7 @@ class UploadView extends StatelessWidget { final snackBar = SnackBar( action: SnackBarAction( label: translate('upload.dismiss'), - textColor: Colors.blue, + textColor: blueColor, onPressed: () { ScaffoldMessenger.of(context).hideCurrentSnackBar(); }, @@ -135,7 +135,7 @@ class UploadView extends StatelessWidget { }); } }, - icon: Icon(Icons.upload_rounded, color: Colors.green), + icon: Icon(Icons.upload_rounded, color: greenColor), label: Text( translate('upload.upload'), style: TextStyle(color: buttonForegroundColor), diff --git a/lib/ui/widgets/about_iconbutton.dart b/lib/ui/widgets/about_iconbutton.dart index 57a39aa..e11f7f9 100644 --- a/lib/ui/widgets/about_iconbutton.dart +++ b/lib/ui/widgets/about_iconbutton.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import '../../core/services/navigation_service.dart'; import '../../locator.dart'; import '../../ui/views/about_view.dart'; +import '../shared/app_colors.dart'; class AboutIconButton extends StatelessWidget { AboutIconButton(); @@ -13,7 +14,7 @@ class AboutIconButton extends StatelessWidget { Widget build(BuildContext context) { return IconButton( icon: Icon(Icons.help), - color: Colors.white, + color: whiteColor, onPressed: () { _navigationService.navigateTo(AboutView.routeName); }); diff --git a/lib/ui/widgets/centered_error_row.dart b/lib/ui/widgets/centered_error_row.dart index 35ea040..b5e6966 100644 --- a/lib/ui/widgets/centered_error_row.dart +++ b/lib/ui/widgets/centered_error_row.dart @@ -21,7 +21,7 @@ class CenteredErrorRow extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ - Expanded(child: Center(child: Text(message, style: TextStyle(color: Colors.red)))), + Expanded(child: Center(child: Text(message, style: TextStyle(color: redColor)))), ], ), (retryCallback != null diff --git a/lib/ui/widgets/login_header_apikey.dart b/lib/ui/widgets/login_header_apikey.dart new file mode 100644 index 0000000..4bea7de --- /dev/null +++ b/lib/ui/widgets/login_header_apikey.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_translate/flutter_translate.dart'; + +import '../shared/app_colors.dart'; +import 'login_text_field.dart'; + +class LoginApiKeyHeaders extends StatelessWidget { + final TextEditingController uriController; + final TextEditingController apiKeyController; + + final String validationMessage; + + LoginApiKeyHeaders({@required this.uriController, @required this.apiKeyController, this.validationMessage}); + + @override + Widget build(BuildContext context) { + return Column(children: [ + this.validationMessage != null ? Text(validationMessage, style: TextStyle(color: redColor)) : Container(), + LoginTextField(uriController, translate('login.url_placeholder'), Icon(Icons.link), + keyboardType: TextInputType.url), + LoginTextField( + apiKeyController, + translate('login.apikey_placeholder'), + Icon(Icons.vpn_key), + obscureText: true, + ), + ]); + } +} diff --git a/lib/ui/widgets/login_header_credentials.dart b/lib/ui/widgets/login_header_credentials.dart new file mode 100644 index 0000000..b4c85a5 --- /dev/null +++ b/lib/ui/widgets/login_header_credentials.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_translate/flutter_translate.dart'; + +import '../shared/app_colors.dart'; +import 'login_text_field.dart'; + +class LoginCredentialsHeaders extends StatelessWidget { + final TextEditingController uriController; + final TextEditingController usernameController; + final TextEditingController passwordController; + + final String validationMessage; + + LoginCredentialsHeaders( + {@required this.uriController, + @required this.usernameController, + @required this.passwordController, + this.validationMessage}); + + @override + Widget build(BuildContext context) { + return Column(children: [ + this.validationMessage != null ? Text(validationMessage, style: TextStyle(color: redColor)) : Container(), + LoginTextField(uriController, translate('login.url_placeholder'), Icon(Icons.link), + keyboardType: TextInputType.url), + LoginTextField(usernameController, translate('login.username_placeholder'), Icon(Icons.person), + keyboardType: TextInputType.name), + LoginTextField(passwordController, translate('login.password_placeholder'), Icon(Icons.vpn_key), + obscureText: true), + ]); + } +} diff --git a/lib/ui/widgets/login_header.dart b/lib/ui/widgets/login_text_field.dart similarity index 50% rename from lib/ui/widgets/login_header.dart rename to lib/ui/widgets/login_text_field.dart index 6ba7a75..c0a6987 100644 --- a/lib/ui/widgets/login_header.dart +++ b/lib/ui/widgets/login_text_field.dart @@ -1,32 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:flutter_translate/flutter_translate.dart'; -class LoginHeaders extends StatelessWidget { - final TextEditingController uriController; - final TextEditingController usernameController; - final TextEditingController passwordController; - - final String validationMessage; - - LoginHeaders( - {@required this.uriController, - @required this.usernameController, - @required this.passwordController, - this.validationMessage}); - - @override - Widget build(BuildContext context) { - return Column(children: [ - this.validationMessage != null ? Text(validationMessage, style: TextStyle(color: Colors.red)) : Container(), - LoginTextField(uriController, translate('login.url_placeholder'), Icon(Icons.link), - keyboardType: TextInputType.url), - LoginTextField(usernameController, translate('login.username_placeholder'), Icon(Icons.person), - keyboardType: TextInputType.name), - LoginTextField(passwordController, translate('login.password_placeholder'), Icon(Icons.vpn_key), - obscureText: true), - ]); - } -} +import '../shared/app_colors.dart'; class LoginTextField extends StatelessWidget { final TextEditingController controller; @@ -45,7 +19,7 @@ class LoginTextField extends StatelessWidget { margin: EdgeInsets.symmetric(horizontal: 10.0, vertical: 10.0), height: 50.0, alignment: Alignment.centerLeft, - decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(10.0)), + decoration: BoxDecoration(color: whiteColor, borderRadius: BorderRadius.circular(10.0)), child: TextFormField( keyboardType: keyboardType, obscureText: obscureText, diff --git a/pubspec.yaml b/pubspec.yaml index 85229be..7ef10d8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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.2.2+8 +version: 1.3.0+9 environment: sdk: ">=2.7.0 <3.0.0"