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

This commit is contained in:
Varakh 2021-04-05 22:06:54 +02:00
parent c5da7ec84d
commit 230de7fe40
31 changed files with 478 additions and 195 deletions

1
.gitignore vendored
View file

@ -16,6 +16,7 @@
*.iws
.idea/
**/out/**
.run/
# Visual Studio Code related
.vscode/

View file

@ -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

View file

@ -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",

View file

@ -9,4 +9,7 @@ enum ErrorCode {
/// A REST error (response code wasn't 200 or 204)
REST_ERROR,
/// Custom errors
INVALID_API_KEY
}

View file

@ -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<String, dynamic> json) => _$ApiKeyFromJson(json);
// JSON Export
Map<String, dynamic> toJson() => _$ApiKeyToJson(this);
}

View file

@ -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<String, ApiKey> apikeys;
ApiKeys({this.apikeys});
// JSON Init
factory ApiKeys.fromJson(Map<String, dynamic> json) => _$ApiKeysFromJson(json);
// JSON Export
Map<String, dynamic> toJson() => _$ApiKeysToJson(this);
}

View file

@ -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<String, dynamic> json) => _$ApiKeysResponseFromJson(json);
// JSON Export
Map<String, dynamic> toJson() => _$ApiKeysResponseToJson(this);
}

View file

@ -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<String, dynamic> json) => _$SessionFromJson(json);

View file

@ -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<ApiKeysResponse> getApiKeys() async {
var response = await _api.post('/user/apikeys');
return ApiKeysResponse.fromJson(json.decode(response.body));
}
}

View file

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

View file

@ -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<Session> sessionController = StreamController<Session>();
Future<bool> 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<bool> 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<bool> logout() async {
_api.removeApiKeyAuthorization();
_api.removeUrl();
unsetApiConfig();
sessionController.add(null);
_logger.d('Session destroyed');
return await _storageService.removeSession();

View file

@ -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<FileService>();
final UserRepository _userRepository = locator<UserRepository>();
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);
}
}
}

View file

@ -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();

View file

@ -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<SessionService>();
final StorageService _storageService = locator<StorageService>();
final UserService _userService = locator<UserService>();
final FileService _fileService = locator<FileService>();
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<bool> login(String url, String username, String password) async {
setState(ViewState.Busy);
Future<bool> 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) {

View file

@ -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<SessionService>();
final DialogService _dialogService = locator<DialogService>();
final LinkService _linkService = locator<LinkService>();
final FileService _fileService = locator<FileService>();
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);
}

View file

@ -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']);

View file

@ -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<int, Color> colors = {
50: Color.fromRGBO(63, 69, 75, .1),
@ -20,4 +20,10 @@ const Map<int, Color> 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;

View file

@ -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: <Widget>[
UIHelper.verticalSpaceMedium(),
Center(child: logo),

View file

@ -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);
});

View file

@ -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<LoginView> {
final NavigationService _navigationService = locator<NavigationService>();
final DialogService _dialogService = locator<DialogService>();
@ -30,7 +27,7 @@ class _LoginViewState extends State<LoginView> {
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<LoginView> {
? 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: <Widget>[
UIHelper.verticalSpaceMedium(),
Center(child: logo),
UIHelper.verticalSpaceMedium(),
Center(
child: Wrap(
crossAxisAlignment: WrapCrossAlignment.center,
@ -63,19 +62,32 @@ class _LoginViewState extends State<LoginView> {
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);
}

View file

@ -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<Session>(context).url;
var apiKey = Provider.of<Session>(context).apiKey;
var config = Provider.of<Session>(context).config;
return BaseView<ProfileModel>(
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: <Widget>[
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: <Widget>[
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();
})),
],
)));
}
}

View file

@ -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(

View file

@ -18,7 +18,15 @@ class AnonymousTabBarState extends State<AnonymousTabBarView> with SingleTickerP
List<Widget> _tabPages = [LoginView()];
List<bool> _hasInit = [true];
List<Widget> _tabsButton = [Tab(icon: Icon(Icons.person_outline, color: Colors.blue), text: translate('tabs.login'))];
List<Widget> _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<AnonymousTabBarView> with SingleTickerP
bottomNavigationBar: BottomAppBar(
child: TabBar(
labelColor: primaryAccentColor,
indicatorColor: Colors.blue,
indicatorColor: blueColor,
indicatorWeight: 3.0,
tabs: _tabsButton,
controller: _tabController,

View file

@ -52,37 +52,47 @@ class AuthenticatedTabBarState extends State<AuthenticatedTabBarView> 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<Widget> _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<AuthenticatedTabBarView> with Singl
child: TabBar(
indicatorSize: TabBarIndicatorSize.label,
labelColor: primaryAccentColor,
indicatorColor: Colors.blue,
indicatorColor: blueColor,
indicatorWeight: 3.0,
labelPadding: EdgeInsets.all(0),
tabs: _tabsButton,

View file

@ -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),

View file

@ -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);
});

View file

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

View file

@ -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: <Widget>[
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,
),
]);
}
}

View file

@ -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: <Widget>[
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),
]);
}
}

View file

@ -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: <Widget>[
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,

View file

@ -11,7 +11,7 @@ description: A mobile client for FileBin.
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
# 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"