fix: Fixed saved views bug, formatted files, minor changes

This commit is contained in:
Anton Stubenbord
2023-06-10 16:29:12 +02:00
parent 3161343c35
commit 4c3f97136e
93 changed files with 1049 additions and 585 deletions

View File

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

View File

@@ -41,5 +41,7 @@ class ThemeModeAdapter extends TypeAdapter<ThemeMode> {
@override @override
bool operator ==(Object other) => bool operator ==(Object other) =>
identical(this, other) || identical(this, other) ||
other is ThemeModeAdapter && runtimeType == other.runtimeType && typeId == other.typeId; other is ThemeModeAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
} }

View File

@@ -9,7 +9,8 @@ import 'package:hive_flutter/adapters.dart';
/// Opens an encrypted box, calls [callback] with the now opened box, awaits /// Opens an encrypted box, calls [callback] with the now opened box, awaits
/// [callback] to return and returns the calculated value. Closes the box after. /// [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 key = await _getEncryptedBoxKey();
final box = await Hive.openBox<T>( final box = await Hive.openBox<T>(
name, name,

View File

@@ -27,6 +27,9 @@ class LocalUserAccount extends HiveObject {
required this.paperlessUser, required this.paperlessUser,
}); });
static LocalUserAccount get current => Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount) static LocalUserAccount get current =>
.get(Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!.currentLoggedInUser)!; Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount).get(
Hive.box<GlobalSettings>(HiveBoxes.globalSettings)
.getValue()!
.currentLoggedInUser)!;
} }

View File

@@ -41,7 +41,10 @@ class LocalUserAppState extends HiveObject {
static LocalUserAppState get current { static LocalUserAppState get current {
final currentLocalUserId = final currentLocalUserId =
Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!.currentLoggedInUser!; Hive.box<GlobalSettings>(HiveBoxes.globalSettings)
return Hive.box<LocalUserAppState>(HiveBoxes.localUserAppState).get(currentLocalUserId)!; .getValue()!
.currentLoggedInUser!;
return Hive.box<LocalUserAppState>(HiveBoxes.localUserAppState)
.get(currentLocalUserId)!;
} }
} }

View File

@@ -3,9 +3,11 @@ import 'package:paperless_api/paperless_api.dart';
abstract class PaperlessApiFactory { abstract class PaperlessApiFactory {
PaperlessDocumentsApi createDocumentsApi(Dio dio, {required int apiVersion}); 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}); 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}); PaperlessTasksApi createTasksApi(Dio dio, {required int apiVersion});
PaperlessAuthenticationApi createAuthenticationApi(Dio dio); PaperlessAuthenticationApi createAuthenticationApi(Dio dio);
PaperlessUserApi createUserApi(Dio dio, {required int apiVersion}); PaperlessUserApi createUserApi(Dio dio, {required int apiVersion});

View File

@@ -19,12 +19,14 @@ class PaperlessApiFactoryImpl implements PaperlessApiFactory {
} }
@override @override
PaperlessSavedViewsApi createSavedViewsApi(Dio dio, {required int apiVersion}) { PaperlessSavedViewsApi createSavedViewsApi(Dio dio,
{required int apiVersion}) {
return PaperlessSavedViewsApiImpl(dio); return PaperlessSavedViewsApiImpl(dio);
} }
@override @override
PaperlessServerStatsApi createServerStatsApi(Dio dio, {required int apiVersion}) { PaperlessServerStatsApi createServerStatsApi(Dio dio,
{required int apiVersion}) {
return PaperlessServerStatsApiImpl(dio); return PaperlessServerStatsApiImpl(dio);
} }

View File

@@ -78,7 +78,8 @@ class DioHttpErrorInterceptor extends Interceptor {
DioError( DioError(
requestOptions: err.requestOptions, requestOptions: err.requestOptions,
type: DioErrorType.badResponse, type: DioErrorType.badResponse,
error: const PaperlessServerException(ErrorCode.missingClientCertificate), error: const PaperlessServerException(
ErrorCode.missingClientCertificate),
), ),
); );
} }

View File

