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:
parent
c5da7ec84d
commit
230de7fe40
31 changed files with 478 additions and 195 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -16,6 +16,7 @@
|
|||
*.iws
|
||||
.idea/
|
||||
**/out/**
|
||||
.run/
|
||||
|
||||
# Visual Studio Code related
|
||||
.vscode/
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -9,4 +9,7 @@ enum ErrorCode {
|
|||
|
||||
/// A REST error (response code wasn't 200 or 204)
|
||||
REST_ERROR,
|
||||
|
||||
/// Custom errors
|
||||
INVALID_API_KEY
|
||||
}
|
||||
|
|
25
lib/core/models/rest/apikey.dart
Normal file
25
lib/core/models/rest/apikey.dart
Normal 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);
|
||||
}
|
19
lib/core/models/rest/apikeys.dart
Normal file
19
lib/core/models/rest/apikeys.dart
Normal 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);
|
||||
}
|
22
lib/core/models/rest/apikeys_response.dart
Normal file
22
lib/core/models/rest/apikeys_response.dart
Normal 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);
|
||||
}
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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,6 +92,7 @@ class LoginModel extends BaseModel {
|
|||
return false;
|
||||
}
|
||||
|
||||
if (useCredentialsLogin) {
|
||||
if (username.isEmpty) {
|
||||
errorMessage = translate('login.errors.empty_username');
|
||||
setState(ViewState.Idle);
|
||||
|
@ -90,13 +104,25 @@ class LoginModel extends BaseModel {
|
|||
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) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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(
|
||||
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);
|
||||
}
|
||||
|
|
|
@ -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,19 +19,10 @@ 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())
|
||||
|
@ -41,24 +31,37 @@ class ProfileView extends StatelessWidget {
|
|||
UIHelper.verticalSpaceMedium(),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 25.0),
|
||||
child: Center(
|
||||
child: Text(
|
||||
translate('profile.welcome'),
|
||||
style: headerStyle,
|
||||
),
|
||||
),
|
||||
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.remove_red_eye, color: Colors.blue),
|
||||
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),
|
||||
|
@ -68,17 +71,17 @@ class ProfileView extends StatelessWidget {
|
|||
})),
|
||||
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)
|
||||
}),
|
||||
)),
|
||||
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();
|
||||
})),
|
||||
],
|
||||
)),
|
||||
);
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -52,6 +52,10 @@ 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,
|
||||
|
@ -59,30 +63,36 @@ class AuthenticatedTabBarState extends State<AuthenticatedTabBarView> with Singl
|
|||
alignment: Alignment.center,
|
||||
child: Tab(
|
||||
icon: Icon(
|
||||
Icons.upload_file,
|
||||
color: _currentTabIndex == 0 ? Colors.blue : primaryAccentColor,
|
||||
_currentTabIndex == 0 ? Icons.upload_outlined : Icons.upload_rounded,
|
||||
color: colorTabItem0,
|
||||
),
|
||||
child: Text(translate('tabs.upload'), style: TextStyle(color: colorTabItem0)),
|
||||
),
|
||||
),
|
||||
text: translate('tabs.upload'))),
|
||||
Container(
|
||||
width: yourWidth,
|
||||
height: yourHeight,
|
||||
alignment: Alignment.center,
|
||||
child: Tab(
|
||||
icon: Icon(
|
||||
Icons.history,
|
||||
color: _currentTabIndex == 1 ? Colors.blue : primaryAccentColor,
|
||||
_currentTabIndex == 1 ? Icons.history_outlined : Icons.history_rounded,
|
||||
color: colorTabItem1,
|
||||
),
|
||||
child: Text(translate('tabs.history'), style: TextStyle(color: colorTabItem1)),
|
||||
),
|
||||
),
|
||||
text: translate('tabs.history'))),
|
||||
Container(
|
||||
width: yourWidth,
|
||||
height: yourHeight,
|
||||
alignment: Alignment.center,
|
||||
child: Tab(
|
||||
icon: Icon(
|
||||
Icons.person,
|
||||
color: _currentTabIndex == 2 ? Colors.blue : primaryAccentColor,
|
||||
_currentTabIndex == 2 ? Icons.person_outlined : Icons.person_rounded,
|
||||
color: colorTabItem2,
|
||||
),
|
||||
child: Text(translate('tabs.profile'), style: TextStyle(color: colorTabItem2)),
|
||||
),
|
||||
),
|
||||
text: translate('tabs.profile'))),
|
||||
];
|
||||
|
||||
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,
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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
|
||||
|
|
29
lib/ui/widgets/login_header_apikey.dart
Normal file
29
lib/ui/widgets/login_header_apikey.dart
Normal 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,
|
||||
),
|
||||
]);
|
||||
}
|
||||
}
|
32
lib/ui/widgets/login_header_credentials.dart
Normal file
32
lib/ui/widgets/login_header_credentials.dart
Normal 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),
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -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,
|
|
@ -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"
|
||||
|
|
Loading…
Reference in a new issue