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
bool operator ==(Object other) =>
identical(this, other) ||
other is ThemeModeAdapter && runtimeType == other.runtimeType && typeId == other.typeId;
other is ThemeModeAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

View File

@@ -9,7 +9,8 @@ import 'package:hive_flutter/adapters.dart';
/// Opens an encrypted box, calls [callback] with the now opened box, awaits
/// [callback] to return and returns the calculated value. Closes the box after.
///
Future<R?> withEncryptedBox<T, R>(String name, FutureOr<R?> Function(Box<T> box) callback) async {
Future<R?> withEncryptedBox<T, R>(
String name, FutureOr<R?> Function(Box<T> box) callback) async {
final key = await _getEncryptedBoxKey();
final box = await Hive.openBox<T>(
name,

View File

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

View File

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

View File

@@ -3,9 +3,11 @@ import 'package:paperless_api/paperless_api.dart';
abstract class PaperlessApiFactory {
PaperlessDocumentsApi createDocumentsApi(Dio dio, {required int apiVersion});
PaperlessSavedViewsApi createSavedViewsApi(Dio dio, {required int apiVersion});
PaperlessSavedViewsApi createSavedViewsApi(Dio dio,
{required int apiVersion});
PaperlessLabelsApi createLabelsApi(Dio dio, {required int apiVersion});
PaperlessServerStatsApi createServerStatsApi(Dio dio, {required int apiVersion});
PaperlessServerStatsApi createServerStatsApi(Dio dio,
{required int apiVersion});
PaperlessTasksApi createTasksApi(Dio dio, {required int apiVersion});
PaperlessAuthenticationApi createAuthenticationApi(Dio dio);
PaperlessUserApi createUserApi(Dio dio, {required int apiVersion});

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,14 +1,19 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:paperless_api/paperless_api.dart';
part 'saved_view_repository_state.freezed.dart';
part 'saved_view_repository_state.g.dart';
part of 'saved_view_repository.dart';
@freezed
class SavedViewRepositoryState with _$SavedViewRepositoryState {
const factory SavedViewRepositoryState({
const factory SavedViewRepositoryState.initial({
@Default({}) Map<int, SavedView> savedViews,
}) = _SavedViewRepositoryState;
}) = _Initial;
const factory SavedViewRepositoryState.loading({
@Default({}) Map<int, SavedView> savedViews,
}) = _Loading;
const factory SavedViewRepositoryState.loaded({
@Default({}) Map<int, SavedView> savedViews,
}) = _Loaded;
const factory SavedViewRepositoryState.error({
@Default({}) Map<int, SavedView> savedViews,
}) = _Error;
factory SavedViewRepositoryState.fromJson(Map<String, dynamic> json) =>
_$SavedViewRepositoryStateFromJson(json);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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_state.dart';
class DocumentSearchCubit extends Cubit<DocumentSearchState> with DocumentPagingBlocMixin {
class DocumentSearchCubit extends Cubit<DocumentSearchState>
with DocumentPagingBlocMixin {
@override
final PaperlessDocumentsApi api;
@@ -23,7 +24,8 @@ class DocumentSearchCubit extends Cubit<DocumentSearchState> with DocumentPaging
this.api,
this.notifier,
this._userAppState,
) : super(DocumentSearchState(searchHistory: _userAppState.documentSearchHistory)) {
) : super(DocumentSearchState(
searchHistory: _userAppState.documentSearchHistory)) {
notifier.addListener(
this,
onDeleted: remove,
@@ -46,7 +48,8 @@ class DocumentSearchCubit extends Cubit<DocumentSearchState> with DocumentPaging
state.copyWith(
searchHistory: [
query,
...state.searchHistory.whereNot((previousQuery) => previousQuery == query)
...state.searchHistory
.whereNot((previousQuery) => previousQuery == query)
],
),
);
@@ -62,7 +65,9 @@ class DocumentSearchCubit extends Cubit<DocumentSearchState> with DocumentPaging
void removeHistoryEntry(String entry) {
emit(
state.copyWith(
searchHistory: state.searchHistory.whereNot((element) => element == entry).toList(),
searchHistory: state.searchHistory
.whereNot((element) => element == entry)
.toList(),
),
);
_userAppState

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -129,8 +129,9 @@ class _FullscreenTagsFormState extends State<FullscreenTagsForm> {
),
],
bottom: PreferredSize(
preferredSize:
!widget.allowOnlySelection ? const Size.fromHeight(32) : const Size.fromHeight(1),
preferredSize: !widget.allowOnlySelection
? const Size.fromHeight(32)
: const Size.fromHeight(1),
child: Column(
children: [
Divider(color: theme.colorScheme.outline),
@@ -233,7 +234,8 @@ class _FullscreenTagsFormState extends State<FullscreenTagsForm> {
yield _buildNotAssignedOption();
}
var matches = _options.where((e) => e.name.trim().toLowerCase().contains(normalizedQuery));
var matches = _options
.where((e) => e.name.trim().toLowerCase().contains(normalizedQuery));
if (matches.isEmpty && widget.allowCreation) {
yield Text(S.of(context)!.noItemsFound);
yield TextButton(
@@ -299,7 +301,9 @@ class SelectableTagWidget extends StatelessWidget {
Widget build(BuildContext context) {
return ListTile(
title: Text(tag.name),
trailing: excluded ? const Icon(Icons.close) : (selected ? const Icon(Icons.done) : null),
trailing: excluded
? const Icon(Icons.close)
: (selected ? const Icon(Icons.done) : null),
leading: CircleAvatar(
backgroundColor: tag.color,
child: (tag.isInboxTag)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

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.
/// Use [loadMore] to load more data.

View File

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

View File

@@ -2,12 +2,12 @@ part of 'saved_view_cubit.dart';
@freezed
class SavedViewState with _$SavedViewState {
const factory SavedViewState.initial() = _SavedViewIntialState;
const factory SavedViewState.initial() = _Initial;
const factory SavedViewState.loading() = _SavedViewLoadingState;
const factory SavedViewState.loading() = _Loading;
const factory SavedViewState.loaded({required Map<int, SavedView> savedViews}) =
_SavedViewLoadedState;
const factory SavedViewState.loaded(
{required Map<int, SavedView> savedViews}) = _Loaded;
const factory SavedViewState.error() = _SavedViewErrorState;
const factory SavedViewState.error() = _Error;
}

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

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';
class SavedViewDetailsCubit extends Cubit<SavedViewDetailsState> with DocumentPagingBlocMixin {
class SavedViewDetailsCubit extends Cubit<SavedViewDetailsState>
with DocumentPagingBlocMixin {
@override
final PaperlessDocumentsApi api;

View File

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

View File

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

View File

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

View File

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

View File

@@ -25,15 +25,18 @@ class DefaultDownloadFileTypeSetting extends StatelessWidget {
options: [
RadioOption(
value: FileDownloadType.alwaysAsk,
label: _downloadFileTypeToString(context, FileDownloadType.alwaysAsk),
label: _downloadFileTypeToString(
context, FileDownloadType.alwaysAsk),
),
RadioOption(
value: FileDownloadType.original,
label: _downloadFileTypeToString(context, FileDownloadType.original),
label: _downloadFileTypeToString(
context, FileDownloadType.original),
),
RadioOption(
value: FileDownloadType.archived,
label: _downloadFileTypeToString(context, FileDownloadType.archived),
label: _downloadFileTypeToString(
context, FileDownloadType.archived),
),
],
initialValue: settings.defaultDownloadType,
@@ -51,7 +54,8 @@ class DefaultDownloadFileTypeSetting extends StatelessWidget {
);
}
String _downloadFileTypeToString(BuildContext context, FileDownloadType type) {
String _downloadFileTypeToString(
BuildContext context, FileDownloadType type) {
switch (type) {
case FileDownloadType.original:
return S.of(context)!.original;

View File

@@ -25,15 +25,18 @@ class DefaultShareFileTypeSetting extends StatelessWidget {
options: [
RadioOption(
value: FileDownloadType.alwaysAsk,
label: _downloadFileTypeToString(context, FileDownloadType.alwaysAsk),
label: _downloadFileTypeToString(
context, FileDownloadType.alwaysAsk),
),
RadioOption(
value: FileDownloadType.original,
label: _downloadFileTypeToString(context, FileDownloadType.original),
label: _downloadFileTypeToString(
context, FileDownloadType.original),
),
RadioOption(
value: FileDownloadType.archived,
label: _downloadFileTypeToString(context, FileDownloadType.archived),
label: _downloadFileTypeToString(
context, FileDownloadType.archived),
),
],
initialValue: settings.defaultShareType,
@@ -51,7 +54,8 @@ class DefaultShareFileTypeSetting extends StatelessWidget {
);
}
String _downloadFileTypeToString(BuildContext context, FileDownloadType type) {
String _downloadFileTypeToString(
BuildContext context, FileDownloadType type) {
switch (type) {
case FileDownloadType.original:
return S.of(context)!.original;

View File

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

View File

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

View File

@@ -50,7 +50,8 @@ class _RadioSettingsDialogState<T> extends State<RadioSettingsDialog<T>> {
mainAxisSize: MainAxisSize.min,
children: [
if (widget.descriptionText != null)
Text(widget.descriptionText!, style: Theme.of(context).textTheme.bodySmall),
Text(widget.descriptionText!,
style: Theme.of(context).textTheme.bodySmall),
...widget.options.map(_buildOptionListTile),
if (widget.footer != null) widget.footer!,
],

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 2.3.3+38
version: 2.3.4+39
environment:
sdk: ">=3.0.0 <4.0.0"