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
|
*.iws
|
||||||
.idea/
|
.idea/
|
||||||
**/out/**
|
**/out/**
|
||||||
|
.run/
|
||||||
|
|
||||||
# Visual Studio Code related
|
# Visual Studio Code related
|
||||||
.vscode/
|
.vscode/
|
||||||
|
|
|
@ -1,5 +1,13 @@
|
||||||
# CHANGELOG
|
# 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
|
## 1.2.2+8
|
||||||
* Adapt status bar color to match app's theme
|
* Adapt status bar color to match app's theme
|
||||||
|
|
||||||
|
|
|
@ -48,9 +48,10 @@
|
||||||
"help": "Login",
|
"help": "Login",
|
||||||
"compatibility_dialog": {
|
"compatibility_dialog": {
|
||||||
"title": "How to login?",
|
"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",
|
"url_placeholder": "https://paste.domain.tld",
|
||||||
|
"apikey_placeholder": "API Key",
|
||||||
"username_placeholder": "Username",
|
"username_placeholder": "Username",
|
||||||
"password_placeholder": "Password",
|
"password_placeholder": "Password",
|
||||||
"button": "Login",
|
"button": "Login",
|
||||||
|
@ -60,8 +61,10 @@
|
||||||
"invalid_url": "Please provide a valid FileBin URL",
|
"invalid_url": "Please provide a valid FileBin URL",
|
||||||
"empty_username": "Please provide a username",
|
"empty_username": "Please provide a username",
|
||||||
"empty_password": "Please provide a password",
|
"empty_password": "Please provide a password",
|
||||||
|
"empty_apikey": "Please provide an API key",
|
||||||
"wrong_credentials": "Credentials are invalid",
|
"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": {
|
"history": {
|
||||||
|
@ -91,22 +94,30 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"about": {
|
"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.",
|
"description": "This application is a mobile client for FileBin and it's open source. It helps you to manage your pastes.\n\nIn order to use the application, you need access to a FileBin instance.",
|
||||||
"faq_headline": "F.A.Q",
|
"faq_headline": "F.A.Q",
|
||||||
"faq": "- How do I login?\nInsert your instance URL and valid credentials you also use in the web interface of FileBin.\n\n- Why is storage permission required?\nIt's not required, but highly advised to grant it. Otherwise sharing files with the app won't work correctly and you might think that sharing has no effect.\n\n- When I am logged out, sharing files via share with the app won't list all files I selected after I login.\nPlease login before you start using the app. Account information are persisted. You only need to do it once.",
|
"faq": "- How do I login?\nInsert your instance URL and valid credentials you also use in the web interface of FileBin.\n\n- Why is storage permission required?\nIt's not required, but highly advised to grant it. Otherwise sharing files with the app won't work correctly and you might think that sharing has no effect.\n\n- When I am logged out, sharing files via share with the app won't list all files I selected after I login.\nPlease login before you start using the app. Account information are persisted. You only need to do it once.",
|
||||||
"contact_us": "Feedback? Issues?",
|
"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": {
|
"profile": {
|
||||||
"welcome": "Hi!",
|
"instance": "Instance",
|
||||||
"connection": "You're currently connected to:\n\nURL: {url}",
|
"connection": "{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}",
|
"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",
|
"reveal_api_key": "Reveal API key",
|
||||||
"revealed_api_key": {
|
"revealed_api_key": {
|
||||||
"title": "API key",
|
"title": "API key",
|
||||||
"description": "{apiKey}"
|
"description": "{apiKey}"
|
||||||
}
|
},
|
||||||
|
"logout": "Logout"
|
||||||
},
|
},
|
||||||
"logout": {
|
"logout": {
|
||||||
"title": "Logout",
|
"title": "Logout",
|
||||||
|
|
|
@ -9,4 +9,7 @@ enum ErrorCode {
|
||||||
|
|
||||||
/// A REST error (response code wasn't 200 or 204)
|
/// A REST error (response code wasn't 200 or 204)
|
||||||
REST_ERROR,
|
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 'package:json_annotation/json_annotation.dart';
|
||||||
|
|
||||||
import 'rest/config.dart';
|
|
||||||
|
|
||||||
part 'session.g.dart';
|
part 'session.g.dart';
|
||||||
|
|
||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
class Session {
|
class Session {
|
||||||
final String url;
|
final String url;
|
||||||
final String apiKey;
|
final String apiKey;
|
||||||
final Config config;
|
|
||||||
|
|
||||||
Session({this.url, this.apiKey, this.config});
|
Session({this.url, this.apiKey});
|
||||||
|
|
||||||
Session.initial()
|
Session.initial()
|
||||||
: url = '',
|
: url = '',
|
||||||
apiKey = '',
|
apiKey = '';
|
||||||
config = null;
|
|
||||||
|
|
||||||
factory Session.fromJson(Map<String, dynamic> json) => _$SessionFromJson(json);
|
factory Session.fromJson(Map<String, dynamic> json) => _$SessionFromJson(json);
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import '../../locator.dart';
|
import '../../locator.dart';
|
||||||
|
import '../models/rest/apikeys_response.dart';
|
||||||
import '../models/rest/create_apikey_response.dart';
|
import '../models/rest/create_apikey_response.dart';
|
||||||
import '../services/api.dart';
|
import '../services/api.dart';
|
||||||
|
|
||||||
|
@ -15,4 +16,9 @@ class UserRepository {
|
||||||
fields: {'username': username, 'password': password, 'access_level': accessLevel, 'comment': comment});
|
fields: {'username': username, 'password': password, 'access_level': accessLevel, 'comment': comment});
|
||||||
return CreateApiKeyResponse.fromJson(json.decode(response.body));
|
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';
|
import '../enums/refresh_event.dart';
|
||||||
|
|
||||||
class RefreshService {
|
class RefreshService {
|
||||||
StreamController<RefreshEvent> refreshHistoryController = StreamController<RefreshEvent>();
|
StreamController<RefreshEvent> refreshHistoryController = StreamController<RefreshEvent>.broadcast();
|
||||||
|
|
||||||
void addEvent(RefreshEvent event) {
|
void addEvent(RefreshEvent event) {
|
||||||
if (refreshHistoryController.hasListener) {
|
if (refreshHistoryController.hasListener) {
|
||||||
|
|
|
@ -4,7 +4,6 @@ import 'package:logger/logger.dart';
|
||||||
|
|
||||||
import '../../core/services/stoppable_service.dart';
|
import '../../core/services/stoppable_service.dart';
|
||||||
import '../../locator.dart';
|
import '../../locator.dart';
|
||||||
import '../models/rest/config.dart';
|
|
||||||
import '../models/session.dart';
|
import '../models/session.dart';
|
||||||
import '../services/storage_service.dart';
|
import '../services/storage_service.dart';
|
||||||
import '../util/logger.dart';
|
import '../util/logger.dart';
|
||||||
|
@ -17,11 +16,22 @@ class SessionService extends StoppableService {
|
||||||
|
|
||||||
StreamController<Session> sessionController = StreamController<Session>();
|
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.setUrl(url);
|
||||||
_api.addApiKeyAuthorization(apiKey);
|
_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);
|
sessionController.add(session);
|
||||||
await _storageService.storeSession(session);
|
await _storageService.storeSession(session);
|
||||||
_logger.d('Session created');
|
_logger.d('Session created');
|
||||||
|
@ -29,9 +39,7 @@ class SessionService extends StoppableService {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> logout() async {
|
Future<bool> logout() async {
|
||||||
_api.removeApiKeyAuthorization();
|
unsetApiConfig();
|
||||||
_api.removeUrl();
|
|
||||||
|
|
||||||
sessionController.add(null);
|
sessionController.add(null);
|
||||||
_logger.d('Session destroyed');
|
_logger.d('Session destroyed');
|
||||||
return await _storageService.removeSession();
|
return await _storageService.removeSession();
|
||||||
|
|
|
@ -1,12 +1,29 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import '../../locator.dart';
|
import '../../locator.dart';
|
||||||
|
import '../enums/error_code.dart';
|
||||||
|
import '../error/service_exception.dart';
|
||||||
import '../repositories/user_repository.dart';
|
import '../repositories/user_repository.dart';
|
||||||
|
import 'file_service.dart';
|
||||||
|
|
||||||
class UserService {
|
class UserService {
|
||||||
|
final FileService _fileService = locator<FileService>();
|
||||||
final UserRepository _userRepository = locator<UserRepository>();
|
final UserRepository _userRepository = locator<UserRepository>();
|
||||||
|
|
||||||
Future createApiKey(String url, String username, String password, String accessLevel, String comment) async {
|
Future createApiKey(String url, String username, String password, String accessLevel, String comment) async {
|
||||||
return await _userRepository.createApiKey(url, username, password, accessLevel, comment);
|
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;
|
String errorMessage;
|
||||||
|
|
||||||
void init() {
|
void init() {
|
||||||
this._refreshTriggerSubscription = _refreshService.refreshHistoryController.stream.listen((event) {
|
_refreshTriggerSubscription = _refreshService.refreshHistoryController.stream.listen((event) {
|
||||||
if (event == RefreshEvent.RefreshHistory) {
|
if (event == RefreshEvent.RefreshHistory) {
|
||||||
_logger.d('History needs a refresh');
|
_logger.d('History needs a refresh');
|
||||||
getHistory();
|
getHistory();
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_translate/flutter_translate.dart';
|
import 'package:flutter_translate/flutter_translate.dart';
|
||||||
import 'package:logger/logger.dart';
|
import 'package:logger/logger.dart';
|
||||||
import 'package:validators/sanitizers.dart';
|
import 'package:validators/sanitizers.dart';
|
||||||
import 'package:validators/validators.dart';
|
import 'package:validators/validators.dart';
|
||||||
|
|
||||||
import '../../core/services/file_service.dart';
|
|
||||||
import '../../core/services/session_service.dart';
|
import '../../core/services/session_service.dart';
|
||||||
import '../../core/services/storage_service.dart';
|
import '../../core/services/storage_service.dart';
|
||||||
import '../../locator.dart';
|
import '../../locator.dart';
|
||||||
|
@ -14,7 +13,6 @@ import '../enums/error_code.dart';
|
||||||
import '../enums/viewstate.dart';
|
import '../enums/viewstate.dart';
|
||||||
import '../error/rest_service_exception.dart';
|
import '../error/rest_service_exception.dart';
|
||||||
import '../error/service_exception.dart';
|
import '../error/service_exception.dart';
|
||||||
import '../models/rest/config.dart';
|
|
||||||
import '../models/rest/create_apikey_response.dart';
|
import '../models/rest/create_apikey_response.dart';
|
||||||
import '../services/user_service.dart';
|
import '../services/user_service.dart';
|
||||||
import '../util/logger.dart';
|
import '../util/logger.dart';
|
||||||
|
@ -22,23 +20,34 @@ import 'base_model.dart';
|
||||||
|
|
||||||
class LoginModel extends BaseModel {
|
class LoginModel extends BaseModel {
|
||||||
TextEditingController _uriController = new TextEditingController();
|
TextEditingController _uriController = new TextEditingController();
|
||||||
|
|
||||||
final TextEditingController _userNameController = new TextEditingController();
|
final TextEditingController _userNameController = new TextEditingController();
|
||||||
final TextEditingController _passwordController = new TextEditingController();
|
final TextEditingController _passwordController = new TextEditingController();
|
||||||
|
|
||||||
|
final TextEditingController _apiKeyController = new TextEditingController();
|
||||||
|
|
||||||
TextEditingController get uriController => _uriController;
|
TextEditingController get uriController => _uriController;
|
||||||
|
|
||||||
TextEditingController get userNameController => _userNameController;
|
TextEditingController get userNameController => _userNameController;
|
||||||
|
|
||||||
TextEditingController get passwordController => _passwordController;
|
TextEditingController get passwordController => _passwordController;
|
||||||
|
|
||||||
|
TextEditingController get apiKeyController => _apiKeyController;
|
||||||
|
|
||||||
final SessionService _sessionService = locator<SessionService>();
|
final SessionService _sessionService = locator<SessionService>();
|
||||||
final StorageService _storageService = locator<StorageService>();
|
final StorageService _storageService = locator<StorageService>();
|
||||||
final UserService _userService = locator<UserService>();
|
final UserService _userService = locator<UserService>();
|
||||||
final FileService _fileService = locator<FileService>();
|
|
||||||
final Logger _logger = getLogger();
|
final Logger _logger = getLogger();
|
||||||
|
|
||||||
|
bool useCredentialsLogin = true;
|
||||||
String errorMessage;
|
String errorMessage;
|
||||||
|
|
||||||
|
void toggleLoginMethod() {
|
||||||
|
setState(ViewState.Busy);
|
||||||
|
useCredentialsLogin = !useCredentialsLogin;
|
||||||
|
setState(ViewState.Idle);
|
||||||
|
}
|
||||||
|
|
||||||
void init() async {
|
void init() async {
|
||||||
bool hasLastUrl = await _storageService.hasLastUrl();
|
bool hasLastUrl = await _storageService.hasLastUrl();
|
||||||
|
|
||||||
|
@ -54,9 +63,13 @@ class LoginModel extends BaseModel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> login(String url, String username, String password) async {
|
Future<bool> login() async {
|
||||||
setState(ViewState.Busy);
|
var url = uriController.text;
|
||||||
|
var username = userNameController.text;
|
||||||
|
var password = passwordController.text;
|
||||||
|
var apiKey = apiKeyController.text;
|
||||||
|
|
||||||
|
setState(ViewState.Busy);
|
||||||
url = trim(url);
|
url = trim(url);
|
||||||
username = trim(username);
|
username = trim(username);
|
||||||
|
|
||||||
|
@ -79,24 +92,37 @@ class LoginModel extends BaseModel {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (username.isEmpty) {
|
if (useCredentialsLogin) {
|
||||||
errorMessage = translate('login.errors.empty_username');
|
if (username.isEmpty) {
|
||||||
setState(ViewState.Idle);
|
errorMessage = translate('login.errors.empty_username');
|
||||||
return false;
|
setState(ViewState.Idle);
|
||||||
}
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (password.isEmpty) {
|
if (password.isEmpty) {
|
||||||
errorMessage = translate('login.errors.empty_password');
|
errorMessage = translate('login.errors.empty_password');
|
||||||
setState(ViewState.Idle);
|
setState(ViewState.Idle);
|
||||||
return false;
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (apiKey.isEmpty) {
|
||||||
|
errorMessage = translate('login.errors.empty_apikey');
|
||||||
|
setState(ViewState.Idle);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var success = false;
|
var success = false;
|
||||||
try {
|
try {
|
||||||
Config config = await _fileService.getConfig(url);
|
if (useCredentialsLogin) {
|
||||||
CreateApiKeyResponse apiKeyResponse =
|
CreateApiKeyResponse apiKeyResponse = await _userService.createApiKey(
|
||||||
await _userService.createApiKey(url, username, password, 'apikey', 'fbmobile');
|
url, username, password, 'apikey', 'fbmobile-${new DateTime.now().millisecondsSinceEpoch}');
|
||||||
success = await _sessionService.login(url, apiKeyResponse.data['new_key'], config);
|
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;
|
errorMessage = null;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e is RestServiceException) {
|
if (e is RestServiceException) {
|
||||||
|
@ -112,6 +138,8 @@ class LoginModel extends BaseModel {
|
||||||
} else {
|
} else {
|
||||||
errorMessage = translate('api.general_rest_error');
|
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) {
|
} else if (e is ServiceException && e.code == ErrorCode.SOCKET_ERROR) {
|
||||||
errorMessage = translate('api.socket_error');
|
errorMessage = translate('api.socket_error');
|
||||||
} else if (e is ServiceException && e.code == ErrorCode.SOCKET_TIMEOUT) {
|
} 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:flutter_translate/flutter_translate.dart';
|
||||||
|
import 'package:logger/logger.dart';
|
||||||
|
|
||||||
import '../../core/services/session_service.dart';
|
import '../../core/services/session_service.dart';
|
||||||
import '../../locator.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/dialog_service.dart';
|
||||||
|
import '../services/file_service.dart';
|
||||||
import '../services/link_service.dart';
|
import '../services/link_service.dart';
|
||||||
|
import '../util/formatter_util.dart';
|
||||||
|
import '../util/logger.dart';
|
||||||
import 'base_model.dart';
|
import 'base_model.dart';
|
||||||
|
|
||||||
class ProfileModel extends BaseModel {
|
class ProfileModel extends BaseModel {
|
||||||
final SessionService _sessionService = locator<SessionService>();
|
final SessionService _sessionService = locator<SessionService>();
|
||||||
final DialogService _dialogService = locator<DialogService>();
|
final DialogService _dialogService = locator<DialogService>();
|
||||||
final LinkService _linkService = locator<LinkService>();
|
final LinkService _linkService = locator<LinkService>();
|
||||||
|
final FileService _fileService = locator<FileService>();
|
||||||
|
final Logger _logger = getLogger();
|
||||||
|
|
||||||
|
String errorMessage;
|
||||||
|
|
||||||
Future logout() async {
|
Future logout() async {
|
||||||
var dialogResult = await _dialogService.showConfirmationDialog(
|
var dialogResult = await _dialogService.showConfirmationDialog(
|
||||||
|
@ -26,6 +41,54 @@ class ProfileModel extends BaseModel {
|
||||||
description: translate('profile.revealed_api_key.description', args: {'apiKey': apiKey}));
|
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) {
|
void openLink(String link) {
|
||||||
_linkService.open(link);
|
_linkService.open(link);
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ import 'locator.dart';
|
||||||
/// main entry point used to configure log level, locales, ...
|
/// main entry point used to configure log level, locales, ...
|
||||||
void main() async {
|
void main() async {
|
||||||
setupLogger(Level.info);
|
setupLogger(Level.info);
|
||||||
// setupLogger(Level.debug);
|
// setupLogger(Level.debug);
|
||||||
setupLocator();
|
setupLocator();
|
||||||
|
|
||||||
var delegate = await LocalizationDelegate.create(fallbackLocale: 'en', supportedLocales: ['en']);
|
var delegate = await LocalizationDelegate.create(fallbackLocale: 'en', supportedLocales: ['en']);
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
const Color backgroundColor = Colors.white;
|
const Color backgroundColor = whiteColor;
|
||||||
|
|
||||||
/// Colors
|
/// Colors
|
||||||
const Color primaryBackgroundColor = Colors.white;
|
const Color primaryBackgroundColor = whiteColor;
|
||||||
|
|
||||||
const Map<int, Color> colors = {
|
const Map<int, Color> colors = {
|
||||||
50: Color.fromRGBO(63, 69, 75, .1),
|
50: Color.fromRGBO(63, 69, 75, .1),
|
||||||
|
@ -20,4 +20,10 @@ const Map<int, Color> colors = {
|
||||||
const MaterialColor myColor = MaterialColor(0xFF3F454B, colors);
|
const MaterialColor myColor = MaterialColor(0xFF3F454B, colors);
|
||||||
const Color primaryAccentColor = myColor;
|
const Color primaryAccentColor = myColor;
|
||||||
const Color buttonBackgroundColor = primaryAccentColor;
|
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),
|
padding: EdgeInsets.all(0),
|
||||||
child: ListView(
|
child: ListView(
|
||||||
shrinkWrap: true,
|
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>[
|
children: <Widget>[
|
||||||
UIHelper.verticalSpaceMedium(),
|
UIHelper.verticalSpaceMedium(),
|
||||||
Center(child: logo),
|
Center(child: logo),
|
||||||
|
|
|
@ -67,13 +67,13 @@ class HistoryView extends StatelessWidget {
|
||||||
var copyWidget = ListTile(
|
var copyWidget = ListTile(
|
||||||
title: Text(translate('history.copy_link.description')),
|
title: Text(translate('history.copy_link.description')),
|
||||||
trailing: IconButton(
|
trailing: IconButton(
|
||||||
icon: Icon(Icons.copy, color: Colors.blue, textDirection: TextDirection.ltr),
|
icon: Icon(Icons.copy, color: blueColor, textDirection: TextDirection.ltr),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
FlutterClipboard.copy(fullPasteUrl).then((value) {
|
FlutterClipboard.copy(fullPasteUrl).then((value) {
|
||||||
final snackBar = SnackBar(
|
final snackBar = SnackBar(
|
||||||
action: SnackBarAction(
|
action: SnackBarAction(
|
||||||
label: translate('history.copy_link.dismiss'),
|
label: translate('history.copy_link.dismiss'),
|
||||||
textColor: Colors.blue,
|
textColor: blueColor,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
ScaffoldMessenger.of(context).hideCurrentSnackBar();
|
ScaffoldMessenger.of(context).hideCurrentSnackBar();
|
||||||
},
|
},
|
||||||
|
@ -88,7 +88,7 @@ class HistoryView extends StatelessWidget {
|
||||||
var deleteWidget = ListTile(
|
var deleteWidget = ListTile(
|
||||||
title: Text(translate('history.delete')),
|
title: Text(translate('history.delete')),
|
||||||
trailing: IconButton(
|
trailing: IconButton(
|
||||||
icon: Icon(Icons.delete, color: Colors.red),
|
icon: Icon(Icons.delete, color: redColor),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
return model.deletePaste(paste.id);
|
return model.deletePaste(paste.id);
|
||||||
}));
|
}));
|
||||||
|
@ -135,14 +135,14 @@ class HistoryView extends StatelessWidget {
|
||||||
iconPlacement: ExpandablePanelIconPlacement.right,
|
iconPlacement: ExpandablePanelIconPlacement.right,
|
||||||
headerAlignment: ExpandablePanelHeaderAlignment.center,
|
headerAlignment: ExpandablePanelHeaderAlignment.center,
|
||||||
hasIcon: true,
|
hasIcon: true,
|
||||||
iconColor: Colors.blue,
|
iconColor: blueColor,
|
||||||
tapHeaderToExpand: true),
|
tapHeaderToExpand: true),
|
||||||
child: ExpandablePanel(
|
child: ExpandablePanel(
|
||||||
header: InkWell(
|
header: InkWell(
|
||||||
onLongPress: () => model.deletePaste(paste.id),
|
onLongPress: () => model.deletePaste(paste.id),
|
||||||
child: Text(
|
child: Text(
|
||||||
paste.id,
|
paste.id,
|
||||||
style: TextStyle(color: Colors.blue),
|
style: TextStyle(color: blueColor),
|
||||||
textAlign: TextAlign.left,
|
textAlign: TextAlign.left,
|
||||||
)),
|
)),
|
||||||
expanded: Column(
|
expanded: Column(
|
||||||
|
@ -158,7 +158,7 @@ class HistoryView extends StatelessWidget {
|
||||||
trailing: Wrap(children: [
|
trailing: Wrap(children: [
|
||||||
openInBrowserButton,
|
openInBrowserButton,
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: Icon(Icons.share, color: Colors.blue, textDirection: TextDirection.ltr),
|
icon: Icon(Icons.share, color: blueColor, textDirection: TextDirection.ltr),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
return Share.share(fullPasteUrl);
|
return Share.share(fullPasteUrl);
|
||||||
})
|
})
|
||||||
|
@ -184,7 +184,7 @@ class HistoryView extends StatelessWidget {
|
||||||
|
|
||||||
Widget _renderOpenInBrowser(HistoryModel model, String url) {
|
Widget _renderOpenInBrowser(HistoryModel model, String url) {
|
||||||
return IconButton(
|
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: () {
|
onPressed: () {
|
||||||
return model.openLink(url);
|
return model.openLink(url);
|
||||||
});
|
});
|
||||||
|
|
|
@ -10,17 +10,14 @@ import '../../ui/shared/text_styles.dart';
|
||||||
import '../../ui/views/home_view.dart';
|
import '../../ui/views/home_view.dart';
|
||||||
import '../../ui/widgets/my_appbar.dart';
|
import '../../ui/widgets/my_appbar.dart';
|
||||||
import '../shared/app_colors.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';
|
import 'base_view.dart';
|
||||||
|
|
||||||
class LoginView extends StatefulWidget {
|
class LoginView extends StatelessWidget {
|
||||||
static const routeName = '/login';
|
static const routeName = '/login';
|
||||||
|
|
||||||
@override
|
|
||||||
_LoginViewState createState() => _LoginViewState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _LoginViewState extends State<LoginView> {
|
|
||||||
final NavigationService _navigationService = locator<NavigationService>();
|
final NavigationService _navigationService = locator<NavigationService>();
|
||||||
final DialogService _dialogService = locator<DialogService>();
|
final DialogService _dialogService = locator<DialogService>();
|
||||||
|
|
||||||
|
@ -30,7 +27,7 @@ class _LoginViewState extends State<LoginView> {
|
||||||
tag: 'hero',
|
tag: 'hero',
|
||||||
child: CircleAvatar(
|
child: CircleAvatar(
|
||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
radius: 96.0,
|
radius: 36.0,
|
||||||
child: Image.asset('assets/logo_caption.png'),
|
child: Image.asset('assets/logo_caption.png'),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -44,9 +41,11 @@ class _LoginViewState extends State<LoginView> {
|
||||||
? Center(child: CircularProgressIndicator())
|
? Center(child: CircularProgressIndicator())
|
||||||
: ListView(
|
: ListView(
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
padding: EdgeInsets.only(left: 24.0, right: 24.0),
|
padding: EdgeInsets.only(left: 10.0, right: 10.0),
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
|
UIHelper.verticalSpaceMedium(),
|
||||||
Center(child: logo),
|
Center(child: logo),
|
||||||
|
UIHelper.verticalSpaceMedium(),
|
||||||
Center(
|
Center(
|
||||||
child: Wrap(
|
child: Wrap(
|
||||||
crossAxisAlignment: WrapCrossAlignment.center,
|
crossAxisAlignment: WrapCrossAlignment.center,
|
||||||
|
@ -63,19 +62,32 @@ class _LoginViewState extends State<LoginView> {
|
||||||
title: translate('login.compatibility_dialog.title'),
|
title: translate('login.compatibility_dialog.title'),
|
||||||
description: translate('login.compatibility_dialog.body'));
|
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(),
|
||||||
validationMessage: model.errorMessage,
|
model.useCredentialsLogin
|
||||||
uriController: model.uriController,
|
? LoginCredentialsHeaders(
|
||||||
usernameController: model.userNameController,
|
validationMessage: model.errorMessage,
|
||||||
passwordController: model.passwordController,
|
uriController: model.uriController,
|
||||||
),
|
usernameController: model.userNameController,
|
||||||
|
passwordController: model.passwordController,
|
||||||
|
)
|
||||||
|
: LoginApiKeyHeaders(
|
||||||
|
validationMessage: model.errorMessage,
|
||||||
|
uriController: model.uriController,
|
||||||
|
apiKeyController: model.apiKeyController),
|
||||||
|
UIHelper.verticalSpaceMedium(),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
child: Text(translate('login.button'), style: TextStyle(color: buttonForegroundColor)),
|
child: Text(translate('login.button'), style: TextStyle(color: buttonForegroundColor)),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
var loginSuccess = await model.login(
|
var loginSuccess = await model.login();
|
||||||
model.uriController.text, model.userNameController.text, model.passwordController.text);
|
|
||||||
if (loginSuccess) {
|
if (loginSuccess) {
|
||||||
_navigationService.navigateAndReplaceTo(HomeView.routeName);
|
_navigationService.navigateAndReplaceTo(HomeView.routeName);
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,6 @@ import 'package:provider/provider.dart';
|
||||||
|
|
||||||
import '../../core/enums/viewstate.dart';
|
import '../../core/enums/viewstate.dart';
|
||||||
import '../../core/models/session.dart';
|
import '../../core/models/session.dart';
|
||||||
import '../../core/util/formatter_util.dart';
|
|
||||||
import '../../core/viewmodels/profile_model.dart';
|
import '../../core/viewmodels/profile_model.dart';
|
||||||
import '../shared/app_colors.dart';
|
import '../shared/app_colors.dart';
|
||||||
import '../shared/text_styles.dart';
|
import '../shared/text_styles.dart';
|
||||||
|
@ -20,65 +19,69 @@ class ProfileView extends StatelessWidget {
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
var url = Provider.of<Session>(context).url;
|
var url = Provider.of<Session>(context).url;
|
||||||
var apiKey = Provider.of<Session>(context).apiKey;
|
var apiKey = Provider.of<Session>(context).apiKey;
|
||||||
var config = Provider.of<Session>(context).config;
|
|
||||||
|
|
||||||
return BaseView<ProfileModel>(
|
return BaseView<ProfileModel>(
|
||||||
builder: (context, model, child) => Scaffold(
|
builder: (context, model, child) => Scaffold(
|
||||||
appBar: MyAppBar(title: Text(translate('titles.profile'))),
|
appBar: MyAppBar(title: Text(translate('titles.profile'))),
|
||||||
floatingActionButton: FloatingActionButton(
|
backgroundColor: backgroundColor,
|
||||||
heroTag: "logoutButton",
|
body: model.state == ViewState.Busy
|
||||||
child: Icon(Icons.exit_to_app),
|
? Center(child: CircularProgressIndicator())
|
||||||
backgroundColor: primaryAccentColor,
|
: ListView(
|
||||||
onPressed: () {
|
children: <Widget>[
|
||||||
model.logout();
|
UIHelper.verticalSpaceMedium(),
|
||||||
},
|
Padding(
|
||||||
),
|
padding: const EdgeInsets.only(left: 25.0),
|
||||||
backgroundColor: backgroundColor,
|
child: Center(
|
||||||
body: model.state == ViewState.Busy
|
child: Text(
|
||||||
? Center(child: CircularProgressIndicator())
|
translate('profile.instance'),
|
||||||
: ListView(
|
style: subHeaderStyle,
|
||||||
children: <Widget>[
|
))),
|
||||||
UIHelper.verticalSpaceMedium(),
|
UIHelper.verticalSpaceMedium(),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(left: 25.0),
|
padding: const EdgeInsets.only(left: 25.0),
|
||||||
child: Text(
|
child: Center(
|
||||||
translate('profile.welcome'),
|
child: Linkify(
|
||||||
style: headerStyle,
|
onOpen: (link) => model.openLink(link.url),
|
||||||
),
|
text: translate('profile.connection', args: {'url': url}),
|
||||||
),
|
options: LinkifyOptions(humanize: false),
|
||||||
UIHelper.verticalSpaceMedium(),
|
))),
|
||||||
Padding(
|
UIHelper.verticalSpaceMedium(),
|
||||||
padding: const EdgeInsets.only(left: 25.0),
|
Padding(
|
||||||
child: Linkify(
|
padding: const EdgeInsets.only(left: 25.0, right: 25.0),
|
||||||
onOpen: (link) => model.openLink(link.url),
|
child: ElevatedButton.icon(
|
||||||
text: translate('profile.connection', args: {'url': url}),
|
icon: Icon(Icons.settings, color: blueColor),
|
||||||
options: LinkifyOptions(humanize: false),
|
label: Text(
|
||||||
)),
|
translate('profile.show_config'),
|
||||||
UIHelper.verticalSpaceMedium(),
|
style: TextStyle(color: buttonForegroundColor),
|
||||||
Padding(
|
),
|
||||||
padding: const EdgeInsets.only(left: 25.0, right: 25.0),
|
onPressed: () {
|
||||||
child: ElevatedButton.icon(
|
return model.showConfig(url);
|
||||||
icon: Icon(Icons.remove_red_eye, color: Colors.blue),
|
})),
|
||||||
label: Text(
|
UIHelper.verticalSpaceMedium(),
|
||||||
translate('profile.reveal_api_key'),
|
Padding(
|
||||||
style: TextStyle(color: buttonForegroundColor),
|
padding: const EdgeInsets.only(left: 25.0, right: 25.0),
|
||||||
),
|
child: ElevatedButton.icon(
|
||||||
onPressed: () {
|
icon: Icon(Icons.lock, color: orangeColor),
|
||||||
return model.revealApiKey(apiKey);
|
label: Text(
|
||||||
})),
|
translate('profile.reveal_api_key'),
|
||||||
UIHelper.verticalSpaceMedium(),
|
style: TextStyle(color: buttonForegroundColor),
|
||||||
Padding(
|
),
|
||||||
padding: const EdgeInsets.only(left: 25.0),
|
onPressed: () {
|
||||||
child: Text(
|
return model.revealApiKey(apiKey);
|
||||||
translate('profile.config', args: {
|
})),
|
||||||
'uploadMaxSize': FormatterUtil.formatBytes(config.uploadMaxSize, 2),
|
UIHelper.verticalSpaceMedium(),
|
||||||
'maxFilesPerRequest': config.maxFilesPerRequest,
|
Padding(
|
||||||
'maxInputVars': config.maxInputVars,
|
padding: const EdgeInsets.only(left: 25.0, right: 25.0),
|
||||||
'requestMaxSize': FormatterUtil.formatBytes(config.requestMaxSize, 2)
|
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/enums/viewstate.dart';
|
||||||
import '../../core/viewmodels/startup_model.dart';
|
import '../../core/viewmodels/startup_model.dart';
|
||||||
|
import '../shared/app_colors.dart';
|
||||||
|
|
||||||
class StartUpView extends StatelessWidget {
|
class StartUpView extends StatelessWidget {
|
||||||
static const routeName = '/';
|
static const routeName = '/';
|
||||||
|
@ -13,7 +14,7 @@ class StartUpView extends StatelessWidget {
|
||||||
viewModelBuilder: () => StartUpViewModel(),
|
viewModelBuilder: () => StartUpViewModel(),
|
||||||
onModelReady: (model) => model.handleStartUpLogic(),
|
onModelReady: (model) => model.handleStartUpLogic(),
|
||||||
builder: (context, model, child) => Scaffold(
|
builder: (context, model, child) => Scaffold(
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: whiteColor,
|
||||||
body: model.state == ViewState.Busy
|
body: model.state == ViewState.Busy
|
||||||
? Center(
|
? Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
|
|
|
@ -18,7 +18,15 @@ class AnonymousTabBarState extends State<AnonymousTabBarView> with SingleTickerP
|
||||||
List<Widget> _tabPages = [LoginView()];
|
List<Widget> _tabPages = [LoginView()];
|
||||||
List<bool> _hasInit = [true];
|
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
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
@ -49,7 +57,7 @@ class AnonymousTabBarState extends State<AnonymousTabBarView> with SingleTickerP
|
||||||
bottomNavigationBar: BottomAppBar(
|
bottomNavigationBar: BottomAppBar(
|
||||||
child: TabBar(
|
child: TabBar(
|
||||||
labelColor: primaryAccentColor,
|
labelColor: primaryAccentColor,
|
||||||
indicatorColor: Colors.blue,
|
indicatorColor: blueColor,
|
||||||
indicatorWeight: 3.0,
|
indicatorWeight: 3.0,
|
||||||
tabs: _tabsButton,
|
tabs: _tabsButton,
|
||||||
controller: _tabController,
|
controller: _tabController,
|
||||||
|
|
|
@ -52,37 +52,47 @@ class AuthenticatedTabBarState extends State<AuthenticatedTabBarView> with Singl
|
||||||
double yourWidth = width / 3;
|
double yourWidth = width / 3;
|
||||||
double yourHeight = 55;
|
double yourHeight = 55;
|
||||||
|
|
||||||
|
Color colorTabItem0 = _currentTabIndex == 0 ? blueColor : primaryAccentColor;
|
||||||
|
Color colorTabItem1 = _currentTabIndex == 1 ? blueColor : primaryAccentColor;
|
||||||
|
Color colorTabItem2 = _currentTabIndex == 2 ? blueColor : primaryAccentColor;
|
||||||
|
|
||||||
List<Widget> _tabsButton = [
|
List<Widget> _tabsButton = [
|
||||||
Container(
|
Container(
|
||||||
width: yourWidth,
|
width: yourWidth,
|
||||||
height: yourHeight,
|
height: yourHeight,
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
child: Tab(
|
child: Tab(
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
Icons.upload_file,
|
_currentTabIndex == 0 ? Icons.upload_outlined : Icons.upload_rounded,
|
||||||
color: _currentTabIndex == 0 ? Colors.blue : primaryAccentColor,
|
color: colorTabItem0,
|
||||||
),
|
),
|
||||||
text: translate('tabs.upload'))),
|
child: Text(translate('tabs.upload'), style: TextStyle(color: colorTabItem0)),
|
||||||
|
),
|
||||||
|
),
|
||||||
Container(
|
Container(
|
||||||
width: yourWidth,
|
width: yourWidth,
|
||||||
height: yourHeight,
|
height: yourHeight,
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
child: Tab(
|
child: Tab(
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
Icons.history,
|
_currentTabIndex == 1 ? Icons.history_outlined : Icons.history_rounded,
|
||||||
color: _currentTabIndex == 1 ? Colors.blue : primaryAccentColor,
|
color: colorTabItem1,
|
||||||
),
|
),
|
||||||
text: translate('tabs.history'))),
|
child: Text(translate('tabs.history'), style: TextStyle(color: colorTabItem1)),
|
||||||
|
),
|
||||||
|
),
|
||||||
Container(
|
Container(
|
||||||
width: yourWidth,
|
width: yourWidth,
|
||||||
height: yourHeight,
|
height: yourHeight,
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
child: Tab(
|
child: Tab(
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
Icons.person,
|
_currentTabIndex == 2 ? Icons.person_outlined : Icons.person_rounded,
|
||||||
color: _currentTabIndex == 2 ? Colors.blue : primaryAccentColor,
|
color: colorTabItem2,
|
||||||
),
|
),
|
||||||
text: translate('tabs.profile'))),
|
child: Text(translate('tabs.profile'), style: TextStyle(color: colorTabItem2)),
|
||||||
|
),
|
||||||
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
|
@ -91,7 +101,7 @@ class AuthenticatedTabBarState extends State<AuthenticatedTabBarView> with Singl
|
||||||
child: TabBar(
|
child: TabBar(
|
||||||
indicatorSize: TabBarIndicatorSize.label,
|
indicatorSize: TabBarIndicatorSize.label,
|
||||||
labelColor: primaryAccentColor,
|
labelColor: primaryAccentColor,
|
||||||
indicatorColor: Colors.blue,
|
indicatorColor: blueColor,
|
||||||
indicatorWeight: 3.0,
|
indicatorWeight: 3.0,
|
||||||
labelPadding: EdgeInsets.all(0),
|
labelPadding: EdgeInsets.all(0),
|
||||||
tabs: _tabsButton,
|
tabs: _tabsButton,
|
||||||
|
|
|
@ -72,14 +72,14 @@ class UploadView extends StatelessWidget {
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
ElevatedButton.icon(
|
ElevatedButton.icon(
|
||||||
icon: Icon(Icons.file_copy_sharp, color: Colors.blue),
|
icon: Icon(Icons.file_copy_sharp, color: blueColor),
|
||||||
onPressed: () => model.openFileExplorer(),
|
onPressed: () => model.openFileExplorer(),
|
||||||
label: Text(
|
label: Text(
|
||||||
translate('upload.open_file_explorer'),
|
translate('upload.open_file_explorer'),
|
||||||
style: TextStyle(color: buttonForegroundColor),
|
style: TextStyle(color: buttonForegroundColor),
|
||||||
)),
|
)),
|
||||||
ElevatedButton.icon(
|
ElevatedButton.icon(
|
||||||
icon: Icon(Icons.cancel, color: Colors.orange),
|
icon: Icon(Icons.cancel, color: orangeColor),
|
||||||
onPressed: model.paths != null && model.paths.length > 0
|
onPressed: model.paths != null && model.paths.length > 0
|
||||||
? () => model.clearCachedFiles()
|
? () => model.clearCachedFiles()
|
||||||
: null,
|
: null,
|
||||||
|
@ -123,7 +123,7 @@ class UploadView extends StatelessWidget {
|
||||||
final snackBar = SnackBar(
|
final snackBar = SnackBar(
|
||||||
action: SnackBarAction(
|
action: SnackBarAction(
|
||||||
label: translate('upload.dismiss'),
|
label: translate('upload.dismiss'),
|
||||||
textColor: Colors.blue,
|
textColor: blueColor,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
ScaffoldMessenger.of(context).hideCurrentSnackBar();
|
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(
|
label: Text(
|
||||||
translate('upload.upload'),
|
translate('upload.upload'),
|
||||||
style: TextStyle(color: buttonForegroundColor),
|
style: TextStyle(color: buttonForegroundColor),
|
||||||
|
|
|
@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
||||||
import '../../core/services/navigation_service.dart';
|
import '../../core/services/navigation_service.dart';
|
||||||
import '../../locator.dart';
|
import '../../locator.dart';
|
||||||
import '../../ui/views/about_view.dart';
|
import '../../ui/views/about_view.dart';
|
||||||
|
import '../shared/app_colors.dart';
|
||||||
|
|
||||||
class AboutIconButton extends StatelessWidget {
|
class AboutIconButton extends StatelessWidget {
|
||||||
AboutIconButton();
|
AboutIconButton();
|
||||||
|
@ -13,7 +14,7 @@ class AboutIconButton extends StatelessWidget {
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return IconButton(
|
return IconButton(
|
||||||
icon: Icon(Icons.help),
|
icon: Icon(Icons.help),
|
||||||
color: Colors.white,
|
color: whiteColor,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
_navigationService.navigateTo(AboutView.routeName);
|
_navigationService.navigateTo(AboutView.routeName);
|
||||||
});
|
});
|
||||||
|
|
|
@ -21,7 +21,7 @@ class CenteredErrorRow extends StatelessWidget {
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: <Widget>[
|
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
|
(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/material.dart';
|
||||||
import 'package:flutter_translate/flutter_translate.dart';
|
|
||||||
|
|
||||||
class LoginHeaders extends StatelessWidget {
|
import '../shared/app_colors.dart';
|
||||||
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),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class LoginTextField extends StatelessWidget {
|
class LoginTextField extends StatelessWidget {
|
||||||
final TextEditingController controller;
|
final TextEditingController controller;
|
||||||
|
@ -45,7 +19,7 @@ class LoginTextField extends StatelessWidget {
|
||||||
margin: EdgeInsets.symmetric(horizontal: 10.0, vertical: 10.0),
|
margin: EdgeInsets.symmetric(horizontal: 10.0, vertical: 10.0),
|
||||||
height: 50.0,
|
height: 50.0,
|
||||||
alignment: Alignment.centerLeft,
|
alignment: Alignment.centerLeft,
|
||||||
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(10.0)),
|
decoration: BoxDecoration(color: whiteColor, borderRadius: BorderRadius.circular(10.0)),
|
||||||
child: TextFormField(
|
child: TextFormField(
|
||||||
keyboardType: keyboardType,
|
keyboardType: keyboardType,
|
||||||
obscureText: obscureText,
|
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.
|
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
|
||||||
# Read more about iOS versioning at
|
# Read more about iOS versioning at
|
||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
version: 1.2.2+8
|
version: 1.3.0+9
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=2.7.0 <3.0.0"
|
sdk: ">=2.7.0 <3.0.0"
|
||||||
|
|
Loading…
Reference in a new issue