mirror of
https://github.com/Xevion/paperless-mobile.git
synced 2025-12-15 04:12:30 -06:00
Updated onboarding, reformatted files, improved referenced documents view, updated error handling
This commit is contained in:
@@ -9,13 +9,20 @@ class ConnectivityCubit extends Cubit<ConnectivityState> {
|
||||
final ConnectivityStatusService connectivityStatusService;
|
||||
late final StreamSubscription<bool> _sub;
|
||||
|
||||
ConnectivityCubit(this.connectivityStatusService) : super(ConnectivityState.undefined);
|
||||
ConnectivityCubit(this.connectivityStatusService)
|
||||
: super(ConnectivityState.undefined);
|
||||
|
||||
Future<void> initialize() async {
|
||||
final bool isConnected = await connectivityStatusService.isConnectedToInternet();
|
||||
emit(isConnected ? ConnectivityState.connected : ConnectivityState.notConnected);
|
||||
_sub = connectivityStatusService.connectivityChanges().listen((isConnected) {
|
||||
emit(isConnected ? ConnectivityState.connected : ConnectivityState.notConnected);
|
||||
final bool isConnected =
|
||||
await connectivityStatusService.isConnectedToInternet();
|
||||
emit(isConnected
|
||||
? ConnectivityState.connected
|
||||
: ConnectivityState.notConnected);
|
||||
_sub =
|
||||
connectivityStatusService.connectivityChanges().listen((isConnected) {
|
||||
emit(isConnected
|
||||
? ConnectivityState.connected
|
||||
: ConnectivityState.notConnected);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
46
lib/core/bloc/global_error_cubit.dart
Normal file
46
lib/core/bloc/global_error_cubit.dart
Normal file
@@ -0,0 +1,46 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:paperless_mobile/core/model/error_message.dart';
|
||||
|
||||
///
|
||||
/// Class for handling generic errors which usually only require to inform the user via a Snackbar
|
||||
/// or similar that an error has occurred.
|
||||
///
|
||||
@singleton
|
||||
class GlobalErrorCubit extends Cubit<GlobalErrorState> {
|
||||
static const _waitBeforeNextErrorDuration = Duration(seconds: 5);
|
||||
|
||||
GlobalErrorCubit() : super(GlobalErrorState.initial);
|
||||
|
||||
///
|
||||
/// Adds a new error to this bloc. If the new error is equal to the current error, the new error
|
||||
/// will not be published unless the previous error occured over 5 seconds ago.
|
||||
///
|
||||
void add(ErrorMessage error) {
|
||||
final now = DateTime.now();
|
||||
if (error != state.error || (error == state.error && _canEmitNewError())) {
|
||||
emit(GlobalErrorState(error: error, errorTimestamp: now));
|
||||
}
|
||||
}
|
||||
|
||||
bool _canEmitNewError() {
|
||||
if (state.errorTimestamp != null) {
|
||||
return DateTime.now().difference(state.errorTimestamp!).inSeconds >= 5;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void reset() {
|
||||
emit(GlobalErrorState.initial);
|
||||
}
|
||||
}
|
||||
|
||||
class GlobalErrorState {
|
||||
static const GlobalErrorState initial = GlobalErrorState();
|
||||
final ErrorMessage? error;
|
||||
final DateTime? errorTimestamp;
|
||||
|
||||
const GlobalErrorState({this.error, this.errorTimestamp});
|
||||
|
||||
bool get hasError => error != null;
|
||||
}
|
||||
@@ -1,40 +1,75 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:paperless_mobile/core/bloc/global_error_cubit.dart';
|
||||
import 'package:paperless_mobile/core/model/error_message.dart';
|
||||
import 'package:paperless_mobile/features/labels/model/label.model.dart';
|
||||
import 'package:paperless_mobile/features/labels/repository/label_repository.dart';
|
||||
|
||||
abstract class LabelCubit<T extends Label> extends Cubit<Map<int, T>> {
|
||||
final LabelRepository labelRepository;
|
||||
LabelCubit(this.labelRepository) : super({});
|
||||
final GlobalErrorCubit errorCubit;
|
||||
|
||||
LabelCubit(this.labelRepository, this.errorCubit) : super({});
|
||||
|
||||
@protected
|
||||
void loadFrom(Iterable<T> items) => emit(Map.fromIterable(items, key: (e) => (e as T).id!));
|
||||
void loadFrom(Iterable<T> items) =>
|
||||
emit(Map.fromIterable(items, key: (e) => (e as T).id!));
|
||||
|
||||
Future<T> add(T item) async {
|
||||
Future<T> add(
|
||||
T item, {
|
||||
bool propagateEventOnError = true,
|
||||
}) async {
|
||||
assert(item.id == null);
|
||||
final addedItem = await save(item);
|
||||
final newState = {...state};
|
||||
newState.putIfAbsent(addedItem.id!, () => addedItem);
|
||||
emit(newState);
|
||||
return addedItem;
|
||||
try {
|
||||
final addedItem = await save(item);
|
||||
final newState = {...state};
|
||||
newState.putIfAbsent(addedItem.id!, () => addedItem);
|
||||
emit(newState);
|
||||
return addedItem;
|
||||
} on ErrorMessage catch (error) {
|
||||
if (propagateEventOnError) {
|
||||
errorCubit.add(error);
|
||||
}
|
||||
return Future.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
Future<T> replace(T item) async {
|
||||
Future<T> replace(
|
||||
T item, {
|
||||
bool propagateEventOnError = true,
|
||||
}) async {
|
||||
assert(item.id != null);
|
||||
final updatedItem = await update(item);
|
||||
final newState = {...state};
|
||||
newState[item.id!] = updatedItem;
|
||||
emit(newState);
|
||||
return updatedItem;
|
||||
try {
|
||||
final updatedItem = await update(item);
|
||||
final newState = {...state};
|
||||
newState[item.id!] = updatedItem;
|
||||
emit(newState);
|
||||
return updatedItem;
|
||||
} on ErrorMessage catch (error) {
|
||||
if (propagateEventOnError) {
|
||||
errorCubit.add(error);
|
||||
}
|
||||
return Future.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> remove(T item) async {
|
||||
Future<void> remove(
|
||||
T item, {
|
||||
bool propagateEventOnError = true,
|
||||
}) async {
|
||||
assert(item.id != null);
|
||||
if (state.containsKey(item.id)) {
|
||||
final deletedId = await delete(item);
|
||||
final newState = {...state};
|
||||
newState.remove(deletedId);
|
||||
emit(newState);
|
||||
try {
|
||||
final deletedId = await delete(item);
|
||||
final newState = {...state};
|
||||
newState.remove(deletedId);
|
||||
emit(newState);
|
||||
} on ErrorMessage catch (error) {
|
||||
if (propagateEventOnError) {
|
||||
errorCubit.add(error);
|
||||
}
|
||||
return Future.error(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,13 +22,19 @@ class AuthenticationInterceptor implements InterceptorContract {
|
||||
}
|
||||
return request.copyWith(
|
||||
//Append server Url
|
||||
url: Uri.parse(authState.authentication!.serverUrl + request.url.toString()),
|
||||
url: Uri.parse(
|
||||
authState.authentication!.serverUrl + request.url.toString()),
|
||||
headers: authState.authentication!.token.isEmpty
|
||||
? request.headers
|
||||
: {...request.headers, 'Authorization': 'Token ${authState.authentication!.token}'},
|
||||
: {
|
||||
...request.headers,
|
||||
'Authorization': 'Token ${authState.authentication!.token}'
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<BaseResponse> interceptResponse({required BaseResponse response}) async => response;
|
||||
Future<BaseResponse> interceptResponse(
|
||||
{required BaseResponse response}) async =>
|
||||
response;
|
||||
}
|
||||
|
||||
@@ -4,24 +4,22 @@ import 'package:paperless_mobile/features/login/bloc/authentication_cubit.dart';
|
||||
import 'package:http_interceptor/http_interceptor.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
|
||||
@Deprecated("Delegated to TimeoutClient!")
|
||||
@injectable
|
||||
class ConnectionStateInterceptor implements InterceptorContract {
|
||||
final AuthenticationCubit authenticationCubit;
|
||||
final ConnectivityStatusService connectivityStatusService;
|
||||
ConnectionStateInterceptor(this.authenticationCubit, this.connectivityStatusService);
|
||||
|
||||
ConnectionStateInterceptor(
|
||||
this.authenticationCubit, this.connectivityStatusService);
|
||||
|
||||
@override
|
||||
Future<BaseRequest> interceptRequest({required BaseRequest request}) async {
|
||||
if (!(await connectivityStatusService.isConnectedToInternet())) {
|
||||
throw const ErrorMessage(ErrorCode.deviceOffline);
|
||||
}
|
||||
final isServerReachable = await connectivityStatusService.isServerReachable(request.url.origin);
|
||||
if (!isServerReachable) {
|
||||
throw const ErrorMessage(ErrorCode.serverUnreachable);
|
||||
}
|
||||
return request;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<BaseResponse> interceptResponse({required BaseResponse response}) async => response;
|
||||
Future<BaseResponse> interceptResponse(
|
||||
{required BaseResponse response}) async =>
|
||||
response;
|
||||
}
|
||||
|
||||
@@ -14,12 +14,15 @@ class LanguageHeaderInterceptor implements InterceptorContract {
|
||||
if (appSettingsCubit.state.preferredLocaleSubtag == "en") {
|
||||
languages = "en";
|
||||
} else {
|
||||
languages = appSettingsCubit.state.preferredLocaleSubtag + ",en;q=0.7,en-US;q=0.6";
|
||||
languages = appSettingsCubit.state.preferredLocaleSubtag +
|
||||
",en;q=0.7,en-US;q=0.6";
|
||||
}
|
||||
request.headers.addAll({"Accept-Language": languages});
|
||||
return request;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<BaseResponse> interceptResponse({required BaseResponse response}) async => response;
|
||||
Future<BaseResponse> interceptResponse(
|
||||
{required BaseResponse response}) async =>
|
||||
response;
|
||||
}
|
||||
|
||||
@@ -7,11 +7,14 @@ const interceptedRoutes = ['thumb/'];
|
||||
@injectable
|
||||
class ResponseConversionInterceptor implements InterceptorContract {
|
||||
@override
|
||||
Future<BaseRequest> interceptRequest({required BaseRequest request}) async => request;
|
||||
Future<BaseRequest> interceptRequest({required BaseRequest request}) async =>
|
||||
request;
|
||||
|
||||
@override
|
||||
Future<BaseResponse> interceptResponse({required BaseResponse response}) async {
|
||||
final String requestUrl = response.request?.url.toString().split("?").first ?? '';
|
||||
Future<BaseResponse> interceptResponse(
|
||||
{required BaseResponse response}) async {
|
||||
final String requestUrl =
|
||||
response.request?.url.toString().split("?").first ?? '';
|
||||
if (response.request?.method == "GET" &&
|
||||
interceptedRoutes.any((element) => requestUrl.endsWith(element))) {
|
||||
final resp = response as Response;
|
||||
|
||||
@@ -3,7 +3,8 @@ import 'dart:typed_data';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:paperless_mobile/core/model/error_message.dart';
|
||||
import 'package:paperless_mobile/core/type/json.dart';
|
||||
import 'package:paperless_mobile/core/service/connectivity_status.service.dart';
|
||||
import 'package:paperless_mobile/core/type/types.dart';
|
||||
import 'package:paperless_mobile/di_initializer.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
@@ -14,13 +15,17 @@ import 'package:injectable/injectable.dart';
|
||||
@Injectable(as: BaseClient)
|
||||
@Named("timeoutClient")
|
||||
class TimeoutClient implements BaseClient {
|
||||
final ConnectivityStatusService connectivityStatusService;
|
||||
static const Duration requestTimeout = Duration(seconds: 25);
|
||||
|
||||
TimeoutClient(this.connectivityStatusService);
|
||||
|
||||
@override
|
||||
Future<StreamedResponse> send(BaseRequest request) async {
|
||||
return getIt<BaseClient>().send(request).timeout(
|
||||
requestTimeout,
|
||||
onTimeout: () => Future.error(const ErrorMessage(ErrorCode.requestTimedOut)),
|
||||
onTimeout: () =>
|
||||
Future.error(const ErrorMessage(ErrorCode.requestTimedOut)),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -32,32 +37,38 @@ class TimeoutClient implements BaseClient {
|
||||
@override
|
||||
Future<Response> delete(Uri url,
|
||||
{Map<String, String>? headers, Object? body, Encoding? encoding}) async {
|
||||
await _handleOfflineState();
|
||||
return _handle400Error(
|
||||
await getIt<BaseClient>()
|
||||
.delete(url, headers: headers, body: body, encoding: encoding)
|
||||
.timeout(
|
||||
requestTimeout,
|
||||
onTimeout: () => Future.error(const ErrorMessage(ErrorCode.requestTimedOut)),
|
||||
onTimeout: () =>
|
||||
Future.error(const ErrorMessage(ErrorCode.requestTimedOut)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Response> get(Uri url, {Map<String, String>? headers}) async {
|
||||
await _handleOfflineState();
|
||||
return _handle400Error(
|
||||
await getIt<BaseClient>().get(url, headers: headers).timeout(
|
||||
requestTimeout,
|
||||
onTimeout: () => Future.error(const ErrorMessage(ErrorCode.requestTimedOut)),
|
||||
onTimeout: () =>
|
||||
Future.error(const ErrorMessage(ErrorCode.requestTimedOut)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Response> head(Uri url, {Map<String, String>? headers}) async {
|
||||
await _handleOfflineState();
|
||||
return _handle400Error(
|
||||
await getIt<BaseClient>().head(url, headers: headers).timeout(
|
||||
requestTimeout,
|
||||
onTimeout: () => Future.error(const ErrorMessage(ErrorCode.requestTimedOut)),
|
||||
onTimeout: () =>
|
||||
Future.error(const ErrorMessage(ErrorCode.requestTimedOut)),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -65,12 +76,14 @@ class TimeoutClient implements BaseClient {
|
||||
@override
|
||||
Future<Response> patch(Uri url,
|
||||
{Map<String, String>? headers, Object? body, Encoding? encoding}) async {
|
||||
await _handleOfflineState();
|
||||
return _handle400Error(
|
||||
await getIt<BaseClient>()
|
||||
.patch(url, headers: headers, body: body, encoding: encoding)
|
||||
.timeout(
|
||||
requestTimeout,
|
||||
onTimeout: () => Future.error(const ErrorMessage(ErrorCode.requestTimedOut)),
|
||||
onTimeout: () =>
|
||||
Future.error(const ErrorMessage(ErrorCode.requestTimedOut)),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -78,10 +91,14 @@ class TimeoutClient implements BaseClient {
|
||||
@override
|
||||
Future<Response> post(Uri url,
|
||||
{Map<String, String>? headers, Object? body, Encoding? encoding}) async {
|
||||
await _handleOfflineState();
|
||||
return _handle400Error(
|
||||
await getIt<BaseClient>().post(url, headers: headers, body: body, encoding: encoding).timeout(
|
||||
await getIt<BaseClient>()
|
||||
.post(url, headers: headers, body: body, encoding: encoding)
|
||||
.timeout(
|
||||
requestTimeout,
|
||||
onTimeout: () => Future.error(const ErrorMessage(ErrorCode.requestTimedOut)),
|
||||
onTimeout: () =>
|
||||
Future.error(const ErrorMessage(ErrorCode.requestTimedOut)),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -89,27 +106,35 @@ class TimeoutClient implements BaseClient {
|
||||
@override
|
||||
Future<Response> put(Uri url,
|
||||
{Map<String, String>? headers, Object? body, Encoding? encoding}) async {
|
||||
await _handleOfflineState();
|
||||
return _handle400Error(
|
||||
await getIt<BaseClient>().put(url, headers: headers, body: body, encoding: encoding).timeout(
|
||||
await getIt<BaseClient>()
|
||||
.put(url, headers: headers, body: body, encoding: encoding)
|
||||
.timeout(
|
||||
requestTimeout,
|
||||
onTimeout: () => Future.error(const ErrorMessage(ErrorCode.requestTimedOut)),
|
||||
onTimeout: () =>
|
||||
Future.error(const ErrorMessage(ErrorCode.requestTimedOut)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> read(Uri url, {Map<String, String>? headers}) async {
|
||||
await _handleOfflineState();
|
||||
return getIt<BaseClient>().read(url, headers: headers).timeout(
|
||||
requestTimeout,
|
||||
onTimeout: () => Future.error(const ErrorMessage(ErrorCode.requestTimedOut)),
|
||||
onTimeout: () =>
|
||||
Future.error(const ErrorMessage(ErrorCode.requestTimedOut)),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Uint8List> readBytes(Uri url, {Map<String, String>? headers}) {
|
||||
Future<Uint8List> readBytes(Uri url, {Map<String, String>? headers}) async {
|
||||
await _handleOfflineState();
|
||||
return getIt<BaseClient>().readBytes(url, headers: headers).timeout(
|
||||
requestTimeout,
|
||||
onTimeout: () => Future.error(const ErrorMessage(ErrorCode.requestTimedOut)),
|
||||
onTimeout: () =>
|
||||
Future.error(const ErrorMessage(ErrorCode.requestTimedOut)),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -117,11 +142,12 @@ class TimeoutClient implements BaseClient {
|
||||
if (response.statusCode == 400) {
|
||||
// try to parse contained error message, otherwise return response
|
||||
final JSON json = jsonDecode(utf8.decode(response.bodyBytes));
|
||||
final Map<String, String> errorMessages = {};
|
||||
final PaperlessValidationErrors errorMessages = {};
|
||||
//TODO: This could be simplified, look at error message format of paperless-ngx
|
||||
for (final entry in json.entries) {
|
||||
if (entry.value is List) {
|
||||
errorMessages.putIfAbsent(entry.key, () => (entry.value as List).cast<String>().first);
|
||||
errorMessages.putIfAbsent(
|
||||
entry.key, () => (entry.value as List).cast<String>().first);
|
||||
} else if (entry.value is String) {
|
||||
errorMessages.putIfAbsent(entry.key, () => entry.value);
|
||||
} else {
|
||||
@@ -132,4 +158,10 @@ class TimeoutClient implements BaseClient {
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
Future<void> _handleOfflineState() async {
|
||||
if (!(await connectivityStatusService.isConnectedToInternet())) {
|
||||
throw const ErrorMessage(ErrorCode.deviceOffline);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,12 +17,15 @@ class ConnectivityStatusServiceImpl implements ConnectivityStatusService {
|
||||
|
||||
@override
|
||||
Stream<bool> connectivityChanges() {
|
||||
return connectivity.onConnectivityChanged.map(_hasActiveInternetConnection).asBroadcastStream();
|
||||
return connectivity.onConnectivityChanged
|
||||
.map(_hasActiveInternetConnection)
|
||||
.asBroadcastStream();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> isConnectedToInternet() async {
|
||||
return _hasActiveInternetConnection(await (Connectivity().checkConnectivity()));
|
||||
return _hasActiveInternetConnection(
|
||||
await (Connectivity().checkConnectivity()));
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -13,8 +13,8 @@ import 'package:injectable/injectable.dart';
|
||||
import 'package:web_socket_channel/io.dart';
|
||||
|
||||
abstract class StatusService {
|
||||
Future<void> startListeningBeforeDocumentUpload(
|
||||
String httpUrl, AuthenticationInformation credentials, String documentFileName);
|
||||
Future<void> startListeningBeforeDocumentUpload(String httpUrl,
|
||||
AuthenticationInformation credentials, String documentFileName);
|
||||
}
|
||||
|
||||
@Singleton(as: StatusService)
|
||||
@@ -86,9 +86,11 @@ class LongPollingStatusService implements StatusService {
|
||||
|
||||
do {
|
||||
final response = await httpClient.get(
|
||||
Uri.parse('$httpUrl/api/documents/?query=$documentFileName added:${formatDate(today)}'),
|
||||
Uri.parse(
|
||||
'$httpUrl/api/documents/?query=$documentFileName added:${formatDate(today)}'),
|
||||
);
|
||||
final data = PagedSearchResult.fromJson(jsonDecode(response.body), DocumentModel.fromJson);
|
||||
final data = PagedSearchResult.fromJson(
|
||||
jsonDecode(response.body), DocumentModel.fromJson);
|
||||
if (data.count > 0) {
|
||||
consumptionFinished = true;
|
||||
final docId = data.results[0].id;
|
||||
|
||||
@@ -34,11 +34,13 @@ class LocalVault {
|
||||
}
|
||||
|
||||
Future<ClientCertificate?> loadCertificate() async {
|
||||
return loadAuthenticationInformation().then((value) => value?.clientCertificate);
|
||||
return loadAuthenticationInformation()
|
||||
.then((value) => value?.clientCertificate);
|
||||
}
|
||||
|
||||
Future<bool> storeApplicationSettings(ApplicationSettingsState settings) {
|
||||
return sharedPreferences.setString(applicationSettingsKey, json.encode(settings.toJson()));
|
||||
return sharedPreferences.setString(
|
||||
applicationSettingsKey, json.encode(settings.toJson()));
|
||||
}
|
||||
|
||||
Future<ApplicationSettingsState?> loadApplicationSettings() async {
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
typedef JSON = Map<String, dynamic>;
|
||||
2
lib/core/type/types.dart
Normal file
2
lib/core/type/types.dart
Normal file
@@ -0,0 +1,2 @@
|
||||
typedef JSON = Map<String, dynamic>;
|
||||
typedef PaperlessValidationErrors = Map<String, String>;
|
||||
@@ -4,7 +4,7 @@ import 'dart:typed_data';
|
||||
|
||||
import 'package:paperless_mobile/core/logic/timeout_client.dart';
|
||||
import 'package:paperless_mobile/core/model/error_message.dart';
|
||||
import 'package:paperless_mobile/core/type/json.dart';
|
||||
import 'package:paperless_mobile/core/type/types.dart';
|
||||
import 'package:paperless_mobile/di_initializer.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
@@ -45,7 +45,10 @@ Future<List<T>> getCollection<T>(
|
||||
if (body['count'] == 0) {
|
||||
return <T>[];
|
||||
} else {
|
||||
return body['results'].cast<JSON>().map<T>((result) => fromJson(result)).toList();
|
||||
return body['results']
|
||||
.cast<JSON>()
|
||||
.map<T>((result) => fromJson(result))
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,9 +5,12 @@ import 'package:flutter/material.dart';
|
||||
|
||||
class ElevatedConfirmationButton extends StatefulWidget {
|
||||
factory ElevatedConfirmationButton.icon(BuildContext context,
|
||||
{required void Function() onPressed, required Icon icon, required Widget label}) {
|
||||
{required void Function() onPressed,
|
||||
required Icon icon,
|
||||
required Widget label}) {
|
||||
final double scale = MediaQuery.maybeOf(context)?.textScaleFactor ?? 1;
|
||||
final double gap = scale <= 1 ? 8 : lerpDouble(8, 4, math.min(scale - 1, 1))!;
|
||||
final double gap =
|
||||
scale <= 1 ? 8 : lerpDouble(8, 4, math.min(scale - 1, 1))!;
|
||||
return ElevatedConfirmationButton(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@@ -30,10 +33,12 @@ class ElevatedConfirmationButton extends StatefulWidget {
|
||||
final Widget child;
|
||||
final Widget confirmWidget;
|
||||
@override
|
||||
State<ElevatedConfirmationButton> createState() => _ElevatedConfirmationButtonState();
|
||||
State<ElevatedConfirmationButton> createState() =>
|
||||
_ElevatedConfirmationButtonState();
|
||||
}
|
||||
|
||||
class _ElevatedConfirmationButtonState extends State<ElevatedConfirmationButton> {
|
||||
class _ElevatedConfirmationButtonState
|
||||
extends State<ElevatedConfirmationButton> {
|
||||
bool _clickedOnce = false;
|
||||
double? _originalWidth;
|
||||
final GlobalKey _originalWidgetKey = GlobalKey();
|
||||
@@ -46,8 +51,10 @@ class _ElevatedConfirmationButtonState extends State<ElevatedConfirmationButton>
|
||||
backgroundColor: MaterialStateProperty.all(widget.color),
|
||||
),
|
||||
onPressed: () {
|
||||
_originalWidth =
|
||||
(_originalWidgetKey.currentContext?.findRenderObject() as RenderBox).size.width;
|
||||
_originalWidth = (_originalWidgetKey.currentContext
|
||||
?.findRenderObject() as RenderBox)
|
||||
.size
|
||||
.width;
|
||||
setState(() => _clickedOnce = true);
|
||||
},
|
||||
child: widget.child,
|
||||
|
||||
@@ -38,10 +38,13 @@ class DocumentsListLoadingWidget extends StatelessWidget {
|
||||
titleLengths[r.nextInt(titleLengths.length - 1)];
|
||||
return ListTile(
|
||||
isThreeLine: true,
|
||||
leading: Container(
|
||||
color: Colors.white,
|
||||
height: 50,
|
||||
width: 50,
|
||||
leading: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Container(
|
||||
color: Colors.white,
|
||||
height: 50,
|
||||
width: 35,
|
||||
),
|
||||
),
|
||||
title: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 2.0),
|
||||
@@ -65,7 +68,7 @@ class DocumentsListLoadingWidget extends StatelessWidget {
|
||||
spacing: 2.0,
|
||||
children: List.generate(
|
||||
tagCount,
|
||||
(index) => Chip(
|
||||
(index) => InputChip(
|
||||
label: Text(tags[r.nextInt(tags.length)]),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -71,8 +71,9 @@ class HighlightedText extends StatelessWidget {
|
||||
int _start = 0;
|
||||
|
||||
String _text = caseSensitive ? text : text.toLowerCase();
|
||||
List<String> _highlights =
|
||||
caseSensitive ? highlights : highlights.map((e) => e.toLowerCase()).toList();
|
||||
List<String> _highlights = caseSensitive
|
||||
? highlights
|
||||
: highlights.map((e) => e.toLowerCase()).toList();
|
||||
|
||||
while (true) {
|
||||
Map<int, String> _highlightsMap = {}; //key (index), value (highlight).
|
||||
@@ -95,7 +96,8 @@ class HighlightedText extends StatelessWidget {
|
||||
_spans.add(_highlightSpan(_currentHighlight));
|
||||
_start += _currentHighlight.length;
|
||||
} else {
|
||||
_spans.add(_normalSpan(text.substring(_start, _currentIndex), context));
|
||||
_spans
|
||||
.add(_normalSpan(text.substring(_start, _currentIndex), context));
|
||||
_spans.add(_highlightSpan(_currentHighlight));
|
||||
_start = _currentIndex + _currentHighlight.length;
|
||||
}
|
||||
|
||||
@@ -11,7 +11,9 @@ class OfflineWidget extends StatelessWidget {
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.mood_bad, size: (Theme.of(context).iconTheme.size ?? 24) * 3),
|
||||
Icon(Icons.wifi_off,
|
||||
color: Theme.of(context).disabledColor,
|
||||
size: (Theme.of(context).iconTheme.size ?? 24) * 3),
|
||||
Text(
|
||||
S.of(context).offlineWidgetText,
|
||||
textAlign: TextAlign.center,
|
||||
|
||||
Reference in New Issue
Block a user