@@ -1,5 +1,6 @@
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.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. // Providers unfortunately have to be passed to the routes since they are children of the Navigator, not ancestors.
Future<void> pushDocumentSearchPage(BuildContext context) { Future<void> pushDocumentSearchPage(BuildContext context) {
final currentUser = final currentUser = Hive.box<GlobalSettings>(HiveBoxes.globalSettings)
Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!.currentLoggedInUser; .getValue()!
.currentLoggedInUser;
final userRepo = context.read<UserRepository>(); final userRepo = context.read<UserRepository>();
return Navigator.of(context).push( return Navigator.of(context).push(
MaterialPageRoute( MaterialPageRoute(
@@ -53,7 +55,8 @@ Future<void> pushDocumentSearchPage(BuildContext context) {
create: (context) => DocumentSearchCubit( create: (context) => DocumentSearchCubit(
context.read(), context.read(),
context.read(), context.read(),
Hive.box<LocalUserAppState>(HiveBoxes.localUserAppState).get(currentUser)!, Hive.box<LocalUserAppState>(HiveBoxes.localUserAppState)
.get(currentUser)!,
), ),
child: const DocumentSearchPage(), child: const DocumentSearchPage(),
); );
@@ -103,7 +106,8 @@ Future<void> pushSavedViewDetailsRoute(
builder: (_) => MultiProvider( builder: (_) => MultiProvider(
providers: [ providers: [
Provider.value(value: apiVersion), 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<LabelRepository>()),
Provider.value(value: context.read<DocumentChangedNotifier>()), Provider.value(value: context.read<DocumentChangedNotifier>()),
Provider.value(value: context.read<PaperlessDocumentsApi>()), Provider.value(value: context.read<PaperlessDocumentsApi>()),
@@ -119,7 +123,8 @@ Future<void> pushSavedViewDetailsRoute(
LocalUserAppState.current, LocalUserAppState.current,
savedView: savedView, 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?>( return Navigator.of(context).push<SavedView?>(
MaterialPageRoute( MaterialPageRoute(
builder: (_) => AddSavedViewPage( 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( return Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
@@ -196,7 +203,9 @@ Future<void> pushBulkEditCorrespondentRoute(
labelMapper: (document) => document.correspondent, labelMapper: (document) => document.correspondent,
leadingIcon: const Icon(Icons.person_outline), leadingIcon: const Icon(Icons.person_outline),
hintText: S.of(context)!.startTyping, hintText: S.of(context)!.startTyping,
onSubmit: context.read<DocumentBulkActionCubit>().bulkModifyCorrespondent, onSubmit: context
.read<DocumentBulkActionCubit>()
.bulkModifyCorrespondent,
assignMessageBuilder: (int count, String name) { assignMessageBuilder: (int count, String name) {
return S.of(context)!.bulkEditCorrespondentAssignMessage( return S.of(context)!.bulkEditCorrespondentAssignMessage(
name, name,
@@ -204,7 +213,9 @@ Future<void> pushBulkEditCorrespondentRoute(
); );
}, },
removeMessageBuilder: (int count) { 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, labelMapper: (document) => document.storagePath,
leadingIcon: const Icon(Icons.folder_outlined), leadingIcon: const Icon(Icons.folder_outlined),
hintText: S.of(context)!.startTyping, hintText: S.of(context)!.startTyping,
onSubmit: context.read<DocumentBulkActionCubit>().bulkModifyStoragePath, onSubmit: context
.read<DocumentBulkActionCubit>()
.bulkModifyStoragePath,
assignMessageBuilder: (int count, String name) { assignMessageBuilder: (int count, String name) {
return S.of(context)!.bulkEditStoragePathAssignMessage( return S.of(context)!.bulkEditStoragePathAssignMessage(
count, count,
@@ -308,7 +321,9 @@ Future<void> pushBulkEditDocumentTypeRoute(BuildContext context,
labelMapper: (document) => document.documentType, labelMapper: (document) => document.documentType,
leadingIcon: const Icon(Icons.description_outlined), leadingIcon: const Icon(Icons.description_outlined),
hintText: S.of(context)!.startTyping, hintText: S.of(context)!.startTyping,
onSubmit: context.read<DocumentBulkActionCubit>().bulkModifyDocumentType, onSubmit: context
.read<DocumentBulkActionCubit>()
.bulkModifyDocumentType,
assignMessageBuilder: (int count, String name) { assignMessageBuilder: (int count, String name) {
return S.of(context)!.bulkEditDocumentTypeAssignMessage( return S.of(context)!.bulkEditDocumentTypeAssignMessage(
count, count,
@@ -316,7 +331,9 @@ Future<void> pushBulkEditDocumentTypeRoute(BuildContext context,
); );
}, },
removeMessageBuilder: (int count) { 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 labelRepo = context.read<LabelRepository>();
final docsApi = context.read<PaperlessDocumentsApi>(); final docsApi = context.read<PaperlessDocumentsApi>();
final connectivity = context.read<Connectivity>();
return Navigator.of(context).push<DocumentUploadResult>( return Navigator.of(context).push<DocumentUploadResult>(
MaterialPageRoute( MaterialPageRoute(
builder: (_) => MultiProvider( builder: (_) => MultiProvider(
providers: [ providers: [
Provider.value(value: labelRepo), Provider.value(value: labelRepo),
Provider.value(value: docsApi), Provider.value(value: docsApi),
Provider.value(value: connectivity),
], ],
builder: (_, child) => BlocProvider( builder: (_, child) => BlocProvider(
create: (_) => DocumentUploadCubit( create: (_) => DocumentUploadCubit(
context.read(), context.read(),
context.read(), context.read(),
context.read(),
), ),
child: DocumentUploadPreparationPage( child: DocumentUploadPreparationPage(
fileBytes: bytes, fileBytes: bytes,

View File

@@ -1,39 +1,55 @@
import 'dart:async'; import 'dart:async';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/persistent_repository.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 PaperlessSavedViewsApi _api;
final Completer _initialized = Completer();
SavedViewRepository(this._api) : super(const SavedViewRepositoryState()) { SavedViewRepository(this._api)
initialize(); : 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 { Future<SavedView> create(SavedView object) async {
await _initialized.future;
final created = await _api.save(object); final created = await _api.save(object);
final updatedState = {...state.savedViews}..putIfAbsent(created.id!, () => created); final updatedState = {...state.savedViews}
emit(state.copyWith(savedViews: updatedState)); ..putIfAbsent(created.id!, () => created);
emit(SavedViewRepositoryState.loaded(savedViews: updatedState));
return created; return created;
} }
Future<int> delete(SavedView view) async { Future<int> delete(SavedView view) async {
await _initialized.future;
await _api.delete(view); await _api.delete(view);
final updatedState = {...state.savedViews}..remove(view.id); final updatedState = {...state.savedViews}..remove(view.id);
emit(state.copyWith(savedViews: updatedState)); emit(SavedViewRepositoryState.loaded(savedViews: updatedState));
return view.id!; return view.id!;
} }
Future<SavedView?> find(int id) async { Future<SavedView?> find(int id) async {
await _initialized.future;
final found = await _api.find(id); final found = await _api.find(id);
if (found != null) { if (found != null) {
final updatedState = {...state.savedViews}..update(id, (_) => found, ifAbsent: () => found); final updatedState = {...state.savedViews}
emit(state.copyWith(savedViews: updatedState)); ..update(id, (_) => found, ifAbsent: () => found);
emit(SavedViewRepositoryState.loaded(savedViews: updatedState));
} }
return found; return found;
} }
@@ -44,14 +60,15 @@ class SavedViewRepository extends PersistentRepository<SavedViewRepositoryState>
...state.savedViews, ...state.savedViews,
...{for (final view in found) view.id!: view}, ...{for (final view in found) view.id!: view},
}; };
emit(state.copyWith(savedViews: updatedState)); emit(SavedViewRepositoryState.loaded(savedViews: updatedState));
return found; return found;
} }
@override @override
Future<void> clear() async { Future<void> clear() async {
await _initialized.future;
await super.clear(); await super.clear();
emit(const SavedViewRepositoryState()); emit(const SavedViewRepositoryState.initial());
} }
@override @override

View File

@@ -1,14 +1,19 @@
import 'package:freezed_annotation/freezed_annotation.dart'; part of 'saved_view_repository.dart';
import 'package:paperless_api/paperless_api.dart';
part 'saved_view_repository_state.freezed.dart';
part 'saved_view_repository_state.g.dart';
@freezed @freezed
class SavedViewRepositoryState with _$SavedViewRepositoryState { class SavedViewRepositoryState with _$SavedViewRepositoryState {
const factory SavedViewRepositoryState({ const factory SavedViewRepositoryState.initial({
@Default({}) Map<int, SavedView> savedViews, @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) => factory SavedViewRepositoryState.fromJson(Map<String, dynamic> json) =>
_$SavedViewRepositoryStateFromJson(json); _$SavedViewRepositoryStateFromJson(json);

View File

@@ -13,7 +13,8 @@ import 'package:pretty_dio_logger/pretty_dio_logger.dart';
class SessionManager extends ValueNotifier<Dio> { class SessionManager extends ValueNotifier<Dio> {
Dio get client => value; 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) { static Dio _initDio(List<Interceptor> interceptors) {
//en- and decoded by utf8 by default //en- and decoded by utf8 by default
@@ -21,8 +22,8 @@ class SessionManager extends ValueNotifier<Dio> {
BaseOptions(contentType: Headers.jsonContentType), BaseOptions(contentType: Headers.jsonContentType),
); );
dio.options dio.options
..receiveTimeout = const Duration(seconds: 15) ..receiveTimeout = const Duration(seconds: 20)
..sendTimeout = const Duration(seconds: 10) ..sendTimeout = const Duration(seconds: 60)
..responseType = ResponseType.json; ..responseType = ResponseType.json;
(dio.httpClientAdapter as IOHttpClientAdapter).onHttpClientCreate = (dio.httpClientAdapter as IOHttpClientAdapter).onHttpClientCreate =
(client) => client..badCertificateCallback = (cert, host, port) => true; (client) => client..badCertificateCallback = (cert, host, port) => true;
@@ -62,7 +63,8 @@ class SessionManager extends ValueNotifier<Dio> {
); );
final adapter = IOHttpClientAdapter() final adapter = IOHttpClientAdapter()
..onHttpClientCreate = (client) => HttpClient(context: context) ..onHttpClientCreate = (client) => HttpClient(context: context)
..badCertificateCallback = (X509Certificate cert, String host, int port) => true; ..badCertificateCallback =
(X509Certificate cert, String host, int port) => true;
client.httpClientAdapter = adapter; client.httpClientAdapter = adapter;
} }

View File

@@ -32,7 +32,8 @@ class ConnectivityStatusServiceImpl implements ConnectivityStatusService {
@override @override
Future<bool> isConnectedToInternet() async { Future<bool> isConnectedToInternet() async {
return _hasActiveInternetConnection(await (Connectivity().checkConnectivity())); return _hasActiveInternetConnection(
await (Connectivity().checkConnectivity()));
} }
@override @override
@@ -71,7 +72,8 @@ class ConnectivityStatusServiceImpl implements ConnectivityStatusService {
return ReachabilityStatus.unknown; return ReachabilityStatus.unknown;
} }
try { try {
SessionManager manager = SessionManager([ServerReachabilityErrorInterceptor()]) SessionManager manager =
SessionManager([ServerReachabilityErrorInterceptor()])
..updateSettings(clientCertificate: clientCertificate) ..updateSettings(clientCertificate: clientCertificate)
..client.options.connectTimeout = const Duration(seconds: 5) ..client.options.connectTimeout = const Duration(seconds: 5)
..client.options.receiveTimeout = const Duration(seconds: 5); ..client.options.receiveTimeout = const Duration(seconds: 5);
@@ -82,7 +84,8 @@ class ConnectivityStatusServiceImpl implements ConnectivityStatusService {
} }
return ReachabilityStatus.notReachable; return ReachabilityStatus.notReachable;
} on DioError catch (error) { } 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; return error.error as ReachabilityStatus;
} }
} on TlsException catch (error) { } on TlsException catch (error) {

View File

@@ -1,4 +1,3 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:paperless_mobile/core/model/github_error_report.model.dart'; import 'package:paperless_mobile/core/model/github_error_report.model.dart';
import 'package:paperless_mobile/core/widgets/error_report_page.dart'; import 'package:paperless_mobile/core/widgets/error_report_page.dart';

View File

@@ -1,4 +1,3 @@
typedef JSON = Map<String, dynamic>; typedef JSON = Map<String, dynamic>;
typedef PaperlessValidationErrors = Map<String, String>; typedef PaperlessValidationErrors = Map<String, String>;
typedef PaperlessLocalizedErrorMessage = String; typedef PaperlessLocalizedErrorMessage = String;

View File

@@ -181,7 +181,8 @@ class FormBuilderColorPickerField extends FormBuilderField<Color> {
); );
@override @override
FormBuilderColorPickerFieldState createState() => FormBuilderColorPickerFieldState(); FormBuilderColorPickerFieldState createState() =>
FormBuilderColorPickerFieldState();
} }
class FormBuilderColorPickerFieldState class FormBuilderColorPickerFieldState

View File

@@ -334,7 +334,8 @@ class FormBuilderTypeAhead<T> extends FormBuilderField<T> {
// TODO HACK to satisfy strictness // TODO HACK to satisfy strictness
suggestionsCallback: suggestionsCallback, suggestionsCallback: suggestionsCallback,
itemBuilder: itemBuilder, itemBuilder: itemBuilder,
transitionBuilder: (context, suggestionsBox, controller) => suggestionsBox, transitionBuilder: (context, suggestionsBox, controller) =>
suggestionsBox,
onSuggestionSelected: (T suggestion) { onSuggestionSelected: (T suggestion) {
state.didChange(suggestion); state.didChange(suggestion);
onSuggestionSelected?.call(suggestion); onSuggestionSelected?.call(suggestion);
@@ -356,7 +357,8 @@ class FormBuilderTypeAhead<T> extends FormBuilderField<T> {
keepSuggestionsOnLoading: keepSuggestionsOnLoading, keepSuggestionsOnLoading: keepSuggestionsOnLoading,
autoFlipDirection: autoFlipDirection, autoFlipDirection: autoFlipDirection,
suggestionsBoxController: suggestionsBoxController, suggestionsBoxController: suggestionsBoxController,
keepSuggestionsOnSuggestionSelected: keepSuggestionsOnSuggestionSelected, keepSuggestionsOnSuggestionSelected:
keepSuggestionsOnSuggestionSelected,
hideKeyboard: hideKeyboard, hideKeyboard: hideKeyboard,
scrollController: scrollController, scrollController: scrollController,
); );
@@ -367,14 +369,15 @@ class FormBuilderTypeAhead<T> extends FormBuilderField<T> {
FormBuilderTypeAheadState<T> createState() => FormBuilderTypeAheadState<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; late TextEditingController _typeAheadController;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_typeAheadController = _typeAheadController = widget.controller ??
widget.controller ?? TextEditingController(text: _getTextString(initialValue)); TextEditingController(text: _getTextString(initialValue));
// _typeAheadController.addListener(_handleControllerChanged); // _typeAheadController.addListener(_handleControllerChanged);
} }

View File

@@ -27,7 +27,8 @@ import 'package:flutter/services.dart';
typedef ChipsInputSuggestions<T> = Future<List<T>> Function(String query); typedef ChipsInputSuggestions<T> = Future<List<T>> Function(String query);
typedef ChipSelected<T> = void Function(T data, bool selected); 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 { class ChipsInput<T> extends StatefulWidget {
const ChipsInput({ const ChipsInput({
@@ -70,7 +71,8 @@ class ChipsInputState<T> extends State<ChipsInput<T>> {
TextEditingValue get currentTextEditingValue => _value; TextEditingValue get currentTextEditingValue => _value;
bool get _hasInputConnection => _connection != null && (_connection?.attached ?? false); bool get _hasInputConnection =>
_connection != null && (_connection?.attached ?? false);
void requestKeyboard() { void requestKeyboard() {
if (_focusNode.hasFocus) { if (_focusNode.hasFocus) {
@@ -189,7 +191,8 @@ class ChipsInputState<T> extends State<ChipsInput<T>> {
child: ListView.builder( child: ListView.builder(
itemCount: _suggestions.length, itemCount: _suggestions.length,
itemBuilder: (BuildContext context, int index) { 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) { int _countReplacements(TextEditingValue value) {
return value.text.codeUnits.where((ch) => ch == kObjectReplacementChar).length; return value.text.codeUnits
.where((ch) => ch == kObjectReplacementChar)
.length;
} }
void _updateTextInputState() { void _updateTextInputState() {
final text = String.fromCharCodes(_chips.map((_) => kObjectReplacementChar)); final text =
String.fromCharCodes(_chips.map((_) => kObjectReplacementChar));
_value = TextEditingValue( _value = TextEditingValue(
text: text, text: text,
selection: TextSelection.collapsed(offset: text.length), selection: TextSelection.collapsed(offset: text.length),
@@ -227,8 +233,9 @@ class ChipsInputState<T> extends State<ChipsInput<T>> {
final localId = ++_searchId; final localId = ++_searchId;
final results = await widget.findSuggestions(value); final results = await widget.findSuggestions(value);
if (_searchId == localId && mounted) { if (_searchId == localId && mounted) {
setState(() => _suggestions = setState(() => _suggestions = results
results.where((profile) => !_chips.contains(profile)).toList(growable: false)); .where((profile) => !_chips.contains(profile))
.toList(growable: false));
} }
} }
} }
@@ -244,7 +251,8 @@ class _TextCaret extends StatefulWidget {
_TextCursorState createState() => _TextCursorState(); _TextCursorState createState() => _TextCursorState();
} }
class _TextCursorState extends State<_TextCaret> with SingleTickerProviderStateMixin { class _TextCursorState extends State<_TextCaret>
with SingleTickerProviderStateMixin {
bool _displayed = false; bool _displayed = false;
late Timer _timer; late Timer _timer;

View File

@@ -95,7 +95,8 @@ class AppDrawer extends StatelessWidget {
MaterialPageRoute( MaterialPageRoute(
builder: (_) => MultiProvider( builder: (_) => MultiProvider(
providers: [ providers: [
Provider.value(value: context.read<PaperlessServerStatsApi>()), Provider.value(
value: context.read<PaperlessServerStatsApi>()),
Provider.value(value: context.read<ApiVersion>()), Provider.value(value: context.read<ApiVersion>()),
], ],
child: const SettingsPage(), child: const SettingsPage(),
@@ -128,7 +129,8 @@ class AppDrawer extends StatelessWidget {
), ),
RichText( RichText(
text: TextSpan( text: TextSpan(
style: theme.textTheme.bodyMedium?.copyWith(color: colorScheme.onSurface), style: theme.textTheme.bodyMedium
?.copyWith(color: colorScheme.onSurface),
children: [ children: [
TextSpan( TextSpan(
text: S.of(context)!.findTheSourceCodeOn, text: S.of(context)!.findTheSourceCodeOn,
@@ -151,11 +153,13 @@ class AppDrawer extends StatelessWidget {
const SizedBox(height: 16), const SizedBox(height: 16),
Text( Text(
'Credits', 'Credits',
style: theme.textTheme.titleMedium?.copyWith(color: colorScheme.onSurface), style: theme.textTheme.titleMedium
?.copyWith(color: colorScheme.onSurface),
), ),
RichText( RichText(
text: TextSpan( text: TextSpan(
style: theme.textTheme.bodyMedium?.copyWith(color: colorScheme.onSurface), style: theme.textTheme.bodyMedium
?.copyWith(color: colorScheme.onSurface),
children: [ children: [
const TextSpan( const TextSpan(
text: 'Onboarding images by ', text: 'Onboarding images by ',
@@ -205,16 +209,16 @@ class AppDrawer extends StatelessWidget {
} }
//Wrap( //Wrap(
// children: [ // children: [
// const Text('Onboarding images by '), // const Text('Onboarding images by '),
// GestureDetector( // GestureDetector(
// onTap: followLink, // onTap: followLink,
// child: RichText( // child: RichText(
// 'pch.vector', // 'pch.vector',
// style: TextStyle(color: Colors.blue), // style: TextStyle(color: Colors.blue),
// ), // ),
// ), // ),
// const Text(' on Freepik.') // const Text(' on Freepik.')
// ], // ],
// ) // )

View File

@@ -10,7 +10,8 @@ class ApplicationIntroSlideshow extends StatefulWidget {
const ApplicationIntroSlideshow({super.key}); const ApplicationIntroSlideshow({super.key});
@override @override
State<ApplicationIntroSlideshow> createState() => _ApplicationIntroSlideshowState(); State<ApplicationIntroSlideshow> createState() =>
_ApplicationIntroSlideshowState();
} }
//TODO: INTL ALL //TODO: INTL ALL

View File

@@ -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/features/labels/view/widgets/label_form_field.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.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 { class BulkEditLabelBottomSheet<T extends Label> extends StatefulWidget {
final String title; final String title;
@@ -31,16 +32,19 @@ class BulkEditLabelBottomSheet<T extends Label> extends StatefulWidget {
}); });
@override @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>(); final _formKey = GlobalKey<FormBuilderState>();
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Padding( return Padding(
padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), padding:
EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
child: BlocBuilder<DocumentBulkActionCubit, DocumentBulkActionState>( child: BlocBuilder<DocumentBulkActionCubit, DocumentBulkActionState>(
builder: (context, state) { builder: (context, state) {
return Padding( return Padding(
@@ -76,11 +80,13 @@ class _BulkEditLabelBottomSheetState<T extends Label> extends State<BulkEditLabe
const SizedBox(width: 16), const SizedBox(width: 16),
FilledButton( FilledButton(
onPressed: () { onPressed: () {
if (_formKey.currentState?.saveAndValidate() ?? false) { if (_formKey.currentState?.saveAndValidate() ??
final value = _formKey.currentState?.getRawValue('labelFormField') false) {
final value = _formKey.currentState
?.getRawValue('labelFormField')
as IdQueryParameter?; as IdQueryParameter?;
widget widget.onSubmit(value?.maybeWhen(
.onSubmit(value?.maybeWhen(fromId: (id) => id, orElse: () => null)); fromId: (id) => id, orElse: () => null));
} }
}, },
child: Text(S.of(context)!.apply), child: Text(S.of(context)!.apply),

View File

@@ -91,8 +91,8 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
_notifier.notifyUpdated(updatedDocument); _notifier.notifyUpdated(updatedDocument);
} else { } else {
final int autoAsn = await _api.findNextAsn(); final int autoAsn = await _api.findNextAsn();
final updatedDocument = final updatedDocument = await _api
await _api.update(document.copyWith(archiveSerialNumber: () => autoAsn)); .update(document.copyWith(archiveSerialNumber: () => autoAsn));
_notifier.notifyUpdated(updatedDocument); _notifier.notifyUpdated(updatedDocument);
} }
} }
@@ -104,7 +104,8 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
if (state.metaData == null) { if (state.metaData == null) {
await loadMetaData(); await loadMetaData();
} }
final desc = FileDescription.fromPath(state.metaData!.mediaFilename.replaceAll("/", " ")); final desc = FileDescription.fromPath(
state.metaData!.mediaFilename.replaceAll("/", " "));
final fileName = "${desc.filename}.pdf"; final fileName = "${desc.filename}.pdf";
final file = File("${cacheDir.path}/$fileName"); final file = File("${cacheDir.path}/$fileName");
@@ -138,7 +139,8 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
await FileService.downloadsDirectory, await FileService.downloadsDirectory,
); );
final desc = FileDescription.fromPath( final desc = FileDescription.fromPath(
state.metaData!.mediaFilename.replaceAll("/", " "), // Flatten directory structure state.metaData!.mediaFilename
.replaceAll("/", " "), // Flatten directory structure
); );
if (!File(filePath).existsSync()) { if (!File(filePath).existsSync()) {
File(filePath).createSync(); File(filePath).createSync();
@@ -205,7 +207,8 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
if (state.metaData == null) { if (state.metaData == null) {
await loadMetaData(); await loadMetaData();
} }
final filePath = _buildDownloadFilePath(false, await FileService.temporaryDirectory); final filePath =
_buildDownloadFilePath(false, await FileService.temporaryDirectory);
await _api.downloadToFile( await _api.downloadToFile(
state.document, state.document,
filePath, filePath,
@@ -223,7 +226,8 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
String _buildDownloadFilePath(bool original, Directory dir) { String _buildDownloadFilePath(bool original, Directory dir) {
final description = FileDescription.fromPath( final description = FileDescription.fromPath(
state.metaData!.mediaFilename.replaceAll("/", " "), // Flatten directory structure state.metaData!.mediaFilename
.replaceAll("/", " "), // Flatten directory structure
); );
final extension = original ? description.extension : 'pdf'; final extension = original ? description.extension : 'pdf';
return "${dir.path}/${description.filename}.$extension"; return "${dir.path}/${description.filename}.$extension";

View File

@@ -45,7 +45,8 @@ class _SelectFileTypeDialogState extends State<SelectFileTypeDialog> {
CheckboxListTile( CheckboxListTile(
controlAffinity: ListTileControlAffinity.leading, controlAffinity: ListTileControlAffinity.leading,
value: _rememberSelection, value: _rememberSelection,
onChanged: (value) => setState(() => _rememberSelection = value ?? false), onChanged: (value) =>
setState(() => _rememberSelection = value ?? false),
title: Text( title: Text(
S.of(context)!.rememberDecision, S.of(context)!.rememberDecision,
style: Theme.of(context).textTheme.labelMedium, style: Theme.of(context).textTheme.labelMedium,
@@ -61,7 +62,8 @@ class _SelectFileTypeDialogState extends State<SelectFileTypeDialog> {
if (_rememberSelection) { if (_rememberSelection) {
widget.onRememberSelection(_downloadType); widget.onRememberSelection(_downloadType);
} }
Navigator.of(context).pop(_downloadType == FileDownloadType.original); Navigator.of(context)
.pop(_downloadType == FileDownloadType.original);
}, },
), ),
], ],

View File

@@ -51,27 +51,35 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
final tabLength = 4 + (apiVersion.hasMultiUserSupport ? 1 : 0); final tabLength = 4 + (apiVersion.hasMultiUserSupport ? 1 : 0);
return WillPopScope( return WillPopScope(
onWillPop: () async { onWillPop: () async {
Navigator.of(context).pop(context.read<DocumentDetailsCubit>().state.document); Navigator.of(context)
.pop(context.read<DocumentDetailsCubit>().state.document);
return false; return false;
}, },
child: DefaultTabController( child: DefaultTabController(
length: tabLength, length: tabLength,
child: BlocListener<ConnectivityCubit, ConnectivityState>( child: BlocListener<ConnectivityCubit, ConnectivityState>(
listenWhen: (previous, current) => !previous.isConnected && current.isConnected, listenWhen: (previous, current) =>
!previous.isConnected && current.isConnected,
listener: (context, state) { listener: (context, state) {
context.read<DocumentDetailsCubit>().loadMetaData(); context.read<DocumentDetailsCubit>().loadMetaData();
}, },
child: Scaffold( child: Scaffold(
extendBodyBehindAppBar: false, extendBodyBehindAppBar: false,
floatingActionButtonLocation: FloatingActionButtonLocation.endDocked, floatingActionButtonLocation:
FloatingActionButtonLocation.endDocked,
floatingActionButton: _buildEditButton(), floatingActionButton: _buildEditButton(),
bottomNavigationBar: _buildBottomAppBar(), bottomNavigationBar: _buildBottomAppBar(),
body: NestedScrollView( body: NestedScrollView(
headerSliverBuilder: (context, innerBoxIsScrolled) => [ headerSliverBuilder: (context, innerBoxIsScrolled) => [
SliverOverlapAbsorber( SliverOverlapAbsorber(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), handle:
NestedScrollView.sliverOverlapAbsorberHandleFor(context),
sliver: SliverAppBar( sliver: SliverAppBar(
title: Text(context.watch<DocumentDetailsCubit>().state.document.title), title: Text(context
.watch<DocumentDetailsCubit>()
.state
.document
.title),
leading: const BackButton(), leading: const BackButton(),
pinned: true, pinned: true,
forceElevated: innerBoxIsScrolled, forceElevated: innerBoxIsScrolled,
@@ -81,7 +89,8 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
background: Stack( background: Stack(
alignment: Alignment.topCenter, alignment: Alignment.topCenter,
children: [ children: [
BlocBuilder<DocumentDetailsCubit, DocumentDetailsState>( BlocBuilder<DocumentDetailsCubit,
DocumentDetailsState>(
builder: (context, state) { builder: (context, state) {
return Positioned.fill( return Positioned.fill(
child: DocumentPreview( child: DocumentPreview(
@@ -97,8 +106,14 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
decoration: BoxDecoration( decoration: BoxDecoration(
gradient: LinearGradient( gradient: LinearGradient(
colors: [ colors: [
Theme.of(context).colorScheme.background.withOpacity(0.8), Theme.of(context)
Theme.of(context).colorScheme.background.withOpacity(0.5), .colorScheme
.background
.withOpacity(0.8),
Theme.of(context)
.colorScheme
.background
.withOpacity(0.5),
Colors.transparent, Colors.transparent,
Colors.transparent, Colors.transparent,
Colors.transparent, Colors.transparent,
@@ -120,7 +135,9 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
child: Text( child: Text(
S.of(context)!.overview, S.of(context)!.overview,
style: TextStyle( 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( child: Text(
S.of(context)!.content, S.of(context)!.content,
style: TextStyle( 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( child: Text(
S.of(context)!.metaData, S.of(context)!.metaData,
style: TextStyle( 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( child: Text(
S.of(context)!.similarDocuments, S.of(context)!.similarDocuments,
style: TextStyle( 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( child: Text(
"Permissions", "Permissions",
style: TextStyle( style: TextStyle(
color: Theme.of(context).colorScheme.onPrimaryContainer, color: Theme.of(context)
.colorScheme
.onPrimaryContainer,
), ),
), ),
), ),
@@ -182,7 +207,8 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
CustomScrollView( CustomScrollView(
slivers: [ slivers: [
SliverOverlapInjector( SliverOverlapInjector(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), handle: NestedScrollView
.sliverOverlapAbsorberHandleFor(context),
), ),
DocumentOverviewWidget( DocumentOverviewWidget(
document: state.document, document: state.document,
@@ -198,7 +224,8 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
CustomScrollView( CustomScrollView(
slivers: [ slivers: [
SliverOverlapInjector( SliverOverlapInjector(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), handle: NestedScrollView
.sliverOverlapAbsorberHandleFor(context),
), ),
DocumentContentWidget( DocumentContentWidget(
isFullContentLoaded: state.isFullContentLoaded, isFullContentLoaded: state.isFullContentLoaded,
@@ -211,7 +238,8 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
CustomScrollView( CustomScrollView(
slivers: [ slivers: [
SliverOverlapInjector( SliverOverlapInjector(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), handle: NestedScrollView
.sliverOverlapAbsorberHandleFor(context),
), ),
DocumentMetaDataWidget( DocumentMetaDataWidget(
document: state.document, document: state.document,
@@ -223,7 +251,8 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
controller: _pagingScrollController, controller: _pagingScrollController,
slivers: [ slivers: [
SliverOverlapInjector( SliverOverlapInjector(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), handle: NestedScrollView
.sliverOverlapAbsorberHandleFor(context),
), ),
SimilarDocumentsView( SimilarDocumentsView(
pagingScrollController: _pagingScrollController, pagingScrollController: _pagingScrollController,
@@ -235,7 +264,8 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
controller: _pagingScrollController, controller: _pagingScrollController,
slivers: [ slivers: [
SliverOverlapInjector( SliverOverlapInjector(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), handle: NestedScrollView
.sliverOverlapAbsorberHandleFor(context),
), ),
DocumentPermissionsWidget( DocumentPermissionsWidget(
document: state.document, document: state.document,
@@ -289,15 +319,16 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
final isConnected = connectivityState.isConnected; final isConnected = connectivityState.isConnected;
final canDelete = isConnected && final canDelete = isConnected &&
LocalUserAccount.current.paperlessUser LocalUserAccount.current.paperlessUser.hasPermission(
.hasPermission(PermissionAction.delete, PermissionTarget.document); PermissionAction.delete, PermissionTarget.document);
return Row( return Row(
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
children: [ children: [
IconButton( IconButton(
tooltip: S.of(context)!.deleteDocumentTooltip, tooltip: S.of(context)!.deleteDocumentTooltip,
icon: const Icon(Icons.delete), icon: const Icon(Icons.delete),
onPressed: canDelete ? () => _onDelete(state.document) : null, onPressed:
canDelete ? () => _onDelete(state.document) : null,
).paddedSymmetrically(horizontal: 4), ).paddedSymmetrically(horizontal: 4),
DocumentDownloadButton( DocumentDownloadButton(
document: state.document, document: state.document,
@@ -307,7 +338,8 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
IconButton( IconButton(
tooltip: S.of(context)!.previewTooltip, tooltip: S.of(context)!.previewTooltip,
icon: const Icon(Icons.visibility), icon: const Icon(Icons.visibility),
onPressed: (isConnected) ? () => _onOpen(state.document) : null, onPressed:
(isConnected) ? () => _onOpen(state.document) : null,
).paddedOnly(right: 4.0), ).paddedOnly(right: 4.0),
IconButton( IconButton(
tooltip: S.of(context)!.openInSystemViewer, tooltip: S.of(context)!.openInSystemViewer,
@@ -317,7 +349,8 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
DocumentShareButton(document: state.document), DocumentShareButton(document: state.document),
IconButton( IconButton(
tooltip: S.of(context)!.print, //TODO: INTL tooltip: S.of(context)!.print, //TODO: INTL
onPressed: () => context.read<DocumentDetailsCubit>().printDocument(), onPressed: () =>
context.read<DocumentDetailsCubit>().printDocument(),
icon: const Icon(Icons.print), icon: const Icon(Icons.print),
), ),
], ],
@@ -350,7 +383,8 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
), ),
], ],
child: BlocListener<DocumentEditCubit, DocumentEditState>( child: BlocListener<DocumentEditCubit, DocumentEditState>(
listenWhen: (previous, current) => previous.document != current.document, listenWhen: (previous, current) =>
previous.document != current.document,
listener: (context, state) { listener: (context, state) {
cubit.replace(state.document); cubit.replace(state.document);
}, },
@@ -370,7 +404,8 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
} }
void _onOpenFileInSystemViewer() async { 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.done) return;
if (status == ResultType.noAppToOpen) { if (status == ResultType.noAppToOpen) {
showGenericError(context, S.of(context)!.noAppToDisplayPDFFilesFound); showGenericError(context, S.of(context)!.noAppToDisplayPDFFilesFound);
@@ -379,14 +414,16 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
showGenericError(context, translateError(context, ErrorCode.unknown)); showGenericError(context, translateError(context, ErrorCode.unknown));
} }
if (status == ResultType.permissionDenied) { if (status == ResultType.permissionDenied) {
showGenericError(context, S.of(context)!.couldNotOpenFilePermissionDenied); showGenericError(
context, S.of(context)!.couldNotOpenFilePermissionDenied);
} }
} }
void _onDelete(DocumentModel document) async { void _onDelete(DocumentModel document) async {
final delete = await showDialog( final delete = await showDialog(
context: context, context: context,
builder: (context) => DeleteDocumentConfirmationDialog(document: document), builder: (context) =>
DeleteDocumentConfirmationDialog(document: document),
) ?? ) ??
false; false;
if (delete) { if (delete) {
@@ -406,7 +443,8 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
Navigator.of(context).push( Navigator.of(context).push(
MaterialPageRoute( MaterialPageRoute(
builder: (_) => DocumentView( builder: (_) => DocumentView(
documentBytes: context.read<PaperlessDocumentsApi>().download(document), documentBytes:
context.read<PaperlessDocumentsApi>().download(document),
title: document.title, title: document.title,
), ),
), ),

View File

@@ -18,7 +18,8 @@ class ArchiveSerialNumberField extends StatefulWidget {
}); });
@override @override
State<ArchiveSerialNumberField> createState() => _ArchiveSerialNumberFieldState(); State<ArchiveSerialNumberField> createState() =>
_ArchiveSerialNumberFieldState();
} }
class _ArchiveSerialNumberFieldState extends State<ArchiveSerialNumberField> { class _ArchiveSerialNumberFieldState extends State<ArchiveSerialNumberField> {
@@ -39,21 +40,25 @@ class _ArchiveSerialNumberFieldState extends State<ArchiveSerialNumberField> {
void _clearButtonListener() { void _clearButtonListener() {
setState(() { setState(() {
_showClearButton = _asnEditingController.text.isNotEmpty; _showClearButton = _asnEditingController.text.isNotEmpty;
_canUpdate = int.tryParse(_asnEditingController.text) != widget.document.archiveSerialNumber; _canUpdate = int.tryParse(_asnEditingController.text) !=
widget.document.archiveSerialNumber;
}); });
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final userCanEditDocument = LocalUserAccount.current.paperlessUser.hasPermission( final userCanEditDocument =
LocalUserAccount.current.paperlessUser.hasPermission(
PermissionAction.change, PermissionAction.change,
PermissionTarget.document, PermissionTarget.document,
); );
return BlocListener<DocumentDetailsCubit, DocumentDetailsState>( return BlocListener<DocumentDetailsCubit, DocumentDetailsState>(
listenWhen: (previous, current) => listenWhen: (previous, current) =>
previous.document.archiveSerialNumber != current.document.archiveSerialNumber, previous.document.archiveSerialNumber !=
current.document.archiveSerialNumber,
listener: (context, state) { listener: (context, state) {
_asnEditingController.text = state.document.archiveSerialNumber?.toString() ?? ''; _asnEditingController.text =
state.document.archiveSerialNumber?.toString() ?? '';
setState(() { setState(() {
_canUpdate = false; _canUpdate = false;
}); });
@@ -80,13 +85,17 @@ class _ArchiveSerialNumberFieldState extends State<ArchiveSerialNumberField> {
IconButton( IconButton(
icon: const Icon(Icons.clear), icon: const Icon(Icons.clear),
color: Theme.of(context).colorScheme.primary, color: Theme.of(context).colorScheme.primary,
onPressed: userCanEditDocument ? _asnEditingController.clear : null, onPressed: userCanEditDocument
? _asnEditingController.clear
: null,
), ),
IconButton( IconButton(
icon: const Icon(Icons.plus_one_rounded), icon: const Icon(Icons.plus_one_rounded),
color: Theme.of(context).colorScheme.primary, color: Theme.of(context).colorScheme.primary,
onPressed: onPressed:
context.watchInternetConnection && !_showClearButton ? _onAutoAssign : null, context.watchInternetConnection && !_showClearButton
? _onAutoAssign
: null,
).paddedOnly(right: 8), ).paddedOnly(right: 8),
], ],
), ),
@@ -97,7 +106,9 @@ class _ArchiveSerialNumberFieldState extends State<ArchiveSerialNumberField> {
), ),
TextButton.icon( TextButton.icon(
icon: const Icon(Icons.done), icon: const Icon(Icons.done),
onPressed: context.watchInternetConnection && _canUpdate ? _onSubmitted : null, onPressed: context.watchInternetConnection && _canUpdate
? _onSubmitted
: null,
label: Text(S.of(context)!.save), label: Text(S.of(context)!.save),
).padded(), ).padded(),
], ],

View File

@@ -24,7 +24,8 @@ class DetailsItem extends StatelessWidget {
} }
DetailsItem.text( DetailsItem.text(
String text, {super.key, String text, {
super.key,
required this.label, required this.label,
required BuildContext context, required BuildContext context,
}) : content = Text( }) : content = Text(

View File

@@ -44,14 +44,16 @@ class _DocumentDownloadButtonState extends State<DocumentDownloadButton> {
width: 16, width: 16,
) )
: const Icon(Icons.download), : const Icon(Icons.download),
onPressed: onPressed: widget.document != null && widget.enabled
widget.document != null && widget.enabled ? () => _onDownload(widget.document!) : null, ? () => _onDownload(widget.document!)
: null,
).paddedOnly(right: 4); ).paddedOnly(right: 4);
} }
Future<void> _onDownload(DocumentModel document) async { Future<void> _onDownload(DocumentModel document) async {
try { try {
final globalSettings = Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!; final globalSettings =
Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!;
bool original; bool original;
switch (globalSettings.defaultDownloadType) { switch (globalSettings.defaultDownloadType) {

View File

@@ -6,7 +6,8 @@ class DocumentPermissionsWidget extends StatefulWidget {
const DocumentPermissionsWidget({super.key, required this.document}); const DocumentPermissionsWidget({super.key, required this.document});
@override @override
State<DocumentPermissionsWidget> createState() => _DocumentPermissionsWidgetState(); State<DocumentPermissionsWidget> createState() =>
_DocumentPermissionsWidgetState();
} }
class _DocumentPermissionsWidgetState extends State<DocumentPermissionsWidget> { class _DocumentPermissionsWidgetState extends State<DocumentPermissionsWidget> {

View File

@@ -43,14 +43,16 @@ class _DocumentShareButtonState extends State<DocumentShareButton> {
child: CircularProgressIndicator(), child: CircularProgressIndicator(),
) )
: const Icon(Icons.share), : const Icon(Icons.share),
onPressed: onPressed: widget.document != null && widget.enabled
widget.document != null && widget.enabled ? () => _onShare(widget.document!) : null, ? () => _onShare(widget.document!)
: null,
).paddedOnly(right: 4); ).paddedOnly(right: 4);
} }
Future<void> _onShare(DocumentModel document) async { Future<void> _onShare(DocumentModel document) async {
try { try {
final globalSettings = Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!; final globalSettings =
Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!;
bool original; bool original;
switch (globalSettings.defaultShareType) { switch (globalSettings.defaultShareType) {

View File

@@ -49,8 +49,8 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_filteredSuggestions = _filteredSuggestions = widget.suggestions
widget.suggestions?.documentDifference(context.read<DocumentEditCubit>().state.document); ?.documentDifference(context.read<DocumentEditCubit>().state.document);
} }
@override @override
@@ -94,14 +94,16 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
ListView( ListView(
children: [ children: [
_buildTitleFormField(state.document.title).padded(), _buildTitleFormField(state.document.title).padded(),
_buildCreatedAtFormField(state.document.created).padded(), _buildCreatedAtFormField(state.document.created)
.padded(),
// Correspondent form field // Correspondent form field
Column( Column(
children: [ children: [
LabelFormField<Correspondent>( LabelFormField<Correspondent>(
showAnyAssignedOption: false, showAnyAssignedOption: false,
showNotAssignedOption: false, showNotAssignedOption: false,
addLabelPageBuilder: (initialValue) => RepositoryProvider.value( addLabelPageBuilder: (initialValue) =>
RepositoryProvider.value(
value: context.read<LabelRepository>(), value: context.read<LabelRepository>(),
child: AddCorrespondentPage( child: AddCorrespondentPage(
initialName: initialValue, initialName: initialValue,
@@ -109,26 +111,39 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
), ),
addLabelText: S.of(context)!.addCorrespondent, addLabelText: S.of(context)!.addCorrespondent,
labelText: S.of(context)!.correspondent, labelText: S.of(context)!.correspondent,
options: context.watch<DocumentEditCubit>().state.correspondents, options: context
initialValue: state.document.correspondent != null .watch<DocumentEditCubit>()
? IdQueryParameter.fromId(state.document.correspondent!) .state
.correspondents,
initialValue:
state.document.correspondent != null
? IdQueryParameter.fromId(
state.document.correspondent!)
: const IdQueryParameter.unset(), : const IdQueryParameter.unset(),
name: fkCorrespondent, name: fkCorrespondent,
prefixIcon: const Icon(Icons.person_outlined), prefixIcon: const Icon(Icons.person_outlined),
allowSelectUnassigned: true, allowSelectUnassigned: true,
canCreateNewLabel: canCreateNewLabel: LocalUserAccount
LocalUserAccount.current.paperlessUser.hasPermission( .current.paperlessUser
.hasPermission(
PermissionAction.add, PermissionAction.add,
PermissionTarget.correspondent, PermissionTarget.correspondent,
), ),
), ),
if (_filteredSuggestions?.hasSuggestedCorrespondents ?? false) if (_filteredSuggestions
?.hasSuggestedCorrespondents ??
false)
_buildSuggestionsSkeleton<int>( _buildSuggestionsSkeleton<int>(
suggestions: _filteredSuggestions!.correspondents, suggestions:
itemBuilder: (context, itemData) => ActionChip( _filteredSuggestions!.correspondents,
label: Text(state.correspondents[itemData]!.name), itemBuilder: (context, itemData) =>
ActionChip(
label: Text(
state.correspondents[itemData]!.name),
onPressed: () { onPressed: () {
_formKey.currentState?.fields[fkCorrespondent]?.didChange( _formKey
.currentState?.fields[fkCorrespondent]
?.didChange(
IdQueryParameter.fromId(itemData), IdQueryParameter.fromId(itemData),
); );
}, },
@@ -142,34 +157,45 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
LabelFormField<DocumentType>( LabelFormField<DocumentType>(
showAnyAssignedOption: false, showAnyAssignedOption: false,
showNotAssignedOption: false, showNotAssignedOption: false,
addLabelPageBuilder: (currentInput) => RepositoryProvider.value( addLabelPageBuilder: (currentInput) =>
RepositoryProvider.value(
value: context.read<LabelRepository>(), value: context.read<LabelRepository>(),
child: AddDocumentTypePage( child: AddDocumentTypePage(
initialName: currentInput, initialName: currentInput,
), ),
), ),
canCreateNewLabel: canCreateNewLabel: LocalUserAccount
LocalUserAccount.current.paperlessUser.hasPermission( .current.paperlessUser
.hasPermission(
PermissionAction.add, PermissionAction.add,
PermissionTarget.documentType, PermissionTarget.documentType,
), ),
addLabelText: S.of(context)!.addDocumentType, addLabelText: S.of(context)!.addDocumentType,
labelText: S.of(context)!.documentType, labelText: S.of(context)!.documentType,
initialValue: state.document.documentType != null initialValue:
? IdQueryParameter.fromId(state.document.documentType!) state.document.documentType != null
? IdQueryParameter.fromId(
state.document.documentType!)
: const IdQueryParameter.unset(), : const IdQueryParameter.unset(),
options: state.documentTypes, options: state.documentTypes,
name: _DocumentEditPageState.fkDocumentType, name: _DocumentEditPageState.fkDocumentType,
prefixIcon: const Icon(Icons.description_outlined), prefixIcon:
const Icon(Icons.description_outlined),
allowSelectUnassigned: true, allowSelectUnassigned: true,
), ),
if (_filteredSuggestions?.hasSuggestedDocumentTypes ?? false) if (_filteredSuggestions
?.hasSuggestedDocumentTypes ??
false)
_buildSuggestionsSkeleton<int>( _buildSuggestionsSkeleton<int>(
suggestions: _filteredSuggestions!.documentTypes, suggestions:
itemBuilder: (context, itemData) => ActionChip( _filteredSuggestions!.documentTypes,
label: Text(state.documentTypes[itemData]!.name), itemBuilder: (context, itemData) =>
onPressed: () => ActionChip(
_formKey.currentState?.fields[fkDocumentType]?.didChange( label: Text(
state.documentTypes[itemData]!.name),
onPressed: () => _formKey
.currentState?.fields[fkDocumentType]
?.didChange(
IdQueryParameter.fromId(itemData), IdQueryParameter.fromId(itemData),
), ),
), ),
@@ -182,12 +208,15 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
LabelFormField<StoragePath>( LabelFormField<StoragePath>(
showAnyAssignedOption: false, showAnyAssignedOption: false,
showNotAssignedOption: false, showNotAssignedOption: false,
addLabelPageBuilder: (initialValue) => RepositoryProvider.value( addLabelPageBuilder: (initialValue) =>
RepositoryProvider.value(
value: context.read<LabelRepository>(), value: context.read<LabelRepository>(),
child: AddStoragePathPage(initalName: initialValue), child: AddStoragePathPage(
initalName: initialValue),
), ),
canCreateNewLabel: canCreateNewLabel: LocalUserAccount
LocalUserAccount.current.paperlessUser.hasPermission( .current.paperlessUser
.hasPermission(
PermissionAction.add, PermissionAction.add,
PermissionTarget.storagePath, PermissionTarget.storagePath,
), ),
@@ -195,7 +224,8 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
labelText: S.of(context)!.storagePath, labelText: S.of(context)!.storagePath,
options: state.storagePaths, options: state.storagePaths,
initialValue: state.document.storagePath != null initialValue: state.document.storagePath != null
? IdQueryParameter.fromId(state.document.storagePath!) ? IdQueryParameter.fromId(
state.document.storagePath!)
: const IdQueryParameter.unset(), : const IdQueryParameter.unset(),
name: fkStoragePath, name: fkStoragePath,
prefixIcon: const Icon(Icons.folder_outlined), prefixIcon: const Icon(Icons.folder_outlined),
@@ -220,7 +250,8 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
.isNotEmpty ?? .isNotEmpty ??
false) false)
_buildSuggestionsSkeleton<int>( _buildSuggestionsSkeleton<int>(
suggestions: (_filteredSuggestions?.tags.toSet() ?? {}), suggestions:
(_filteredSuggestions?.tags.toSet() ?? {}),
itemBuilder: (context, itemData) { itemBuilder: (context, itemData) {
final tag = state.tags[itemData]!; final tag = state.tags[itemData]!;
return ActionChip( return ActionChip(
@@ -230,13 +261,17 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
), ),
backgroundColor: tag.color, backgroundColor: tag.color,
onPressed: () { onPressed: () {
final currentTags = final currentTags = _formKey.currentState
_formKey.currentState?.fields[fkTags]?.value as TagsQuery; ?.fields[fkTags]?.value as TagsQuery;
_formKey.currentState?.fields[fkTags]?.didChange( _formKey.currentState?.fields[fkTags]
?.didChange(
currentTags.maybeWhen( currentTags.maybeWhen(
ids: (include, exclude) => TagsQuery.ids( ids: (include, exclude) =>
include: [...include, itemData], exclude: exclude), TagsQuery.ids(
orElse: () => TagsQuery.ids(include: [itemData]), include: [...include, itemData],
exclude: exclude),
orElse: () =>
TagsQuery.ids(include: [itemData]),
), ),
); );
}, },
@@ -278,12 +313,12 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
var mergedDocument = document.copyWith( var mergedDocument = document.copyWith(
title: values[fkTitle], title: values[fkTitle],
created: values[fkCreatedDate], created: values[fkCreatedDate],
documentType: () => documentType: () => (values[fkDocumentType] as IdQueryParameter)
(values[fkDocumentType] as IdQueryParameter).whenOrNull(fromId: (id) => id), .whenOrNull(fromId: (id) => id),
correspondent: () => correspondent: () => (values[fkCorrespondent] as IdQueryParameter)
(values[fkCorrespondent] as IdQueryParameter).whenOrNull(fromId: (id) => id), .whenOrNull(fromId: (id) => id),
storagePath: () => storagePath: () => (values[fkStoragePath] as IdQueryParameter)
(values[fkStoragePath] as IdQueryParameter).whenOrNull(fromId: (id) => id), .whenOrNull(fromId: (id) => id),
tags: (values[fkTags] as IdsTagsQuery).include, tags: (values[fkTags] as IdsTagsQuery).include,
content: values[fkContent], content: values[fkContent],
); );
@@ -340,7 +375,8 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
suggestions: _filteredSuggestions!.dates, suggestions: _filteredSuggestions!.dates,
itemBuilder: (context, itemData) => ActionChip( itemBuilder: (context, itemData) => ActionChip(
label: Text(DateFormat.yMMMd().format(itemData)), 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( itemBuilder: (context, index) => ColoredChipWrapper(
child: itemBuilder(context, suggestions.elementAt(index)), 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 List<Option> options;
// final void Function(Option option) onAddOption; // final void Function(Option option) onAddOption;
// const OptionsFormField({ // const OptionsFormField({
// super.key, // super.key,
// required this.options, // required this.options,

View File

@@ -35,9 +35,12 @@ class ScannerPage extends StatefulWidget {
State<ScannerPage> createState() => _ScannerPageState(); State<ScannerPage> createState() => _ScannerPageState();
} }
class _ScannerPageState extends State<ScannerPage> with SingleTickerProviderStateMixin { class _ScannerPageState extends State<ScannerPage>
final SliverOverlapAbsorberHandle searchBarHandle = SliverOverlapAbsorberHandle(); with SingleTickerProviderStateMixin {
final SliverOverlapAbsorberHandle actionsHandle = SliverOverlapAbsorberHandle(); final SliverOverlapAbsorberHandle searchBarHandle =
SliverOverlapAbsorberHandle();
final SliverOverlapAbsorberHandle actionsHandle =
SliverOverlapAbsorberHandle();
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -120,7 +123,6 @@ class _ScannerPageState extends State<ScannerPage> with SingleTickerProviderStat
? () => Navigator.of(context).push( ? () => Navigator.of(context).push(
MaterialPageRoute( MaterialPageRoute(
builder: (context) => DocumentView( builder: (context) => DocumentView(
documentBytes: _assembleFileBytes( documentBytes: _assembleFileBytes(
state, state,
forcePdf: true, forcePdf: true,
@@ -175,7 +177,8 @@ class _ScannerPageState extends State<ScannerPage> with SingleTickerProviderStat
final success = await EdgeDetection.detectEdge(file.path); final success = await EdgeDetection.detectEdge(file.path);
if (!success) { if (!success) {
if (kDebugMode) { 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; return;
} }
@@ -197,7 +200,9 @@ class _ScannerPageState extends State<ScannerPage> with SingleTickerProviderStat
if ((uploadResult?.success ?? false) && uploadResult?.taskId != null) { if ((uploadResult?.success ?? false) && uploadResult?.taskId != null) {
// For paperless version older than 1.11.3, task id will always be null! // For paperless version older than 1.11.3, task id will always be null!
context.read<DocumentScannerCubit>().reset(); context.read<DocumentScannerCubit>().reset();
context.read<TaskStatusCubit>().listenToTaskChanges(uploadResult!.taskId!); context
.read<TaskStatusCubit>()
.listenToTaskChanges(uploadResult!.taskId!);
} }
} }

View File

@@ -11,7 +11,8 @@ import 'package:paperless_mobile/features/settings/model/view_type.dart';
part 'document_search_cubit.g.dart'; part 'document_search_cubit.g.dart';
part 'document_search_state.dart'; part 'document_search_state.dart';
class DocumentSearchCubit extends Cubit<DocumentSearchState> with DocumentPagingBlocMixin { class DocumentSearchCubit extends Cubit<DocumentSearchState>
with DocumentPagingBlocMixin {
@override @override
final PaperlessDocumentsApi api; final PaperlessDocumentsApi api;
@@ -23,7 +24,8 @@ class DocumentSearchCubit extends Cubit<DocumentSearchState> with DocumentPaging
this.api, this.api,
this.notifier, this.notifier,
this._userAppState, this._userAppState,
) : super(DocumentSearchState(searchHistory: _userAppState.documentSearchHistory)) { ) : super(DocumentSearchState(
searchHistory: _userAppState.documentSearchHistory)) {
notifier.addListener( notifier.addListener(
this, this,
onDeleted: remove, onDeleted: remove,
@@ -46,7 +48,8 @@ class DocumentSearchCubit extends Cubit<DocumentSearchState> with DocumentPaging
state.copyWith( state.copyWith(
searchHistory: [ searchHistory: [
query, 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) { void removeHistoryEntry(String entry) {
emit( emit(
state.copyWith( state.copyWith(
searchHistory: state.searchHistory.whereNot((element) => element == entry).toList(), searchHistory: state.searchHistory
.whereNot((element) => element == entry)
.toList(),
), ),
); );
_userAppState _userAppState

View File

@@ -65,7 +65,10 @@ class _DocumentSearchBarState extends State<DocumentSearchBar> {
Flexible( Flexible(
child: Text( child: Text(
S.of(context)!.searchDocuments, S.of(context)!.searchDocuments,
style: Theme.of(context).textTheme.bodyLarge?.copyWith( style: Theme.of(context)
.textTheme
.bodyLarge
?.copyWith(
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
color: Theme.of(context).hintColor, color: Theme.of(context).hintColor,
), ),
@@ -112,7 +115,9 @@ class _DocumentSearchBarState extends State<DocumentSearchBar> {
icon: GlobalSettingsBuilder( icon: GlobalSettingsBuilder(
builder: (context, settings) { builder: (context, settings) {
return ValueListenableBuilder( return ValueListenableBuilder(
valueListenable: Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount).listenable(), valueListenable:
Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount)
.listenable(),
builder: (context, box, _) { builder: (context, box, _) {
final account = box.get(settings.currentLoggedInUser!)!; final account = box.get(settings.currentLoggedInUser!)!;
return UserAvatar(account: account); return UserAvatar(account: account);

View File

@@ -1,9 +1,11 @@
import 'dart:async'; import 'dart:async';
import 'package:bloc/bloc.dart'; import 'package:bloc/bloc.dart';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:paperless_api/paperless_api.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'; import 'package:paperless_mobile/core/repository/label_repository.dart';
part 'document_upload_state.dart'; part 'document_upload_state.dart';
@@ -12,9 +14,13 @@ class DocumentUploadCubit extends Cubit<DocumentUploadState> {
final PaperlessDocumentsApi _documentApi; final PaperlessDocumentsApi _documentApi;
final LabelRepository _labelRepository; final LabelRepository _labelRepository;
final Connectivity _connectivity;
DocumentUploadCubit(this._labelRepository, this._documentApi) DocumentUploadCubit(
: super(const DocumentUploadState()) { this._labelRepository,
this._documentApi,
this._connectivity,
) : super(const DocumentUploadState()) {
_labelRepository.addListener( _labelRepository.addListener(
this, this,
onChanged: (labels) { onChanged: (labels) {
@@ -31,6 +37,7 @@ class DocumentUploadCubit extends Cubit<DocumentUploadState> {
Uint8List bytes, { Uint8List bytes, {
required String filename, required String filename,
required String title, required String title,
required String userId,
int? documentType, int? documentType,
int? correspondent, int? correspondent,
Iterable<int> tags = const [], Iterable<int> tags = const [],

View File

@@ -3,10 +3,13 @@ import 'dart:typed_data';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_form_builder/flutter_form_builder.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/date_symbol_data_local.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:paperless_api/paperless_api.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/database/tables/local_user_account.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart'; import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/type/types.dart'; import 'package:paperless_mobile/core/type/types.dart';
@@ -42,10 +45,12 @@ class DocumentUploadPreparationPage extends StatefulWidget {
}) : super(key: key); }) : super(key: key);
@override @override
State<DocumentUploadPreparationPage> createState() => _DocumentUploadPreparationPageState(); State<DocumentUploadPreparationPage> createState() =>
_DocumentUploadPreparationPageState();
} }
class _DocumentUploadPreparationPageState extends State<DocumentUploadPreparationPage> { class _DocumentUploadPreparationPageState
extends State<DocumentUploadPreparationPage> {
static const fkFileName = "filename"; static const fkFileName = "filename";
static final fileNameDateFormat = DateFormat("yyyy_MM_ddTHH_mm_ss"); static final fileNameDateFormat = DateFormat("yyyy_MM_ddTHH_mm_ss");
@@ -72,7 +77,8 @@ class _DocumentUploadPreparationPageState extends State<DocumentUploadPreparatio
title: Text(S.of(context)!.prepareDocument), title: Text(S.of(context)!.prepareDocument),
bottom: _isUploadLoading bottom: _isUploadLoading
? const PreferredSize( ? const PreferredSize(
child: LinearProgressIndicator(), preferredSize: Size.fromHeight(4.0)) child: LinearProgressIndicator(),
preferredSize: Size.fromHeight(4.0))
: null, : null,
), ),
floatingActionButton: Visibility( floatingActionButton: Visibility(
@@ -93,7 +99,8 @@ class _DocumentUploadPreparationPageState extends State<DocumentUploadPreparatio
FormBuilderTextField( FormBuilderTextField(
autovalidateMode: AutovalidateMode.always, autovalidateMode: AutovalidateMode.always,
name: DocumentModel.titleKey, name: DocumentModel.titleKey,
initialValue: widget.title ?? "scan_${fileNameDateFormat.format(_now)}", initialValue:
widget.title ?? "scan_${fileNameDateFormat.format(_now)}",
validator: (value) { validator: (value) {
if (value?.trim().isEmpty ?? true) { if (value?.trim().isEmpty ?? true) {
return S.of(context)!.thisFieldIsRequired; return S.of(context)!.thisFieldIsRequired;
@@ -105,18 +112,22 @@ class _DocumentUploadPreparationPageState extends State<DocumentUploadPreparatio
suffixIcon: IconButton( suffixIcon: IconButton(
icon: const Icon(Icons.close), icon: const Icon(Icons.close),
onPressed: () { onPressed: () {
_formKey.currentState?.fields[DocumentModel.titleKey]?.didChange(""); _formKey.currentState?.fields[DocumentModel.titleKey]
?.didChange("");
if (_syncTitleAndFilename) { if (_syncTitleAndFilename) {
_formKey.currentState?.fields[fkFileName]?.didChange(""); _formKey.currentState?.fields[fkFileName]
?.didChange("");
} }
}, },
), ),
errorText: _errors[DocumentModel.titleKey], errorText: _errors[DocumentModel.titleKey],
), ),
onChanged: (value) { onChanged: (value) {
final String transformedValue = _formatFilename(value ?? ''); final String transformedValue =
_formatFilename(value ?? '');
if (_syncTitleAndFilename) { 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, suffixText: widget.fileExtension,
suffixIcon: IconButton( suffixIcon: IconButton(
icon: const Icon(Icons.clear), 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 // Synchronize title and filename
SwitchListTile( SwitchListTile(
@@ -144,10 +157,13 @@ class _DocumentUploadPreparationPageState extends State<DocumentUploadPreparatio
() => _syncTitleAndFilename = value, () => _syncTitleAndFilename = value,
); );
if (_syncTitleAndFilename) { if (_syncTitleAndFilename) {
final String transformedValue = _formatFilename( final String transformedValue = _formatFilename(_formKey
_formKey.currentState?.fields[DocumentModel.titleKey]?.value as String); .currentState
?.fields[DocumentModel.titleKey]
?.value as String);
if (_syncTitleAndFilename) { if (_syncTitleAndFilename) {
_formKey.currentState?.fields[fkFileName]?.didChange(transformedValue); _formKey.currentState?.fields[fkFileName]
?.didChange(transformedValue);
} }
} }
}, },
@@ -172,7 +188,8 @@ class _DocumentUploadPreparationPageState extends State<DocumentUploadPreparatio
? IconButton( ? IconButton(
icon: const Icon(Icons.close), icon: const Icon(Icons.close),
onPressed: () { onPressed: () {
_formKey.currentState!.fields[DocumentModel.createdKey] _formKey.currentState!
.fields[DocumentModel.createdKey]
?.didChange(null); ?.didChange(null);
}, },
) )
@@ -183,7 +200,8 @@ class _DocumentUploadPreparationPageState extends State<DocumentUploadPreparatio
LabelFormField<Correspondent>( LabelFormField<Correspondent>(
showAnyAssignedOption: false, showAnyAssignedOption: false,
showNotAssignedOption: false, showNotAssignedOption: false,
addLabelPageBuilder: (initialName) => RepositoryProvider.value( addLabelPageBuilder: (initialName) =>
RepositoryProvider.value(
value: context.read<LabelRepository>(), value: context.read<LabelRepository>(),
child: AddCorrespondentPage(initialName: initialName), child: AddCorrespondentPage(initialName: initialName),
), ),
@@ -193,7 +211,8 @@ class _DocumentUploadPreparationPageState extends State<DocumentUploadPreparatio
options: state.correspondents, options: state.correspondents,
prefixIcon: const Icon(Icons.person_outline), prefixIcon: const Icon(Icons.person_outline),
allowSelectUnassigned: true, allowSelectUnassigned: true,
canCreateNewLabel: LocalUserAccount.current.paperlessUser.hasPermission( canCreateNewLabel:
LocalUserAccount.current.paperlessUser.hasPermission(
PermissionAction.add, PermissionAction.add,
PermissionTarget.correspondent, PermissionTarget.correspondent,
), ),
@@ -202,7 +221,8 @@ class _DocumentUploadPreparationPageState extends State<DocumentUploadPreparatio
LabelFormField<DocumentType>( LabelFormField<DocumentType>(
showAnyAssignedOption: false, showAnyAssignedOption: false,
showNotAssignedOption: false, showNotAssignedOption: false,
addLabelPageBuilder: (initialName) => RepositoryProvider.value( addLabelPageBuilder: (initialName) =>
RepositoryProvider.value(
value: context.read<LabelRepository>(), value: context.read<LabelRepository>(),
child: AddDocumentTypePage(initialName: initialName), child: AddDocumentTypePage(initialName: initialName),
), ),
@@ -212,7 +232,8 @@ class _DocumentUploadPreparationPageState extends State<DocumentUploadPreparatio
options: state.documentTypes, options: state.documentTypes,
prefixIcon: const Icon(Icons.description_outlined), prefixIcon: const Icon(Icons.description_outlined),
allowSelectUnassigned: true, allowSelectUnassigned: true,
canCreateNewLabel: LocalUserAccount.current.paperlessUser.hasPermission( canCreateNewLabel:
LocalUserAccount.current.paperlessUser.hasPermission(
PermissionAction.add, PermissionAction.add,
PermissionTarget.documentType, PermissionTarget.documentType,
), ),
@@ -252,7 +273,8 @@ class _DocumentUploadPreparationPageState extends State<DocumentUploadPreparatio
final tags = (fv[DocumentModel.tagsKey] as TagsQuery?) final tags = (fv[DocumentModel.tagsKey] as TagsQuery?)
?.whenOrNull(ids: (include, exclude) => include) ?? ?.whenOrNull(ids: (include, exclude) => include) ??
[]; [];
final correspondent = (fv[DocumentModel.correspondentKey] as IdQueryParameter?) final correspondent =
(fv[DocumentModel.correspondentKey] as IdQueryParameter?)
?.whenOrNull(fromId: (id) => id); ?.whenOrNull(fromId: (id) => id);
final asn = fv[DocumentModel.asnKey] as int?; final asn = fv[DocumentModel.asnKey] as int?;
final taskId = await cubit.upload( final taskId = await cubit.upload(
@@ -261,6 +283,9 @@ class _DocumentUploadPreparationPageState extends State<DocumentUploadPreparatio
_formKey.currentState?.value[fkFileName], _formKey.currentState?.value[fkFileName],
widget.fileExtension, widget.fileExtension,
), ),
userId: Hive.box<GlobalSettings>(HiveBoxes.globalSettings)
.getValue()!
.currentLoggedInUser!,
title: title, title: title,
documentType: docType, documentType: docType,
correspondent: correspondent, correspondent: correspondent,
@@ -282,7 +307,8 @@ class _DocumentUploadPreparationPageState extends State<DocumentUploadPreparatio
setState(() => _errors = errors); setState(() => _errors = errors);
} catch (unknownError, stackTrace) { } catch (unknownError, stackTrace) {
debugPrint(unknownError.toString()); debugPrint(unknownError.toString());
showErrorMessage(context, const PaperlessServerException.unknown(), stackTrace); showErrorMessage(
context, const PaperlessServerException.unknown(), stackTrace);
} finally { } finally {
setState(() { setState(() {
_isUploadLoading = false; _isUploadLoading = false;

View File

@@ -14,7 +14,8 @@ import 'package:paperless_mobile/features/settings/model/view_type.dart';
part 'documents_cubit.g.dart'; part 'documents_cubit.g.dart';
part 'documents_state.dart'; part 'documents_state.dart';
class DocumentsCubit extends HydratedCubit<DocumentsState> with DocumentPagingBlocMixin { class DocumentsCubit extends HydratedCubit<DocumentsState>
with DocumentPagingBlocMixin {
@override @override
final PaperlessDocumentsApi api; final PaperlessDocumentsApi api;
@@ -40,7 +41,9 @@ class DocumentsCubit extends HydratedCubit<DocumentsState> with DocumentPagingBl
replace(document); replace(document);
emit( emit(
state.copyWith( 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); remove(document);
emit( emit(
state.copyWith( 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)) { if (state.selectedIds.contains(model.id)) {
emit( emit(
state.copyWith( state.copyWith(
selection: state.selection.where((element) => element.id != model.id).toList(), selection: state.selection
.where((element) => element.id != model.id)
.toList(),
), ),
); );
} else { } else {

View File

@@ -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); Map<String, dynamic> toJson() => _$DocumentsStateToJson(this);
} }

View File

@@ -22,7 +22,8 @@ class _DocumentViewState extends State<DocumentView> {
@override @override
Widget build(BuildContext context) { 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 canGoToNextPage = isInitialized && _currentPage! + 1 < _totalPages!;
final canGoToPreviousPage = isInitialized && _currentPage! > 0; final canGoToPreviousPage = isInitialized && _currentPage! > 0;
return Scaffold( return Scaffold(

View File

@@ -161,9 +161,7 @@ class _DocumentsPageState extends State<DocumentsPage>
} }
return true; return true;
}, },
child: Stack( child: NestedScrollView(
children: [
NestedScrollView(
floatHeaderSlivers: true, floatHeaderSlivers: true,
headerSliverBuilder: (context, innerBoxIsScrolled) => [ headerSliverBuilder: (context, innerBoxIsScrolled) => [
SliverOverlapAbsorber( SliverOverlapAbsorber(
@@ -217,8 +215,7 @@ class _DocumentsPageState extends State<DocumentsPage>
return true; return true;
} }
final desiredTab = final desiredTab =
(metrics.pixels / metrics.maxScrollExtent) (metrics.pixels / metrics.maxScrollExtent).round();
.round();
if (metrics.axis == Axis.horizontal && if (metrics.axis == Axis.horizontal &&
_currentTab != desiredTab) { _currentTab != desiredTab) {
setState(() => _currentTab = desiredTab); setState(() => _currentTab = desiredTab);
@@ -255,8 +252,6 @@ class _DocumentsPageState extends State<DocumentsPage>
), ),
), ),
), ),
],
),
), ),
), ),
); );

View File

@@ -43,7 +43,9 @@ class DocumentPreview extends StatelessWidget {
fit: fit, fit: fit,
alignment: alignment, alignment: alignment,
cacheKey: "thumb_${document.id}", cacheKey: "thumb_${document.id}",
imageUrl: context.read<PaperlessDocumentsApi>().getThumbnailUrl(document.id), imageUrl: context
.read<PaperlessDocumentsApi>()
.getThumbnailUrl(document.id),
errorWidget: (ctxt, msg, __) => Text(msg), errorWidget: (ctxt, msg, __) => Text(msg),
placeholder: (context, value) => Shimmer.fromColors( placeholder: (context, value) => Shimmer.fromColors(
baseColor: Colors.grey[300]!, baseColor: Colors.grey[300]!,

View File

@@ -42,8 +42,9 @@ class DocumentDetailedItem extends DocumentItem {
padding.bottom - padding.bottom -
kBottomNavigationBarHeight - kBottomNavigationBarHeight -
kToolbarHeight; kToolbarHeight;
final maxHeight = final maxHeight = highlights != null
highlights != null ? min(600.0, availableHeight) : min(500.0, availableHeight); ? min(600.0, availableHeight)
: min(500.0, availableHeight);
return Card( return Card(
color: isSelected ? Theme.of(context).colorScheme.inversePrimary : null, color: isSelected ? Theme.of(context).colorScheme.inversePrimary : null,
child: InkWell( child: InkWell(
@@ -114,8 +115,10 @@ class DocumentDetailedItem extends DocumentItem {
textStyle: Theme.of(context).textTheme.titleSmall?.apply( textStyle: Theme.of(context).textTheme.titleSmall?.apply(
color: Theme.of(context).colorScheme.onSurfaceVariant, color: Theme.of(context).colorScheme.onSurfaceVariant,
), ),
correspondent: correspondent: context
context.watch<LabelRepository>().state.correspondents[document.correspondent], .watch<LabelRepository>()
.state
.correspondents[document.correspondent],
), ),
], ],
).paddedLTRB(8, 0, 8, 4), ).paddedLTRB(8, 0, 8, 4),
@@ -130,8 +133,10 @@ class DocumentDetailedItem extends DocumentItem {
textStyle: Theme.of(context).textTheme.titleSmall?.apply( textStyle: Theme.of(context).textTheme.titleSmall?.apply(
color: Theme.of(context).colorScheme.onSurfaceVariant, color: Theme.of(context).colorScheme.onSurfaceVariant,
), ),
documentType: documentType: context
context.watch<LabelRepository>().state.documentTypes[document.documentType], .watch<LabelRepository>()
.state
.documentTypes[document.documentType],
), ),
], ],
).paddedLTRB(8, 0, 8, 4), ).paddedLTRB(8, 0, 8, 4),

View File

@@ -30,8 +30,9 @@ class DocumentGridItem extends DocumentItem {
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: Card( child: Card(
elevation: 1.0, elevation: 1.0,
color: color: isSelected
isSelected ? Theme.of(context).colorScheme.inversePrimary : Theme.of(context).cardColor, ? Theme.of(context).colorScheme.inversePrimary
: Theme.of(context).cardColor,
child: InkWell( child: InkWell(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
onTap: _onTap, onTap: _onTap,
@@ -74,7 +75,8 @@ class DocumentGridItem extends DocumentItem {
const Spacer(), const Spacer(),
TagsWidget( TagsWidget(
tags: document.tags tags: document.tags
.map((e) => context.watch<LabelRepository>().state.tags[e]!) .map((e) =>
context.watch<LabelRepository>().state.tags[e]!)
.toList(), .toList(),
isMultiLine: false, isMultiLine: false,
onTagSelected: onTagSelected, onTagSelected: onTagSelected,

View File

@@ -22,14 +22,17 @@ class DocumentFilterForm extends StatefulWidget {
formKey.currentState?.save(); formKey.currentState?.save();
final v = formKey.currentState!.value; final v = formKey.currentState!.value;
return DocumentFilter( return DocumentFilter(
correspondent: v[DocumentFilterForm.fkCorrespondent] as IdQueryParameter? ?? correspondent:
v[DocumentFilterForm.fkCorrespondent] as IdQueryParameter? ??
DocumentFilter.initial.correspondent, DocumentFilter.initial.correspondent,
documentType: v[DocumentFilterForm.fkDocumentType] as IdQueryParameter? ?? documentType: v[DocumentFilterForm.fkDocumentType] as IdQueryParameter? ??
DocumentFilter.initial.documentType, DocumentFilter.initial.documentType,
storagePath: v[DocumentFilterForm.fkStoragePath] as IdQueryParameter? ?? storagePath: v[DocumentFilterForm.fkStoragePath] as IdQueryParameter? ??
DocumentFilter.initial.storagePath, DocumentFilter.initial.storagePath,
tags: v[DocumentModel.tagsKey] as TagsQuery? ?? DocumentFilter.initial.tags, tags:
query: v[DocumentFilterForm.fkQuery] as TextQuery? ?? DocumentFilter.initial.query, v[DocumentModel.tagsKey] as TagsQuery? ?? DocumentFilter.initial.tags,
query: v[DocumentFilterForm.fkQuery] as TextQuery? ??
DocumentFilter.initial.query,
created: (v[DocumentFilterForm.fkCreatedAt] as DateRangeQuery), created: (v[DocumentFilterForm.fkCreatedAt] as DateRangeQuery),
added: (v[DocumentFilterForm.fkAddedAt] as DateRangeQuery), added: (v[DocumentFilterForm.fkAddedAt] as DateRangeQuery),
asnQuery: initialFilter.asnQuery, asnQuery: initialFilter.asnQuery,
@@ -134,12 +137,15 @@ class _DocumentFilterFormState extends State<DocumentFilterForm> {
} }
void _checkQueryConstraints() { void _checkQueryConstraints() {
final filter = DocumentFilterForm.assembleFilter(widget.formKey, widget.initialFilter); final filter =
DocumentFilterForm.assembleFilter(widget.formKey, widget.initialFilter);
if (filter.forceExtendedQuery) { if (filter.forceExtendedQuery) {
setState(() => _allowOnlyExtendedQuery = true); setState(() => _allowOnlyExtendedQuery = true);
final queryField = widget.formKey.currentState?.fields[DocumentFilterForm.fkQuery]; final queryField =
widget.formKey.currentState?.fields[DocumentFilterForm.fkQuery];
queryField?.didChange( queryField?.didChange(
(queryField.value as TextQuery?)?.copyWith(queryType: QueryType.extended), (queryField.value as TextQuery?)
?.copyWith(queryType: QueryType.extended),
); );
} else { } else {
setState(() => _allowOnlyExtendedQuery = false); setState(() => _allowOnlyExtendedQuery = false);

View File

@@ -27,10 +27,12 @@ class SortFieldSelectionBottomSheet extends StatefulWidget {
}); });
@override @override
State<SortFieldSelectionBottomSheet> createState() => _SortFieldSelectionBottomSheetState(); State<SortFieldSelectionBottomSheet> createState() =>
_SortFieldSelectionBottomSheetState();
} }
class _SortFieldSelectionBottomSheetState extends State<SortFieldSelectionBottomSheet> { class _SortFieldSelectionBottomSheetState
extends State<SortFieldSelectionBottomSheet> {
late SortField? _currentSortField; late SortField? _currentSortField;
late SortOrder _currentSortOrder; late SortOrder _currentSortOrder;

View File

@@ -33,12 +33,15 @@ class DocumentSelectionSliverAppBar extends StatelessWidget {
onPressed: () async { onPressed: () async {
final shouldDelete = await showDialog<bool>( final shouldDelete = await showDialog<bool>(
context: context, context: context,
builder: (context) => BulkDeleteConfirmationDialog(state: state), builder: (context) =>
BulkDeleteConfirmationDialog(state: state),
) ?? ) ??
false; false;
if (shouldDelete) { if (shouldDelete) {
try { try {
await context.read<DocumentsCubit>().bulkDelete(state.selection); await context
.read<DocumentsCubit>()
.bulkDelete(state.selection);
showSnackBar( showSnackBar(
context, context,
S.of(context)!.documentsSuccessfullyDeleted, S.of(context)!.documentsSuccessfullyDeleted,
@@ -62,21 +65,24 @@ class DocumentSelectionSliverAppBar extends StatelessWidget {
label: Text(S.of(context)!.correspondent), label: Text(S.of(context)!.correspondent),
avatar: const Icon(Icons.edit), avatar: const Icon(Icons.edit),
onPressed: () { onPressed: () {
pushBulkEditCorrespondentRoute(context, selection: state.selection); pushBulkEditCorrespondentRoute(context,
selection: state.selection);
}, },
).paddedOnly(left: 8, right: 4), ).paddedOnly(left: 8, right: 4),
ActionChip( ActionChip(
label: Text(S.of(context)!.documentType), label: Text(S.of(context)!.documentType),
avatar: const Icon(Icons.edit), avatar: const Icon(Icons.edit),
onPressed: () async { onPressed: () async {
pushBulkEditDocumentTypeRoute(context, selection: state.selection); pushBulkEditDocumentTypeRoute(context,
selection: state.selection);
}, },
).paddedOnly(left: 8, right: 4), ).paddedOnly(left: 8, right: 4),
ActionChip( ActionChip(
label: Text(S.of(context)!.storagePath), label: Text(S.of(context)!.storagePath),
avatar: const Icon(Icons.edit), avatar: const Icon(Icons.edit),
onPressed: () async { onPressed: () async {
pushBulkEditStoragePathRoute(context, selection: state.selection); pushBulkEditStoragePathRoute(context,
selection: state.selection);
}, },
).paddedOnly(left: 8, right: 4), ).paddedOnly(left: 8, right: 4),
_buildBulkEditTagsChip(context).paddedOnly(left: 4, right: 4), _buildBulkEditTagsChip(context).paddedOnly(left: 4, right: 4),

View File

@@ -50,7 +50,9 @@ class SortDocumentsButton extends StatelessWidget {
initialSortField: state.filter.sortField, initialSortField: state.filter.sortField,
initialSortOrder: state.filter.sortOrder, initialSortOrder: state.filter.sortOrder,
onSubmit: (field, order) { onSubmit: (field, order) {
return context.read<DocumentsCubit>().updateCurrentFilter( return context
.read<DocumentsCubit>()
.updateCurrentFilter(
(filter) => filter.copyWith( (filter) => filter.copyWith(
sortField: field, sortField: field,
sortOrder: order, sortOrder: order,

View File

@@ -20,8 +20,10 @@ class EditCorrespondentPage extends StatelessWidget {
return EditLabelPage<Correspondent>( return EditLabelPage<Correspondent>(
label: correspondent, label: correspondent,
fromJsonT: Correspondent.fromJson, fromJsonT: Correspondent.fromJson,
onSubmit: (context, label) => context.read<EditLabelCubit>().replaceCorrespondent(label), onSubmit: (context, label) =>
onDelete: (context, label) => context.read<EditLabelCubit>().removeCorrespondent(label), context.read<EditLabelCubit>().replaceCorrespondent(label),
onDelete: (context, label) =>
context.read<EditLabelCubit>().removeCorrespondent(label),
canDelete: LocalUserAccount.current.paperlessUser.hasPermission( canDelete: LocalUserAccount.current.paperlessUser.hasPermission(
PermissionAction.delete, PermissionAction.delete,
PermissionTarget.correspondent, PermissionTarget.correspondent,

View File

@@ -18,8 +18,10 @@ class EditDocumentTypePage extends StatelessWidget {
child: EditLabelPage<DocumentType>( child: EditLabelPage<DocumentType>(
label: documentType, label: documentType,
fromJsonT: DocumentType.fromJson, fromJsonT: DocumentType.fromJson,
onSubmit: (context, label) => context.read<EditLabelCubit>().replaceDocumentType(label), onSubmit: (context, label) =>
onDelete: (context, label) => context.read<EditLabelCubit>().removeDocumentType(label), context.read<EditLabelCubit>().replaceDocumentType(label),
onDelete: (context, label) =>
context.read<EditLabelCubit>().removeDocumentType(label),
canDelete: LocalUserAccount.current.paperlessUser.hasPermission( canDelete: LocalUserAccount.current.paperlessUser.hasPermission(
PermissionAction.delete, PermissionAction.delete,
PermissionTarget.documentType, PermissionTarget.documentType,

View File

@@ -19,8 +19,10 @@ class EditStoragePathPage extends StatelessWidget {
child: EditLabelPage<StoragePath>( child: EditLabelPage<StoragePath>(
label: storagePath, label: storagePath,
fromJsonT: StoragePath.fromJson, fromJsonT: StoragePath.fromJson,
onSubmit: (context, label) => context.read<EditLabelCubit>().replaceStoragePath(label), onSubmit: (context, label) =>
onDelete: (context, label) => context.read<EditLabelCubit>().removeStoragePath(label), context.read<EditLabelCubit>().replaceStoragePath(label),
onDelete: (context, label) =>
context.read<EditLabelCubit>().removeStoragePath(label),
canDelete: LocalUserAccount.current.paperlessUser.hasPermission( canDelete: LocalUserAccount.current.paperlessUser.hasPermission(
PermissionAction.delete, PermissionAction.delete,
PermissionTarget.storagePath, PermissionTarget.storagePath,

View File

@@ -22,8 +22,10 @@ class EditTagPage extends StatelessWidget {
child: EditLabelPage<Tag>( child: EditLabelPage<Tag>(
label: tag, label: tag,
fromJsonT: Tag.fromJson, fromJsonT: Tag.fromJson,
onSubmit: (context, label) => context.read<EditLabelCubit>().replaceTag(label), onSubmit: (context, label) =>
onDelete: (context, label) => context.read<EditLabelCubit>().removeTag(label), context.read<EditLabelCubit>().replaceTag(label),
onDelete: (context, label) =>
context.read<EditLabelCubit>().removeTag(label),
canDelete: LocalUserAccount.current.paperlessUser.hasPermission( canDelete: LocalUserAccount.current.paperlessUser.hasPermission(
PermissionAction.delete, PermissionAction.delete,
PermissionTarget.tag, PermissionTarget.tag,

View File

@@ -6,8 +6,11 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:fluttertoast/fluttertoast.dart'; import 'package:fluttertoast/fluttertoast.dart';
import 'package:hive/hive.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/bloc/connectivity_cubit.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/database/tables/local_user_account.dart';
import 'package:paperless_mobile/core/global/constants.dart'; import 'package:paperless_mobile/core/global/constants.dart';
import 'package:paperless_mobile/core/navigation/push_routes.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 { class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
int _currentIndex = 0; int _currentIndex = 0;
late Timer _inboxTimer; late Timer _inboxTimer;
late final StreamSubscription _shareMediaSubscription;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
WidgetsBinding.instance.addObserver(this); WidgetsBinding.instance.addObserver(this);
_listenToInboxChanges(); _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) { WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
_listenForReceivedFiles(); _listenForReceivedFiles();
}); });
@@ -59,7 +73,7 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
} }
void _listenToInboxChanges() { void _listenToInboxChanges() {
_inboxTimer = Timer.periodic(const Duration(seconds: 10), (timer) { _inboxTimer = Timer.periodic(const Duration(seconds: 60), (timer) {
if (!mounted) { if (!mounted) {
timer.cancel(); timer.cancel();
} else { } else {
@@ -93,17 +107,21 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
void dispose() { void dispose() {
WidgetsBinding.instance.removeObserver(this); WidgetsBinding.instance.removeObserver(this);
_inboxTimer.cancel(); _inboxTimer.cancel();
_shareMediaSubscription.cancel();
super.dispose(); super.dispose();
} }
void _listenForReceivedFiles() async { void _listenForReceivedFiles() async {
if (ShareIntentQueue.instance.hasUnhandledFiles) { final currentUser = Hive.box<GlobalSettings>(HiveBoxes.globalSettings)
await _handleReceivedFile(ShareIntentQueue.instance.pop()!); .getValue()!
.currentLoggedInUser!;
if (ShareIntentQueue.instance.userHasUnhandlesFiles(currentUser)) {
await _handleReceivedFile(ShareIntentQueue.instance.pop(currentUser)!);
} }
ShareIntentQueue.instance.addListener(() async { ShareIntentQueue.instance.addListener(() async {
final queue = ShareIntentQueue.instance; final queue = ShareIntentQueue.instance;
while (queue.hasUnhandledFiles) { while (queue.userHasUnhandlesFiles(currentUser)) {
final file = queue.pop()!; final file = queue.pop(currentUser)!;
await _handleReceivedFile(file); 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; SharedMediaFile mediaFile;
if (Platform.isIOS) { if (Platform.isIOS) {
// Workaround for file not found on iOS: https://stackoverflow.com/a/72813212 // Workaround for file not found on iOS: https://stackoverflow.com/a/72813212
@@ -128,7 +146,7 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
} else { } else {
mediaFile = file; mediaFile = file;
} }
debugPrint("Consuming media file: ${mediaFile.path}");
if (!_isFileTypeSupported(mediaFile)) { if (!_isFileTypeSupported(mediaFile)) {
Fluttertoast.showToast( Fluttertoast.showToast(
msg: translateError(context, ErrorCode.unsupportedFileFormat), msg: translateError(context, ErrorCode.unsupportedFileFormat),
@@ -149,7 +167,7 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
} }
final fileDescription = FileDescription.fromPath(mediaFile.path); final fileDescription = FileDescription.fromPath(mediaFile.path);
if (await File(mediaFile.path).exists()) { if (await File(mediaFile.path).exists()) {
final bytes = File(mediaFile.path).readAsBytesSync(); final bytes = await File(mediaFile.path).readAsBytes();
final result = await pushDocumentUploadPreparationPage( final result = await pushDocumentUploadPreparationPage(
context, context,
bytes: bytes, bytes: bytes,

View File

@@ -53,36 +53,42 @@ class HomeRoute extends StatelessWidget {
Config( Config(
// Isolated cache per user. // Isolated cache per user.
localUserId, localUserId,
fileService: DioFileService(context.read<SessionManager>().client), fileService:
DioFileService(context.read<SessionManager>().client),
), ),
), ),
), ),
ProxyProvider<SessionManager, PaperlessDocumentsApi>( ProxyProvider<SessionManager, PaperlessDocumentsApi>(
update: (context, value, previous) => paperlessProviderFactory.createDocumentsApi( update: (context, value, previous) =>
paperlessProviderFactory.createDocumentsApi(
value.client, value.client,
apiVersion: paperlessApiVersion, apiVersion: paperlessApiVersion,
), ),
), ),
ProxyProvider<SessionManager, PaperlessLabelsApi>( ProxyProvider<SessionManager, PaperlessLabelsApi>(
update: (context, value, previous) => paperlessProviderFactory.createLabelsApi( update: (context, value, previous) =>
paperlessProviderFactory.createLabelsApi(
value.client, value.client,
apiVersion: paperlessApiVersion, apiVersion: paperlessApiVersion,
), ),
), ),
ProxyProvider<SessionManager, PaperlessSavedViewsApi>( ProxyProvider<SessionManager, PaperlessSavedViewsApi>(
update: (context, value, previous) => paperlessProviderFactory.createSavedViewsApi( update: (context, value, previous) =>
paperlessProviderFactory.createSavedViewsApi(
value.client, value.client,
apiVersion: paperlessApiVersion, apiVersion: paperlessApiVersion,
), ),
), ),
ProxyProvider<SessionManager, PaperlessServerStatsApi>( ProxyProvider<SessionManager, PaperlessServerStatsApi>(
update: (context, value, previous) => paperlessProviderFactory.createServerStatsApi( update: (context, value, previous) =>
paperlessProviderFactory.createServerStatsApi(
value.client, value.client,
apiVersion: paperlessApiVersion, apiVersion: paperlessApiVersion,
), ),
), ),
ProxyProvider<SessionManager, PaperlessTasksApi>( ProxyProvider<SessionManager, PaperlessTasksApi>(
update: (context, value, previous) => paperlessProviderFactory.createTasksApi( update: (context, value, previous) =>
paperlessProviderFactory.createTasksApi(
value.client, value.client,
apiVersion: paperlessApiVersion, apiVersion: paperlessApiVersion,
), ),
@@ -98,29 +104,41 @@ class HomeRoute extends StatelessWidget {
return MultiProvider( return MultiProvider(
providers: [ providers: [
ProxyProvider<PaperlessLabelsApi, LabelRepository>( ProxyProvider<PaperlessLabelsApi, LabelRepository>(
update: (context, value, previous) => LabelRepository(value)..initialize(), update: (context, value, previous) =>
LabelRepository(value)..initialize(),
), ),
ProxyProvider<PaperlessSavedViewsApi, SavedViewRepository>( ProxyProvider<PaperlessSavedViewsApi, SavedViewRepository>(
update: (context, value, previous) => SavedViewRepository(value)..initialize(), update: (context, value, previous) =>
SavedViewRepository(value)..initialize(),
), ),
], ],
builder: (context, child) { builder: (context, child) {
return MultiProvider( return MultiProvider(
providers: [ providers: [
ProxyProvider3<PaperlessDocumentsApi, DocumentChangedNotifier, LabelRepository, ProxyProvider3<
PaperlessDocumentsApi,
DocumentChangedNotifier,
LabelRepository,
DocumentsCubit>( DocumentsCubit>(
update: (context, docApi, notifier, labelRepo, previous) => DocumentsCubit( update:
(context, docApi, notifier, labelRepo, previous) =>
DocumentsCubit(
docApi, docApi,
notifier, notifier,
labelRepo, labelRepo,
Hive.box<LocalUserAppState>(HiveBoxes.localUserAppState) Hive.box<LocalUserAppState>(HiveBoxes.localUserAppState)
.get(currentLocalUserId)!, .get(currentLocalUserId)!,
)..reload(), )..initialize(),
), ),
Provider(create: (context) => DocumentScannerCubit()), Provider(create: (context) => DocumentScannerCubit()),
ProxyProvider4<PaperlessDocumentsApi, PaperlessServerStatsApi, LabelRepository, ProxyProvider4<
DocumentChangedNotifier, InboxCubit>( PaperlessDocumentsApi,
update: (context, docApi, statsApi, labelRepo, notifier, previous) => PaperlessServerStatsApi,
LabelRepository,
DocumentChangedNotifier,
InboxCubit>(
update: (context, docApi, statsApi, labelRepo, notifier,
previous) =>
InboxCubit( InboxCubit(
docApi, docApi,
statsApi, statsApi,
@@ -129,19 +147,22 @@ class HomeRoute extends StatelessWidget {
)..initialize(), )..initialize(),
), ),
ProxyProvider<SavedViewRepository, SavedViewCubit>( ProxyProvider<SavedViewRepository, SavedViewCubit>(
update: (context, savedViewRepo, previous) => SavedViewCubit( update: (context, savedViewRepo, previous) =>
SavedViewCubit(
savedViewRepo, savedViewRepo,
)..initialize(), ),
), ),
ProxyProvider<LabelRepository, LabelCubit>( ProxyProvider<LabelRepository, LabelCubit>(
update: (context, value, previous) => LabelCubit(value), update: (context, value, previous) => LabelCubit(value),
), ),
ProxyProvider<PaperlessTasksApi, TaskStatusCubit>( ProxyProvider<PaperlessTasksApi, TaskStatusCubit>(
update: (context, value, previous) => TaskStatusCubit(value), update: (context, value, previous) =>
TaskStatusCubit(value),
), ),
if (paperlessApiVersion >= 3) if (paperlessApiVersion >= 3)
ProxyProvider<PaperlessUserApiV3, UserRepository>( ProxyProvider<PaperlessUserApiV3, UserRepository>(
update: (context, value, previous) => UserRepository(value)..initialize(), update: (context, value, previous) =>
UserRepository(value)..initialize(),
), ),
], ],
child: HomePage(paperlessApiVersion: paperlessApiVersion), child: HomePage(paperlessApiVersion: paperlessApiVersion),

View File

@@ -30,7 +30,9 @@ class VerifyIdentityPage extends StatelessWidget {
return Column( return Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Text(S.of(context)!.useTheConfiguredBiometricFactorToAuthenticate) Text(S
.of(context)!
.useTheConfiguredBiometricFactorToAuthenticate)
.paddedSymmetrically(horizontal: 16), .paddedSymmetrically(horizontal: 16),
const Icon( const Icon(
Icons.fingerprint, Icons.fingerprint,
@@ -52,7 +54,9 @@ class VerifyIdentityPage extends StatelessWidget {
), ),
), ),
ElevatedButton( ElevatedButton(
onPressed: () => context.read<AuthenticationCubit>().restoreSessionState(), onPressed: () => context
.read<AuthenticationCubit>()
.restoreSessionState(),
child: Text(S.of(context)!.verifyIdentity), child: Text(S.of(context)!.verifyIdentity),
), ),
], ],

View File

@@ -129,8 +129,9 @@ class _FullscreenTagsFormState extends State<FullscreenTagsForm> {
), ),
], ],
bottom: PreferredSize( bottom: PreferredSize(
preferredSize: preferredSize: !widget.allowOnlySelection
!widget.allowOnlySelection ? const Size.fromHeight(32) : const Size.fromHeight(1), ? const Size.fromHeight(32)
: const Size.fromHeight(1),
child: Column( child: Column(
children: [ children: [
Divider(color: theme.colorScheme.outline), Divider(color: theme.colorScheme.outline),
@@ -233,7 +234,8 @@ class _FullscreenTagsFormState extends State<FullscreenTagsForm> {
yield _buildNotAssignedOption(); 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) { if (matches.isEmpty && widget.allowCreation) {
yield Text(S.of(context)!.noItemsFound); yield Text(S.of(context)!.noItemsFound);
yield TextButton( yield TextButton(
@@ -299,7 +301,9 @@ class SelectableTagWidget extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ListTile( return ListTile(
title: Text(tag.name), 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( leading: CircleAvatar(
backgroundColor: tag.color, backgroundColor: tag.color,
child: (tag.isInboxTag) child: (tag.isInboxTag)

View File

@@ -32,8 +32,8 @@ class TagsFormField extends StatelessWidget {
initialValue: initialValue, initialValue: initialValue,
builder: (field) { builder: (field) {
final values = _generateOptions(context, field.value, field).toList(); final values = _generateOptions(context, field.value, field).toList();
final isEmpty = final isEmpty = (field.value is IdsTagsQuery &&
(field.value is IdsTagsQuery && (field.value as IdsTagsQuery).include.isEmpty) || (field.value as IdsTagsQuery).include.isEmpty) ||
field.value == null; field.value == null;
bool anyAssigned = field.value is AnyAssignedTagsQuery; bool anyAssigned = field.value is AnyAssignedTagsQuery;
return OpenContainer<TagsQuery>( return OpenContainer<TagsQuery>(
@@ -59,7 +59,8 @@ class TagsFormField extends StatelessWidget {
height: 32, height: 32,
child: ListView.separated( child: ListView.separated(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
separatorBuilder: (context, index) => const SizedBox(width: 4), separatorBuilder: (context, index) =>
const SizedBox(width: 4),
itemBuilder: (context, index) => values[index], itemBuilder: (context, index) => values[index],
itemCount: values.length, itemCount: values.length,
), ),
@@ -99,11 +100,14 @@ class TagsFormField extends StatelessWidget {
} else { } else {
final widgets = query.map( final widgets = query.map(
ids: (value) => [ ids: (value) => [
for (var inc in value.include) _buildTagIdQueryWidget(context, inc, field, false), for (var inc in value.include)
for (var exc in value.exclude) _buildTagIdQueryWidget(context, exc, field, true), _buildTagIdQueryWidget(context, inc, field, false),
for (var exc in value.exclude)
_buildTagIdQueryWidget(context, exc, field, true),
], ],
anyAssigned: (value) => [ 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)], notAssigned: (value) => [_buildNotAssignedTagWidget(context, field)],
); );
@@ -124,15 +128,19 @@ class TagsFormField extends StatelessWidget {
final tag = options[id]!; final tag = options[id]!;
return QueryTagChip( return QueryTagChip(
onDeleted: () => field.didChange(formValue.copyWith( onDeleted: () => field.didChange(formValue.copyWith(
include: formValue.include.whereNot((element) => element == id).toList(), include:
exclude: formValue.exclude.whereNot((element) => element == id).toList(), formValue.include.whereNot((element) => element == id).toList(),
exclude:
formValue.exclude.whereNot((element) => element == id).toList(),
)), )),
onSelected: allowExclude onSelected: allowExclude
? () { ? () {
if (formValue.include.contains(id)) { if (formValue.include.contains(id)) {
field.didChange( field.didChange(
formValue.copyWith( formValue.copyWith(
include: formValue.include.whereNot((element) => element == id).toList(), include: formValue.include
.whereNot((element) => element == id)
.toList(),
exclude: [...formValue.exclude, id], exclude: [...formValue.exclude, id],
), ),
); );
@@ -140,7 +148,9 @@ class TagsFormField extends StatelessWidget {
field.didChange( field.didChange(
formValue.copyWith( formValue.copyWith(
include: [...formValue.include, id], include: [...formValue.include, id],
exclude: formValue.exclude.whereNot((element) => element == id).toList(), exclude: formValue.exclude
.whereNot((element) => element == id)
.toList(),
), ),
); );
} }

View File

@@ -35,7 +35,8 @@ class FullscreenLabelForm<T extends Label> extends StatefulWidget {
!(initialValue?.isOnlyAssigned() ?? false) || showAnyAssignedOption, !(initialValue?.isOnlyAssigned() ?? false) || showAnyAssignedOption,
), ),
assert( assert(
!(initialValue?.isOnlyNotAssigned() ?? false) || showNotAssignedOption, !(initialValue?.isOnlyNotAssigned() ?? false) ||
showNotAssignedOption,
), ),
assert((addNewLabelText != null) == (onCreateNewLabel != null)); assert((addNewLabelText != null) == (onCreateNewLabel != null));
@@ -43,7 +44,8 @@ class FullscreenLabelForm<T extends Label> extends StatefulWidget {
State<FullscreenLabelForm> createState() => _FullscreenLabelFormState(); 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; bool _showClearIcon = false;
final _textEditingController = TextEditingController(); final _textEditingController = TextEditingController();
final _focusNode = FocusNode(); final _focusNode = FocusNode();
@@ -133,9 +135,11 @@ class _FullscreenLabelFormState<T extends Label> extends State<FullscreenLabelFo
itemCount: options.length, itemCount: options.length,
itemBuilder: (BuildContext context, int index) { itemBuilder: (BuildContext context, int index) {
final option = options.elementAt(index); final option = options.elementAt(index);
final highlight = AutocompleteHighlightedOption.of(context) == index; final highlight =
AutocompleteHighlightedOption.of(context) == index;
if (highlight) { if (highlight) {
SchedulerBinding.instance.addPostFrameCallback((Duration timeStamp) { SchedulerBinding.instance
.addPostFrameCallback((Duration timeStamp) {
Scrollable.ensureVisible( Scrollable.ensureVisible(
context, context,
alignment: 0, alignment: 0,
@@ -191,7 +195,8 @@ class _FullscreenLabelFormState<T extends Label> extends State<FullscreenLabelFo
for (final option in widget.options.values) { for (final option in widget.options.values) {
// Don't include the initial value in the selection // Don't include the initial value in the selection
final initialValue = widget.initialValue; final initialValue = widget.initialValue;
if (initialValue is SetIdQueryParameter && option.id == initialValue.id) { if (initialValue is SetIdQueryParameter &&
option.id == initialValue.id) {
continue; continue;
} }
yield IdQueryParameter.fromId(option.id!); yield IdQueryParameter.fromId(option.id!);
@@ -199,8 +204,8 @@ class _FullscreenLabelFormState<T extends Label> extends State<FullscreenLabelFo
} }
} else { } else {
// Show filtered options, if no matching option is found, always show not assigned and any assigned (if enabled) and proceed. // Show filtered options, if no matching option is found, always show not assigned and any assigned (if enabled) and proceed.
final matches = final matches = widget.options.values
widget.options.values.where((e) => e.name.trim().toLowerCase().contains(normalizedQuery)); .where((e) => e.name.trim().toLowerCase().contains(normalizedQuery));
if (matches.isNotEmpty) { if (matches.isNotEmpty) {
for (final match in matches) { for (final match in matches) {
yield IdQueryParameter.fromId(match.id!); yield IdQueryParameter.fromId(match.id!);
@@ -270,7 +275,9 @@ class _FullscreenLabelFormState<T extends Label> extends State<FullscreenLabelFo
selectedTileColor: Theme.of(context).focusColor, selectedTileColor: Theme.of(context).focusColor,
title: Text(widget.options[id]!.name), title: Text(widget.options[id]!.name),
onTap: onTap, 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 )!; // Never null, since we already return on unset before
} }

View File

@@ -58,8 +58,8 @@ class LabelFormField<T extends Label> extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final isEnabled = final isEnabled = options.values.any((e) => (e.documentCount ?? 0) > 0) ||
options.values.any((e) => (e.documentCount ?? 0) > 0) || addLabelPageBuilder != null; addLabelPageBuilder != null;
return FormBuilderField<IdQueryParameter>( return FormBuilderField<IdQueryParameter>(
name: name, name: name,
initialValue: initialValue, initialValue: initialValue,
@@ -70,7 +70,9 @@ class LabelFormField<T extends Label> extends StatelessWidget {
text: _buildText(context, field.value), text: _buildText(context, field.value),
); );
final displayedSuggestions = suggestions 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(); .toList();
return Column( return Column(
@@ -95,7 +97,8 @@ class LabelFormField<T extends Label> extends StatelessWidget {
suffixIcon: controller.text.isNotEmpty suffixIcon: controller.text.isNotEmpty
? IconButton( ? IconButton(
icon: const Icon(Icons.clear), icon: const Icon(Icons.clear),
onPressed: () => field.didChange(const IdQueryParameter.unset()), onPressed: () =>
field.didChange(const IdQueryParameter.unset()),
) )
: null, : null,
), ),
@@ -110,7 +113,8 @@ class LabelFormField<T extends Label> extends StatelessWidget {
? (initialName) { ? (initialName) {
return Navigator.of(context).push<T>( return Navigator.of(context).push<T>(
MaterialPageRoute( MaterialPageRoute(
builder: (context) => addLabelPageBuilder!(initialName), builder: (context) =>
addLabelPageBuilder!(initialName),
), ),
); );
} }
@@ -141,7 +145,8 @@ class LabelFormField<T extends Label> extends StatelessWidget {
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
itemCount: displayedSuggestions.length, itemCount: displayedSuggestions.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final suggestion = displayedSuggestions.elementAt(index); final suggestion =
displayedSuggestions.elementAt(index);
return ColoredChipWrapper( return ColoredChipWrapper(
child: ActionChip( child: ActionChip(
label: Text(suggestion.name), label: Text(suggestion.name),

View File

@@ -74,8 +74,11 @@ class LabelTabView<T extends Label> extends StatelessWidget {
name: l.name, name: l.name,
content: contentBuilder?.call(l) ?? content: contentBuilder?.call(l) ??
Text( Text(
translateMatchingAlgorithmName(context, l.matchingAlgorithm) + translateMatchingAlgorithmName(
((l.match?.isNotEmpty ?? false) ? ": ${l.match}" : ""), context, l.matchingAlgorithm) +
((l.match?.isNotEmpty ?? false)
? ": ${l.match}"
: ""),
maxLines: 2, maxLines: 2,
), ),
onOpenEditPage: canEdit ? onEdit : null, onOpenEditPage: canEdit ? onEdit : null,

View File

@@ -49,7 +49,8 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
final apiVersion = await _getApiVersion(_sessionManager.client); final apiVersion = await _getApiVersion(_sessionManager.client);
// Mark logged in user as currently active user. // 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; globalSettings.currentLoggedInUser = localUserId;
await globalSettings.save(); await globalSettings.save();
@@ -64,11 +65,13 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
/// Switches to another account if it exists. /// Switches to another account if it exists.
Future<void> switchAccount(String localUserId) async { Future<void> switchAccount(String localUserId) async {
emit(const AuthenticationState.switchingAccounts()); emit(const AuthenticationState.switchingAccounts());
final globalSettings = Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!; final globalSettings =
Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!;
if (globalSettings.currentLoggedInUser == localUserId) { if (globalSettings.currentLoggedInUser == localUserId) {
return; return;
} }
final userAccountBox = Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount); final userAccountBox =
Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount);
if (!userAccountBox.containsKey(localUserId)) { if (!userAccountBox.containsKey(localUserId)) {
debugPrint("User $localUserId not yet registered."); debugPrint("User $localUserId not yet registered.");
@@ -78,15 +81,15 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
final account = userAccountBox.get(localUserId)!; final account = userAccountBox.get(localUserId)!;
if (account.settings.isBiometricAuthenticationEnabled) { if (account.settings.isBiometricAuthenticationEnabled) {
final authenticated = final authenticated = await _localAuthService
await _localAuthService.authenticateLocalUser("Authenticate to switch your account."); .authenticateLocalUser("Authenticate to switch your account.");
if (!authenticated) { if (!authenticated) {
debugPrint("User not authenticated."); debugPrint("User not authenticated.");
return; return;
} }
} }
await withEncryptedBox<UserCredentials, void>(HiveBoxes.localUserCredentials, await withEncryptedBox<UserCredentials, void>(
(credentialsBox) async { HiveBoxes.localUserCredentials, (credentialsBox) async {
if (!credentialsBox.containsKey(localUserId)) { if (!credentialsBox.containsKey(localUserId)) {
await credentialsBox.close(); await credentialsBox.close();
debugPrint("Invalid authentication for $localUserId"); debugPrint("Invalid authentication for $localUserId");
@@ -108,7 +111,8 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
await _updateRemoteUser( await _updateRemoteUser(
_sessionManager, _sessionManager,
Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount).get(localUserId)!, Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount)
.get(localUserId)!,
apiVersion, apiVersion,
); );
@@ -140,12 +144,15 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
} }
Future<void> removeAccount(String userId) async { Future<void> removeAccount(String userId) async {
final userAccountBox = Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount); final userAccountBox =
final userAppStateBox = Hive.box<LocalUserAppState>(HiveBoxes.localUserAppState); Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount);
final userAppStateBox =
Hive.box<LocalUserAppState>(HiveBoxes.localUserAppState);
await userAccountBox.delete(userId); await userAccountBox.delete(userId);
await userAppStateBox.delete(userId); await userAppStateBox.delete(userId);
await withEncryptedBox<UserCredentials, void>(HiveBoxes.localUserCredentials, (box) { await withEncryptedBox<UserCredentials, void>(
HiveBoxes.localUserCredentials, (box) {
box.delete(userId); box.delete(userId);
}); });
} }
@@ -154,25 +161,28 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
/// Performs a conditional hydration based on the local authentication success. /// Performs a conditional hydration based on the local authentication success.
/// ///
Future<void> restoreSessionState() async { Future<void> restoreSessionState() async {
final globalSettings = Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!; final globalSettings =
Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!;
final localUserId = globalSettings.currentLoggedInUser; final localUserId = globalSettings.currentLoggedInUser;
if (localUserId == null) { if (localUserId == null) {
// If there is nothing to restore, we can quit here. // If there is nothing to restore, we can quit here.
return; return;
} }
final localUserAccountBox = Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount); final localUserAccountBox =
Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount);
final localUserAccount = localUserAccountBox.get(localUserId)!; final localUserAccount = localUserAccountBox.get(localUserId)!;
if (localUserAccount.settings.isBiometricAuthenticationEnabled) { if (localUserAccount.settings.isBiometricAuthenticationEnabled) {
final localAuthSuccess = final localAuthSuccess = await _localAuthService
await _localAuthService.authenticateLocalUser("Authenticate to log back in"); //TODO: INTL .authenticateLocalUser("Authenticate to log back in"); //TODO: INTL
if (!localAuthSuccess) { if (!localAuthSuccess) {
emit(const AuthenticationState.requriresLocalAuthentication()); emit(const AuthenticationState.requriresLocalAuthentication());
return; return;
} }
} }
final authentication = await withEncryptedBox<UserCredentials, UserCredentials>( final authentication =
await withEncryptedBox<UserCredentials, UserCredentials>(
HiveBoxes.localUserCredentials, (box) { HiveBoxes.localUserCredentials, (box) {
return box.get(globalSettings.currentLoggedInUser!); return box.get(globalSettings.currentLoggedInUser!);
}); });
@@ -202,7 +212,8 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
Future<void> logout() async { Future<void> logout() async {
await _resetExternalState(); await _resetExternalState();
final globalSettings = Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!; final globalSettings =
Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!;
globalSettings.currentLoggedInUser = null; globalSettings.currentLoggedInUser = null;
await globalSettings.save(); await globalSettings.save();
emit(const AuthenticationState.unauthenticated()); emit(const AuthenticationState.unauthenticated());
@@ -240,8 +251,10 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
authToken: token, authToken: token,
); );
final userAccountBox = Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount); final userAccountBox =
final userStateBox = Hive.box<LocalUserAppState>(HiveBoxes.localUserAppState); Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount);
final userStateBox =
Hive.box<LocalUserAppState>(HiveBoxes.localUserAppState);
if (userAccountBox.containsKey(localUserId)) { if (userAccountBox.containsKey(localUserId)) {
throw Exception("User already exists!"); throw Exception("User already exists!");

View File

@@ -3,7 +3,8 @@ part of 'authentication_cubit.dart';
@freezed @freezed
class AuthenticationState with _$AuthenticationState { class AuthenticationState with _$AuthenticationState {
const factory AuthenticationState.unauthenticated() = _Unauthenticated; const factory AuthenticationState.unauthenticated() = _Unauthenticated;
const factory AuthenticationState.requriresLocalAuthentication() = _RequiresLocalAuthentication; const factory AuthenticationState.requriresLocalAuthentication() =
_RequiresLocalAuthentication;
const factory AuthenticationState.authenticated({ const factory AuthenticationState.authenticated({
required String localUserId, required String localUserId,
required int apiVersion, required int apiVersion,

View File

@@ -27,8 +27,8 @@ class OldAuthenticationState with EquatableMixin {
}) { }) {
return OldAuthenticationState( return OldAuthenticationState(
isAuthenticated: isAuthenticated ?? this.isAuthenticated, isAuthenticated: isAuthenticated ?? this.isAuthenticated,
showBiometricAuthenticationScreen: showBiometricAuthenticationScreen: showBiometricAuthenticationScreen ??
showBiometricAuthenticationScreen ?? this.showBiometricAuthenticationScreen, this.showBiometricAuthenticationScreen,
username: username ?? this.username, username: username ?? this.username,
fullName: fullName ?? this.fullName, fullName: fullName ?? this.fullName,
localUserId: localUserId ?? this.localUserId, localUserId: localUserId ?? this.localUserId,

View File

@@ -54,7 +54,8 @@ class _LoginPageState extends State<LoginPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final localAccounts = Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount); final localAccounts =
Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount);
return Scaffold( return Scaffold(
resizeToAvoidBottomInset: false, resizeToAvoidBottomInset: false,
body: FormBuilder( body: FormBuilder(
@@ -91,7 +92,9 @@ class _LoginPageState extends State<LoginPage> {
child: UserAccountListTile( child: UserAccountListTile(
account: account, account: account,
onTap: () { 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; final form = _formKey.currentState!.value;
ClientCertificate? clientCert; ClientCertificate? clientCert;
final clientCertFormModel = final clientCertFormModel =
form[ClientCertificateFormField.fkClientCertificate] as ClientCertificateFormModel?; form[ClientCertificateFormField.fkClientCertificate]
as ClientCertificateFormModel?;
if (clientCertFormModel != null) { if (clientCertFormModel != null) {
clientCert = ClientCertificate( clientCert = ClientCertificate(
bytes: clientCertFormModel.bytes, bytes: clientCertFormModel.bytes,
passphrase: clientCertFormModel.passphrase, passphrase: clientCertFormModel.passphrase,
); );
} }
final credentials = form[UserCredentialsFormField.fkCredentials] as LoginFormCredentials; final credentials =
form[UserCredentialsFormField.fkCredentials] as LoginFormCredentials;
try { try {
await widget.onSubmit( await widget.onSubmit(
context, context,

View File

@@ -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/features/login/model/client_certificate_form_model.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
import 'obscured_input_text_form_field.dart'; import 'obscured_input_text_form_field.dart';
class ClientCertificateFormField extends StatefulWidget { class ClientCertificateFormField extends StatefulWidget {
@@ -20,10 +19,12 @@ class ClientCertificateFormField extends StatefulWidget {
}) : super(key: key); }) : super(key: key);
@override @override
State<ClientCertificateFormField> createState() => _ClientCertificateFormFieldState(); State<ClientCertificateFormField> createState() =>
_ClientCertificateFormFieldState();
} }
class _ClientCertificateFormFieldState extends State<ClientCertificateFormField> { class _ClientCertificateFormFieldState
extends State<ClientCertificateFormField> {
File? _selectedFile; File? _selectedFile;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -42,7 +43,8 @@ class _ClientCertificateFormFieldState extends State<ClientCertificateFormField>
return null; return null;
}, },
builder: (field) { builder: (field) {
final theme = Theme.of(context).copyWith(dividerColor: Colors.transparent); //new final theme =
Theme.of(context).copyWith(dividerColor: Colors.transparent); //new
return Theme( return Theme(
data: theme, data: theme,
child: ExpansionTile( 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( FilePickerResult? result = await FilePicker.platform.pickFiles(
allowMultiple: false, allowMultiple: false,
); );
@@ -128,13 +131,15 @@ class _ClientCertificateFormFieldState extends State<ClientCertificateFormField>
setState(() { setState(() {
_selectedFile = file; _selectedFile = file;
}); });
final changedValue = field.value?.copyWith(bytes: file.readAsBytesSync()) ?? final changedValue =
field.value?.copyWith(bytes: file.readAsBytesSync()) ??
ClientCertificateFormModel(bytes: file.readAsBytesSync()); ClientCertificateFormModel(bytes: file.readAsBytesSync());
field.didChange(changedValue); field.didChange(changedValue);
} }
} }
Widget _buildSelectedFileText(FormFieldState<ClientCertificateFormModel?> field) { Widget _buildSelectedFileText(
FormFieldState<ClientCertificateFormModel?> field) {
if (field.value == null) { if (field.value == null) {
assert(_selectedFile == null); assert(_selectedFile == null);
return Text( return Text(

View File

@@ -67,7 +67,8 @@ class _ServerAddressFormFieldState extends State<ServerAddressFormField> {
.where((element) => element.contains(textEditingValue.text)); .where((element) => element.contains(textEditingValue.text));
}, },
onSelected: (option) => _formatInput(), onSelected: (option) => _formatInput(),
fieldViewBuilder: (context, textEditingController, focusNode, onFieldSubmitted) { fieldViewBuilder:
(context, textEditingController, focusNode, onFieldSubmitted) {
return TextField( return TextField(
controller: textEditingController, controller: textEditingController,
focusNode: focusNode, focusNode: focusNode,
@@ -146,9 +147,11 @@ class _AutocompleteOptions extends StatelessWidget {
onSelected(option); onSelected(option);
}, },
child: Builder(builder: (BuildContext context) { child: Builder(builder: (BuildContext context) {
final bool highlight = AutocompleteHighlightedOption.of(context) == index; final bool highlight =
AutocompleteHighlightedOption.of(context) == index;
if (highlight) { if (highlight) {
SchedulerBinding.instance.addPostFrameCallback((Duration timeStamp) { SchedulerBinding.instance
.addPostFrameCallback((Duration timeStamp) {
Scrollable.ensureVisible(context, alignment: 0.5); Scrollable.ensureVisible(context, alignment: 0.5);
}); });
} }

View File

@@ -14,7 +14,8 @@ class UserCredentialsFormField extends StatefulWidget {
}) : super(key: key); }) : super(key: key);
@override @override
State<UserCredentialsFormField> createState() => _UserCredentialsFormFieldState(); State<UserCredentialsFormField> createState() =>
_UserCredentialsFormFieldState();
} }
class _UserCredentialsFormFieldState extends State<UserCredentialsFormField> { class _UserCredentialsFormFieldState extends State<UserCredentialsFormField> {

View File

@@ -38,8 +38,9 @@ class _ServerConnectionPageState extends State<ServerConnectionPage> {
toolbarHeight: kToolbarHeight - 4, toolbarHeight: kToolbarHeight - 4,
title: Text(widget.titleString), title: Text(widget.titleString),
bottom: PreferredSize( bottom: PreferredSize(
child: child: _isCheckingConnection
_isCheckingConnection ? const LinearProgressIndicator() : const SizedBox(height: 4.0), ? const LinearProgressIndicator()
: const SizedBox(height: 4.0),
preferredSize: const Size.fromHeight(4.0), preferredSize: const Size.fromHeight(4.0),
), ),
), ),
@@ -69,8 +70,9 @@ class _ServerConnectionPageState extends State<ServerConnectionPage> {
), ),
FilledButton( FilledButton(
child: Text(S.of(context)!.continueLabel), child: Text(S.of(context)!.continueLabel),
onPressed: onPressed: _reachabilityStatus == ReachabilityStatus.reachable
_reachabilityStatus == ReachabilityStatus.reachable ? widget.onContinue : null, ? widget.onContinue
: null,
), ),
], ],
), ),
@@ -85,12 +87,15 @@ class _ServerConnectionPageState extends State<ServerConnectionPage> {
final certForm = widget.formBuilderKey.currentState final certForm = widget.formBuilderKey.currentState
?.getRawValue(ClientCertificateFormField.fkClientCertificate) ?.getRawValue(ClientCertificateFormField.fkClientCertificate)
as ClientCertificateFormModel?; as ClientCertificateFormModel?;
final status = await context.read<ConnectivityStatusService>().isPaperlessServerReachable( final status = await context
.read<ConnectivityStatusService>()
.isPaperlessServerReachable(
address ?? address ??
widget.formBuilderKey.currentState! widget.formBuilderKey.currentState!
.getRawValue(ServerAddressFormField.fkServerAddress), .getRawValue(ServerAddressFormField.fkServerAddress),
certForm != null certForm != null
? ClientCertificate(bytes: certForm.bytes, passphrase: certForm.passphrase) ? ClientCertificate(
bytes: certForm.bytes, passphrase: certForm.passphrase)
: null, : null,
); );
setState(() { setState(() {

View File

@@ -25,7 +25,8 @@ class _ServerLoginPageState extends State<ServerLoginPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final serverAddress = (widget.formBuilderKey.currentState final serverAddress = (widget.formBuilderKey.currentState
?.getRawValue(ServerAddressFormField.fkServerAddress) as String?) ?.getRawValue(ServerAddressFormField.fkServerAddress)
as String?)
?.replaceAll(RegExp(r'https?://'), '') ?? ?.replaceAll(RegExp(r'https?://'), '') ??
''; '';
return Scaffold( return Scaffold(

View File

@@ -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. /// Updates document filter and automatically reloads documents. Always resets page to 1.
/// Use [loadMore] to load more data. /// Use [loadMore] to load more data.

View File

@@ -11,17 +11,17 @@ part 'saved_view_cubit.freezed.dart';
class SavedViewCubit extends Cubit<SavedViewState> { class SavedViewCubit extends Cubit<SavedViewState> {
final SavedViewRepository _savedViewRepository; final SavedViewRepository _savedViewRepository;
SavedViewCubit(this._savedViewRepository) : super(const SavedViewState.initial()) { SavedViewCubit(this._savedViewRepository)
: super(const SavedViewState.initial()) {
_savedViewRepository.addListener( _savedViewRepository.addListener(
this, this,
onChanged: (views) { onChanged: (views) {
emit( views.when(
state.maybeWhen( initial: (savedViews) => emit(const SavedViewState.initial()),
loaded: (savedViews) => (state as _SavedViewLoadedState).copyWith( loading: (savedViews) => emit(const SavedViewState.loading()),
savedViews: views.savedViews, loaded: (savedViews) =>
), emit(SavedViewState.loaded(savedViews: savedViews)),
orElse: () => state, error: (savedViews) => emit(const SavedViewState.error()),
),
); );
}, },
); );
@@ -35,7 +35,7 @@ class SavedViewCubit extends Cubit<SavedViewState> {
return _savedViewRepository.delete(view); return _savedViewRepository.delete(view);
} }
Future<void> initialize() async { Future<void> reload() async {
final views = await _savedViewRepository.findAll(); final views = await _savedViewRepository.findAll();
final values = {for (var element in views) element.id!: element}; final values = {for (var element in views) element.id!: element};
if (!isClosed) { if (!isClosed) {
@@ -47,8 +47,6 @@ class SavedViewCubit extends Cubit<SavedViewState> {
} }
} }
Future<void> reload() => initialize();
@override @override
Future<void> close() { Future<void> close() {
_savedViewRepository.removeListener(this); _savedViewRepository.removeListener(this);

View File

@@ -2,12 +2,12 @@ part of 'saved_view_cubit.dart';
@freezed @freezed
class SavedViewState with _$SavedViewState { 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}) = const factory SavedViewState.loaded(
_SavedViewLoadedState; {required Map<int, SavedView> savedViews}) = _Loaded;
const factory SavedViewState.error() = _SavedViewErrorState; const factory SavedViewState.error() = _Error;
} }

View File

@@ -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/navigation/push_routes.dart';
import 'package:paperless_mobile/core/widgets/hint_card.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/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'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
class SavedViewList extends StatelessWidget { class SavedViewList extends StatelessWidget {
@@ -16,23 +17,20 @@ class SavedViewList extends StatelessWidget {
return BlocBuilder<SavedViewCubit, SavedViewState>( return BlocBuilder<SavedViewCubit, SavedViewState>(
builder: (context, state) { builder: (context, state) {
return state.when( return state.when(
initial: () => SliverToBoxAdapter(child: Container()), initial: () => const SavedViewLoadingSliverList(),
loading: () => const SliverToBoxAdapter( loading: () => const SavedViewLoadingSliverList(),
child: Center(
child: Text("Saved views loading..."), //TODO: INTL
),
),
loaded: (savedViews) { loaded: (savedViews) {
if (savedViews.isEmpty) { if (savedViews.isEmpty) {
return SliverToBoxAdapter( return SliverToBoxAdapter(
child: HintCard( child: HintCard(
hintText: S.of(context)!.createViewsToQuicklyFilterYourDocuments, hintText: S
.of(context)!
.createViewsToQuicklyFilterYourDocuments,
), ),
); );
} }
return SliverList( return SliverList.builder(
delegate: SliverChildBuilderDelegate( itemBuilder: (context, index) {
(context, index) {
final view = savedViews.values.elementAt(index); final view = savedViews.values.elementAt(index);
return ListTile( return ListTile(
enabled: connectivity.isConnected, 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( child: Text(
"An error occurred while trying to load the saved views.", "An error occurred while trying to load the saved views.",
), ),
), ),
),
); );
}, },
); );

View File

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

View File

@@ -9,7 +9,8 @@ import 'package:paperless_mobile/features/settings/model/view_type.dart';
part 'saved_view_details_state.dart'; part 'saved_view_details_state.dart';
class SavedViewDetailsCubit extends Cubit<SavedViewDetailsState> with DocumentPagingBlocMixin { class SavedViewDetailsCubit extends Cubit<SavedViewDetailsState>
with DocumentPagingBlocMixin {
@override @override
final PaperlessDocumentsApi api; final PaperlessDocumentsApi api;

View File

@@ -26,11 +26,14 @@ class ManageAccountsPage extends StatelessWidget {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
return ValueListenableBuilder( return ValueListenableBuilder(
valueListenable: Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount).listenable(), valueListenable:
Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount)
.listenable(),
builder: (context, box, _) { builder: (context, box, _) {
final userIds = box.keys.toList().cast<String>(); final userIds = box.keys.toList().cast<String>();
final otherAccounts = userIds final otherAccounts = userIds
.whereNot((element) => element == globalSettings.currentLoggedInUser) .whereNot(
(element) => element == globalSettings.currentLoggedInUser)
.toList(); .toList();
return SimpleDialog( return SimpleDialog(
insetPadding: const EdgeInsets.all(24), insetPadding: const EdgeInsets.all(24),
@@ -68,10 +71,13 @@ class ManageAccountsPage extends StatelessWidget {
], ],
onSelected: (value) async { onSelected: (value) async {
if (value == 0) { if (value == 0) {
final currentUser = globalSettings.currentLoggedInUser!; final currentUser =
globalSettings.currentLoggedInUser!;
await context.read<AuthenticationCubit>().logout(); await context.read<AuthenticationCubit>().logout();
Navigator.of(context).pop(); 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( PopupMenuItem(
child: ListTile( child: ListTile(
title: Text(S.of(context)!.switchAccount), title: Text(S.of(context)!.switchAccount),
leading: const Icon(Icons.switch_account_rounded), leading:
const Icon(Icons.switch_account_rounded),
), ),
value: 0, value: 0,
), ),
@@ -150,7 +157,8 @@ class ManageAccountsPage extends StatelessWidget {
MaterialPageRoute( MaterialPageRoute(
builder: (context) => LoginPage( builder: (context) => LoginPage(
titleString: S.of(context)!.addAccount, 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( final userId = await context.read<AuthenticationCubit>().addAccount(
credentials: LoginFormCredentials( credentials: LoginFormCredentials(
username: username, 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; if (currentUser == newUser) return;
Navigator.of(context).pop(); Navigator.of(context).pop();

View File

@@ -26,7 +26,9 @@ class SettingsPage extends StatelessWidget {
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
subtitle: FutureBuilder<PaperlessServerInformationModel>( subtitle: FutureBuilder<PaperlessServerInformationModel>(
future: context.read<PaperlessServerStatsApi>().getServerInformation(), future: context
.read<PaperlessServerStatsApi>()
.getServerInformation(),
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.hasError) { if (snapshot.hasError) {
return Text( return Text(

View File

@@ -14,8 +14,8 @@ class ClearCacheSetting extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ListTile( return ListTile(
title: const Text("Clear downloaded files"), //TODO: INTL title: const Text("Clear downloaded files"), //TODO: INTL
subtitle: subtitle: const Text(
const Text("Deletes all files downloaded from this app."), //TODO: INTL "Deletes all files downloaded from this app."), //TODO: INTL
onTap: () async { onTap: () async {
final dir = await FileService.downloadsDirectory; final dir = await FileService.downloadsDirectory;
final deletedSize = _dirSize(dir); final deletedSize = _dirSize(dir);

View File

@@ -32,7 +32,8 @@ class ColorSchemeOptionSetting extends StatelessWidget {
options: [ options: [
RadioOption( RadioOption(
value: ColorSchemeOption.classic, value: ColorSchemeOption.classic,
label: translateColorSchemeOption(context, ColorSchemeOption.classic), label: translateColorSchemeOption(
context, ColorSchemeOption.classic),
), ),
RadioOption( RadioOption(
value: ColorSchemeOption.dynamic, value: ColorSchemeOption.dynamic,

View File

@@ -25,15 +25,18 @@ class DefaultDownloadFileTypeSetting extends StatelessWidget {
options: [ options: [
RadioOption( RadioOption(
value: FileDownloadType.alwaysAsk, value: FileDownloadType.alwaysAsk,
label: _downloadFileTypeToString(context, FileDownloadType.alwaysAsk), label: _downloadFileTypeToString(
context, FileDownloadType.alwaysAsk),
), ),
RadioOption( RadioOption(
value: FileDownloadType.original, value: FileDownloadType.original,
label: _downloadFileTypeToString(context, FileDownloadType.original), label: _downloadFileTypeToString(
context, FileDownloadType.original),
), ),
RadioOption( RadioOption(
value: FileDownloadType.archived, value: FileDownloadType.archived,
label: _downloadFileTypeToString(context, FileDownloadType.archived), label: _downloadFileTypeToString(
context, FileDownloadType.archived),
), ),
], ],
initialValue: settings.defaultDownloadType, 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) { switch (type) {
case FileDownloadType.original: case FileDownloadType.original:
return S.of(context)!.original; return S.of(context)!.original;

View File

@@ -25,15 +25,18 @@ class DefaultShareFileTypeSetting extends StatelessWidget {
options: [ options: [
RadioOption( RadioOption(
value: FileDownloadType.alwaysAsk, value: FileDownloadType.alwaysAsk,
label: _downloadFileTypeToString(context, FileDownloadType.alwaysAsk), label: _downloadFileTypeToString(
context, FileDownloadType.alwaysAsk),
), ),
RadioOption( RadioOption(
value: FileDownloadType.original, value: FileDownloadType.original,
label: _downloadFileTypeToString(context, FileDownloadType.original), label: _downloadFileTypeToString(
context, FileDownloadType.original),
), ),
RadioOption( RadioOption(
value: FileDownloadType.archived, value: FileDownloadType.archived,
label: _downloadFileTypeToString(context, FileDownloadType.archived), label: _downloadFileTypeToString(
context, FileDownloadType.archived),
), ),
], ],
initialValue: settings.defaultShareType, 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) { switch (type) {
case FileDownloadType.original: case FileDownloadType.original:
return S.of(context)!.original; return S.of(context)!.original;

View File

@@ -10,7 +10,8 @@ class GlobalSettingsBuilder extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ValueListenableBuilder( return ValueListenableBuilder(
valueListenable: Hive.box<GlobalSettings>(HiveBoxes.globalSettings).listenable(), valueListenable:
Hive.box<GlobalSettings>(HiveBoxes.globalSettings).listenable(),
builder: (context, value, _) { builder: (context, value, _) {
final settings = value.getValue()!; final settings = value.getValue()!;
return builder(context, settings); return builder(context, settings);

View File

@@ -7,7 +7,8 @@ class LanguageSelectionSetting extends StatefulWidget {
const LanguageSelectionSetting({super.key}); const LanguageSelectionSetting({super.key});
@override @override
State<LanguageSelectionSetting> createState() => _LanguageSelectionSettingState(); State<LanguageSelectionSetting> createState() =>
_LanguageSelectionSettingState();
} }
class _LanguageSelectionSettingState extends State<LanguageSelectionSetting> { class _LanguageSelectionSettingState extends State<LanguageSelectionSetting> {
@@ -27,7 +28,8 @@ class _LanguageSelectionSettingState extends State<LanguageSelectionSetting> {
builder: (context, settings) { builder: (context, settings) {
return ListTile( return ListTile(
title: Text(S.of(context)!.language), title: Text(S.of(context)!.language),
subtitle: Text(_languageOptions[settings.preferredLocaleSubtag]!.name), subtitle:
Text(_languageOptions[settings.preferredLocaleSubtag]!.name),
onTap: () => showDialog<String>( onTap: () => showDialog<String>(
context: context, context: context,
builder: (_) => RadioSettingsDialog<String>( builder: (_) => RadioSettingsDialog<String>(
@@ -39,7 +41,8 @@ class _LanguageSelectionSettingState extends State<LanguageSelectionSetting> {
for (var language in _languageOptions.entries) for (var language in _languageOptions.entries)
RadioOption( RadioOption(
value: language.key, value: language.key,
label: language.value.name + (language.value.isComplete ? '' : '*'), label: language.value.name +
(language.value.isComplete ? '' : '*'),
), ),
], ],
initialValue: settings.preferredLocaleSubtag, initialValue: settings.preferredLocaleSubtag,

View File

@@ -50,7 +50,8 @@ class _RadioSettingsDialogState<T> extends State<RadioSettingsDialog<T>> {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
if (widget.descriptionText != null) 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), ...widget.options.map(_buildOptionListTile),
if (widget.footer != null) widget.footer!, if (widget.footer != null) widget.footer!,
], ],

View File

@@ -12,7 +12,8 @@ class ThemeModeSetting extends StatelessWidget {
builder: (context, settings) { builder: (context, settings) {
return ListTile( return ListTile(
title: Text(S.of(context)!.appearance), title: Text(S.of(context)!.appearance),
subtitle: Text(_mapThemeModeToLocalizedString(settings.preferredThemeMode, context)), subtitle: Text(_mapThemeModeToLocalizedString(
settings.preferredThemeMode, context)),
onTap: () => showDialog<ThemeMode>( onTap: () => showDialog<ThemeMode>(
context: context, context: context,
builder: (_) => RadioSettingsDialog<ThemeMode>( builder: (_) => RadioSettingsDialog<ThemeMode>(

View File

@@ -11,10 +11,13 @@ class UserAvatar extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final backgroundColor = Colors.primaries[account.id.hashCode % Colors.primaries.length]; final backgroundColor =
final foregroundColor = backgroundColor.computeLuminance() > 0.5 ? Colors.black : Colors.white; Colors.primaries[account.id.hashCode % Colors.primaries.length];
final foregroundColor =
backgroundColor.computeLuminance() > 0.5 ? Colors.black : Colors.white;
return CircleAvatar( return CircleAvatar(
child: Text((account.paperlessUser.fullName ?? account.paperlessUser.username) child: Text(
(account.paperlessUser.fullName ?? account.paperlessUser.username)
.split(" ") .split(" ")
.take(2) .take(2)
.map((e) => e.substring(0, 1)) .map((e) => e.substring(0, 1))

View File

@@ -18,10 +18,12 @@ class UserAccountBuilder extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ValueListenableBuilder<Box<LocalUserAccount>>( return ValueListenableBuilder<Box<LocalUserAccount>>(
valueListenable: Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount).listenable(), valueListenable:
Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount).listenable(),
builder: (context, accountBox, _) { builder: (context, accountBox, _) {
final currentUser = final currentUser = Hive.box<GlobalSettings>(HiveBoxes.globalSettings)
Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!.currentLoggedInUser; .getValue()!
.currentLoggedInUser;
if (currentUser != null) { if (currentUser != null) {
final account = accountBox.get(currentUser); final account = accountBox.get(currentUser);
return builder(context, account); return builder(context, account);

View File

@@ -4,33 +4,53 @@ import 'package:flutter/widgets.dart';
import 'package:receive_sharing_intent/receive_sharing_intent.dart'; import 'package:receive_sharing_intent/receive_sharing_intent.dart';
class ShareIntentQueue extends ChangeNotifier { class ShareIntentQueue extends ChangeNotifier {
final Queue<SharedMediaFile> _queue = Queue(); final Map<String, Queue<SharedMediaFile>> _queues = {};
ShareIntentQueue._(); ShareIntentQueue._();
static final instance = ShareIntentQueue._(); static final instance = ShareIntentQueue._();
void add(SharedMediaFile file) { void add(
SharedMediaFile file, {
required String userId,
}) {
debugPrint("Adding received file to queue: ${file.path}"); debugPrint("Adding received file to queue: ${file.path}");
_queue.add(file); _getQueue(userId).add(file);
notifyListeners(); notifyListeners();
} }
void addAll(Iterable<SharedMediaFile> files) { void addAll(
Iterable<SharedMediaFile> files, {
required String userId,
}) {
debugPrint( debugPrint(
"Adding received files to queue: ${files.map((e) => e.path).join(",")}"); "Adding received files to queue: ${files.map((e) => e.path).join(",")}");
_queue.addAll(files); _getQueue(userId).addAll(files);
notifyListeners(); notifyListeners();
} }
SharedMediaFile? pop() { SharedMediaFile? pop(String userId) {
if (hasUnhandledFiles) { if (userHasUnhandlesFiles(userId)) {
return _queue.removeFirst(); return _getQueue(userId).removeFirst();
// Don't notify listeners, only when new item is added. // Don't notify listeners, only when new item is added.
} else { } else {
return null; 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);
} }

View File

@@ -7,7 +7,8 @@ import 'package:paperless_mobile/features/paged_document_view/cubit/paged_docume
part 'similar_documents_state.dart'; part 'similar_documents_state.dart';
class SimilarDocumentsCubit extends Cubit<SimilarDocumentsState> with DocumentPagingBlocMixin { class SimilarDocumentsCubit extends Cubit<SimilarDocumentsState>
with DocumentPagingBlocMixin {
final int documentId; final int documentId;
@override @override

View File

@@ -35,8 +35,10 @@ class _SimilarDocumentsViewState extends State<SimilarDocumentsView>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocConsumer<ConnectivityCubit, ConnectivityState>( return BlocConsumer<ConnectivityCubit, ConnectivityState>(
listenWhen: (previous, current) => !previous.isConnected && current.isConnected, listenWhen: (previous, current) =>
listener: (context, state) => context.read<SimilarDocumentsCubit>().initialize(), !previous.isConnected && current.isConnected,
listener: (context, state) =>
context.read<SimilarDocumentsCubit>().initialize(),
builder: (context, connectivity) { builder: (context, connectivity) {
return BlocBuilder<SimilarDocumentsCubit, SimilarDocumentsState>( return BlocBuilder<SimilarDocumentsCubit, SimilarDocumentsState>(
builder: (context, state) { builder: (context, state) {
@@ -45,7 +47,9 @@ class _SimilarDocumentsViewState extends State<SimilarDocumentsView>
child: OfflineWidget(), child: OfflineWidget(),
); );
} }
if (state.hasLoaded && !state.isLoading && state.documents.isEmpty) { if (state.hasLoaded &&
!state.isLoading &&
state.documents.isEmpty) {
return SliverToBoxAdapter( return SliverToBoxAdapter(
child: Center( child: Center(
child: Text(S.of(context)!.noItemsFound), child: Text(S.of(context)!.noItemsFound),

View File

@@ -25,7 +25,8 @@ class UserAccountListTile extends StatelessWidget {
subtitle: Column( subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
if (account.paperlessUser.fullName != null) Text(account.paperlessUser.fullName!), if (account.paperlessUser.fullName != null)
Text(account.paperlessUser.fullName!),
Text( Text(
account.serverUrl.replaceFirst(RegExp(r'https://?'), ''), account.serverUrl.replaceFirst(RegExp(r'https://?'), ''),
style: TextStyle(color: theme.colorScheme.primary), style: TextStyle(color: theme.colorScheme.primary),

View File

@@ -52,7 +52,8 @@ import 'package:mock_server/mock_server.dart';
String get defaultPreferredLocaleSubtag { String get defaultPreferredLocaleSubtag {
String preferredLocale = Platform.localeName.split("_").first; String preferredLocale = Platform.localeName.split("_").first;
if (!S.supportedLocales.any((locale) => locale.languageCode == preferredLocale)) { if (!S.supportedLocales
.any((locale) => locale.languageCode == preferredLocale)) {
preferredLocale = 'en'; preferredLocale = 'en';
} }
return preferredLocale; return preferredLocale;
@@ -65,7 +66,8 @@ Future<void> _initHive() async {
await Hive.openBox<LocalUserAccount>(HiveBoxes.localUserAccount); await Hive.openBox<LocalUserAccount>(HiveBoxes.localUserAccount);
await Hive.openBox<LocalUserAppState>(HiveBoxes.localUserAppState); await Hive.openBox<LocalUserAppState>(HiveBoxes.localUserAppState);
await Hive.openBox<String>(HiveBoxes.hosts); 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) { if (!globalSettingsBox.hasValue) {
await globalSettingsBox.setValue( await globalSettingsBox.setValue(
@@ -132,7 +134,8 @@ void main() async {
//Update language header in interceptor on language change. //Update language header in interceptor on language change.
globalSettingsBox.listenable().addListener(() { globalSettingsBox.listenable().addListener(() {
languageHeaderInterceptor.preferredLocaleSubtag = globalSettings.preferredLocaleSubtag; languageHeaderInterceptor.preferredLocaleSubtag =
globalSettings.preferredLocaleSubtag;
}); });
final apiFactory = PaperlessApiFactoryImpl(sessionManager); final apiFactory = PaperlessApiFactoryImpl(sessionManager);
@@ -142,15 +145,19 @@ void main() async {
providers: [ providers: [
ChangeNotifierProvider.value(value: sessionManager), ChangeNotifierProvider.value(value: sessionManager),
Provider<LocalAuthenticationService>.value(value: localAuthService), Provider<LocalAuthenticationService>.value(value: localAuthService),
Provider<ConnectivityStatusService>.value(value: connectivityStatusService), Provider<Connectivity>.value(value: connectivity),
Provider<LocalNotificationService>.value(value: localNotificationService), Provider<ConnectivityStatusService>.value(
value: connectivityStatusService),
Provider<LocalNotificationService>.value(
value: localNotificationService),
Provider.value(value: DocumentChangedNotifier()), Provider.value(value: DocumentChangedNotifier()),
], ],
child: MultiBlocProvider( child: MultiBlocProvider(
providers: [ providers: [
BlocProvider<ConnectivityCubit>.value(value: connectivityCubit), BlocProvider<ConnectivityCubit>.value(value: connectivityCubit),
BlocProvider( BlocProvider(
create: (context) => AuthenticationCubit(localAuthService, apiFactory, sessionManager), create: (context) => AuthenticationCubit(
localAuthService, apiFactory, sessionManager),
) )
], ],
child: PaperlessMobileEntrypoint( child: PaperlessMobileEntrypoint(
@@ -169,7 +176,8 @@ class PaperlessMobileEntrypoint extends StatefulWidget {
}) : super(key: key); }) : super(key: key);
@override @override
State<PaperlessMobileEntrypoint> createState() => _PaperlessMobileEntrypointState(); State<PaperlessMobileEntrypoint> createState() =>
_PaperlessMobileEntrypointState();
} }
class _PaperlessMobileEntrypointState extends State<PaperlessMobileEntrypoint> { class _PaperlessMobileEntrypointState extends State<PaperlessMobileEntrypoint> {
@@ -224,7 +232,6 @@ class AuthenticationWrapper extends StatefulWidget {
} }
class _AuthenticationWrapperState extends State<AuthenticationWrapper> { class _AuthenticationWrapperState extends State<AuthenticationWrapper> {
late final StreamSubscription _shareMediaSubscription;
@override @override
void didChangeDependencies() { void didChangeDependencies() {
super.didChangeDependencies(); super.didChangeDependencies();
@@ -233,12 +240,6 @@ class _AuthenticationWrapperState extends State<AuthenticationWrapper> {
}); });
} }
@override
void dispose() {
_shareMediaSubscription.cancel();
super.dispose();
}
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@@ -248,11 +249,6 @@ class _AuthenticationWrapperState extends State<AuthenticationWrapper> {
_setOptimalDisplayMode(); _setOptimalDisplayMode();
} }
initializeDateFormatting(); 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 { Future<void> _setOptimalDisplayMode() async {
@@ -264,7 +260,8 @@ class _AuthenticationWrapperState extends State<AuthenticationWrapper> {
.toList() .toList()
..sort((a, b) => b.refreshRate.compareTo(a.refreshRate)); ..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}'); debugPrint('Setting refresh rate to ${mostOptimalMode.refreshRate}');
await FlutterDisplayMode.setPreferredMode(mostOptimalMode); await FlutterDisplayMode.setPreferredMode(mostOptimalMode);
@@ -303,12 +300,14 @@ class _AuthenticationWrapperState extends State<AuthenticationWrapper> {
) async { ) async {
try { try {
await context.read<AuthenticationCubit>().login( await context.read<AuthenticationCubit>().login(
credentials: LoginFormCredentials(username: username, password: password), credentials:
LoginFormCredentials(username: username, password: password),
serverUrl: serverUrl, serverUrl: serverUrl,
clientCertificate: clientCertificate, clientCertificate: clientCertificate,
); );
// Show onboarding after first login! // Show onboarding after first login!
final globalSettings = Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!; final globalSettings =
Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!;
if (globalSettings.showOnboarding) { if (globalSettings.showOnboarding) {
Navigator.push( Navigator.push(
context, context,

View File

@@ -250,7 +250,7 @@ class FilterRule with EquatableMixin {
filterRules.add(docTypeRule); filterRules.add(docTypeRule);
} }
final sPathRule = filter.documentType.whenOrNull( final sPathRule = filter.storagePath.whenOrNull(
notAssigned: () => FilterRule(storagePathRule, null), notAssigned: () => FilterRule(storagePathRule, null),
fromId: (id) => FilterRule(storagePathRule, id.toString()), fromId: (id) => FilterRule(storagePathRule, id.toString()),
); );
@@ -344,8 +344,7 @@ class FilterRule with EquatableMixin {
} }
//Join values of all extended filter rules if exist //Join values of all extended filter rules if exist
if (filterRules.isNotEmpty && if (filterRules.where((e) => e.ruleType == FilterRule.extendedRule).isNotEmpty) {
filterRules.where((e) => e.ruleType == FilterRule.extendedRule).length > 1) {
final mergedExtendedRule = filterRules final mergedExtendedRule = filterRules
.where((r) => r.ruleType == FilterRule.extendedRule) .where((r) => r.ruleType == FilterRule.extendedRule)
.map((e) => e.value) .map((e) => e.value)

View File

@@ -52,6 +52,9 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi {
final response = await client.post( final response = await client.post(
'/api/documents/post_document/', '/api/documents/post_document/',
data: formData, data: formData,
onSendProgress: (count, total) {
debugPrint("Uploading ${(count / total) * 100}%...");
},
); );
if (response.statusCode == 200) { if (response.statusCode == 200) {
if (response.data is String && response.data != "OK") { if (response.data is String && response.data != "OK") {

View File

@@ -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. # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
# Read more about iOS versioning at # Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 2.3.3+38 version: 2.3.4+39
environment: environment:
sdk: ">=3.0.0 <4.0.0" sdk: ">=3.0.0 <4.0.0"