mirror of
https://github.com/Xevion/paperless-mobile.git
synced 2025-12-06 07:15:43 -06:00
fix: Fixed saved views bug, formatted files, minor changes
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
* Fixed bug where document types and correspondents were not correctly synced and loaded
|
||||
* Fixed bug where saved views were not correctly created and loaded
|
||||
*
|
||||
@@ -41,5 +41,7 @@ class ThemeModeAdapter extends TypeAdapter<ThemeMode> {
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is ThemeModeAdapter && runtimeType == other.runtimeType && typeId == other.typeId;
|
||||
other is ThemeModeAdapter &&
|
||||
runtimeType == other.runtimeType &&
|
||||
typeId == other.typeId;
|
||||
}
|
||||
|
||||
@@ -9,7 +9,8 @@ import 'package:hive_flutter/adapters.dart';
|
||||
/// Opens an encrypted box, calls [callback] with the now opened box, awaits
|
||||
/// [callback] to return and returns the calculated value. Closes the box after.
|
||||
///
|
||||
Future<R?> withEncryptedBox<T, R>(String name, FutureOr<R?> Function(Box<T> box) callback) async {
|
||||
Future<R?> withEncryptedBox<T, R>(
|
||||
String name, FutureOr<R?> Function(Box<T> box) callback) async {
|
||||
final key = await _getEncryptedBoxKey();
|
||||
final box = await Hive.openBox<T>(
|
||||
name,
|
||||
|
||||
@@ -27,6 +27,9 @@ class LocalUserAccount extends HiveObject {
|
||||
required this.paperlessUser,
|
||||
});
|
||||
|
||||
static LocalUserAccount get current => Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount)
|
||||
.get(Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!.currentLoggedInUser)!;
|
||||
static LocalUserAccount get current =>
|
||||
Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount).get(
|
||||
Hive.box<GlobalSettings>(HiveBoxes.globalSettings)
|
||||
.getValue()!
|
||||
.currentLoggedInUser)!;
|
||||
}
|
||||
|
||||
@@ -41,7 +41,10 @@ class LocalUserAppState extends HiveObject {
|
||||
|
||||
static LocalUserAppState get current {
|
||||
final currentLocalUserId =
|
||||
Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!.currentLoggedInUser!;
|
||||
return Hive.box<LocalUserAppState>(HiveBoxes.localUserAppState).get(currentLocalUserId)!;
|
||||
Hive.box<GlobalSettings>(HiveBoxes.globalSettings)
|
||||
.getValue()!
|
||||
.currentLoggedInUser!;
|
||||
return Hive.box<LocalUserAppState>(HiveBoxes.localUserAppState)
|
||||
.get(currentLocalUserId)!;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,11 @@ import 'package:paperless_api/paperless_api.dart';
|
||||
|
||||
abstract class PaperlessApiFactory {
|
||||
PaperlessDocumentsApi createDocumentsApi(Dio dio, {required int apiVersion});
|
||||
PaperlessSavedViewsApi createSavedViewsApi(Dio dio, {required int apiVersion});
|
||||
PaperlessSavedViewsApi createSavedViewsApi(Dio dio,
|
||||
{required int apiVersion});
|
||||
PaperlessLabelsApi createLabelsApi(Dio dio, {required int apiVersion});
|
||||
PaperlessServerStatsApi createServerStatsApi(Dio dio, {required int apiVersion});
|
||||
PaperlessServerStatsApi createServerStatsApi(Dio dio,
|
||||
{required int apiVersion});
|
||||
PaperlessTasksApi createTasksApi(Dio dio, {required int apiVersion});
|
||||
PaperlessAuthenticationApi createAuthenticationApi(Dio dio);
|
||||
PaperlessUserApi createUserApi(Dio dio, {required int apiVersion});
|
||||
|
||||
@@ -19,12 +19,14 @@ class PaperlessApiFactoryImpl implements PaperlessApiFactory {
|
||||
}
|
||||
|
||||
@override
|
||||
PaperlessSavedViewsApi createSavedViewsApi(Dio dio, {required int apiVersion}) {
|
||||
PaperlessSavedViewsApi createSavedViewsApi(Dio dio,
|
||||
{required int apiVersion}) {
|
||||
return PaperlessSavedViewsApiImpl(dio);
|
||||
}
|
||||
|
||||
@override
|
||||
PaperlessServerStatsApi createServerStatsApi(Dio dio, {required int apiVersion}) {
|
||||
PaperlessServerStatsApi createServerStatsApi(Dio dio,
|
||||
{required int apiVersion}) {
|
||||
return PaperlessServerStatsApiImpl(dio);
|
||||
}
|
||||
|
||||
|
||||
@@ -78,7 +78,8 @@ class DioHttpErrorInterceptor extends Interceptor {
|
||||
DioError(
|
||||
requestOptions: err.requestOptions,
|
||||
type: DioErrorType.badResponse,
|
||||
error: const PaperlessServerException(ErrorCode.missingClientCertificate),
|
||||
error: const PaperlessServerException(
|
||||
ErrorCode.missingClientCertificate),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||
@@ -35,8 +36,9 @@ import 'package:provider/provider.dart';
|
||||
// Providers unfortunately have to be passed to the routes since they are children of the Navigator, not ancestors.
|
||||
|
||||
Future<void> pushDocumentSearchPage(BuildContext context) {
|
||||
final currentUser =
|
||||
Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!.currentLoggedInUser;
|
||||
final currentUser = Hive.box<GlobalSettings>(HiveBoxes.globalSettings)
|
||||
.getValue()!
|
||||
.currentLoggedInUser;
|
||||
final userRepo = context.read<UserRepository>();
|
||||
return Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
@@ -53,7 +55,8 @@ Future<void> pushDocumentSearchPage(BuildContext context) {
|
||||
create: (context) => DocumentSearchCubit(
|
||||
context.read(),
|
||||
context.read(),
|
||||
Hive.box<LocalUserAppState>(HiveBoxes.localUserAppState).get(currentUser)!,
|
||||
Hive.box<LocalUserAppState>(HiveBoxes.localUserAppState)
|
||||
.get(currentUser)!,
|
||||
),
|
||||
child: const DocumentSearchPage(),
|
||||
);
|
||||
@@ -103,7 +106,8 @@ Future<void> pushSavedViewDetailsRoute(
|
||||
builder: (_) => MultiProvider(
|
||||
providers: [
|
||||
Provider.value(value: apiVersion),
|
||||
if (apiVersion.hasMultiUserSupport) Provider.value(value: context.read<UserRepository>()),
|
||||
if (apiVersion.hasMultiUserSupport)
|
||||
Provider.value(value: context.read<UserRepository>()),
|
||||
Provider.value(value: context.read<LabelRepository>()),
|
||||
Provider.value(value: context.read<DocumentChangedNotifier>()),
|
||||
Provider.value(value: context.read<PaperlessDocumentsApi>()),
|
||||
@@ -119,7 +123,8 @@ Future<void> pushSavedViewDetailsRoute(
|
||||
LocalUserAppState.current,
|
||||
savedView: savedView,
|
||||
),
|
||||
child: SavedViewDetailsPage(onDelete: context.read<SavedViewCubit>().remove),
|
||||
child: SavedViewDetailsPage(
|
||||
onDelete: context.read<SavedViewCubit>().remove),
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -127,7 +132,8 @@ Future<void> pushSavedViewDetailsRoute(
|
||||
);
|
||||
}
|
||||
|
||||
Future<SavedView?> pushAddSavedViewRoute(BuildContext context, {required DocumentFilter filter}) {
|
||||
Future<SavedView?> pushAddSavedViewRoute(BuildContext context,
|
||||
{required DocumentFilter filter}) {
|
||||
return Navigator.of(context).push<SavedView?>(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => AddSavedViewPage(
|
||||
@@ -141,7 +147,8 @@ Future<SavedView?> pushAddSavedViewRoute(BuildContext context, {required Documen
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> pushLinkedDocumentsView(BuildContext context, {required DocumentFilter filter}) {
|
||||
Future<void> pushLinkedDocumentsView(BuildContext context,
|
||||
{required DocumentFilter filter}) {
|
||||
return Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
@@ -196,7 +203,9 @@ Future<void> pushBulkEditCorrespondentRoute(
|
||||
labelMapper: (document) => document.correspondent,
|
||||
leadingIcon: const Icon(Icons.person_outline),
|
||||
hintText: S.of(context)!.startTyping,
|
||||
onSubmit: context.read<DocumentBulkActionCubit>().bulkModifyCorrespondent,
|
||||
onSubmit: context
|
||||
.read<DocumentBulkActionCubit>()
|
||||
.bulkModifyCorrespondent,
|
||||
assignMessageBuilder: (int count, String name) {
|
||||
return S.of(context)!.bulkEditCorrespondentAssignMessage(
|
||||
name,
|
||||
@@ -204,7 +213,9 @@ Future<void> pushBulkEditCorrespondentRoute(
|
||||
);
|
||||
},
|
||||
removeMessageBuilder: (int count) {
|
||||
return S.of(context)!.bulkEditCorrespondentRemoveMessage(count);
|
||||
return S
|
||||
.of(context)!
|
||||
.bulkEditCorrespondentRemoveMessage(count);
|
||||
},
|
||||
);
|
||||
},
|
||||
@@ -240,7 +251,9 @@ Future<void> pushBulkEditStoragePathRoute(
|
||||
labelMapper: (document) => document.storagePath,
|
||||
leadingIcon: const Icon(Icons.folder_outlined),
|
||||
hintText: S.of(context)!.startTyping,
|
||||
onSubmit: context.read<DocumentBulkActionCubit>().bulkModifyStoragePath,
|
||||
onSubmit: context
|
||||
.read<DocumentBulkActionCubit>()
|
||||
.bulkModifyStoragePath,
|
||||
assignMessageBuilder: (int count, String name) {
|
||||
return S.of(context)!.bulkEditStoragePathAssignMessage(
|
||||
count,
|
||||
@@ -308,7 +321,9 @@ Future<void> pushBulkEditDocumentTypeRoute(BuildContext context,
|
||||
labelMapper: (document) => document.documentType,
|
||||
leadingIcon: const Icon(Icons.description_outlined),
|
||||
hintText: S.of(context)!.startTyping,
|
||||
onSubmit: context.read<DocumentBulkActionCubit>().bulkModifyDocumentType,
|
||||
onSubmit: context
|
||||
.read<DocumentBulkActionCubit>()
|
||||
.bulkModifyDocumentType,
|
||||
assignMessageBuilder: (int count, String name) {
|
||||
return S.of(context)!.bulkEditDocumentTypeAssignMessage(
|
||||
count,
|
||||
@@ -316,7 +331,9 @@ Future<void> pushBulkEditDocumentTypeRoute(BuildContext context,
|
||||
);
|
||||
},
|
||||
removeMessageBuilder: (int count) {
|
||||
return S.of(context)!.bulkEditDocumentTypeRemoveMessage(count);
|
||||
return S
|
||||
.of(context)!
|
||||
.bulkEditDocumentTypeRemoveMessage(count);
|
||||
},
|
||||
);
|
||||
},
|
||||
@@ -336,17 +353,20 @@ Future<DocumentUploadResult?> pushDocumentUploadPreparationPage(
|
||||
}) {
|
||||
final labelRepo = context.read<LabelRepository>();
|
||||
final docsApi = context.read<PaperlessDocumentsApi>();
|
||||
final connectivity = context.read<Connectivity>();
|
||||
return Navigator.of(context).push<DocumentUploadResult>(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => MultiProvider(
|
||||
providers: [
|
||||
Provider.value(value: labelRepo),
|
||||
Provider.value(value: docsApi),
|
||||
Provider.value(value: connectivity),
|
||||
],
|
||||
builder: (_, child) => BlocProvider(
|
||||
create: (_) => DocumentUploadCubit(
|
||||
context.read(),
|
||||
context.read(),
|
||||
context.read(),
|
||||
),
|
||||
child: DocumentUploadPreparationPage(
|
||||
fileBytes: bytes,
|
||||
|
||||
@@ -1,39 +1,55 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/core/repository/persistent_repository.dart';
|
||||
import 'package:paperless_mobile/core/repository/saved_view_repository_state.dart';
|
||||
|
||||
class SavedViewRepository extends PersistentRepository<SavedViewRepositoryState> {
|
||||
part 'saved_view_repository_state.dart';
|
||||
part 'saved_view_repository.g.dart';
|
||||
part 'saved_view_repository.freezed.dart';
|
||||
|
||||
class SavedViewRepository
|
||||
extends PersistentRepository<SavedViewRepositoryState> {
|
||||
final PaperlessSavedViewsApi _api;
|
||||
final Completer _initialized = Completer();
|
||||
|
||||
SavedViewRepository(this._api) : super(const SavedViewRepositoryState()) {
|
||||
initialize();
|
||||
SavedViewRepository(this._api)
|
||||
: super(const SavedViewRepositoryState.initial());
|
||||
|
||||
Future<void> initialize() async {
|
||||
try {
|
||||
await findAll();
|
||||
_initialized.complete();
|
||||
} catch (e) {
|
||||
_initialized.completeError(e);
|
||||
emit(const SavedViewRepositoryState.error());
|
||||
}
|
||||
|
||||
Future<void> initialize() {
|
||||
return findAll();
|
||||
}
|
||||
|
||||
Future<SavedView> create(SavedView object) async {
|
||||
await _initialized.future;
|
||||
final created = await _api.save(object);
|
||||
final updatedState = {...state.savedViews}..putIfAbsent(created.id!, () => created);
|
||||
emit(state.copyWith(savedViews: updatedState));
|
||||
final updatedState = {...state.savedViews}
|
||||
..putIfAbsent(created.id!, () => created);
|
||||
emit(SavedViewRepositoryState.loaded(savedViews: updatedState));
|
||||
return created;
|
||||
}
|
||||
|
||||
Future<int> delete(SavedView view) async {
|
||||
await _initialized.future;
|
||||
await _api.delete(view);
|
||||
final updatedState = {...state.savedViews}..remove(view.id);
|
||||
emit(state.copyWith(savedViews: updatedState));
|
||||
emit(SavedViewRepositoryState.loaded(savedViews: updatedState));
|
||||
return view.id!;
|
||||
}
|
||||
|
||||
Future<SavedView?> find(int id) async {
|
||||
await _initialized.future;
|
||||
final found = await _api.find(id);
|
||||
if (found != null) {
|
||||
final updatedState = {...state.savedViews}..update(id, (_) => found, ifAbsent: () => found);
|
||||
emit(state.copyWith(savedViews: updatedState));
|
||||
final updatedState = {...state.savedViews}
|
||||
..update(id, (_) => found, ifAbsent: () => found);
|
||||
emit(SavedViewRepositoryState.loaded(savedViews: updatedState));
|
||||
}
|
||||
return found;
|
||||
}
|
||||
@@ -44,14 +60,15 @@ class SavedViewRepository extends PersistentRepository<SavedViewRepositoryState>
|
||||
...state.savedViews,
|
||||
...{for (final view in found) view.id!: view},
|
||||
};
|
||||
emit(state.copyWith(savedViews: updatedState));
|
||||
emit(SavedViewRepositoryState.loaded(savedViews: updatedState));
|
||||
return found;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> clear() async {
|
||||
await _initialized.future;
|
||||
await super.clear();
|
||||
emit(const SavedViewRepositoryState());
|
||||
emit(const SavedViewRepositoryState.initial());
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
|
||||
part 'saved_view_repository_state.freezed.dart';
|
||||
part 'saved_view_repository_state.g.dart';
|
||||
part of 'saved_view_repository.dart';
|
||||
|
||||
@freezed
|
||||
class SavedViewRepositoryState with _$SavedViewRepositoryState {
|
||||
const factory SavedViewRepositoryState({
|
||||
const factory SavedViewRepositoryState.initial({
|
||||
@Default({}) Map<int, SavedView> savedViews,
|
||||
}) = _SavedViewRepositoryState;
|
||||
}) = _Initial;
|
||||
const factory SavedViewRepositoryState.loading({
|
||||
@Default({}) Map<int, SavedView> savedViews,
|
||||
}) = _Loading;
|
||||
const factory SavedViewRepositoryState.loaded({
|
||||
@Default({}) Map<int, SavedView> savedViews,
|
||||
}) = _Loaded;
|
||||
const factory SavedViewRepositoryState.error({
|
||||
@Default({}) Map<int, SavedView> savedViews,
|
||||
}) = _Error;
|
||||
|
||||
factory SavedViewRepositoryState.fromJson(Map<String, dynamic> json) =>
|
||||
_$SavedViewRepositoryStateFromJson(json);
|
||||
|
||||
@@ -13,7 +13,8 @@ import 'package:pretty_dio_logger/pretty_dio_logger.dart';
|
||||
class SessionManager extends ValueNotifier<Dio> {
|
||||
Dio get client => value;
|
||||
|
||||
SessionManager([List<Interceptor> interceptors = const []]) : super(_initDio(interceptors));
|
||||
SessionManager([List<Interceptor> interceptors = const []])
|
||||
: super(_initDio(interceptors));
|
||||
|
||||
static Dio _initDio(List<Interceptor> interceptors) {
|
||||
//en- and decoded by utf8 by default
|
||||
@@ -21,8 +22,8 @@ class SessionManager extends ValueNotifier<Dio> {
|
||||
BaseOptions(contentType: Headers.jsonContentType),
|
||||
);
|
||||
dio.options
|
||||
..receiveTimeout = const Duration(seconds: 15)
|
||||
..sendTimeout = const Duration(seconds: 10)
|
||||
..receiveTimeout = const Duration(seconds: 20)
|
||||
..sendTimeout = const Duration(seconds: 60)
|
||||
..responseType = ResponseType.json;
|
||||
(dio.httpClientAdapter as IOHttpClientAdapter).onHttpClientCreate =
|
||||
(client) => client..badCertificateCallback = (cert, host, port) => true;
|
||||
@@ -62,7 +63,8 @@ class SessionManager extends ValueNotifier<Dio> {
|
||||
);
|
||||
final adapter = IOHttpClientAdapter()
|
||||
..onHttpClientCreate = (client) => HttpClient(context: context)
|
||||
..badCertificateCallback = (X509Certificate cert, String host, int port) => true;
|
||||
..badCertificateCallback =
|
||||
(X509Certificate cert, String host, int port) => true;
|
||||
|
||||
client.httpClientAdapter = adapter;
|
||||
}
|
||||
|
||||
@@ -32,7 +32,8 @@ class ConnectivityStatusServiceImpl implements ConnectivityStatusService {
|
||||
|
||||
@override
|
||||
Future<bool> isConnectedToInternet() async {
|
||||
return _hasActiveInternetConnection(await (Connectivity().checkConnectivity()));
|
||||
return _hasActiveInternetConnection(
|
||||
await (Connectivity().checkConnectivity()));
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -71,7 +72,8 @@ class ConnectivityStatusServiceImpl implements ConnectivityStatusService {
|
||||
return ReachabilityStatus.unknown;
|
||||
}
|
||||
try {
|
||||
SessionManager manager = SessionManager([ServerReachabilityErrorInterceptor()])
|
||||
SessionManager manager =
|
||||
SessionManager([ServerReachabilityErrorInterceptor()])
|
||||
..updateSettings(clientCertificate: clientCertificate)
|
||||
..client.options.connectTimeout = const Duration(seconds: 5)
|
||||
..client.options.receiveTimeout = const Duration(seconds: 5);
|
||||
@@ -82,7 +84,8 @@ class ConnectivityStatusServiceImpl implements ConnectivityStatusService {
|
||||
}
|
||||
return ReachabilityStatus.notReachable;
|
||||
} on DioError catch (error) {
|
||||
if (error.type == DioErrorType.unknown && error.error is ReachabilityStatus) {
|
||||
if (error.type == DioErrorType.unknown &&
|
||||
error.error is ReachabilityStatus) {
|
||||
return error.error as ReachabilityStatus;
|
||||
}
|
||||
} on TlsException catch (error) {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:paperless_mobile/core/model/github_error_report.model.dart';
|
||||
import 'package:paperless_mobile/core/widgets/error_report_page.dart';
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
typedef JSON = Map<String, dynamic>;
|
||||
typedef PaperlessValidationErrors = Map<String, String>;
|
||||
typedef PaperlessLocalizedErrorMessage = String;
|
||||
|
||||
@@ -181,7 +181,8 @@ class FormBuilderColorPickerField extends FormBuilderField<Color> {
|
||||
);
|
||||
|
||||
@override
|
||||
FormBuilderColorPickerFieldState createState() => FormBuilderColorPickerFieldState();
|
||||
FormBuilderColorPickerFieldState createState() =>
|
||||
FormBuilderColorPickerFieldState();
|
||||
}
|
||||
|
||||
class FormBuilderColorPickerFieldState
|
||||
|
||||
@@ -334,7 +334,8 @@ class FormBuilderTypeAhead<T> extends FormBuilderField<T> {
|
||||
// TODO HACK to satisfy strictness
|
||||
suggestionsCallback: suggestionsCallback,
|
||||
itemBuilder: itemBuilder,
|
||||
transitionBuilder: (context, suggestionsBox, controller) => suggestionsBox,
|
||||
transitionBuilder: (context, suggestionsBox, controller) =>
|
||||
suggestionsBox,
|
||||
onSuggestionSelected: (T suggestion) {
|
||||
state.didChange(suggestion);
|
||||
onSuggestionSelected?.call(suggestion);
|
||||
@@ -356,7 +357,8 @@ class FormBuilderTypeAhead<T> extends FormBuilderField<T> {
|
||||
keepSuggestionsOnLoading: keepSuggestionsOnLoading,
|
||||
autoFlipDirection: autoFlipDirection,
|
||||
suggestionsBoxController: suggestionsBoxController,
|
||||
keepSuggestionsOnSuggestionSelected: keepSuggestionsOnSuggestionSelected,
|
||||
keepSuggestionsOnSuggestionSelected:
|
||||
keepSuggestionsOnSuggestionSelected,
|
||||
hideKeyboard: hideKeyboard,
|
||||
scrollController: scrollController,
|
||||
);
|
||||
@@ -367,14 +369,15 @@ class FormBuilderTypeAhead<T> extends FormBuilderField<T> {
|
||||
FormBuilderTypeAheadState<T> createState() => FormBuilderTypeAheadState<T>();
|
||||
}
|
||||
|
||||
class FormBuilderTypeAheadState<T> extends FormBuilderFieldState<FormBuilderTypeAhead<T>, T> {
|
||||
class FormBuilderTypeAheadState<T>
|
||||
extends FormBuilderFieldState<FormBuilderTypeAhead<T>, T> {
|
||||
late TextEditingController _typeAheadController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_typeAheadController =
|
||||
widget.controller ?? TextEditingController(text: _getTextString(initialValue));
|
||||
_typeAheadController = widget.controller ??
|
||||
TextEditingController(text: _getTextString(initialValue));
|
||||
// _typeAheadController.addListener(_handleControllerChanged);
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,8 @@ import 'package:flutter/services.dart';
|
||||
|
||||
typedef ChipsInputSuggestions<T> = Future<List<T>> Function(String query);
|
||||
typedef ChipSelected<T> = void Function(T data, bool selected);
|
||||
typedef ChipsBuilder<T> = Widget Function(BuildContext context, ChipsInputState<T> state, T data);
|
||||
typedef ChipsBuilder<T> = Widget Function(
|
||||
BuildContext context, ChipsInputState<T> state, T data);
|
||||
|
||||
class ChipsInput<T> extends StatefulWidget {
|
||||
const ChipsInput({
|
||||
@@ -70,7 +71,8 @@ class ChipsInputState<T> extends State<ChipsInput<T>> {
|
||||
|
||||
TextEditingValue get currentTextEditingValue => _value;
|
||||
|
||||
bool get _hasInputConnection => _connection != null && (_connection?.attached ?? false);
|
||||
bool get _hasInputConnection =>
|
||||
_connection != null && (_connection?.attached ?? false);
|
||||
|
||||
void requestKeyboard() {
|
||||
if (_focusNode.hasFocus) {
|
||||
@@ -189,7 +191,8 @@ class ChipsInputState<T> extends State<ChipsInput<T>> {
|
||||
child: ListView.builder(
|
||||
itemCount: _suggestions.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return widget.suggestionBuilder(context, this, _suggestions[index]);
|
||||
return widget.suggestionBuilder(
|
||||
context, this, _suggestions[index]);
|
||||
},
|
||||
),
|
||||
),
|
||||
@@ -210,11 +213,14 @@ class ChipsInputState<T> extends State<ChipsInput<T>> {
|
||||
}
|
||||
|
||||
int _countReplacements(TextEditingValue value) {
|
||||
return value.text.codeUnits.where((ch) => ch == kObjectReplacementChar).length;
|
||||
return value.text.codeUnits
|
||||
.where((ch) => ch == kObjectReplacementChar)
|
||||
.length;
|
||||
}
|
||||
|
||||
void _updateTextInputState() {
|
||||
final text = String.fromCharCodes(_chips.map((_) => kObjectReplacementChar));
|
||||
final text =
|
||||
String.fromCharCodes(_chips.map((_) => kObjectReplacementChar));
|
||||
_value = TextEditingValue(
|
||||
text: text,
|
||||
selection: TextSelection.collapsed(offset: text.length),
|
||||
@@ -227,8 +233,9 @@ class ChipsInputState<T> extends State<ChipsInput<T>> {
|
||||
final localId = ++_searchId;
|
||||
final results = await widget.findSuggestions(value);
|
||||
if (_searchId == localId && mounted) {
|
||||
setState(() => _suggestions =
|
||||
results.where((profile) => !_chips.contains(profile)).toList(growable: false));
|
||||
setState(() => _suggestions = results
|
||||
.where((profile) => !_chips.contains(profile))
|
||||
.toList(growable: false));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -244,7 +251,8 @@ class _TextCaret extends StatefulWidget {
|
||||
_TextCursorState createState() => _TextCursorState();
|
||||
}
|
||||
|
||||
class _TextCursorState extends State<_TextCaret> with SingleTickerProviderStateMixin {
|
||||
class _TextCursorState extends State<_TextCaret>
|
||||
with SingleTickerProviderStateMixin {
|
||||
bool _displayed = false;
|
||||
late Timer _timer;
|
||||
|
||||
|
||||
@@ -95,7 +95,8 @@ class AppDrawer extends StatelessWidget {
|
||||
MaterialPageRoute(
|
||||
builder: (_) => MultiProvider(
|
||||
providers: [
|
||||
Provider.value(value: context.read<PaperlessServerStatsApi>()),
|
||||
Provider.value(
|
||||
value: context.read<PaperlessServerStatsApi>()),
|
||||
Provider.value(value: context.read<ApiVersion>()),
|
||||
],
|
||||
child: const SettingsPage(),
|
||||
@@ -128,7 +129,8 @@ class AppDrawer extends StatelessWidget {
|
||||
),
|
||||
RichText(
|
||||
text: TextSpan(
|
||||
style: theme.textTheme.bodyMedium?.copyWith(color: colorScheme.onSurface),
|
||||
style: theme.textTheme.bodyMedium
|
||||
?.copyWith(color: colorScheme.onSurface),
|
||||
children: [
|
||||
TextSpan(
|
||||
text: S.of(context)!.findTheSourceCodeOn,
|
||||
@@ -151,11 +153,13 @@ class AppDrawer extends StatelessWidget {
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Credits',
|
||||
style: theme.textTheme.titleMedium?.copyWith(color: colorScheme.onSurface),
|
||||
style: theme.textTheme.titleMedium
|
||||
?.copyWith(color: colorScheme.onSurface),
|
||||
),
|
||||
RichText(
|
||||
text: TextSpan(
|
||||
style: theme.textTheme.bodyMedium?.copyWith(color: colorScheme.onSurface),
|
||||
style: theme.textTheme.bodyMedium
|
||||
?.copyWith(color: colorScheme.onSurface),
|
||||
children: [
|
||||
const TextSpan(
|
||||
text: 'Onboarding images by ',
|
||||
@@ -205,16 +209,16 @@ class AppDrawer extends StatelessWidget {
|
||||
}
|
||||
|
||||
//Wrap(
|
||||
// children: [
|
||||
// const Text('Onboarding images by '),
|
||||
// GestureDetector(
|
||||
// onTap: followLink,
|
||||
// child: RichText(
|
||||
// children: [
|
||||
// const Text('Onboarding images by '),
|
||||
// GestureDetector(
|
||||
// onTap: followLink,
|
||||
// child: RichText(
|
||||
|
||||
// 'pch.vector',
|
||||
// style: TextStyle(color: Colors.blue),
|
||||
// ),
|
||||
// ),
|
||||
// const Text(' on Freepik.')
|
||||
// ],
|
||||
// )
|
||||
// 'pch.vector',
|
||||
// style: TextStyle(color: Colors.blue),
|
||||
// ),
|
||||
// ),
|
||||
// const Text(' on Freepik.')
|
||||
// ],
|
||||
// )
|
||||
|
||||
@@ -10,7 +10,8 @@ class ApplicationIntroSlideshow extends StatefulWidget {
|
||||
const ApplicationIntroSlideshow({super.key});
|
||||
|
||||
@override
|
||||
State<ApplicationIntroSlideshow> createState() => _ApplicationIntroSlideshowState();
|
||||
State<ApplicationIntroSlideshow> createState() =>
|
||||
_ApplicationIntroSlideshowState();
|
||||
}
|
||||
|
||||
//TODO: INTL ALL
|
||||
|
||||
@@ -8,7 +8,8 @@ import 'package:paperless_mobile/features/document_bulk_action/cubit/document_bu
|
||||
import 'package:paperless_mobile/features/labels/view/widgets/label_form_field.dart';
|
||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||
|
||||
typedef LabelOptionsSelector<T extends Label> = Map<int, T> Function(DocumentBulkActionState state);
|
||||
typedef LabelOptionsSelector<T extends Label> = Map<int, T> Function(
|
||||
DocumentBulkActionState state);
|
||||
|
||||
class BulkEditLabelBottomSheet<T extends Label> extends StatefulWidget {
|
||||
final String title;
|
||||
@@ -31,16 +32,19 @@ class BulkEditLabelBottomSheet<T extends Label> extends StatefulWidget {
|
||||
});
|
||||
|
||||
@override
|
||||
State<BulkEditLabelBottomSheet<T>> createState() => _BulkEditLabelBottomSheetState<T>();
|
||||
State<BulkEditLabelBottomSheet<T>> createState() =>
|
||||
_BulkEditLabelBottomSheetState<T>();
|
||||
}
|
||||
|
||||
class _BulkEditLabelBottomSheetState<T extends Label> extends State<BulkEditLabelBottomSheet<T>> {
|
||||
class _BulkEditLabelBottomSheetState<T extends Label>
|
||||
extends State<BulkEditLabelBottomSheet<T>> {
|
||||
final _formKey = GlobalKey<FormBuilderState>();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
|
||||
padding:
|
||||
EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
|
||||
child: BlocBuilder<DocumentBulkActionCubit, DocumentBulkActionState>(
|
||||
builder: (context, state) {
|
||||
return Padding(
|
||||
@@ -76,11 +80,13 @@ class _BulkEditLabelBottomSheetState<T extends Label> extends State<BulkEditLabe
|
||||
const SizedBox(width: 16),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
if (_formKey.currentState?.saveAndValidate() ?? false) {
|
||||
final value = _formKey.currentState?.getRawValue('labelFormField')
|
||||
if (_formKey.currentState?.saveAndValidate() ??
|
||||
false) {
|
||||
final value = _formKey.currentState
|
||||
?.getRawValue('labelFormField')
|
||||
as IdQueryParameter?;
|
||||
widget
|
||||
.onSubmit(value?.maybeWhen(fromId: (id) => id, orElse: () => null));
|
||||
widget.onSubmit(value?.maybeWhen(
|
||||
fromId: (id) => id, orElse: () => null));
|
||||
}
|
||||
},
|
||||
child: Text(S.of(context)!.apply),
|
||||
|
||||
@@ -91,8 +91,8 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
|
||||
_notifier.notifyUpdated(updatedDocument);
|
||||
} else {
|
||||
final int autoAsn = await _api.findNextAsn();
|
||||
final updatedDocument =
|
||||
await _api.update(document.copyWith(archiveSerialNumber: () => autoAsn));
|
||||
final updatedDocument = await _api
|
||||
.update(document.copyWith(archiveSerialNumber: () => autoAsn));
|
||||
_notifier.notifyUpdated(updatedDocument);
|
||||
}
|
||||
}
|
||||
@@ -104,7 +104,8 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
|
||||
if (state.metaData == null) {
|
||||
await loadMetaData();
|
||||
}
|
||||
final desc = FileDescription.fromPath(state.metaData!.mediaFilename.replaceAll("/", " "));
|
||||
final desc = FileDescription.fromPath(
|
||||
state.metaData!.mediaFilename.replaceAll("/", " "));
|
||||
|
||||
final fileName = "${desc.filename}.pdf";
|
||||
final file = File("${cacheDir.path}/$fileName");
|
||||
@@ -138,7 +139,8 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
|
||||
await FileService.downloadsDirectory,
|
||||
);
|
||||
final desc = FileDescription.fromPath(
|
||||
state.metaData!.mediaFilename.replaceAll("/", " "), // Flatten directory structure
|
||||
state.metaData!.mediaFilename
|
||||
.replaceAll("/", " "), // Flatten directory structure
|
||||
);
|
||||
if (!File(filePath).existsSync()) {
|
||||
File(filePath).createSync();
|
||||
@@ -205,7 +207,8 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
|
||||
if (state.metaData == null) {
|
||||
await loadMetaData();
|
||||
}
|
||||
final filePath = _buildDownloadFilePath(false, await FileService.temporaryDirectory);
|
||||
final filePath =
|
||||
_buildDownloadFilePath(false, await FileService.temporaryDirectory);
|
||||
await _api.downloadToFile(
|
||||
state.document,
|
||||
filePath,
|
||||
@@ -223,7 +226,8 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
|
||||
|
||||
String _buildDownloadFilePath(bool original, Directory dir) {
|
||||
final description = FileDescription.fromPath(
|
||||
state.metaData!.mediaFilename.replaceAll("/", " "), // Flatten directory structure
|
||||
state.metaData!.mediaFilename
|
||||
.replaceAll("/", " "), // Flatten directory structure
|
||||
);
|
||||
final extension = original ? description.extension : 'pdf';
|
||||
return "${dir.path}/${description.filename}.$extension";
|
||||
|
||||
@@ -45,7 +45,8 @@ class _SelectFileTypeDialogState extends State<SelectFileTypeDialog> {
|
||||
CheckboxListTile(
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
value: _rememberSelection,
|
||||
onChanged: (value) => setState(() => _rememberSelection = value ?? false),
|
||||
onChanged: (value) =>
|
||||
setState(() => _rememberSelection = value ?? false),
|
||||
title: Text(
|
||||
S.of(context)!.rememberDecision,
|
||||
style: Theme.of(context).textTheme.labelMedium,
|
||||
@@ -61,7 +62,8 @@ class _SelectFileTypeDialogState extends State<SelectFileTypeDialog> {
|
||||
if (_rememberSelection) {
|
||||
widget.onRememberSelection(_downloadType);
|
||||
}
|
||||
Navigator.of(context).pop(_downloadType == FileDownloadType.original);
|
||||
Navigator.of(context)
|
||||
.pop(_downloadType == FileDownloadType.original);
|
||||
},
|
||||
),
|
||||
],
|
||||
|
||||
@@ -51,27 +51,35 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
||||
final tabLength = 4 + (apiVersion.hasMultiUserSupport ? 1 : 0);
|
||||
return WillPopScope(
|
||||
onWillPop: () async {
|
||||
Navigator.of(context).pop(context.read<DocumentDetailsCubit>().state.document);
|
||||
Navigator.of(context)
|
||||
.pop(context.read<DocumentDetailsCubit>().state.document);
|
||||
return false;
|
||||
},
|
||||
child: DefaultTabController(
|
||||
length: tabLength,
|
||||
child: BlocListener<ConnectivityCubit, ConnectivityState>(
|
||||
listenWhen: (previous, current) => !previous.isConnected && current.isConnected,
|
||||
listenWhen: (previous, current) =>
|
||||
!previous.isConnected && current.isConnected,
|
||||
listener: (context, state) {
|
||||
context.read<DocumentDetailsCubit>().loadMetaData();
|
||||
},
|
||||
child: Scaffold(
|
||||
extendBodyBehindAppBar: false,
|
||||
floatingActionButtonLocation: FloatingActionButtonLocation.endDocked,
|
||||
floatingActionButtonLocation:
|
||||
FloatingActionButtonLocation.endDocked,
|
||||
floatingActionButton: _buildEditButton(),
|
||||
bottomNavigationBar: _buildBottomAppBar(),
|
||||
body: NestedScrollView(
|
||||
headerSliverBuilder: (context, innerBoxIsScrolled) => [
|
||||
SliverOverlapAbsorber(
|
||||
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
|
||||
handle:
|
||||
NestedScrollView.sliverOverlapAbsorberHandleFor(context),
|
||||
sliver: SliverAppBar(
|
||||
title: Text(context.watch<DocumentDetailsCubit>().state.document.title),
|
||||
title: Text(context
|
||||
.watch<DocumentDetailsCubit>()
|
||||
.state
|
||||
.document
|
||||
.title),
|
||||
leading: const BackButton(),
|
||||
pinned: true,
|
||||
forceElevated: innerBoxIsScrolled,
|
||||
@@ -81,7 +89,8 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
||||
background: Stack(
|
||||
alignment: Alignment.topCenter,
|
||||
children: [
|
||||
BlocBuilder<DocumentDetailsCubit, DocumentDetailsState>(
|
||||
BlocBuilder<DocumentDetailsCubit,
|
||||
DocumentDetailsState>(
|
||||
builder: (context, state) {
|
||||
return Positioned.fill(
|
||||
child: DocumentPreview(
|
||||
@@ -97,8 +106,14 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
Theme.of(context).colorScheme.background.withOpacity(0.8),
|
||||
Theme.of(context).colorScheme.background.withOpacity(0.5),
|
||||
Theme.of(context)
|
||||
.colorScheme
|
||||
.background
|
||||
.withOpacity(0.8),
|
||||
Theme.of(context)
|
||||
.colorScheme
|
||||
.background
|
||||
.withOpacity(0.5),
|
||||
Colors.transparent,
|
||||
Colors.transparent,
|
||||
Colors.transparent,
|
||||
@@ -120,7 +135,9 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
||||
child: Text(
|
||||
S.of(context)!.overview,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onPrimaryContainer,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -128,7 +145,9 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
||||
child: Text(
|
||||
S.of(context)!.content,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onPrimaryContainer,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -136,7 +155,9 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
||||
child: Text(
|
||||
S.of(context)!.metaData,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onPrimaryContainer,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -144,7 +165,9 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
||||
child: Text(
|
||||
S.of(context)!.similarDocuments,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onPrimaryContainer,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -153,7 +176,9 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
||||
child: Text(
|
||||
"Permissions",
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onPrimaryContainer,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -182,7 +207,8 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
||||
CustomScrollView(
|
||||
slivers: [
|
||||
SliverOverlapInjector(
|
||||
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
|
||||
handle: NestedScrollView
|
||||
.sliverOverlapAbsorberHandleFor(context),
|
||||
),
|
||||
DocumentOverviewWidget(
|
||||
document: state.document,
|
||||
@@ -198,7 +224,8 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
||||
CustomScrollView(
|
||||
slivers: [
|
||||
SliverOverlapInjector(
|
||||
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
|
||||
handle: NestedScrollView
|
||||
.sliverOverlapAbsorberHandleFor(context),
|
||||
),
|
||||
DocumentContentWidget(
|
||||
isFullContentLoaded: state.isFullContentLoaded,
|
||||
@@ -211,7 +238,8 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
||||
CustomScrollView(
|
||||
slivers: [
|
||||
SliverOverlapInjector(
|
||||
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
|
||||
handle: NestedScrollView
|
||||
.sliverOverlapAbsorberHandleFor(context),
|
||||
),
|
||||
DocumentMetaDataWidget(
|
||||
document: state.document,
|
||||
@@ -223,7 +251,8 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
||||
controller: _pagingScrollController,
|
||||
slivers: [
|
||||
SliverOverlapInjector(
|
||||
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
|
||||
handle: NestedScrollView
|
||||
.sliverOverlapAbsorberHandleFor(context),
|
||||
),
|
||||
SimilarDocumentsView(
|
||||
pagingScrollController: _pagingScrollController,
|
||||
@@ -235,7 +264,8 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
||||
controller: _pagingScrollController,
|
||||
slivers: [
|
||||
SliverOverlapInjector(
|
||||
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
|
||||
handle: NestedScrollView
|
||||
.sliverOverlapAbsorberHandleFor(context),
|
||||
),
|
||||
DocumentPermissionsWidget(
|
||||
document: state.document,
|
||||
@@ -289,15 +319,16 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
||||
final isConnected = connectivityState.isConnected;
|
||||
|
||||
final canDelete = isConnected &&
|
||||
LocalUserAccount.current.paperlessUser
|
||||
.hasPermission(PermissionAction.delete, PermissionTarget.document);
|
||||
LocalUserAccount.current.paperlessUser.hasPermission(
|
||||
PermissionAction.delete, PermissionTarget.document);
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
IconButton(
|
||||
tooltip: S.of(context)!.deleteDocumentTooltip,
|
||||
icon: const Icon(Icons.delete),
|
||||
onPressed: canDelete ? () => _onDelete(state.document) : null,
|
||||
onPressed:
|
||||
canDelete ? () => _onDelete(state.document) : null,
|
||||
).paddedSymmetrically(horizontal: 4),
|
||||
DocumentDownloadButton(
|
||||
document: state.document,
|
||||
@@ -307,7 +338,8 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
||||
IconButton(
|
||||
tooltip: S.of(context)!.previewTooltip,
|
||||
icon: const Icon(Icons.visibility),
|
||||
onPressed: (isConnected) ? () => _onOpen(state.document) : null,
|
||||
onPressed:
|
||||
(isConnected) ? () => _onOpen(state.document) : null,
|
||||
).paddedOnly(right: 4.0),
|
||||
IconButton(
|
||||
tooltip: S.of(context)!.openInSystemViewer,
|
||||
@@ -317,7 +349,8 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
||||
DocumentShareButton(document: state.document),
|
||||
IconButton(
|
||||
tooltip: S.of(context)!.print, //TODO: INTL
|
||||
onPressed: () => context.read<DocumentDetailsCubit>().printDocument(),
|
||||
onPressed: () =>
|
||||
context.read<DocumentDetailsCubit>().printDocument(),
|
||||
icon: const Icon(Icons.print),
|
||||
),
|
||||
],
|
||||
@@ -350,7 +383,8 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
||||
),
|
||||
],
|
||||
child: BlocListener<DocumentEditCubit, DocumentEditState>(
|
||||
listenWhen: (previous, current) => previous.document != current.document,
|
||||
listenWhen: (previous, current) =>
|
||||
previous.document != current.document,
|
||||
listener: (context, state) {
|
||||
cubit.replace(state.document);
|
||||
},
|
||||
@@ -370,7 +404,8 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
||||
}
|
||||
|
||||
void _onOpenFileInSystemViewer() async {
|
||||
final status = await context.read<DocumentDetailsCubit>().openDocumentInSystemViewer();
|
||||
final status =
|
||||
await context.read<DocumentDetailsCubit>().openDocumentInSystemViewer();
|
||||
if (status == ResultType.done) return;
|
||||
if (status == ResultType.noAppToOpen) {
|
||||
showGenericError(context, S.of(context)!.noAppToDisplayPDFFilesFound);
|
||||
@@ -379,14 +414,16 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
||||
showGenericError(context, translateError(context, ErrorCode.unknown));
|
||||
}
|
||||
if (status == ResultType.permissionDenied) {
|
||||
showGenericError(context, S.of(context)!.couldNotOpenFilePermissionDenied);
|
||||
showGenericError(
|
||||
context, S.of(context)!.couldNotOpenFilePermissionDenied);
|
||||
}
|
||||
}
|
||||
|
||||
void _onDelete(DocumentModel document) async {
|
||||
final delete = await showDialog(
|
||||
context: context,
|
||||
builder: (context) => DeleteDocumentConfirmationDialog(document: document),
|
||||
builder: (context) =>
|
||||
DeleteDocumentConfirmationDialog(document: document),
|
||||
) ??
|
||||
false;
|
||||
if (delete) {
|
||||
@@ -406,7 +443,8 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => DocumentView(
|
||||
documentBytes: context.read<PaperlessDocumentsApi>().download(document),
|
||||
documentBytes:
|
||||
context.read<PaperlessDocumentsApi>().download(document),
|
||||
title: document.title,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -18,7 +18,8 @@ class ArchiveSerialNumberField extends StatefulWidget {
|
||||
});
|
||||
|
||||
@override
|
||||
State<ArchiveSerialNumberField> createState() => _ArchiveSerialNumberFieldState();
|
||||
State<ArchiveSerialNumberField> createState() =>
|
||||
_ArchiveSerialNumberFieldState();
|
||||
}
|
||||
|
||||
class _ArchiveSerialNumberFieldState extends State<ArchiveSerialNumberField> {
|
||||
@@ -39,21 +40,25 @@ class _ArchiveSerialNumberFieldState extends State<ArchiveSerialNumberField> {
|
||||
void _clearButtonListener() {
|
||||
setState(() {
|
||||
_showClearButton = _asnEditingController.text.isNotEmpty;
|
||||
_canUpdate = int.tryParse(_asnEditingController.text) != widget.document.archiveSerialNumber;
|
||||
_canUpdate = int.tryParse(_asnEditingController.text) !=
|
||||
widget.document.archiveSerialNumber;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final userCanEditDocument = LocalUserAccount.current.paperlessUser.hasPermission(
|
||||
final userCanEditDocument =
|
||||
LocalUserAccount.current.paperlessUser.hasPermission(
|
||||
PermissionAction.change,
|
||||
PermissionTarget.document,
|
||||
);
|
||||
return BlocListener<DocumentDetailsCubit, DocumentDetailsState>(
|
||||
listenWhen: (previous, current) =>
|
||||
previous.document.archiveSerialNumber != current.document.archiveSerialNumber,
|
||||
previous.document.archiveSerialNumber !=
|
||||
current.document.archiveSerialNumber,
|
||||
listener: (context, state) {
|
||||
_asnEditingController.text = state.document.archiveSerialNumber?.toString() ?? '';
|
||||
_asnEditingController.text =
|
||||
state.document.archiveSerialNumber?.toString() ?? '';
|
||||
setState(() {
|
||||
_canUpdate = false;
|
||||
});
|
||||
@@ -80,13 +85,17 @@ class _ArchiveSerialNumberFieldState extends State<ArchiveSerialNumberField> {
|
||||
IconButton(
|
||||
icon: const Icon(Icons.clear),
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
onPressed: userCanEditDocument ? _asnEditingController.clear : null,
|
||||
onPressed: userCanEditDocument
|
||||
? _asnEditingController.clear
|
||||
: null,
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.plus_one_rounded),
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
onPressed:
|
||||
context.watchInternetConnection && !_showClearButton ? _onAutoAssign : null,
|
||||
context.watchInternetConnection && !_showClearButton
|
||||
? _onAutoAssign
|
||||
: null,
|
||||
).paddedOnly(right: 8),
|
||||
],
|
||||
),
|
||||
@@ -97,7 +106,9 @@ class _ArchiveSerialNumberFieldState extends State<ArchiveSerialNumberField> {
|
||||
),
|
||||
TextButton.icon(
|
||||
icon: const Icon(Icons.done),
|
||||
onPressed: context.watchInternetConnection && _canUpdate ? _onSubmitted : null,
|
||||
onPressed: context.watchInternetConnection && _canUpdate
|
||||
? _onSubmitted
|
||||
: null,
|
||||
label: Text(S.of(context)!.save),
|
||||
).padded(),
|
||||
],
|
||||
|
||||
@@ -24,7 +24,8 @@ class DetailsItem extends StatelessWidget {
|
||||
}
|
||||
|
||||
DetailsItem.text(
|
||||
String text, {super.key,
|
||||
String text, {
|
||||
super.key,
|
||||
required this.label,
|
||||
required BuildContext context,
|
||||
}) : content = Text(
|
||||
|
||||
@@ -44,14 +44,16 @@ class _DocumentDownloadButtonState extends State<DocumentDownloadButton> {
|
||||
width: 16,
|
||||
)
|
||||
: const Icon(Icons.download),
|
||||
onPressed:
|
||||
widget.document != null && widget.enabled ? () => _onDownload(widget.document!) : null,
|
||||
onPressed: widget.document != null && widget.enabled
|
||||
? () => _onDownload(widget.document!)
|
||||
: null,
|
||||
).paddedOnly(right: 4);
|
||||
}
|
||||
|
||||
Future<void> _onDownload(DocumentModel document) async {
|
||||
try {
|
||||
final globalSettings = Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!;
|
||||
final globalSettings =
|
||||
Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!;
|
||||
bool original;
|
||||
|
||||
switch (globalSettings.defaultDownloadType) {
|
||||
|
||||
@@ -6,7 +6,8 @@ class DocumentPermissionsWidget extends StatefulWidget {
|
||||
const DocumentPermissionsWidget({super.key, required this.document});
|
||||
|
||||
@override
|
||||
State<DocumentPermissionsWidget> createState() => _DocumentPermissionsWidgetState();
|
||||
State<DocumentPermissionsWidget> createState() =>
|
||||
_DocumentPermissionsWidgetState();
|
||||
}
|
||||
|
||||
class _DocumentPermissionsWidgetState extends State<DocumentPermissionsWidget> {
|
||||
|
||||
@@ -43,14 +43,16 @@ class _DocumentShareButtonState extends State<DocumentShareButton> {
|
||||
child: CircularProgressIndicator(),
|
||||
)
|
||||
: const Icon(Icons.share),
|
||||
onPressed:
|
||||
widget.document != null && widget.enabled ? () => _onShare(widget.document!) : null,
|
||||
onPressed: widget.document != null && widget.enabled
|
||||
? () => _onShare(widget.document!)
|
||||
: null,
|
||||
).paddedOnly(right: 4);
|
||||
}
|
||||
|
||||
Future<void> _onShare(DocumentModel document) async {
|
||||
try {
|
||||
final globalSettings = Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!;
|
||||
final globalSettings =
|
||||
Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!;
|
||||
bool original;
|
||||
|
||||
switch (globalSettings.defaultShareType) {
|
||||
|
||||
@@ -49,8 +49,8 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_filteredSuggestions =
|
||||
widget.suggestions?.documentDifference(context.read<DocumentEditCubit>().state.document);
|
||||
_filteredSuggestions = widget.suggestions
|
||||
?.documentDifference(context.read<DocumentEditCubit>().state.document);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -94,14 +94,16 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
|
||||
ListView(
|
||||
children: [
|
||||
_buildTitleFormField(state.document.title).padded(),
|
||||
_buildCreatedAtFormField(state.document.created).padded(),
|
||||
_buildCreatedAtFormField(state.document.created)
|
||||
.padded(),
|
||||
// Correspondent form field
|
||||
Column(
|
||||
children: [
|
||||
LabelFormField<Correspondent>(
|
||||
showAnyAssignedOption: false,
|
||||
showNotAssignedOption: false,
|
||||
addLabelPageBuilder: (initialValue) => RepositoryProvider.value(
|
||||
addLabelPageBuilder: (initialValue) =>
|
||||
RepositoryProvider.value(
|
||||
value: context.read<LabelRepository>(),
|
||||
child: AddCorrespondentPage(
|
||||
initialName: initialValue,
|
||||
@@ -109,26 +111,39 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
|
||||
),
|
||||
addLabelText: S.of(context)!.addCorrespondent,
|
||||
labelText: S.of(context)!.correspondent,
|
||||
options: context.watch<DocumentEditCubit>().state.correspondents,
|
||||
initialValue: state.document.correspondent != null
|
||||
? IdQueryParameter.fromId(state.document.correspondent!)
|
||||
options: context
|
||||
.watch<DocumentEditCubit>()
|
||||
.state
|
||||
.correspondents,
|
||||
initialValue:
|
||||
state.document.correspondent != null
|
||||
? IdQueryParameter.fromId(
|
||||
state.document.correspondent!)
|
||||
: const IdQueryParameter.unset(),
|
||||
name: fkCorrespondent,
|
||||
prefixIcon: const Icon(Icons.person_outlined),
|
||||
allowSelectUnassigned: true,
|
||||
canCreateNewLabel:
|
||||
LocalUserAccount.current.paperlessUser.hasPermission(
|
||||
canCreateNewLabel: LocalUserAccount
|
||||
.current.paperlessUser
|
||||
.hasPermission(
|
||||
PermissionAction.add,
|
||||
PermissionTarget.correspondent,
|
||||
),
|
||||
),
|
||||
if (_filteredSuggestions?.hasSuggestedCorrespondents ?? false)
|
||||
if (_filteredSuggestions
|
||||
?.hasSuggestedCorrespondents ??
|
||||
false)
|
||||
_buildSuggestionsSkeleton<int>(
|
||||
suggestions: _filteredSuggestions!.correspondents,
|
||||
itemBuilder: (context, itemData) => ActionChip(
|
||||
label: Text(state.correspondents[itemData]!.name),
|
||||
suggestions:
|
||||
_filteredSuggestions!.correspondents,
|
||||
itemBuilder: (context, itemData) =>
|
||||
ActionChip(
|
||||
label: Text(
|
||||
state.correspondents[itemData]!.name),
|
||||
onPressed: () {
|
||||
_formKey.currentState?.fields[fkCorrespondent]?.didChange(
|
||||
_formKey
|
||||
.currentState?.fields[fkCorrespondent]
|
||||
?.didChange(
|
||||
IdQueryParameter.fromId(itemData),
|
||||
);
|
||||
},
|
||||
@@ -142,34 +157,45 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
|
||||
LabelFormField<DocumentType>(
|
||||
showAnyAssignedOption: false,
|
||||
showNotAssignedOption: false,
|
||||
addLabelPageBuilder: (currentInput) => RepositoryProvider.value(
|
||||
addLabelPageBuilder: (currentInput) =>
|
||||
RepositoryProvider.value(
|
||||
value: context.read<LabelRepository>(),
|
||||
child: AddDocumentTypePage(
|
||||
initialName: currentInput,
|
||||
),
|
||||
),
|
||||
canCreateNewLabel:
|
||||
LocalUserAccount.current.paperlessUser.hasPermission(
|
||||
canCreateNewLabel: LocalUserAccount
|
||||
.current.paperlessUser
|
||||
.hasPermission(
|
||||
PermissionAction.add,
|
||||
PermissionTarget.documentType,
|
||||
),
|
||||
addLabelText: S.of(context)!.addDocumentType,
|
||||
labelText: S.of(context)!.documentType,
|
||||
initialValue: state.document.documentType != null
|
||||
? IdQueryParameter.fromId(state.document.documentType!)
|
||||
initialValue:
|
||||
state.document.documentType != null
|
||||
? IdQueryParameter.fromId(
|
||||
state.document.documentType!)
|
||||
: const IdQueryParameter.unset(),
|
||||
options: state.documentTypes,
|
||||
name: _DocumentEditPageState.fkDocumentType,
|
||||
prefixIcon: const Icon(Icons.description_outlined),
|
||||
prefixIcon:
|
||||
const Icon(Icons.description_outlined),
|
||||
allowSelectUnassigned: true,
|
||||
),
|
||||
if (_filteredSuggestions?.hasSuggestedDocumentTypes ?? false)
|
||||
if (_filteredSuggestions
|
||||
?.hasSuggestedDocumentTypes ??
|
||||
false)
|
||||
_buildSuggestionsSkeleton<int>(
|
||||
suggestions: _filteredSuggestions!.documentTypes,
|
||||
itemBuilder: (context, itemData) => ActionChip(
|
||||
label: Text(state.documentTypes[itemData]!.name),
|
||||
onPressed: () =>
|
||||
_formKey.currentState?.fields[fkDocumentType]?.didChange(
|
||||
suggestions:
|
||||
_filteredSuggestions!.documentTypes,
|
||||
itemBuilder: (context, itemData) =>
|
||||
ActionChip(
|
||||
label: Text(
|
||||
state.documentTypes[itemData]!.name),
|
||||
onPressed: () => _formKey
|
||||
.currentState?.fields[fkDocumentType]
|
||||
?.didChange(
|
||||
IdQueryParameter.fromId(itemData),
|
||||
),
|
||||
),
|
||||
@@ -182,12 +208,15 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
|
||||
LabelFormField<StoragePath>(
|
||||
showAnyAssignedOption: false,
|
||||
showNotAssignedOption: false,
|
||||
addLabelPageBuilder: (initialValue) => RepositoryProvider.value(
|
||||
addLabelPageBuilder: (initialValue) =>
|
||||
RepositoryProvider.value(
|
||||
value: context.read<LabelRepository>(),
|
||||
child: AddStoragePathPage(initalName: initialValue),
|
||||
child: AddStoragePathPage(
|
||||
initalName: initialValue),
|
||||
),
|
||||
canCreateNewLabel:
|
||||
LocalUserAccount.current.paperlessUser.hasPermission(
|
||||
canCreateNewLabel: LocalUserAccount
|
||||
.current.paperlessUser
|
||||
.hasPermission(
|
||||
PermissionAction.add,
|
||||
PermissionTarget.storagePath,
|
||||
),
|
||||
@@ -195,7 +224,8 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
|
||||
labelText: S.of(context)!.storagePath,
|
||||
options: state.storagePaths,
|
||||
initialValue: state.document.storagePath != null
|
||||
? IdQueryParameter.fromId(state.document.storagePath!)
|
||||
? IdQueryParameter.fromId(
|
||||
state.document.storagePath!)
|
||||
: const IdQueryParameter.unset(),
|
||||
name: fkStoragePath,
|
||||
prefixIcon: const Icon(Icons.folder_outlined),
|
||||
@@ -220,7 +250,8 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
|
||||
.isNotEmpty ??
|
||||
false)
|
||||
_buildSuggestionsSkeleton<int>(
|
||||
suggestions: (_filteredSuggestions?.tags.toSet() ?? {}),
|
||||
suggestions:
|
||||
(_filteredSuggestions?.tags.toSet() ?? {}),
|
||||
itemBuilder: (context, itemData) {
|
||||
final tag = state.tags[itemData]!;
|
||||
return ActionChip(
|
||||
@@ -230,13 +261,17 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
|
||||
),
|
||||
backgroundColor: tag.color,
|
||||
onPressed: () {
|
||||
final currentTags =
|
||||
_formKey.currentState?.fields[fkTags]?.value as TagsQuery;
|
||||
_formKey.currentState?.fields[fkTags]?.didChange(
|
||||
final currentTags = _formKey.currentState
|
||||
?.fields[fkTags]?.value as TagsQuery;
|
||||
_formKey.currentState?.fields[fkTags]
|
||||
?.didChange(
|
||||
currentTags.maybeWhen(
|
||||
ids: (include, exclude) => TagsQuery.ids(
|
||||
include: [...include, itemData], exclude: exclude),
|
||||
orElse: () => TagsQuery.ids(include: [itemData]),
|
||||
ids: (include, exclude) =>
|
||||
TagsQuery.ids(
|
||||
include: [...include, itemData],
|
||||
exclude: exclude),
|
||||
orElse: () =>
|
||||
TagsQuery.ids(include: [itemData]),
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -278,12 +313,12 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
|
||||
var mergedDocument = document.copyWith(
|
||||
title: values[fkTitle],
|
||||
created: values[fkCreatedDate],
|
||||
documentType: () =>
|
||||
(values[fkDocumentType] as IdQueryParameter).whenOrNull(fromId: (id) => id),
|
||||
correspondent: () =>
|
||||
(values[fkCorrespondent] as IdQueryParameter).whenOrNull(fromId: (id) => id),
|
||||
storagePath: () =>
|
||||
(values[fkStoragePath] as IdQueryParameter).whenOrNull(fromId: (id) => id),
|
||||
documentType: () => (values[fkDocumentType] as IdQueryParameter)
|
||||
.whenOrNull(fromId: (id) => id),
|
||||
correspondent: () => (values[fkCorrespondent] as IdQueryParameter)
|
||||
.whenOrNull(fromId: (id) => id),
|
||||
storagePath: () => (values[fkStoragePath] as IdQueryParameter)
|
||||
.whenOrNull(fromId: (id) => id),
|
||||
tags: (values[fkTags] as IdsTagsQuery).include,
|
||||
content: values[fkContent],
|
||||
);
|
||||
@@ -340,7 +375,8 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
|
||||
suggestions: _filteredSuggestions!.dates,
|
||||
itemBuilder: (context, itemData) => ActionChip(
|
||||
label: Text(DateFormat.yMMMd().format(itemData)),
|
||||
onPressed: () => _formKey.currentState?.fields[fkCreatedDate]?.didChange(itemData),
|
||||
onPressed: () => _formKey.currentState?.fields[fkCreatedDate]
|
||||
?.didChange(itemData),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -369,7 +405,8 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
|
||||
itemBuilder: (context, index) => ColoredChipWrapper(
|
||||
child: itemBuilder(context, suggestions.elementAt(index)),
|
||||
),
|
||||
separatorBuilder: (BuildContext context, int index) => const SizedBox(width: 4.0),
|
||||
separatorBuilder: (BuildContext context, int index) =>
|
||||
const SizedBox(width: 4.0),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -405,7 +442,6 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
|
||||
// final List<Option> options;
|
||||
// final void Function(Option option) onAddOption;
|
||||
|
||||
|
||||
// const OptionsFormField({
|
||||
// super.key,
|
||||
// required this.options,
|
||||
|
||||
@@ -35,9 +35,12 @@ class ScannerPage extends StatefulWidget {
|
||||
State<ScannerPage> createState() => _ScannerPageState();
|
||||
}
|
||||
|
||||
class _ScannerPageState extends State<ScannerPage> with SingleTickerProviderStateMixin {
|
||||
final SliverOverlapAbsorberHandle searchBarHandle = SliverOverlapAbsorberHandle();
|
||||
final SliverOverlapAbsorberHandle actionsHandle = SliverOverlapAbsorberHandle();
|
||||
class _ScannerPageState extends State<ScannerPage>
|
||||
with SingleTickerProviderStateMixin {
|
||||
final SliverOverlapAbsorberHandle searchBarHandle =
|
||||
SliverOverlapAbsorberHandle();
|
||||
final SliverOverlapAbsorberHandle actionsHandle =
|
||||
SliverOverlapAbsorberHandle();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -120,7 +123,6 @@ class _ScannerPageState extends State<ScannerPage> with SingleTickerProviderStat
|
||||
? () => Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => DocumentView(
|
||||
|
||||
documentBytes: _assembleFileBytes(
|
||||
state,
|
||||
forcePdf: true,
|
||||
@@ -175,7 +177,8 @@ class _ScannerPageState extends State<ScannerPage> with SingleTickerProviderStat
|
||||
final success = await EdgeDetection.detectEdge(file.path);
|
||||
if (!success) {
|
||||
if (kDebugMode) {
|
||||
dev.log('[ScannerPage] Scan either not successful or canceled by user.');
|
||||
dev.log(
|
||||
'[ScannerPage] Scan either not successful or canceled by user.');
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -197,7 +200,9 @@ class _ScannerPageState extends State<ScannerPage> with SingleTickerProviderStat
|
||||
if ((uploadResult?.success ?? false) && uploadResult?.taskId != null) {
|
||||
// For paperless version older than 1.11.3, task id will always be null!
|
||||
context.read<DocumentScannerCubit>().reset();
|
||||
context.read<TaskStatusCubit>().listenToTaskChanges(uploadResult!.taskId!);
|
||||
context
|
||||
.read<TaskStatusCubit>()
|
||||
.listenToTaskChanges(uploadResult!.taskId!);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,8 @@ import 'package:paperless_mobile/features/settings/model/view_type.dart';
|
||||
part 'document_search_cubit.g.dart';
|
||||
part 'document_search_state.dart';
|
||||
|
||||
class DocumentSearchCubit extends Cubit<DocumentSearchState> with DocumentPagingBlocMixin {
|
||||
class DocumentSearchCubit extends Cubit<DocumentSearchState>
|
||||
with DocumentPagingBlocMixin {
|
||||
@override
|
||||
final PaperlessDocumentsApi api;
|
||||
|
||||
@@ -23,7 +24,8 @@ class DocumentSearchCubit extends Cubit<DocumentSearchState> with DocumentPaging
|
||||
this.api,
|
||||
this.notifier,
|
||||
this._userAppState,
|
||||
) : super(DocumentSearchState(searchHistory: _userAppState.documentSearchHistory)) {
|
||||
) : super(DocumentSearchState(
|
||||
searchHistory: _userAppState.documentSearchHistory)) {
|
||||
notifier.addListener(
|
||||
this,
|
||||
onDeleted: remove,
|
||||
@@ -46,7 +48,8 @@ class DocumentSearchCubit extends Cubit<DocumentSearchState> with DocumentPaging
|
||||
state.copyWith(
|
||||
searchHistory: [
|
||||
query,
|
||||
...state.searchHistory.whereNot((previousQuery) => previousQuery == query)
|
||||
...state.searchHistory
|
||||
.whereNot((previousQuery) => previousQuery == query)
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -62,7 +65,9 @@ class DocumentSearchCubit extends Cubit<DocumentSearchState> with DocumentPaging
|
||||
void removeHistoryEntry(String entry) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
searchHistory: state.searchHistory.whereNot((element) => element == entry).toList(),
|
||||
searchHistory: state.searchHistory
|
||||
.whereNot((element) => element == entry)
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
_userAppState
|
||||
|
||||
@@ -65,7 +65,10 @@ class _DocumentSearchBarState extends State<DocumentSearchBar> {
|
||||
Flexible(
|
||||
child: Text(
|
||||
S.of(context)!.searchDocuments,
|
||||
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyLarge
|
||||
?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
@@ -112,7 +115,9 @@ class _DocumentSearchBarState extends State<DocumentSearchBar> {
|
||||
icon: GlobalSettingsBuilder(
|
||||
builder: (context, settings) {
|
||||
return ValueListenableBuilder(
|
||||
valueListenable: Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount).listenable(),
|
||||
valueListenable:
|
||||
Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount)
|
||||
.listenable(),
|
||||
builder: (context, box, _) {
|
||||
final account = box.get(settings.currentLoggedInUser!)!;
|
||||
return UserAvatar(account: account);
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
|
||||
import 'package:paperless_mobile/core/repository/label_repository.dart';
|
||||
|
||||
part 'document_upload_state.dart';
|
||||
@@ -12,9 +14,13 @@ class DocumentUploadCubit extends Cubit<DocumentUploadState> {
|
||||
final PaperlessDocumentsApi _documentApi;
|
||||
|
||||
final LabelRepository _labelRepository;
|
||||
final Connectivity _connectivity;
|
||||
|
||||
DocumentUploadCubit(this._labelRepository, this._documentApi)
|
||||
: super(const DocumentUploadState()) {
|
||||
DocumentUploadCubit(
|
||||
this._labelRepository,
|
||||
this._documentApi,
|
||||
this._connectivity,
|
||||
) : super(const DocumentUploadState()) {
|
||||
_labelRepository.addListener(
|
||||
this,
|
||||
onChanged: (labels) {
|
||||
@@ -31,6 +37,7 @@ class DocumentUploadCubit extends Cubit<DocumentUploadState> {
|
||||
Uint8List bytes, {
|
||||
required String filename,
|
||||
required String title,
|
||||
required String userId,
|
||||
int? documentType,
|
||||
int? correspondent,
|
||||
Iterable<int> tags = const [],
|
||||
|
||||
@@ -3,10 +3,13 @@ import 'dart:typed_data';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
|
||||
import 'package:intl/date_symbol_data_local.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/global_settings.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
|
||||
import 'package:paperless_mobile/core/repository/label_repository.dart';
|
||||
import 'package:paperless_mobile/core/type/types.dart';
|
||||
@@ -42,10 +45,12 @@ class DocumentUploadPreparationPage extends StatefulWidget {
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<DocumentUploadPreparationPage> createState() => _DocumentUploadPreparationPageState();
|
||||
State<DocumentUploadPreparationPage> createState() =>
|
||||
_DocumentUploadPreparationPageState();
|
||||
}
|
||||
|
||||
class _DocumentUploadPreparationPageState extends State<DocumentUploadPreparationPage> {
|
||||
class _DocumentUploadPreparationPageState
|
||||
extends State<DocumentUploadPreparationPage> {
|
||||
static const fkFileName = "filename";
|
||||
static final fileNameDateFormat = DateFormat("yyyy_MM_ddTHH_mm_ss");
|
||||
|
||||
@@ -72,7 +77,8 @@ class _DocumentUploadPreparationPageState extends State<DocumentUploadPreparatio
|
||||
title: Text(S.of(context)!.prepareDocument),
|
||||
bottom: _isUploadLoading
|
||||
? const PreferredSize(
|
||||
child: LinearProgressIndicator(), preferredSize: Size.fromHeight(4.0))
|
||||
child: LinearProgressIndicator(),
|
||||
preferredSize: Size.fromHeight(4.0))
|
||||
: null,
|
||||
),
|
||||
floatingActionButton: Visibility(
|
||||
@@ -93,7 +99,8 @@ class _DocumentUploadPreparationPageState extends State<DocumentUploadPreparatio
|
||||
FormBuilderTextField(
|
||||
autovalidateMode: AutovalidateMode.always,
|
||||
name: DocumentModel.titleKey,
|
||||
initialValue: widget.title ?? "scan_${fileNameDateFormat.format(_now)}",
|
||||
initialValue:
|
||||
widget.title ?? "scan_${fileNameDateFormat.format(_now)}",
|
||||
validator: (value) {
|
||||
if (value?.trim().isEmpty ?? true) {
|
||||
return S.of(context)!.thisFieldIsRequired;
|
||||
@@ -105,18 +112,22 @@ class _DocumentUploadPreparationPageState extends State<DocumentUploadPreparatio
|
||||
suffixIcon: IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: () {
|
||||
_formKey.currentState?.fields[DocumentModel.titleKey]?.didChange("");
|
||||
_formKey.currentState?.fields[DocumentModel.titleKey]
|
||||
?.didChange("");
|
||||
if (_syncTitleAndFilename) {
|
||||
_formKey.currentState?.fields[fkFileName]?.didChange("");
|
||||
_formKey.currentState?.fields[fkFileName]
|
||||
?.didChange("");
|
||||
}
|
||||
},
|
||||
),
|
||||
errorText: _errors[DocumentModel.titleKey],
|
||||
),
|
||||
onChanged: (value) {
|
||||
final String transformedValue = _formatFilename(value ?? '');
|
||||
final String transformedValue =
|
||||
_formatFilename(value ?? '');
|
||||
if (_syncTitleAndFilename) {
|
||||
_formKey.currentState?.fields[fkFileName]?.didChange(transformedValue);
|
||||
_formKey.currentState?.fields[fkFileName]
|
||||
?.didChange(transformedValue);
|
||||
}
|
||||
},
|
||||
),
|
||||
@@ -131,10 +142,12 @@ class _DocumentUploadPreparationPageState extends State<DocumentUploadPreparatio
|
||||
suffixText: widget.fileExtension,
|
||||
suffixIcon: IconButton(
|
||||
icon: const Icon(Icons.clear),
|
||||
onPressed: () => _formKey.currentState?.fields[fkFileName]?.didChange(''),
|
||||
onPressed: () => _formKey.currentState?.fields[fkFileName]
|
||||
?.didChange(''),
|
||||
),
|
||||
),
|
||||
initialValue: widget.filename ?? "scan_${fileNameDateFormat.format(_now)}",
|
||||
initialValue: widget.filename ??
|
||||
"scan_${fileNameDateFormat.format(_now)}",
|
||||
),
|
||||
// Synchronize title and filename
|
||||
SwitchListTile(
|
||||
@@ -144,10 +157,13 @@ class _DocumentUploadPreparationPageState extends State<DocumentUploadPreparatio
|
||||
() => _syncTitleAndFilename = value,
|
||||
);
|
||||
if (_syncTitleAndFilename) {
|
||||
final String transformedValue = _formatFilename(
|
||||
_formKey.currentState?.fields[DocumentModel.titleKey]?.value as String);
|
||||
final String transformedValue = _formatFilename(_formKey
|
||||
.currentState
|
||||
?.fields[DocumentModel.titleKey]
|
||||
?.value as String);
|
||||
if (_syncTitleAndFilename) {
|
||||
_formKey.currentState?.fields[fkFileName]?.didChange(transformedValue);
|
||||
_formKey.currentState?.fields[fkFileName]
|
||||
?.didChange(transformedValue);
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -172,7 +188,8 @@ class _DocumentUploadPreparationPageState extends State<DocumentUploadPreparatio
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: () {
|
||||
_formKey.currentState!.fields[DocumentModel.createdKey]
|
||||
_formKey.currentState!
|
||||
.fields[DocumentModel.createdKey]
|
||||
?.didChange(null);
|
||||
},
|
||||
)
|
||||
@@ -183,7 +200,8 @@ class _DocumentUploadPreparationPageState extends State<DocumentUploadPreparatio
|
||||
LabelFormField<Correspondent>(
|
||||
showAnyAssignedOption: false,
|
||||
showNotAssignedOption: false,
|
||||
addLabelPageBuilder: (initialName) => RepositoryProvider.value(
|
||||
addLabelPageBuilder: (initialName) =>
|
||||
RepositoryProvider.value(
|
||||
value: context.read<LabelRepository>(),
|
||||
child: AddCorrespondentPage(initialName: initialName),
|
||||
),
|
||||
@@ -193,7 +211,8 @@ class _DocumentUploadPreparationPageState extends State<DocumentUploadPreparatio
|
||||
options: state.correspondents,
|
||||
prefixIcon: const Icon(Icons.person_outline),
|
||||
allowSelectUnassigned: true,
|
||||
canCreateNewLabel: LocalUserAccount.current.paperlessUser.hasPermission(
|
||||
canCreateNewLabel:
|
||||
LocalUserAccount.current.paperlessUser.hasPermission(
|
||||
PermissionAction.add,
|
||||
PermissionTarget.correspondent,
|
||||
),
|
||||
@@ -202,7 +221,8 @@ class _DocumentUploadPreparationPageState extends State<DocumentUploadPreparatio
|
||||
LabelFormField<DocumentType>(
|
||||
showAnyAssignedOption: false,
|
||||
showNotAssignedOption: false,
|
||||
addLabelPageBuilder: (initialName) => RepositoryProvider.value(
|
||||
addLabelPageBuilder: (initialName) =>
|
||||
RepositoryProvider.value(
|
||||
value: context.read<LabelRepository>(),
|
||||
child: AddDocumentTypePage(initialName: initialName),
|
||||
),
|
||||
@@ -212,7 +232,8 @@ class _DocumentUploadPreparationPageState extends State<DocumentUploadPreparatio
|
||||
options: state.documentTypes,
|
||||
prefixIcon: const Icon(Icons.description_outlined),
|
||||
allowSelectUnassigned: true,
|
||||
canCreateNewLabel: LocalUserAccount.current.paperlessUser.hasPermission(
|
||||
canCreateNewLabel:
|
||||
LocalUserAccount.current.paperlessUser.hasPermission(
|
||||
PermissionAction.add,
|
||||
PermissionTarget.documentType,
|
||||
),
|
||||
@@ -252,7 +273,8 @@ class _DocumentUploadPreparationPageState extends State<DocumentUploadPreparatio
|
||||
final tags = (fv[DocumentModel.tagsKey] as TagsQuery?)
|
||||
?.whenOrNull(ids: (include, exclude) => include) ??
|
||||
[];
|
||||
final correspondent = (fv[DocumentModel.correspondentKey] as IdQueryParameter?)
|
||||
final correspondent =
|
||||
(fv[DocumentModel.correspondentKey] as IdQueryParameter?)
|
||||
?.whenOrNull(fromId: (id) => id);
|
||||
final asn = fv[DocumentModel.asnKey] as int?;
|
||||
final taskId = await cubit.upload(
|
||||
@@ -261,6 +283,9 @@ class _DocumentUploadPreparationPageState extends State<DocumentUploadPreparatio
|
||||
_formKey.currentState?.value[fkFileName],
|
||||
widget.fileExtension,
|
||||
),
|
||||
userId: Hive.box<GlobalSettings>(HiveBoxes.globalSettings)
|
||||
.getValue()!
|
||||
.currentLoggedInUser!,
|
||||
title: title,
|
||||
documentType: docType,
|
||||
correspondent: correspondent,
|
||||
@@ -282,7 +307,8 @@ class _DocumentUploadPreparationPageState extends State<DocumentUploadPreparatio
|
||||
setState(() => _errors = errors);
|
||||
} catch (unknownError, stackTrace) {
|
||||
debugPrint(unknownError.toString());
|
||||
showErrorMessage(context, const PaperlessServerException.unknown(), stackTrace);
|
||||
showErrorMessage(
|
||||
context, const PaperlessServerException.unknown(), stackTrace);
|
||||
} finally {
|
||||
setState(() {
|
||||
_isUploadLoading = false;
|
||||
|
||||
@@ -14,7 +14,8 @@ import 'package:paperless_mobile/features/settings/model/view_type.dart';
|
||||
part 'documents_cubit.g.dart';
|
||||
part 'documents_state.dart';
|
||||
|
||||
class DocumentsCubit extends HydratedCubit<DocumentsState> with DocumentPagingBlocMixin {
|
||||
class DocumentsCubit extends HydratedCubit<DocumentsState>
|
||||
with DocumentPagingBlocMixin {
|
||||
@override
|
||||
final PaperlessDocumentsApi api;
|
||||
|
||||
@@ -40,7 +41,9 @@ class DocumentsCubit extends HydratedCubit<DocumentsState> with DocumentPagingBl
|
||||
replace(document);
|
||||
emit(
|
||||
state.copyWith(
|
||||
selection: state.selection.map((e) => e.id == document.id ? document : e).toList(),
|
||||
selection: state.selection
|
||||
.map((e) => e.id == document.id ? document : e)
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -48,7 +51,8 @@ class DocumentsCubit extends HydratedCubit<DocumentsState> with DocumentPagingBl
|
||||
remove(document);
|
||||
emit(
|
||||
state.copyWith(
|
||||
selection: state.selection.where((e) => e.id != document.id).toList(),
|
||||
selection:
|
||||
state.selection.where((e) => e.id != document.id).toList(),
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -82,7 +86,9 @@ class DocumentsCubit extends HydratedCubit<DocumentsState> with DocumentPagingBl
|
||||
if (state.selectedIds.contains(model.id)) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
selection: state.selection.where((element) => element.id != model.id).toList(),
|
||||
selection: state.selection
|
||||
.where((element) => element.id != model.id)
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
|
||||
@@ -86,7 +86,8 @@ class DocumentsState extends DocumentPagingState {
|
||||
);
|
||||
}
|
||||
|
||||
factory DocumentsState.fromJson(Map<String, dynamic> json) => _$DocumentsStateFromJson(json);
|
||||
factory DocumentsState.fromJson(Map<String, dynamic> json) =>
|
||||
_$DocumentsStateFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$DocumentsStateToJson(this);
|
||||
}
|
||||
|
||||
@@ -22,7 +22,8 @@ class _DocumentViewState extends State<DocumentView> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isInitialized = _controller != null && _currentPage != null && _totalPages != null;
|
||||
final isInitialized =
|
||||
_controller != null && _currentPage != null && _totalPages != null;
|
||||
final canGoToNextPage = isInitialized && _currentPage! + 1 < _totalPages!;
|
||||
final canGoToPreviousPage = isInitialized && _currentPage! > 0;
|
||||
return Scaffold(
|
||||
|
||||
@@ -161,9 +161,7 @@ class _DocumentsPageState extends State<DocumentsPage>
|
||||
}
|
||||
return true;
|
||||
},
|
||||
child: Stack(
|
||||
children: [
|
||||
NestedScrollView(
|
||||
child: NestedScrollView(
|
||||
floatHeaderSlivers: true,
|
||||
headerSliverBuilder: (context, innerBoxIsScrolled) => [
|
||||
SliverOverlapAbsorber(
|
||||
@@ -217,8 +215,7 @@ class _DocumentsPageState extends State<DocumentsPage>
|
||||
return true;
|
||||
}
|
||||
final desiredTab =
|
||||
(metrics.pixels / metrics.maxScrollExtent)
|
||||
.round();
|
||||
(metrics.pixels / metrics.maxScrollExtent).round();
|
||||
if (metrics.axis == Axis.horizontal &&
|
||||
_currentTab != desiredTab) {
|
||||
setState(() => _currentTab = desiredTab);
|
||||
@@ -255,8 +252,6 @@ class _DocumentsPageState extends State<DocumentsPage>
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -43,7 +43,9 @@ class DocumentPreview extends StatelessWidget {
|
||||
fit: fit,
|
||||
alignment: alignment,
|
||||
cacheKey: "thumb_${document.id}",
|
||||
imageUrl: context.read<PaperlessDocumentsApi>().getThumbnailUrl(document.id),
|
||||
imageUrl: context
|
||||
.read<PaperlessDocumentsApi>()
|
||||
.getThumbnailUrl(document.id),
|
||||
errorWidget: (ctxt, msg, __) => Text(msg),
|
||||
placeholder: (context, value) => Shimmer.fromColors(
|
||||
baseColor: Colors.grey[300]!,
|
||||
|
||||
@@ -42,8 +42,9 @@ class DocumentDetailedItem extends DocumentItem {
|
||||
padding.bottom -
|
||||
kBottomNavigationBarHeight -
|
||||
kToolbarHeight;
|
||||
final maxHeight =
|
||||
highlights != null ? min(600.0, availableHeight) : min(500.0, availableHeight);
|
||||
final maxHeight = highlights != null
|
||||
? min(600.0, availableHeight)
|
||||
: min(500.0, availableHeight);
|
||||
return Card(
|
||||
color: isSelected ? Theme.of(context).colorScheme.inversePrimary : null,
|
||||
child: InkWell(
|
||||
@@ -114,8 +115,10 @@ class DocumentDetailedItem extends DocumentItem {
|
||||
textStyle: Theme.of(context).textTheme.titleSmall?.apply(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
correspondent:
|
||||
context.watch<LabelRepository>().state.correspondents[document.correspondent],
|
||||
correspondent: context
|
||||
.watch<LabelRepository>()
|
||||
.state
|
||||
.correspondents[document.correspondent],
|
||||
),
|
||||
],
|
||||
).paddedLTRB(8, 0, 8, 4),
|
||||
@@ -130,8 +133,10 @@ class DocumentDetailedItem extends DocumentItem {
|
||||
textStyle: Theme.of(context).textTheme.titleSmall?.apply(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
documentType:
|
||||
context.watch<LabelRepository>().state.documentTypes[document.documentType],
|
||||
documentType: context
|
||||
.watch<LabelRepository>()
|
||||
.state
|
||||
.documentTypes[document.documentType],
|
||||
),
|
||||
],
|
||||
).paddedLTRB(8, 0, 8, 4),
|
||||
|
||||
@@ -30,8 +30,9 @@ class DocumentGridItem extends DocumentItem {
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Card(
|
||||
elevation: 1.0,
|
||||
color:
|
||||
isSelected ? Theme.of(context).colorScheme.inversePrimary : Theme.of(context).cardColor,
|
||||
color: isSelected
|
||||
? Theme.of(context).colorScheme.inversePrimary
|
||||
: Theme.of(context).cardColor,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
onTap: _onTap,
|
||||
@@ -74,7 +75,8 @@ class DocumentGridItem extends DocumentItem {
|
||||
const Spacer(),
|
||||
TagsWidget(
|
||||
tags: document.tags
|
||||
.map((e) => context.watch<LabelRepository>().state.tags[e]!)
|
||||
.map((e) =>
|
||||
context.watch<LabelRepository>().state.tags[e]!)
|
||||
.toList(),
|
||||
isMultiLine: false,
|
||||
onTagSelected: onTagSelected,
|
||||
|
||||
@@ -22,14 +22,17 @@ class DocumentFilterForm extends StatefulWidget {
|
||||
formKey.currentState?.save();
|
||||
final v = formKey.currentState!.value;
|
||||
return DocumentFilter(
|
||||
correspondent: v[DocumentFilterForm.fkCorrespondent] as IdQueryParameter? ??
|
||||
correspondent:
|
||||
v[DocumentFilterForm.fkCorrespondent] as IdQueryParameter? ??
|
||||
DocumentFilter.initial.correspondent,
|
||||
documentType: v[DocumentFilterForm.fkDocumentType] as IdQueryParameter? ??
|
||||
DocumentFilter.initial.documentType,
|
||||
storagePath: v[DocumentFilterForm.fkStoragePath] as IdQueryParameter? ??
|
||||
DocumentFilter.initial.storagePath,
|
||||
tags: v[DocumentModel.tagsKey] as TagsQuery? ?? DocumentFilter.initial.tags,
|
||||
query: v[DocumentFilterForm.fkQuery] as TextQuery? ?? DocumentFilter.initial.query,
|
||||
tags:
|
||||
v[DocumentModel.tagsKey] as TagsQuery? ?? DocumentFilter.initial.tags,
|
||||
query: v[DocumentFilterForm.fkQuery] as TextQuery? ??
|
||||
DocumentFilter.initial.query,
|
||||
created: (v[DocumentFilterForm.fkCreatedAt] as DateRangeQuery),
|
||||
added: (v[DocumentFilterForm.fkAddedAt] as DateRangeQuery),
|
||||
asnQuery: initialFilter.asnQuery,
|
||||
@@ -134,12 +137,15 @@ class _DocumentFilterFormState extends State<DocumentFilterForm> {
|
||||
}
|
||||
|
||||
void _checkQueryConstraints() {
|
||||
final filter = DocumentFilterForm.assembleFilter(widget.formKey, widget.initialFilter);
|
||||
final filter =
|
||||
DocumentFilterForm.assembleFilter(widget.formKey, widget.initialFilter);
|
||||
if (filter.forceExtendedQuery) {
|
||||
setState(() => _allowOnlyExtendedQuery = true);
|
||||
final queryField = widget.formKey.currentState?.fields[DocumentFilterForm.fkQuery];
|
||||
final queryField =
|
||||
widget.formKey.currentState?.fields[DocumentFilterForm.fkQuery];
|
||||
queryField?.didChange(
|
||||
(queryField.value as TextQuery?)?.copyWith(queryType: QueryType.extended),
|
||||
(queryField.value as TextQuery?)
|
||||
?.copyWith(queryType: QueryType.extended),
|
||||
);
|
||||
} else {
|
||||
setState(() => _allowOnlyExtendedQuery = false);
|
||||
|
||||
@@ -27,10 +27,12 @@ class SortFieldSelectionBottomSheet extends StatefulWidget {
|
||||
});
|
||||
|
||||
@override
|
||||
State<SortFieldSelectionBottomSheet> createState() => _SortFieldSelectionBottomSheetState();
|
||||
State<SortFieldSelectionBottomSheet> createState() =>
|
||||
_SortFieldSelectionBottomSheetState();
|
||||
}
|
||||
|
||||
class _SortFieldSelectionBottomSheetState extends State<SortFieldSelectionBottomSheet> {
|
||||
class _SortFieldSelectionBottomSheetState
|
||||
extends State<SortFieldSelectionBottomSheet> {
|
||||
late SortField? _currentSortField;
|
||||
late SortOrder _currentSortOrder;
|
||||
|
||||
|
||||
@@ -33,12 +33,15 @@ class DocumentSelectionSliverAppBar extends StatelessWidget {
|
||||
onPressed: () async {
|
||||
final shouldDelete = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => BulkDeleteConfirmationDialog(state: state),
|
||||
builder: (context) =>
|
||||
BulkDeleteConfirmationDialog(state: state),
|
||||
) ??
|
||||
false;
|
||||
if (shouldDelete) {
|
||||
try {
|
||||
await context.read<DocumentsCubit>().bulkDelete(state.selection);
|
||||
await context
|
||||
.read<DocumentsCubit>()
|
||||
.bulkDelete(state.selection);
|
||||
showSnackBar(
|
||||
context,
|
||||
S.of(context)!.documentsSuccessfullyDeleted,
|
||||
@@ -62,21 +65,24 @@ class DocumentSelectionSliverAppBar extends StatelessWidget {
|
||||
label: Text(S.of(context)!.correspondent),
|
||||
avatar: const Icon(Icons.edit),
|
||||
onPressed: () {
|
||||
pushBulkEditCorrespondentRoute(context, selection: state.selection);
|
||||
pushBulkEditCorrespondentRoute(context,
|
||||
selection: state.selection);
|
||||
},
|
||||
).paddedOnly(left: 8, right: 4),
|
||||
ActionChip(
|
||||
label: Text(S.of(context)!.documentType),
|
||||
avatar: const Icon(Icons.edit),
|
||||
onPressed: () async {
|
||||
pushBulkEditDocumentTypeRoute(context, selection: state.selection);
|
||||
pushBulkEditDocumentTypeRoute(context,
|
||||
selection: state.selection);
|
||||
},
|
||||
).paddedOnly(left: 8, right: 4),
|
||||
ActionChip(
|
||||
label: Text(S.of(context)!.storagePath),
|
||||
avatar: const Icon(Icons.edit),
|
||||
onPressed: () async {
|
||||
pushBulkEditStoragePathRoute(context, selection: state.selection);
|
||||
pushBulkEditStoragePathRoute(context,
|
||||
selection: state.selection);
|
||||
},
|
||||
).paddedOnly(left: 8, right: 4),
|
||||
_buildBulkEditTagsChip(context).paddedOnly(left: 4, right: 4),
|
||||
|
||||
@@ -50,7 +50,9 @@ class SortDocumentsButton extends StatelessWidget {
|
||||
initialSortField: state.filter.sortField,
|
||||
initialSortOrder: state.filter.sortOrder,
|
||||
onSubmit: (field, order) {
|
||||
return context.read<DocumentsCubit>().updateCurrentFilter(
|
||||
return context
|
||||
.read<DocumentsCubit>()
|
||||
.updateCurrentFilter(
|
||||
(filter) => filter.copyWith(
|
||||
sortField: field,
|
||||
sortOrder: order,
|
||||
|
||||
@@ -20,8 +20,10 @@ class EditCorrespondentPage extends StatelessWidget {
|
||||
return EditLabelPage<Correspondent>(
|
||||
label: correspondent,
|
||||
fromJsonT: Correspondent.fromJson,
|
||||
onSubmit: (context, label) => context.read<EditLabelCubit>().replaceCorrespondent(label),
|
||||
onDelete: (context, label) => context.read<EditLabelCubit>().removeCorrespondent(label),
|
||||
onSubmit: (context, label) =>
|
||||
context.read<EditLabelCubit>().replaceCorrespondent(label),
|
||||
onDelete: (context, label) =>
|
||||
context.read<EditLabelCubit>().removeCorrespondent(label),
|
||||
canDelete: LocalUserAccount.current.paperlessUser.hasPermission(
|
||||
PermissionAction.delete,
|
||||
PermissionTarget.correspondent,
|
||||
|
||||
@@ -18,8 +18,10 @@ class EditDocumentTypePage extends StatelessWidget {
|
||||
child: EditLabelPage<DocumentType>(
|
||||
label: documentType,
|
||||
fromJsonT: DocumentType.fromJson,
|
||||
onSubmit: (context, label) => context.read<EditLabelCubit>().replaceDocumentType(label),
|
||||
onDelete: (context, label) => context.read<EditLabelCubit>().removeDocumentType(label),
|
||||
onSubmit: (context, label) =>
|
||||
context.read<EditLabelCubit>().replaceDocumentType(label),
|
||||
onDelete: (context, label) =>
|
||||
context.read<EditLabelCubit>().removeDocumentType(label),
|
||||
canDelete: LocalUserAccount.current.paperlessUser.hasPermission(
|
||||
PermissionAction.delete,
|
||||
PermissionTarget.documentType,
|
||||
|
||||
@@ -19,8 +19,10 @@ class EditStoragePathPage extends StatelessWidget {
|
||||
child: EditLabelPage<StoragePath>(
|
||||
label: storagePath,
|
||||
fromJsonT: StoragePath.fromJson,
|
||||
onSubmit: (context, label) => context.read<EditLabelCubit>().replaceStoragePath(label),
|
||||
onDelete: (context, label) => context.read<EditLabelCubit>().removeStoragePath(label),
|
||||
onSubmit: (context, label) =>
|
||||
context.read<EditLabelCubit>().replaceStoragePath(label),
|
||||
onDelete: (context, label) =>
|
||||
context.read<EditLabelCubit>().removeStoragePath(label),
|
||||
canDelete: LocalUserAccount.current.paperlessUser.hasPermission(
|
||||
PermissionAction.delete,
|
||||
PermissionTarget.storagePath,
|
||||
|
||||
@@ -22,8 +22,10 @@ class EditTagPage extends StatelessWidget {
|
||||
child: EditLabelPage<Tag>(
|
||||
label: tag,
|
||||
fromJsonT: Tag.fromJson,
|
||||
onSubmit: (context, label) => context.read<EditLabelCubit>().replaceTag(label),
|
||||
onDelete: (context, label) => context.read<EditLabelCubit>().removeTag(label),
|
||||
onSubmit: (context, label) =>
|
||||
context.read<EditLabelCubit>().replaceTag(label),
|
||||
onDelete: (context, label) =>
|
||||
context.read<EditLabelCubit>().removeTag(label),
|
||||
canDelete: LocalUserAccount.current.paperlessUser.hasPermission(
|
||||
PermissionAction.delete,
|
||||
PermissionTarget.tag,
|
||||
|
||||
@@ -6,8 +6,11 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
|
||||
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/global_settings.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
|
||||
import 'package:paperless_mobile/core/global/constants.dart';
|
||||
import 'package:paperless_mobile/core/navigation/push_routes.dart';
|
||||
@@ -42,12 +45,23 @@ class HomePage extends StatefulWidget {
|
||||
class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
|
||||
int _currentIndex = 0;
|
||||
late Timer _inboxTimer;
|
||||
late final StreamSubscription _shareMediaSubscription;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
_listenToInboxChanges();
|
||||
final currentUser = Hive.box<GlobalSettings>(HiveBoxes.globalSettings)
|
||||
.getValue()!
|
||||
.currentLoggedInUser!;
|
||||
// For sharing files coming from outside the app while the app is still opened
|
||||
_shareMediaSubscription = ReceiveSharingIntent.getMediaStream().listen(
|
||||
(files) =>
|
||||
ShareIntentQueue.instance.addAll(files, userId: currentUser));
|
||||
// For sharing files coming from outside the app while the app is closed
|
||||
ReceiveSharingIntent.getInitialMedia().then((files) =>
|
||||
ShareIntentQueue.instance.addAll(files, userId: currentUser));
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||
_listenForReceivedFiles();
|
||||
});
|
||||
@@ -59,7 +73,7 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
|
||||
}
|
||||
|
||||
void _listenToInboxChanges() {
|
||||
_inboxTimer = Timer.periodic(const Duration(seconds: 10), (timer) {
|
||||
_inboxTimer = Timer.periodic(const Duration(seconds: 60), (timer) {
|
||||
if (!mounted) {
|
||||
timer.cancel();
|
||||
} else {
|
||||
@@ -93,17 +107,21 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
|
||||
void dispose() {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
_inboxTimer.cancel();
|
||||
_shareMediaSubscription.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _listenForReceivedFiles() async {
|
||||
if (ShareIntentQueue.instance.hasUnhandledFiles) {
|
||||
await _handleReceivedFile(ShareIntentQueue.instance.pop()!);
|
||||
final currentUser = Hive.box<GlobalSettings>(HiveBoxes.globalSettings)
|
||||
.getValue()!
|
||||
.currentLoggedInUser!;
|
||||
if (ShareIntentQueue.instance.userHasUnhandlesFiles(currentUser)) {
|
||||
await _handleReceivedFile(ShareIntentQueue.instance.pop(currentUser)!);
|
||||
}
|
||||
ShareIntentQueue.instance.addListener(() async {
|
||||
final queue = ShareIntentQueue.instance;
|
||||
while (queue.hasUnhandledFiles) {
|
||||
final file = queue.pop()!;
|
||||
while (queue.userHasUnhandlesFiles(currentUser)) {
|
||||
final file = queue.pop(currentUser)!;
|
||||
await _handleReceivedFile(file);
|
||||
}
|
||||
});
|
||||
@@ -115,7 +133,7 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _handleReceivedFile(SharedMediaFile file) async {
|
||||
Future<void> _handleReceivedFile(final SharedMediaFile file) async {
|
||||
SharedMediaFile mediaFile;
|
||||
if (Platform.isIOS) {
|
||||
// Workaround for file not found on iOS: https://stackoverflow.com/a/72813212
|
||||
@@ -128,7 +146,7 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
|
||||
} else {
|
||||
mediaFile = file;
|
||||
}
|
||||
|
||||
debugPrint("Consuming media file: ${mediaFile.path}");
|
||||
if (!_isFileTypeSupported(mediaFile)) {
|
||||
Fluttertoast.showToast(
|
||||
msg: translateError(context, ErrorCode.unsupportedFileFormat),
|
||||
@@ -149,7 +167,7 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
|
||||
}
|
||||
final fileDescription = FileDescription.fromPath(mediaFile.path);
|
||||
if (await File(mediaFile.path).exists()) {
|
||||
final bytes = File(mediaFile.path).readAsBytesSync();
|
||||
final bytes = await File(mediaFile.path).readAsBytes();
|
||||
final result = await pushDocumentUploadPreparationPage(
|
||||
context,
|
||||
bytes: bytes,
|
||||
|
||||
@@ -53,36 +53,42 @@ class HomeRoute extends StatelessWidget {
|
||||
Config(
|
||||
// Isolated cache per user.
|
||||
localUserId,
|
||||
fileService: DioFileService(context.read<SessionManager>().client),
|
||||
fileService:
|
||||
DioFileService(context.read<SessionManager>().client),
|
||||
),
|
||||
),
|
||||
),
|
||||
ProxyProvider<SessionManager, PaperlessDocumentsApi>(
|
||||
update: (context, value, previous) => paperlessProviderFactory.createDocumentsApi(
|
||||
update: (context, value, previous) =>
|
||||
paperlessProviderFactory.createDocumentsApi(
|
||||
value.client,
|
||||
apiVersion: paperlessApiVersion,
|
||||
),
|
||||
),
|
||||
ProxyProvider<SessionManager, PaperlessLabelsApi>(
|
||||
update: (context, value, previous) => paperlessProviderFactory.createLabelsApi(
|
||||
update: (context, value, previous) =>
|
||||
paperlessProviderFactory.createLabelsApi(
|
||||
value.client,
|
||||
apiVersion: paperlessApiVersion,
|
||||
),
|
||||
),
|
||||
ProxyProvider<SessionManager, PaperlessSavedViewsApi>(
|
||||
update: (context, value, previous) => paperlessProviderFactory.createSavedViewsApi(
|
||||
update: (context, value, previous) =>
|
||||
paperlessProviderFactory.createSavedViewsApi(
|
||||
value.client,
|
||||
apiVersion: paperlessApiVersion,
|
||||
),
|
||||
),
|
||||
ProxyProvider<SessionManager, PaperlessServerStatsApi>(
|
||||
update: (context, value, previous) => paperlessProviderFactory.createServerStatsApi(
|
||||
update: (context, value, previous) =>
|
||||
paperlessProviderFactory.createServerStatsApi(
|
||||
value.client,
|
||||
apiVersion: paperlessApiVersion,
|
||||
),
|
||||
),
|
||||
ProxyProvider<SessionManager, PaperlessTasksApi>(
|
||||
update: (context, value, previous) => paperlessProviderFactory.createTasksApi(
|
||||
update: (context, value, previous) =>
|
||||
paperlessProviderFactory.createTasksApi(
|
||||
value.client,
|
||||
apiVersion: paperlessApiVersion,
|
||||
),
|
||||
@@ -98,29 +104,41 @@ class HomeRoute extends StatelessWidget {
|
||||
return MultiProvider(
|
||||
providers: [
|
||||
ProxyProvider<PaperlessLabelsApi, LabelRepository>(
|
||||
update: (context, value, previous) => LabelRepository(value)..initialize(),
|
||||
update: (context, value, previous) =>
|
||||
LabelRepository(value)..initialize(),
|
||||
),
|
||||
ProxyProvider<PaperlessSavedViewsApi, SavedViewRepository>(
|
||||
update: (context, value, previous) => SavedViewRepository(value)..initialize(),
|
||||
update: (context, value, previous) =>
|
||||
SavedViewRepository(value)..initialize(),
|
||||
),
|
||||
],
|
||||
builder: (context, child) {
|
||||
return MultiProvider(
|
||||
providers: [
|
||||
ProxyProvider3<PaperlessDocumentsApi, DocumentChangedNotifier, LabelRepository,
|
||||
ProxyProvider3<
|
||||
PaperlessDocumentsApi,
|
||||
DocumentChangedNotifier,
|
||||
LabelRepository,
|
||||
DocumentsCubit>(
|
||||
update: (context, docApi, notifier, labelRepo, previous) => DocumentsCubit(
|
||||
update:
|
||||
(context, docApi, notifier, labelRepo, previous) =>
|
||||
DocumentsCubit(
|
||||
docApi,
|
||||
notifier,
|
||||
labelRepo,
|
||||
Hive.box<LocalUserAppState>(HiveBoxes.localUserAppState)
|
||||
.get(currentLocalUserId)!,
|
||||
)..reload(),
|
||||
)..initialize(),
|
||||
),
|
||||
Provider(create: (context) => DocumentScannerCubit()),
|
||||
ProxyProvider4<PaperlessDocumentsApi, PaperlessServerStatsApi, LabelRepository,
|
||||
DocumentChangedNotifier, InboxCubit>(
|
||||
update: (context, docApi, statsApi, labelRepo, notifier, previous) =>
|
||||
ProxyProvider4<
|
||||
PaperlessDocumentsApi,
|
||||
PaperlessServerStatsApi,
|
||||
LabelRepository,
|
||||
DocumentChangedNotifier,
|
||||
InboxCubit>(
|
||||
update: (context, docApi, statsApi, labelRepo, notifier,
|
||||
previous) =>
|
||||
InboxCubit(
|
||||
docApi,
|
||||
statsApi,
|
||||
@@ -129,19 +147,22 @@ class HomeRoute extends StatelessWidget {
|
||||
)..initialize(),
|
||||
),
|
||||
ProxyProvider<SavedViewRepository, SavedViewCubit>(
|
||||
update: (context, savedViewRepo, previous) => SavedViewCubit(
|
||||
update: (context, savedViewRepo, previous) =>
|
||||
SavedViewCubit(
|
||||
savedViewRepo,
|
||||
)..initialize(),
|
||||
),
|
||||
),
|
||||
ProxyProvider<LabelRepository, LabelCubit>(
|
||||
update: (context, value, previous) => LabelCubit(value),
|
||||
),
|
||||
ProxyProvider<PaperlessTasksApi, TaskStatusCubit>(
|
||||
update: (context, value, previous) => TaskStatusCubit(value),
|
||||
update: (context, value, previous) =>
|
||||
TaskStatusCubit(value),
|
||||
),
|
||||
if (paperlessApiVersion >= 3)
|
||||
ProxyProvider<PaperlessUserApiV3, UserRepository>(
|
||||
update: (context, value, previous) => UserRepository(value)..initialize(),
|
||||
update: (context, value, previous) =>
|
||||
UserRepository(value)..initialize(),
|
||||
),
|
||||
],
|
||||
child: HomePage(paperlessApiVersion: paperlessApiVersion),
|
||||
|
||||
@@ -30,7 +30,9 @@ class VerifyIdentityPage extends StatelessWidget {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(S.of(context)!.useTheConfiguredBiometricFactorToAuthenticate)
|
||||
Text(S
|
||||
.of(context)!
|
||||
.useTheConfiguredBiometricFactorToAuthenticate)
|
||||
.paddedSymmetrically(horizontal: 16),
|
||||
const Icon(
|
||||
Icons.fingerprint,
|
||||
@@ -52,7 +54,9 @@ class VerifyIdentityPage extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () => context.read<AuthenticationCubit>().restoreSessionState(),
|
||||
onPressed: () => context
|
||||
.read<AuthenticationCubit>()
|
||||
.restoreSessionState(),
|
||||
child: Text(S.of(context)!.verifyIdentity),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -129,8 +129,9 @@ class _FullscreenTagsFormState extends State<FullscreenTagsForm> {
|
||||
),
|
||||
],
|
||||
bottom: PreferredSize(
|
||||
preferredSize:
|
||||
!widget.allowOnlySelection ? const Size.fromHeight(32) : const Size.fromHeight(1),
|
||||
preferredSize: !widget.allowOnlySelection
|
||||
? const Size.fromHeight(32)
|
||||
: const Size.fromHeight(1),
|
||||
child: Column(
|
||||
children: [
|
||||
Divider(color: theme.colorScheme.outline),
|
||||
@@ -233,7 +234,8 @@ class _FullscreenTagsFormState extends State<FullscreenTagsForm> {
|
||||
yield _buildNotAssignedOption();
|
||||
}
|
||||
|
||||
var matches = _options.where((e) => e.name.trim().toLowerCase().contains(normalizedQuery));
|
||||
var matches = _options
|
||||
.where((e) => e.name.trim().toLowerCase().contains(normalizedQuery));
|
||||
if (matches.isEmpty && widget.allowCreation) {
|
||||
yield Text(S.of(context)!.noItemsFound);
|
||||
yield TextButton(
|
||||
@@ -299,7 +301,9 @@ class SelectableTagWidget extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
title: Text(tag.name),
|
||||
trailing: excluded ? const Icon(Icons.close) : (selected ? const Icon(Icons.done) : null),
|
||||
trailing: excluded
|
||||
? const Icon(Icons.close)
|
||||
: (selected ? const Icon(Icons.done) : null),
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: tag.color,
|
||||
child: (tag.isInboxTag)
|
||||
|
||||
@@ -32,8 +32,8 @@ class TagsFormField extends StatelessWidget {
|
||||
initialValue: initialValue,
|
||||
builder: (field) {
|
||||
final values = _generateOptions(context, field.value, field).toList();
|
||||
final isEmpty =
|
||||
(field.value is IdsTagsQuery && (field.value as IdsTagsQuery).include.isEmpty) ||
|
||||
final isEmpty = (field.value is IdsTagsQuery &&
|
||||
(field.value as IdsTagsQuery).include.isEmpty) ||
|
||||
field.value == null;
|
||||
bool anyAssigned = field.value is AnyAssignedTagsQuery;
|
||||
return OpenContainer<TagsQuery>(
|
||||
@@ -59,7 +59,8 @@ class TagsFormField extends StatelessWidget {
|
||||
height: 32,
|
||||
child: ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
separatorBuilder: (context, index) => const SizedBox(width: 4),
|
||||
separatorBuilder: (context, index) =>
|
||||
const SizedBox(width: 4),
|
||||
itemBuilder: (context, index) => values[index],
|
||||
itemCount: values.length,
|
||||
),
|
||||
@@ -99,11 +100,14 @@ class TagsFormField extends StatelessWidget {
|
||||
} else {
|
||||
final widgets = query.map(
|
||||
ids: (value) => [
|
||||
for (var inc in value.include) _buildTagIdQueryWidget(context, inc, field, false),
|
||||
for (var exc in value.exclude) _buildTagIdQueryWidget(context, exc, field, true),
|
||||
for (var inc in value.include)
|
||||
_buildTagIdQueryWidget(context, inc, field, false),
|
||||
for (var exc in value.exclude)
|
||||
_buildTagIdQueryWidget(context, exc, field, true),
|
||||
],
|
||||
anyAssigned: (value) => [
|
||||
for (var id in value.tagIds) _buildAnyAssignedTagWidget(context, id, field, value),
|
||||
for (var id in value.tagIds)
|
||||
_buildAnyAssignedTagWidget(context, id, field, value),
|
||||
],
|
||||
notAssigned: (value) => [_buildNotAssignedTagWidget(context, field)],
|
||||
);
|
||||
@@ -124,15 +128,19 @@ class TagsFormField extends StatelessWidget {
|
||||
final tag = options[id]!;
|
||||
return QueryTagChip(
|
||||
onDeleted: () => field.didChange(formValue.copyWith(
|
||||
include: formValue.include.whereNot((element) => element == id).toList(),
|
||||
exclude: formValue.exclude.whereNot((element) => element == id).toList(),
|
||||
include:
|
||||
formValue.include.whereNot((element) => element == id).toList(),
|
||||
exclude:
|
||||
formValue.exclude.whereNot((element) => element == id).toList(),
|
||||
)),
|
||||
onSelected: allowExclude
|
||||
? () {
|
||||
if (formValue.include.contains(id)) {
|
||||
field.didChange(
|
||||
formValue.copyWith(
|
||||
include: formValue.include.whereNot((element) => element == id).toList(),
|
||||
include: formValue.include
|
||||
.whereNot((element) => element == id)
|
||||
.toList(),
|
||||
exclude: [...formValue.exclude, id],
|
||||
),
|
||||
);
|
||||
@@ -140,7 +148,9 @@ class TagsFormField extends StatelessWidget {
|
||||
field.didChange(
|
||||
formValue.copyWith(
|
||||
include: [...formValue.include, id],
|
||||
exclude: formValue.exclude.whereNot((element) => element == id).toList(),
|
||||
exclude: formValue.exclude
|
||||
.whereNot((element) => element == id)
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -35,7 +35,8 @@ class FullscreenLabelForm<T extends Label> extends StatefulWidget {
|
||||
!(initialValue?.isOnlyAssigned() ?? false) || showAnyAssignedOption,
|
||||
),
|
||||
assert(
|
||||
!(initialValue?.isOnlyNotAssigned() ?? false) || showNotAssignedOption,
|
||||
!(initialValue?.isOnlyNotAssigned() ?? false) ||
|
||||
showNotAssignedOption,
|
||||
),
|
||||
assert((addNewLabelText != null) == (onCreateNewLabel != null));
|
||||
|
||||
@@ -43,7 +44,8 @@ class FullscreenLabelForm<T extends Label> extends StatefulWidget {
|
||||
State<FullscreenLabelForm> createState() => _FullscreenLabelFormState();
|
||||
}
|
||||
|
||||
class _FullscreenLabelFormState<T extends Label> extends State<FullscreenLabelForm<T>> {
|
||||
class _FullscreenLabelFormState<T extends Label>
|
||||
extends State<FullscreenLabelForm<T>> {
|
||||
bool _showClearIcon = false;
|
||||
final _textEditingController = TextEditingController();
|
||||
final _focusNode = FocusNode();
|
||||
@@ -133,9 +135,11 @@ class _FullscreenLabelFormState<T extends Label> extends State<FullscreenLabelFo
|
||||
itemCount: options.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
final option = options.elementAt(index);
|
||||
final highlight = AutocompleteHighlightedOption.of(context) == index;
|
||||
final highlight =
|
||||
AutocompleteHighlightedOption.of(context) == index;
|
||||
if (highlight) {
|
||||
SchedulerBinding.instance.addPostFrameCallback((Duration timeStamp) {
|
||||
SchedulerBinding.instance
|
||||
.addPostFrameCallback((Duration timeStamp) {
|
||||
Scrollable.ensureVisible(
|
||||
context,
|
||||
alignment: 0,
|
||||
@@ -191,7 +195,8 @@ class _FullscreenLabelFormState<T extends Label> extends State<FullscreenLabelFo
|
||||
for (final option in widget.options.values) {
|
||||
// Don't include the initial value in the selection
|
||||
final initialValue = widget.initialValue;
|
||||
if (initialValue is SetIdQueryParameter && option.id == initialValue.id) {
|
||||
if (initialValue is SetIdQueryParameter &&
|
||||
option.id == initialValue.id) {
|
||||
continue;
|
||||
}
|
||||
yield IdQueryParameter.fromId(option.id!);
|
||||
@@ -199,8 +204,8 @@ class _FullscreenLabelFormState<T extends Label> extends State<FullscreenLabelFo
|
||||
}
|
||||
} else {
|
||||
// Show filtered options, if no matching option is found, always show not assigned and any assigned (if enabled) and proceed.
|
||||
final matches =
|
||||
widget.options.values.where((e) => e.name.trim().toLowerCase().contains(normalizedQuery));
|
||||
final matches = widget.options.values
|
||||
.where((e) => e.name.trim().toLowerCase().contains(normalizedQuery));
|
||||
if (matches.isNotEmpty) {
|
||||
for (final match in matches) {
|
||||
yield IdQueryParameter.fromId(match.id!);
|
||||
@@ -270,7 +275,9 @@ class _FullscreenLabelFormState<T extends Label> extends State<FullscreenLabelFo
|
||||
selectedTileColor: Theme.of(context).focusColor,
|
||||
title: Text(widget.options[id]!.name),
|
||||
onTap: onTap,
|
||||
enabled: widget.allowSelectUnassigned ? true : widget.options[id]!.documentCount != 0,
|
||||
enabled: widget.allowSelectUnassigned
|
||||
? true
|
||||
: widget.options[id]!.documentCount != 0,
|
||||
),
|
||||
)!; // Never null, since we already return on unset before
|
||||
}
|
||||
|
||||
@@ -58,8 +58,8 @@ class LabelFormField<T extends Label> extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isEnabled =
|
||||
options.values.any((e) => (e.documentCount ?? 0) > 0) || addLabelPageBuilder != null;
|
||||
final isEnabled = options.values.any((e) => (e.documentCount ?? 0) > 0) ||
|
||||
addLabelPageBuilder != null;
|
||||
return FormBuilderField<IdQueryParameter>(
|
||||
name: name,
|
||||
initialValue: initialValue,
|
||||
@@ -70,7 +70,9 @@ class LabelFormField<T extends Label> extends StatelessWidget {
|
||||
text: _buildText(context, field.value),
|
||||
);
|
||||
final displayedSuggestions = suggestions
|
||||
.whereNot((e) => e.id == field.value?.maybeWhen(fromId: (id) => id, orElse: () => -1))
|
||||
.whereNot((e) =>
|
||||
e.id ==
|
||||
field.value?.maybeWhen(fromId: (id) => id, orElse: () => -1))
|
||||
.toList();
|
||||
|
||||
return Column(
|
||||
@@ -95,7 +97,8 @@ class LabelFormField<T extends Label> extends StatelessWidget {
|
||||
suffixIcon: controller.text.isNotEmpty
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.clear),
|
||||
onPressed: () => field.didChange(const IdQueryParameter.unset()),
|
||||
onPressed: () =>
|
||||
field.didChange(const IdQueryParameter.unset()),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
@@ -110,7 +113,8 @@ class LabelFormField<T extends Label> extends StatelessWidget {
|
||||
? (initialName) {
|
||||
return Navigator.of(context).push<T>(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => addLabelPageBuilder!(initialName),
|
||||
builder: (context) =>
|
||||
addLabelPageBuilder!(initialName),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -141,7 +145,8 @@ class LabelFormField<T extends Label> extends StatelessWidget {
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: displayedSuggestions.length,
|
||||
itemBuilder: (context, index) {
|
||||
final suggestion = displayedSuggestions.elementAt(index);
|
||||
final suggestion =
|
||||
displayedSuggestions.elementAt(index);
|
||||
return ColoredChipWrapper(
|
||||
child: ActionChip(
|
||||
label: Text(suggestion.name),
|
||||
|
||||
@@ -74,8 +74,11 @@ class LabelTabView<T extends Label> extends StatelessWidget {
|
||||
name: l.name,
|
||||
content: contentBuilder?.call(l) ??
|
||||
Text(
|
||||
translateMatchingAlgorithmName(context, l.matchingAlgorithm) +
|
||||
((l.match?.isNotEmpty ?? false) ? ": ${l.match}" : ""),
|
||||
translateMatchingAlgorithmName(
|
||||
context, l.matchingAlgorithm) +
|
||||
((l.match?.isNotEmpty ?? false)
|
||||
? ": ${l.match}"
|
||||
: ""),
|
||||
maxLines: 2,
|
||||
),
|
||||
onOpenEditPage: canEdit ? onEdit : null,
|
||||
|
||||
@@ -49,7 +49,8 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
||||
final apiVersion = await _getApiVersion(_sessionManager.client);
|
||||
|
||||
// Mark logged in user as currently active user.
|
||||
final globalSettings = Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!;
|
||||
final globalSettings =
|
||||
Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!;
|
||||
globalSettings.currentLoggedInUser = localUserId;
|
||||
await globalSettings.save();
|
||||
|
||||
@@ -64,11 +65,13 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
||||
/// Switches to another account if it exists.
|
||||
Future<void> switchAccount(String localUserId) async {
|
||||
emit(const AuthenticationState.switchingAccounts());
|
||||
final globalSettings = Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!;
|
||||
final globalSettings =
|
||||
Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!;
|
||||
if (globalSettings.currentLoggedInUser == localUserId) {
|
||||
return;
|
||||
}
|
||||
final userAccountBox = Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount);
|
||||
final userAccountBox =
|
||||
Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount);
|
||||
|
||||
if (!userAccountBox.containsKey(localUserId)) {
|
||||
debugPrint("User $localUserId not yet registered.");
|
||||
@@ -78,15 +81,15 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
||||
final account = userAccountBox.get(localUserId)!;
|
||||
|
||||
if (account.settings.isBiometricAuthenticationEnabled) {
|
||||
final authenticated =
|
||||
await _localAuthService.authenticateLocalUser("Authenticate to switch your account.");
|
||||
final authenticated = await _localAuthService
|
||||
.authenticateLocalUser("Authenticate to switch your account.");
|
||||
if (!authenticated) {
|
||||
debugPrint("User not authenticated.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
await withEncryptedBox<UserCredentials, void>(HiveBoxes.localUserCredentials,
|
||||
(credentialsBox) async {
|
||||
await withEncryptedBox<UserCredentials, void>(
|
||||
HiveBoxes.localUserCredentials, (credentialsBox) async {
|
||||
if (!credentialsBox.containsKey(localUserId)) {
|
||||
await credentialsBox.close();
|
||||
debugPrint("Invalid authentication for $localUserId");
|
||||
@@ -108,7 +111,8 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
||||
|
||||
await _updateRemoteUser(
|
||||
_sessionManager,
|
||||
Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount).get(localUserId)!,
|
||||
Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount)
|
||||
.get(localUserId)!,
|
||||
apiVersion,
|
||||
);
|
||||
|
||||
@@ -140,12 +144,15 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
||||
}
|
||||
|
||||
Future<void> removeAccount(String userId) async {
|
||||
final userAccountBox = Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount);
|
||||
final userAppStateBox = Hive.box<LocalUserAppState>(HiveBoxes.localUserAppState);
|
||||
final userAccountBox =
|
||||
Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount);
|
||||
final userAppStateBox =
|
||||
Hive.box<LocalUserAppState>(HiveBoxes.localUserAppState);
|
||||
|
||||
await userAccountBox.delete(userId);
|
||||
await userAppStateBox.delete(userId);
|
||||
await withEncryptedBox<UserCredentials, void>(HiveBoxes.localUserCredentials, (box) {
|
||||
await withEncryptedBox<UserCredentials, void>(
|
||||
HiveBoxes.localUserCredentials, (box) {
|
||||
box.delete(userId);
|
||||
});
|
||||
}
|
||||
@@ -154,25 +161,28 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
||||
/// Performs a conditional hydration based on the local authentication success.
|
||||
///
|
||||
Future<void> restoreSessionState() async {
|
||||
final globalSettings = Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!;
|
||||
final globalSettings =
|
||||
Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!;
|
||||
final localUserId = globalSettings.currentLoggedInUser;
|
||||
if (localUserId == null) {
|
||||
// If there is nothing to restore, we can quit here.
|
||||
return;
|
||||
}
|
||||
final localUserAccountBox = Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount);
|
||||
final localUserAccountBox =
|
||||
Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount);
|
||||
final localUserAccount = localUserAccountBox.get(localUserId)!;
|
||||
|
||||
if (localUserAccount.settings.isBiometricAuthenticationEnabled) {
|
||||
final localAuthSuccess =
|
||||
await _localAuthService.authenticateLocalUser("Authenticate to log back in"); //TODO: INTL
|
||||
final localAuthSuccess = await _localAuthService
|
||||
.authenticateLocalUser("Authenticate to log back in"); //TODO: INTL
|
||||
if (!localAuthSuccess) {
|
||||
emit(const AuthenticationState.requriresLocalAuthentication());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
final authentication = await withEncryptedBox<UserCredentials, UserCredentials>(
|
||||
final authentication =
|
||||
await withEncryptedBox<UserCredentials, UserCredentials>(
|
||||
HiveBoxes.localUserCredentials, (box) {
|
||||
return box.get(globalSettings.currentLoggedInUser!);
|
||||
});
|
||||
@@ -202,7 +212,8 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
||||
|
||||
Future<void> logout() async {
|
||||
await _resetExternalState();
|
||||
final globalSettings = Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!;
|
||||
final globalSettings =
|
||||
Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!;
|
||||
globalSettings.currentLoggedInUser = null;
|
||||
await globalSettings.save();
|
||||
emit(const AuthenticationState.unauthenticated());
|
||||
@@ -240,8 +251,10 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
||||
authToken: token,
|
||||
);
|
||||
|
||||
final userAccountBox = Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount);
|
||||
final userStateBox = Hive.box<LocalUserAppState>(HiveBoxes.localUserAppState);
|
||||
final userAccountBox =
|
||||
Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount);
|
||||
final userStateBox =
|
||||
Hive.box<LocalUserAppState>(HiveBoxes.localUserAppState);
|
||||
|
||||
if (userAccountBox.containsKey(localUserId)) {
|
||||
throw Exception("User already exists!");
|
||||
|
||||
@@ -3,7 +3,8 @@ part of 'authentication_cubit.dart';
|
||||
@freezed
|
||||
class AuthenticationState with _$AuthenticationState {
|
||||
const factory AuthenticationState.unauthenticated() = _Unauthenticated;
|
||||
const factory AuthenticationState.requriresLocalAuthentication() = _RequiresLocalAuthentication;
|
||||
const factory AuthenticationState.requriresLocalAuthentication() =
|
||||
_RequiresLocalAuthentication;
|
||||
const factory AuthenticationState.authenticated({
|
||||
required String localUserId,
|
||||
required int apiVersion,
|
||||
|
||||
@@ -27,8 +27,8 @@ class OldAuthenticationState with EquatableMixin {
|
||||
}) {
|
||||
return OldAuthenticationState(
|
||||
isAuthenticated: isAuthenticated ?? this.isAuthenticated,
|
||||
showBiometricAuthenticationScreen:
|
||||
showBiometricAuthenticationScreen ?? this.showBiometricAuthenticationScreen,
|
||||
showBiometricAuthenticationScreen: showBiometricAuthenticationScreen ??
|
||||
this.showBiometricAuthenticationScreen,
|
||||
username: username ?? this.username,
|
||||
fullName: fullName ?? this.fullName,
|
||||
localUserId: localUserId ?? this.localUserId,
|
||||
|
||||
@@ -54,7 +54,8 @@ class _LoginPageState extends State<LoginPage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final localAccounts = Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount);
|
||||
final localAccounts =
|
||||
Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount);
|
||||
return Scaffold(
|
||||
resizeToAvoidBottomInset: false,
|
||||
body: FormBuilder(
|
||||
@@ -91,7 +92,9 @@ class _LoginPageState extends State<LoginPage> {
|
||||
child: UserAccountListTile(
|
||||
account: account,
|
||||
onTap: () {
|
||||
context.read<AuthenticationCubit>().switchAccount(account.id);
|
||||
context
|
||||
.read<AuthenticationCubit>()
|
||||
.switchAccount(account.id);
|
||||
},
|
||||
),
|
||||
);
|
||||
@@ -126,14 +129,16 @@ class _LoginPageState extends State<LoginPage> {
|
||||
final form = _formKey.currentState!.value;
|
||||
ClientCertificate? clientCert;
|
||||
final clientCertFormModel =
|
||||
form[ClientCertificateFormField.fkClientCertificate] as ClientCertificateFormModel?;
|
||||
form[ClientCertificateFormField.fkClientCertificate]
|
||||
as ClientCertificateFormModel?;
|
||||
if (clientCertFormModel != null) {
|
||||
clientCert = ClientCertificate(
|
||||
bytes: clientCertFormModel.bytes,
|
||||
passphrase: clientCertFormModel.passphrase,
|
||||
);
|
||||
}
|
||||
final credentials = form[UserCredentialsFormField.fkCredentials] as LoginFormCredentials;
|
||||
final credentials =
|
||||
form[UserCredentialsFormField.fkCredentials] as LoginFormCredentials;
|
||||
try {
|
||||
await widget.onSubmit(
|
||||
context,
|
||||
|
||||
@@ -7,7 +7,6 @@ import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/features/login/model/client_certificate_form_model.dart';
|
||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||
|
||||
|
||||
import 'obscured_input_text_form_field.dart';
|
||||
|
||||
class ClientCertificateFormField extends StatefulWidget {
|
||||
@@ -20,10 +19,12 @@ class ClientCertificateFormField extends StatefulWidget {
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<ClientCertificateFormField> createState() => _ClientCertificateFormFieldState();
|
||||
State<ClientCertificateFormField> createState() =>
|
||||
_ClientCertificateFormFieldState();
|
||||
}
|
||||
|
||||
class _ClientCertificateFormFieldState extends State<ClientCertificateFormField> {
|
||||
class _ClientCertificateFormFieldState
|
||||
extends State<ClientCertificateFormField> {
|
||||
File? _selectedFile;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -42,7 +43,8 @@ class _ClientCertificateFormFieldState extends State<ClientCertificateFormField>
|
||||
return null;
|
||||
},
|
||||
builder: (field) {
|
||||
final theme = Theme.of(context).copyWith(dividerColor: Colors.transparent); //new
|
||||
final theme =
|
||||
Theme.of(context).copyWith(dividerColor: Colors.transparent); //new
|
||||
return Theme(
|
||||
data: theme,
|
||||
child: ExpansionTile(
|
||||
@@ -119,7 +121,8 @@ class _ClientCertificateFormFieldState extends State<ClientCertificateFormField>
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onSelectFile(FormFieldState<ClientCertificateFormModel?> field) async {
|
||||
Future<void> _onSelectFile(
|
||||
FormFieldState<ClientCertificateFormModel?> field) async {
|
||||
FilePickerResult? result = await FilePicker.platform.pickFiles(
|
||||
allowMultiple: false,
|
||||
);
|
||||
@@ -128,13 +131,15 @@ class _ClientCertificateFormFieldState extends State<ClientCertificateFormField>
|
||||
setState(() {
|
||||
_selectedFile = file;
|
||||
});
|
||||
final changedValue = field.value?.copyWith(bytes: file.readAsBytesSync()) ??
|
||||
final changedValue =
|
||||
field.value?.copyWith(bytes: file.readAsBytesSync()) ??
|
||||
ClientCertificateFormModel(bytes: file.readAsBytesSync());
|
||||
field.didChange(changedValue);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildSelectedFileText(FormFieldState<ClientCertificateFormModel?> field) {
|
||||
Widget _buildSelectedFileText(
|
||||
FormFieldState<ClientCertificateFormModel?> field) {
|
||||
if (field.value == null) {
|
||||
assert(_selectedFile == null);
|
||||
return Text(
|
||||
|
||||
@@ -67,7 +67,8 @@ class _ServerAddressFormFieldState extends State<ServerAddressFormField> {
|
||||
.where((element) => element.contains(textEditingValue.text));
|
||||
},
|
||||
onSelected: (option) => _formatInput(),
|
||||
fieldViewBuilder: (context, textEditingController, focusNode, onFieldSubmitted) {
|
||||
fieldViewBuilder:
|
||||
(context, textEditingController, focusNode, onFieldSubmitted) {
|
||||
return TextField(
|
||||
controller: textEditingController,
|
||||
focusNode: focusNode,
|
||||
@@ -146,9 +147,11 @@ class _AutocompleteOptions extends StatelessWidget {
|
||||
onSelected(option);
|
||||
},
|
||||
child: Builder(builder: (BuildContext context) {
|
||||
final bool highlight = AutocompleteHighlightedOption.of(context) == index;
|
||||
final bool highlight =
|
||||
AutocompleteHighlightedOption.of(context) == index;
|
||||
if (highlight) {
|
||||
SchedulerBinding.instance.addPostFrameCallback((Duration timeStamp) {
|
||||
SchedulerBinding.instance
|
||||
.addPostFrameCallback((Duration timeStamp) {
|
||||
Scrollable.ensureVisible(context, alignment: 0.5);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -14,7 +14,8 @@ class UserCredentialsFormField extends StatefulWidget {
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<UserCredentialsFormField> createState() => _UserCredentialsFormFieldState();
|
||||
State<UserCredentialsFormField> createState() =>
|
||||
_UserCredentialsFormFieldState();
|
||||
}
|
||||
|
||||
class _UserCredentialsFormFieldState extends State<UserCredentialsFormField> {
|
||||
|
||||
@@ -38,8 +38,9 @@ class _ServerConnectionPageState extends State<ServerConnectionPage> {
|
||||
toolbarHeight: kToolbarHeight - 4,
|
||||
title: Text(widget.titleString),
|
||||
bottom: PreferredSize(
|
||||
child:
|
||||
_isCheckingConnection ? const LinearProgressIndicator() : const SizedBox(height: 4.0),
|
||||
child: _isCheckingConnection
|
||||
? const LinearProgressIndicator()
|
||||
: const SizedBox(height: 4.0),
|
||||
preferredSize: const Size.fromHeight(4.0),
|
||||
),
|
||||
),
|
||||
@@ -69,8 +70,9 @@ class _ServerConnectionPageState extends State<ServerConnectionPage> {
|
||||
),
|
||||
FilledButton(
|
||||
child: Text(S.of(context)!.continueLabel),
|
||||
onPressed:
|
||||
_reachabilityStatus == ReachabilityStatus.reachable ? widget.onContinue : null,
|
||||
onPressed: _reachabilityStatus == ReachabilityStatus.reachable
|
||||
? widget.onContinue
|
||||
: null,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -85,12 +87,15 @@ class _ServerConnectionPageState extends State<ServerConnectionPage> {
|
||||
final certForm = widget.formBuilderKey.currentState
|
||||
?.getRawValue(ClientCertificateFormField.fkClientCertificate)
|
||||
as ClientCertificateFormModel?;
|
||||
final status = await context.read<ConnectivityStatusService>().isPaperlessServerReachable(
|
||||
final status = await context
|
||||
.read<ConnectivityStatusService>()
|
||||
.isPaperlessServerReachable(
|
||||
address ??
|
||||
widget.formBuilderKey.currentState!
|
||||
.getRawValue(ServerAddressFormField.fkServerAddress),
|
||||
certForm != null
|
||||
? ClientCertificate(bytes: certForm.bytes, passphrase: certForm.passphrase)
|
||||
? ClientCertificate(
|
||||
bytes: certForm.bytes, passphrase: certForm.passphrase)
|
||||
: null,
|
||||
);
|
||||
setState(() {
|
||||
|
||||
@@ -25,7 +25,8 @@ class _ServerLoginPageState extends State<ServerLoginPage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final serverAddress = (widget.formBuilderKey.currentState
|
||||
?.getRawValue(ServerAddressFormField.fkServerAddress) as String?)
|
||||
?.getRawValue(ServerAddressFormField.fkServerAddress)
|
||||
as String?)
|
||||
?.replaceAll(RegExp(r'https?://'), '') ??
|
||||
'';
|
||||
return Scaffold(
|
||||
|
||||
@@ -35,6 +35,10 @@ mixin DocumentPagingBlocMixin<State extends DocumentPagingState>
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> initialize() {
|
||||
return updateFilter();
|
||||
}
|
||||
|
||||
///
|
||||
/// Updates document filter and automatically reloads documents. Always resets page to 1.
|
||||
/// Use [loadMore] to load more data.
|
||||
|
||||
@@ -11,17 +11,17 @@ part 'saved_view_cubit.freezed.dart';
|
||||
class SavedViewCubit extends Cubit<SavedViewState> {
|
||||
final SavedViewRepository _savedViewRepository;
|
||||
|
||||
SavedViewCubit(this._savedViewRepository) : super(const SavedViewState.initial()) {
|
||||
SavedViewCubit(this._savedViewRepository)
|
||||
: super(const SavedViewState.initial()) {
|
||||
_savedViewRepository.addListener(
|
||||
this,
|
||||
onChanged: (views) {
|
||||
emit(
|
||||
state.maybeWhen(
|
||||
loaded: (savedViews) => (state as _SavedViewLoadedState).copyWith(
|
||||
savedViews: views.savedViews,
|
||||
),
|
||||
orElse: () => state,
|
||||
),
|
||||
views.when(
|
||||
initial: (savedViews) => emit(const SavedViewState.initial()),
|
||||
loading: (savedViews) => emit(const SavedViewState.loading()),
|
||||
loaded: (savedViews) =>
|
||||
emit(SavedViewState.loaded(savedViews: savedViews)),
|
||||
error: (savedViews) => emit(const SavedViewState.error()),
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -35,7 +35,7 @@ class SavedViewCubit extends Cubit<SavedViewState> {
|
||||
return _savedViewRepository.delete(view);
|
||||
}
|
||||
|
||||
Future<void> initialize() async {
|
||||
Future<void> reload() async {
|
||||
final views = await _savedViewRepository.findAll();
|
||||
final values = {for (var element in views) element.id!: element};
|
||||
if (!isClosed) {
|
||||
@@ -47,8 +47,6 @@ class SavedViewCubit extends Cubit<SavedViewState> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> reload() => initialize();
|
||||
|
||||
@override
|
||||
Future<void> close() {
|
||||
_savedViewRepository.removeListener(this);
|
||||
|
||||
@@ -2,12 +2,12 @@ part of 'saved_view_cubit.dart';
|
||||
|
||||
@freezed
|
||||
class SavedViewState with _$SavedViewState {
|
||||
const factory SavedViewState.initial() = _SavedViewIntialState;
|
||||
const factory SavedViewState.initial() = _Initial;
|
||||
|
||||
const factory SavedViewState.loading() = _SavedViewLoadingState;
|
||||
const factory SavedViewState.loading() = _Loading;
|
||||
|
||||
const factory SavedViewState.loaded({required Map<int, SavedView> savedViews}) =
|
||||
_SavedViewLoadedState;
|
||||
const factory SavedViewState.loaded(
|
||||
{required Map<int, SavedView> savedViews}) = _Loaded;
|
||||
|
||||
const factory SavedViewState.error() = _SavedViewErrorState;
|
||||
const factory SavedViewState.error() = _Error;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
|
||||
import 'package:paperless_mobile/core/navigation/push_routes.dart';
|
||||
import 'package:paperless_mobile/core/widgets/hint_card.dart';
|
||||
import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart';
|
||||
import 'package:paperless_mobile/features/saved_view/view/saved_view_loading_sliver_list.dart';
|
||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||
|
||||
class SavedViewList extends StatelessWidget {
|
||||
@@ -16,23 +17,20 @@ class SavedViewList extends StatelessWidget {
|
||||
return BlocBuilder<SavedViewCubit, SavedViewState>(
|
||||
builder: (context, state) {
|
||||
return state.when(
|
||||
initial: () => SliverToBoxAdapter(child: Container()),
|
||||
loading: () => const SliverToBoxAdapter(
|
||||
child: Center(
|
||||
child: Text("Saved views loading..."), //TODO: INTL
|
||||
),
|
||||
),
|
||||
initial: () => const SavedViewLoadingSliverList(),
|
||||
loading: () => const SavedViewLoadingSliverList(),
|
||||
loaded: (savedViews) {
|
||||
if (savedViews.isEmpty) {
|
||||
return SliverToBoxAdapter(
|
||||
child: HintCard(
|
||||
hintText: S.of(context)!.createViewsToQuicklyFilterYourDocuments,
|
||||
hintText: S
|
||||
.of(context)!
|
||||
.createViewsToQuicklyFilterYourDocuments,
|
||||
),
|
||||
);
|
||||
}
|
||||
return SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) {
|
||||
return SliverList.builder(
|
||||
itemBuilder: (context, index) {
|
||||
final view = savedViews.values.elementAt(index);
|
||||
return ListTile(
|
||||
enabled: connectivity.isConnected,
|
||||
@@ -45,15 +43,16 @@ class SavedViewList extends StatelessWidget {
|
||||
},
|
||||
);
|
||||
},
|
||||
childCount: savedViews.length,
|
||||
),
|
||||
itemCount: savedViews.length,
|
||||
);
|
||||
},
|
||||
error: () => const Center(
|
||||
error: () => const SliverToBoxAdapter(
|
||||
child: Center(
|
||||
child: Text(
|
||||
"An error occurred while trying to load the saved views.",
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:paperless_mobile/core/widgets/shimmer_placeholder.dart';
|
||||
|
||||
class SavedViewLoadingSliverList extends StatelessWidget {
|
||||
const SavedViewLoadingSliverList({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SliverList.builder(
|
||||
itemBuilder: (context, index) => ShimmerPlaceholder(
|
||||
child: ListTile(
|
||||
title: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Container(
|
||||
width: 300,
|
||||
height: 14,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
subtitle: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Container(
|
||||
width: 150,
|
||||
height: 12,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,8 @@ import 'package:paperless_mobile/features/settings/model/view_type.dart';
|
||||
|
||||
part 'saved_view_details_state.dart';
|
||||
|
||||
class SavedViewDetailsCubit extends Cubit<SavedViewDetailsState> with DocumentPagingBlocMixin {
|
||||
class SavedViewDetailsCubit extends Cubit<SavedViewDetailsState>
|
||||
with DocumentPagingBlocMixin {
|
||||
@override
|
||||
final PaperlessDocumentsApi api;
|
||||
|
||||
|
||||
@@ -26,11 +26,14 @@ class ManageAccountsPage extends StatelessWidget {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return ValueListenableBuilder(
|
||||
valueListenable: Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount).listenable(),
|
||||
valueListenable:
|
||||
Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount)
|
||||
.listenable(),
|
||||
builder: (context, box, _) {
|
||||
final userIds = box.keys.toList().cast<String>();
|
||||
final otherAccounts = userIds
|
||||
.whereNot((element) => element == globalSettings.currentLoggedInUser)
|
||||
.whereNot(
|
||||
(element) => element == globalSettings.currentLoggedInUser)
|
||||
.toList();
|
||||
return SimpleDialog(
|
||||
insetPadding: const EdgeInsets.all(24),
|
||||
@@ -68,10 +71,13 @@ class ManageAccountsPage extends StatelessWidget {
|
||||
],
|
||||
onSelected: (value) async {
|
||||
if (value == 0) {
|
||||
final currentUser = globalSettings.currentLoggedInUser!;
|
||||
final currentUser =
|
||||
globalSettings.currentLoggedInUser!;
|
||||
await context.read<AuthenticationCubit>().logout();
|
||||
Navigator.of(context).pop();
|
||||
await context.read<AuthenticationCubit>().removeAccount(currentUser);
|
||||
await context
|
||||
.read<AuthenticationCubit>()
|
||||
.removeAccount(currentUser);
|
||||
}
|
||||
},
|
||||
),
|
||||
@@ -89,7 +95,8 @@ class ManageAccountsPage extends StatelessWidget {
|
||||
PopupMenuItem(
|
||||
child: ListTile(
|
||||
title: Text(S.of(context)!.switchAccount),
|
||||
leading: const Icon(Icons.switch_account_rounded),
|
||||
leading:
|
||||
const Icon(Icons.switch_account_rounded),
|
||||
),
|
||||
value: 0,
|
||||
),
|
||||
@@ -150,7 +157,8 @@ class ManageAccountsPage extends StatelessWidget {
|
||||
MaterialPageRoute(
|
||||
builder: (context) => LoginPage(
|
||||
titleString: S.of(context)!.addAccount,
|
||||
onSubmit: (context, username, password, serverUrl, clientCertificate) async {
|
||||
onSubmit: (context, username, password, serverUrl,
|
||||
clientCertificate) async {
|
||||
final userId = await context.read<AuthenticationCubit>().addAccount(
|
||||
credentials: LoginFormCredentials(
|
||||
username: username,
|
||||
@@ -179,7 +187,8 @@ class ManageAccountsPage extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
void _onSwitchAccount(BuildContext context, String currentUser, String newUser) async {
|
||||
void _onSwitchAccount(
|
||||
BuildContext context, String currentUser, String newUser) async {
|
||||
if (currentUser == newUser) return;
|
||||
|
||||
Navigator.of(context).pop();
|
||||
|
||||
@@ -26,7 +26,9 @@ class SettingsPage extends StatelessWidget {
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
subtitle: FutureBuilder<PaperlessServerInformationModel>(
|
||||
future: context.read<PaperlessServerStatsApi>().getServerInformation(),
|
||||
future: context
|
||||
.read<PaperlessServerStatsApi>()
|
||||
.getServerInformation(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasError) {
|
||||
return Text(
|
||||
|
||||
@@ -14,8 +14,8 @@ class ClearCacheSetting extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
title: const Text("Clear downloaded files"), //TODO: INTL
|
||||
subtitle:
|
||||
const Text("Deletes all files downloaded from this app."), //TODO: INTL
|
||||
subtitle: const Text(
|
||||
"Deletes all files downloaded from this app."), //TODO: INTL
|
||||
onTap: () async {
|
||||
final dir = await FileService.downloadsDirectory;
|
||||
final deletedSize = _dirSize(dir);
|
||||
|
||||
@@ -32,7 +32,8 @@ class ColorSchemeOptionSetting extends StatelessWidget {
|
||||
options: [
|
||||
RadioOption(
|
||||
value: ColorSchemeOption.classic,
|
||||
label: translateColorSchemeOption(context, ColorSchemeOption.classic),
|
||||
label: translateColorSchemeOption(
|
||||
context, ColorSchemeOption.classic),
|
||||
),
|
||||
RadioOption(
|
||||
value: ColorSchemeOption.dynamic,
|
||||
|
||||
@@ -25,15 +25,18 @@ class DefaultDownloadFileTypeSetting extends StatelessWidget {
|
||||
options: [
|
||||
RadioOption(
|
||||
value: FileDownloadType.alwaysAsk,
|
||||
label: _downloadFileTypeToString(context, FileDownloadType.alwaysAsk),
|
||||
label: _downloadFileTypeToString(
|
||||
context, FileDownloadType.alwaysAsk),
|
||||
),
|
||||
RadioOption(
|
||||
value: FileDownloadType.original,
|
||||
label: _downloadFileTypeToString(context, FileDownloadType.original),
|
||||
label: _downloadFileTypeToString(
|
||||
context, FileDownloadType.original),
|
||||
),
|
||||
RadioOption(
|
||||
value: FileDownloadType.archived,
|
||||
label: _downloadFileTypeToString(context, FileDownloadType.archived),
|
||||
label: _downloadFileTypeToString(
|
||||
context, FileDownloadType.archived),
|
||||
),
|
||||
],
|
||||
initialValue: settings.defaultDownloadType,
|
||||
@@ -51,7 +54,8 @@ class DefaultDownloadFileTypeSetting extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
String _downloadFileTypeToString(BuildContext context, FileDownloadType type) {
|
||||
String _downloadFileTypeToString(
|
||||
BuildContext context, FileDownloadType type) {
|
||||
switch (type) {
|
||||
case FileDownloadType.original:
|
||||
return S.of(context)!.original;
|
||||
|
||||
@@ -25,15 +25,18 @@ class DefaultShareFileTypeSetting extends StatelessWidget {
|
||||
options: [
|
||||
RadioOption(
|
||||
value: FileDownloadType.alwaysAsk,
|
||||
label: _downloadFileTypeToString(context, FileDownloadType.alwaysAsk),
|
||||
label: _downloadFileTypeToString(
|
||||
context, FileDownloadType.alwaysAsk),
|
||||
),
|
||||
RadioOption(
|
||||
value: FileDownloadType.original,
|
||||
label: _downloadFileTypeToString(context, FileDownloadType.original),
|
||||
label: _downloadFileTypeToString(
|
||||
context, FileDownloadType.original),
|
||||
),
|
||||
RadioOption(
|
||||
value: FileDownloadType.archived,
|
||||
label: _downloadFileTypeToString(context, FileDownloadType.archived),
|
||||
label: _downloadFileTypeToString(
|
||||
context, FileDownloadType.archived),
|
||||
),
|
||||
],
|
||||
initialValue: settings.defaultShareType,
|
||||
@@ -51,7 +54,8 @@ class DefaultShareFileTypeSetting extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
String _downloadFileTypeToString(BuildContext context, FileDownloadType type) {
|
||||
String _downloadFileTypeToString(
|
||||
BuildContext context, FileDownloadType type) {
|
||||
switch (type) {
|
||||
case FileDownloadType.original:
|
||||
return S.of(context)!.original;
|
||||
|
||||
@@ -10,7 +10,8 @@ class GlobalSettingsBuilder extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ValueListenableBuilder(
|
||||
valueListenable: Hive.box<GlobalSettings>(HiveBoxes.globalSettings).listenable(),
|
||||
valueListenable:
|
||||
Hive.box<GlobalSettings>(HiveBoxes.globalSettings).listenable(),
|
||||
builder: (context, value, _) {
|
||||
final settings = value.getValue()!;
|
||||
return builder(context, settings);
|
||||
|
||||
@@ -7,7 +7,8 @@ class LanguageSelectionSetting extends StatefulWidget {
|
||||
const LanguageSelectionSetting({super.key});
|
||||
|
||||
@override
|
||||
State<LanguageSelectionSetting> createState() => _LanguageSelectionSettingState();
|
||||
State<LanguageSelectionSetting> createState() =>
|
||||
_LanguageSelectionSettingState();
|
||||
}
|
||||
|
||||
class _LanguageSelectionSettingState extends State<LanguageSelectionSetting> {
|
||||
@@ -27,7 +28,8 @@ class _LanguageSelectionSettingState extends State<LanguageSelectionSetting> {
|
||||
builder: (context, settings) {
|
||||
return ListTile(
|
||||
title: Text(S.of(context)!.language),
|
||||
subtitle: Text(_languageOptions[settings.preferredLocaleSubtag]!.name),
|
||||
subtitle:
|
||||
Text(_languageOptions[settings.preferredLocaleSubtag]!.name),
|
||||
onTap: () => showDialog<String>(
|
||||
context: context,
|
||||
builder: (_) => RadioSettingsDialog<String>(
|
||||
@@ -39,7 +41,8 @@ class _LanguageSelectionSettingState extends State<LanguageSelectionSetting> {
|
||||
for (var language in _languageOptions.entries)
|
||||
RadioOption(
|
||||
value: language.key,
|
||||
label: language.value.name + (language.value.isComplete ? '' : '*'),
|
||||
label: language.value.name +
|
||||
(language.value.isComplete ? '' : '*'),
|
||||
),
|
||||
],
|
||||
initialValue: settings.preferredLocaleSubtag,
|
||||
|
||||
@@ -50,7 +50,8 @@ class _RadioSettingsDialogState<T> extends State<RadioSettingsDialog<T>> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (widget.descriptionText != null)
|
||||
Text(widget.descriptionText!, style: Theme.of(context).textTheme.bodySmall),
|
||||
Text(widget.descriptionText!,
|
||||
style: Theme.of(context).textTheme.bodySmall),
|
||||
...widget.options.map(_buildOptionListTile),
|
||||
if (widget.footer != null) widget.footer!,
|
||||
],
|
||||
|
||||
@@ -12,7 +12,8 @@ class ThemeModeSetting extends StatelessWidget {
|
||||
builder: (context, settings) {
|
||||
return ListTile(
|
||||
title: Text(S.of(context)!.appearance),
|
||||
subtitle: Text(_mapThemeModeToLocalizedString(settings.preferredThemeMode, context)),
|
||||
subtitle: Text(_mapThemeModeToLocalizedString(
|
||||
settings.preferredThemeMode, context)),
|
||||
onTap: () => showDialog<ThemeMode>(
|
||||
context: context,
|
||||
builder: (_) => RadioSettingsDialog<ThemeMode>(
|
||||
|
||||
@@ -11,10 +11,13 @@ class UserAvatar extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final backgroundColor = Colors.primaries[account.id.hashCode % Colors.primaries.length];
|
||||
final foregroundColor = backgroundColor.computeLuminance() > 0.5 ? Colors.black : Colors.white;
|
||||
final backgroundColor =
|
||||
Colors.primaries[account.id.hashCode % Colors.primaries.length];
|
||||
final foregroundColor =
|
||||
backgroundColor.computeLuminance() > 0.5 ? Colors.black : Colors.white;
|
||||
return CircleAvatar(
|
||||
child: Text((account.paperlessUser.fullName ?? account.paperlessUser.username)
|
||||
child: Text(
|
||||
(account.paperlessUser.fullName ?? account.paperlessUser.username)
|
||||
.split(" ")
|
||||
.take(2)
|
||||
.map((e) => e.substring(0, 1))
|
||||
|
||||
@@ -18,10 +18,12 @@ class UserAccountBuilder extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ValueListenableBuilder<Box<LocalUserAccount>>(
|
||||
valueListenable: Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount).listenable(),
|
||||
valueListenable:
|
||||
Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount).listenable(),
|
||||
builder: (context, accountBox, _) {
|
||||
final currentUser =
|
||||
Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!.currentLoggedInUser;
|
||||
final currentUser = Hive.box<GlobalSettings>(HiveBoxes.globalSettings)
|
||||
.getValue()!
|
||||
.currentLoggedInUser;
|
||||
if (currentUser != null) {
|
||||
final account = accountBox.get(currentUser);
|
||||
return builder(context, account);
|
||||
|
||||
@@ -4,33 +4,53 @@ import 'package:flutter/widgets.dart';
|
||||
import 'package:receive_sharing_intent/receive_sharing_intent.dart';
|
||||
|
||||
class ShareIntentQueue extends ChangeNotifier {
|
||||
final Queue<SharedMediaFile> _queue = Queue();
|
||||
final Map<String, Queue<SharedMediaFile>> _queues = {};
|
||||
|
||||
ShareIntentQueue._();
|
||||
|
||||
static final instance = ShareIntentQueue._();
|
||||
|
||||
void add(SharedMediaFile file) {
|
||||
void add(
|
||||
SharedMediaFile file, {
|
||||
required String userId,
|
||||
}) {
|
||||
debugPrint("Adding received file to queue: ${file.path}");
|
||||
_queue.add(file);
|
||||
_getQueue(userId).add(file);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void addAll(Iterable<SharedMediaFile> files) {
|
||||
void addAll(
|
||||
Iterable<SharedMediaFile> files, {
|
||||
required String userId,
|
||||
}) {
|
||||
debugPrint(
|
||||
"Adding received files to queue: ${files.map((e) => e.path).join(",")}");
|
||||
_queue.addAll(files);
|
||||
_getQueue(userId).addAll(files);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
SharedMediaFile? pop() {
|
||||
if (hasUnhandledFiles) {
|
||||
return _queue.removeFirst();
|
||||
SharedMediaFile? pop(String userId) {
|
||||
if (userHasUnhandlesFiles(userId)) {
|
||||
return _getQueue(userId).removeFirst();
|
||||
// Don't notify listeners, only when new item is added.
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
bool get hasUnhandledFiles => _queue.isNotEmpty;
|
||||
Queue<SharedMediaFile> _getQueue(String userId) {
|
||||
if (!_queues.containsKey(userId)) {
|
||||
_queues[userId] = Queue<SharedMediaFile>();
|
||||
}
|
||||
return _queues[userId]!;
|
||||
}
|
||||
|
||||
bool userHasUnhandlesFiles(String userId) => _getQueue(userId).isNotEmpty;
|
||||
}
|
||||
|
||||
class UserAwareShareMediaFile {
|
||||
final String userId;
|
||||
final SharedMediaFile sharedFile;
|
||||
|
||||
UserAwareShareMediaFile(this.userId, this.sharedFile);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,8 @@ import 'package:paperless_mobile/features/paged_document_view/cubit/paged_docume
|
||||
|
||||
part 'similar_documents_state.dart';
|
||||
|
||||
class SimilarDocumentsCubit extends Cubit<SimilarDocumentsState> with DocumentPagingBlocMixin {
|
||||
class SimilarDocumentsCubit extends Cubit<SimilarDocumentsState>
|
||||
with DocumentPagingBlocMixin {
|
||||
final int documentId;
|
||||
|
||||
@override
|
||||
|
||||
@@ -35,8 +35,10 @@ class _SimilarDocumentsViewState extends State<SimilarDocumentsView>
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocConsumer<ConnectivityCubit, ConnectivityState>(
|
||||
listenWhen: (previous, current) => !previous.isConnected && current.isConnected,
|
||||
listener: (context, state) => context.read<SimilarDocumentsCubit>().initialize(),
|
||||
listenWhen: (previous, current) =>
|
||||
!previous.isConnected && current.isConnected,
|
||||
listener: (context, state) =>
|
||||
context.read<SimilarDocumentsCubit>().initialize(),
|
||||
builder: (context, connectivity) {
|
||||
return BlocBuilder<SimilarDocumentsCubit, SimilarDocumentsState>(
|
||||
builder: (context, state) {
|
||||
@@ -45,7 +47,9 @@ class _SimilarDocumentsViewState extends State<SimilarDocumentsView>
|
||||
child: OfflineWidget(),
|
||||
);
|
||||
}
|
||||
if (state.hasLoaded && !state.isLoading && state.documents.isEmpty) {
|
||||
if (state.hasLoaded &&
|
||||
!state.isLoading &&
|
||||
state.documents.isEmpty) {
|
||||
return SliverToBoxAdapter(
|
||||
child: Center(
|
||||
child: Text(S.of(context)!.noItemsFound),
|
||||
|
||||
@@ -25,7 +25,8 @@ class UserAccountListTile extends StatelessWidget {
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (account.paperlessUser.fullName != null) Text(account.paperlessUser.fullName!),
|
||||
if (account.paperlessUser.fullName != null)
|
||||
Text(account.paperlessUser.fullName!),
|
||||
Text(
|
||||
account.serverUrl.replaceFirst(RegExp(r'https://?'), ''),
|
||||
style: TextStyle(color: theme.colorScheme.primary),
|
||||
|
||||
@@ -52,7 +52,8 @@ import 'package:mock_server/mock_server.dart';
|
||||
|
||||
String get defaultPreferredLocaleSubtag {
|
||||
String preferredLocale = Platform.localeName.split("_").first;
|
||||
if (!S.supportedLocales.any((locale) => locale.languageCode == preferredLocale)) {
|
||||
if (!S.supportedLocales
|
||||
.any((locale) => locale.languageCode == preferredLocale)) {
|
||||
preferredLocale = 'en';
|
||||
}
|
||||
return preferredLocale;
|
||||
@@ -65,7 +66,8 @@ Future<void> _initHive() async {
|
||||
await Hive.openBox<LocalUserAccount>(HiveBoxes.localUserAccount);
|
||||
await Hive.openBox<LocalUserAppState>(HiveBoxes.localUserAppState);
|
||||
await Hive.openBox<String>(HiveBoxes.hosts);
|
||||
final globalSettingsBox = await Hive.openBox<GlobalSettings>(HiveBoxes.globalSettings);
|
||||
final globalSettingsBox =
|
||||
await Hive.openBox<GlobalSettings>(HiveBoxes.globalSettings);
|
||||
|
||||
if (!globalSettingsBox.hasValue) {
|
||||
await globalSettingsBox.setValue(
|
||||
@@ -132,7 +134,8 @@ void main() async {
|
||||
|
||||
//Update language header in interceptor on language change.
|
||||
globalSettingsBox.listenable().addListener(() {
|
||||
languageHeaderInterceptor.preferredLocaleSubtag = globalSettings.preferredLocaleSubtag;
|
||||
languageHeaderInterceptor.preferredLocaleSubtag =
|
||||
globalSettings.preferredLocaleSubtag;
|
||||
});
|
||||
|
||||
final apiFactory = PaperlessApiFactoryImpl(sessionManager);
|
||||
@@ -142,15 +145,19 @@ void main() async {
|
||||
providers: [
|
||||
ChangeNotifierProvider.value(value: sessionManager),
|
||||
Provider<LocalAuthenticationService>.value(value: localAuthService),
|
||||
Provider<ConnectivityStatusService>.value(value: connectivityStatusService),
|
||||
Provider<LocalNotificationService>.value(value: localNotificationService),
|
||||
Provider<Connectivity>.value(value: connectivity),
|
||||
Provider<ConnectivityStatusService>.value(
|
||||
value: connectivityStatusService),
|
||||
Provider<LocalNotificationService>.value(
|
||||
value: localNotificationService),
|
||||
Provider.value(value: DocumentChangedNotifier()),
|
||||
],
|
||||
child: MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider<ConnectivityCubit>.value(value: connectivityCubit),
|
||||
BlocProvider(
|
||||
create: (context) => AuthenticationCubit(localAuthService, apiFactory, sessionManager),
|
||||
create: (context) => AuthenticationCubit(
|
||||
localAuthService, apiFactory, sessionManager),
|
||||
)
|
||||
],
|
||||
child: PaperlessMobileEntrypoint(
|
||||
@@ -169,7 +176,8 @@ class PaperlessMobileEntrypoint extends StatefulWidget {
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<PaperlessMobileEntrypoint> createState() => _PaperlessMobileEntrypointState();
|
||||
State<PaperlessMobileEntrypoint> createState() =>
|
||||
_PaperlessMobileEntrypointState();
|
||||
}
|
||||
|
||||
class _PaperlessMobileEntrypointState extends State<PaperlessMobileEntrypoint> {
|
||||
@@ -224,7 +232,6 @@ class AuthenticationWrapper extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _AuthenticationWrapperState extends State<AuthenticationWrapper> {
|
||||
late final StreamSubscription _shareMediaSubscription;
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
@@ -233,12 +240,6 @@ class _AuthenticationWrapperState extends State<AuthenticationWrapper> {
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_shareMediaSubscription.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -248,11 +249,6 @@ class _AuthenticationWrapperState extends State<AuthenticationWrapper> {
|
||||
_setOptimalDisplayMode();
|
||||
}
|
||||
initializeDateFormatting();
|
||||
// For sharing files coming from outside the app while the app is still opened
|
||||
_shareMediaSubscription =
|
||||
ReceiveSharingIntent.getMediaStream().listen(ShareIntentQueue.instance.addAll);
|
||||
// For sharing files coming from outside the app while the app is closed
|
||||
ReceiveSharingIntent.getInitialMedia().then(ShareIntentQueue.instance.addAll);
|
||||
}
|
||||
|
||||
Future<void> _setOptimalDisplayMode() async {
|
||||
@@ -264,7 +260,8 @@ class _AuthenticationWrapperState extends State<AuthenticationWrapper> {
|
||||
.toList()
|
||||
..sort((a, b) => b.refreshRate.compareTo(a.refreshRate));
|
||||
|
||||
final DisplayMode mostOptimalMode = sameResolution.isNotEmpty ? sameResolution.first : active;
|
||||
final DisplayMode mostOptimalMode =
|
||||
sameResolution.isNotEmpty ? sameResolution.first : active;
|
||||
debugPrint('Setting refresh rate to ${mostOptimalMode.refreshRate}');
|
||||
|
||||
await FlutterDisplayMode.setPreferredMode(mostOptimalMode);
|
||||
@@ -303,12 +300,14 @@ class _AuthenticationWrapperState extends State<AuthenticationWrapper> {
|
||||
) async {
|
||||
try {
|
||||
await context.read<AuthenticationCubit>().login(
|
||||
credentials: LoginFormCredentials(username: username, password: password),
|
||||
credentials:
|
||||
LoginFormCredentials(username: username, password: password),
|
||||
serverUrl: serverUrl,
|
||||
clientCertificate: clientCertificate,
|
||||
);
|
||||
// Show onboarding after first login!
|
||||
final globalSettings = Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!;
|
||||
final globalSettings =
|
||||
Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!;
|
||||
if (globalSettings.showOnboarding) {
|
||||
Navigator.push(
|
||||
context,
|
||||
|
||||
@@ -250,7 +250,7 @@ class FilterRule with EquatableMixin {
|
||||
filterRules.add(docTypeRule);
|
||||
}
|
||||
|
||||
final sPathRule = filter.documentType.whenOrNull(
|
||||
final sPathRule = filter.storagePath.whenOrNull(
|
||||
notAssigned: () => FilterRule(storagePathRule, null),
|
||||
fromId: (id) => FilterRule(storagePathRule, id.toString()),
|
||||
);
|
||||
@@ -344,8 +344,7 @@ class FilterRule with EquatableMixin {
|
||||
}
|
||||
|
||||
//Join values of all extended filter rules if exist
|
||||
if (filterRules.isNotEmpty &&
|
||||
filterRules.where((e) => e.ruleType == FilterRule.extendedRule).length > 1) {
|
||||
if (filterRules.where((e) => e.ruleType == FilterRule.extendedRule).isNotEmpty) {
|
||||
final mergedExtendedRule = filterRules
|
||||
.where((r) => r.ruleType == FilterRule.extendedRule)
|
||||
.map((e) => e.value)
|
||||
|
||||
@@ -52,6 +52,9 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi {
|
||||
final response = await client.post(
|
||||
'/api/documents/post_document/',
|
||||
data: formData,
|
||||
onSendProgress: (count, total) {
|
||||
debugPrint("Uploading ${(count / total) * 100}%...");
|
||||
},
|
||||
);
|
||||
if (response.statusCode == 200) {
|
||||
if (response.data is String && response.data != "OK") {
|
||||
|
||||
@@ -15,7 +15,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
||||
# 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: 2.3.3+38
|
||||
version: 2.3.4+39
|
||||
|
||||
environment:
|
||||
sdk: ">=3.0.0 <4.0.0"
|
||||
|
||||
Reference in New Issue
Block a user