mirror of
https://github.com/Xevion/paperless-mobile.git
synced 2025-12-09 00:07:49 -06:00
feat: Add hive type adapters to api models, migrate to freezed
This commit is contained in:
@@ -8,8 +8,7 @@ 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;
|
||||
@@ -18,7 +17,7 @@ class BulkEditLabelBottomSheet<T extends Label> extends StatefulWidget {
|
||||
final LabelOptionsSelector<T> availableOptionsSelector;
|
||||
final void Function(int? selectedId) onSubmit;
|
||||
final int? initialValue;
|
||||
|
||||
|
||||
const BulkEditLabelBottomSheet({
|
||||
super.key,
|
||||
required this.title,
|
||||
@@ -30,19 +29,16 @@ 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(
|
||||
@@ -59,8 +55,7 @@ class _BulkEditLabelBottomSheetState<T extends Label>
|
||||
FormBuilder(
|
||||
key: _formKey,
|
||||
child: LabelFormField<T>(
|
||||
initialValue:
|
||||
IdQueryParameter.fromId(widget.initialValue),
|
||||
initialValue: IdQueryParameter.fromId(widget.initialValue),
|
||||
name: "labelFormField",
|
||||
options: widget.availableOptionsSelector(state),
|
||||
labelText: widget.formFieldLabel,
|
||||
@@ -75,12 +70,11 @@ class _BulkEditLabelBottomSheetState<T extends Label>
|
||||
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?.id);
|
||||
widget
|
||||
.onSubmit(value?.maybeWhen(fromId: (id) => id, orElse: () => null));
|
||||
}
|
||||
},
|
||||
child: Text(S.of(context)!.apply),
|
||||
|
||||
@@ -5,7 +5,7 @@ import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/features/document_details/cubit/document_details_cubit.dart';
|
||||
import 'package:paperless_mobile/features/document_details/view/dialogs/select_file_type_dialog.dart';
|
||||
import 'package:paperless_mobile/features/settings/model/global_settings.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/global_settings.dart';
|
||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||
|
||||
import 'package:paperless_mobile/helpers/message_helpers.dart';
|
||||
|
||||
@@ -50,8 +50,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
|
||||
@@ -95,16 +95,14 @@ 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,
|
||||
@@ -112,30 +110,20 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
|
||||
),
|
||||
addLabelText: S.of(context)!.addCorrespondent,
|
||||
labelText: S.of(context)!.correspondent,
|
||||
options: context
|
||||
.watch<DocumentEditCubit>()
|
||||
.state
|
||||
.correspondents,
|
||||
options: context.watch<DocumentEditCubit>().state.correspondents,
|
||||
initialValue: IdQueryParameter.fromId(
|
||||
state.document.correspondent,
|
||||
),
|
||||
name: fkCorrespondent,
|
||||
prefixIcon: const Icon(Icons.person_outlined),
|
||||
),
|
||||
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),
|
||||
);
|
||||
},
|
||||
@@ -149,8 +137,7 @@ 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,
|
||||
@@ -158,26 +145,18 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
|
||||
),
|
||||
addLabelText: S.of(context)!.addDocumentType,
|
||||
labelText: S.of(context)!.documentType,
|
||||
initialValue: IdQueryParameter.fromId(
|
||||
state.document.documentType),
|
||||
initialValue: IdQueryParameter.fromId(state.document.documentType),
|
||||
options: state.documentTypes,
|
||||
name: _DocumentEditPageState.fkDocumentType,
|
||||
prefixIcon:
|
||||
const Icon(Icons.description_outlined),
|
||||
prefixIcon: const Icon(Icons.description_outlined),
|
||||
),
|
||||
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),
|
||||
),
|
||||
),
|
||||
@@ -190,17 +169,14 @@ 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),
|
||||
),
|
||||
addLabelText: S.of(context)!.addStoragePath,
|
||||
labelText: S.of(context)!.storagePath,
|
||||
options: state.storagePaths,
|
||||
initialValue: IdQueryParameter.fromId(
|
||||
state.document.storagePath),
|
||||
initialValue: IdQueryParameter.fromId(state.document.storagePath),
|
||||
name: fkStoragePath,
|
||||
prefixIcon: const Icon(Icons.folder_outlined),
|
||||
),
|
||||
@@ -213,8 +189,8 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
|
||||
allowOnlySelection: true,
|
||||
allowCreation: true,
|
||||
allowExclude: false,
|
||||
initialValue: IdsTagsQuery.included(
|
||||
state.document.tags,
|
||||
initialValue: TagsQuery.ids(
|
||||
include: state.document.tags,
|
||||
),
|
||||
).padded(),
|
||||
if (_filteredSuggestions?.tags
|
||||
@@ -223,8 +199,7 @@ 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(
|
||||
@@ -234,17 +209,15 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
|
||||
),
|
||||
backgroundColor: tag.color,
|
||||
onPressed: () {
|
||||
final currentTags = _formKey.currentState
|
||||
?.fields[fkTags]?.value as TagsQuery;
|
||||
if (currentTags is IdsTagsQuery) {
|
||||
_formKey.currentState?.fields[fkTags]
|
||||
?.didChange((IdsTagsQuery.fromIds(
|
||||
{...currentTags.ids, itemData})));
|
||||
} else {
|
||||
_formKey.currentState?.fields[fkTags]
|
||||
?.didChange((IdsTagsQuery.fromIds(
|
||||
{itemData})));
|
||||
}
|
||||
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]),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
@@ -282,13 +255,14 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
|
||||
if (_formKey.currentState?.saveAndValidate() ?? false) {
|
||||
final values = _formKey.currentState!.value;
|
||||
var mergedDocument = document.copyWith(
|
||||
title: values[fkTitle],
|
||||
created: values[fkCreatedDate],
|
||||
documentType: () => (values[fkDocumentType] as IdQueryParameter).id,
|
||||
correspondent: () => (values[fkCorrespondent] as IdQueryParameter).id,
|
||||
storagePath: () => (values[fkStoragePath] as IdQueryParameter).id,
|
||||
tags: (values[fkTags] as IdsTagsQuery).includedIds,
|
||||
content: values[fkContent]);
|
||||
title: values[fkTitle],
|
||||
created: values[fkCreatedDate],
|
||||
documentType: () => (values[fkDocumentType] as SetIdQueryParameter).id,
|
||||
correspondent: () => (values[fkCorrespondent] as SetIdQueryParameter).id,
|
||||
storagePath: () => (values[fkStoragePath] as SetIdQueryParameter).id,
|
||||
tags: (values[fkTags] as IdsTagsQuery).include,
|
||||
content: values[fkContent],
|
||||
);
|
||||
setState(() {
|
||||
_isSubmitLoading = true;
|
||||
});
|
||||
@@ -342,8 +316,7 @@ 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),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -372,8 +345,7 @@ 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),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -3,18 +3,17 @@ import 'package:hydrated_bloc/hydrated_bloc.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart';
|
||||
import 'package:paperless_mobile/core/repository/label_repository.dart';
|
||||
import 'package:paperless_mobile/features/login/model/user_account.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/user_account.dart';
|
||||
import 'package:paperless_mobile/features/paged_document_view/cubit/document_paging_bloc_mixin.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:paperless_mobile/features/paged_document_view/cubit/paged_documents_state.dart';
|
||||
import 'package:paperless_mobile/features/settings/model/user_settings.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/user_settings.dart';
|
||||
import 'package:paperless_mobile/features/settings/model/view_type.dart';
|
||||
|
||||
part 'document_search_state.dart';
|
||||
part 'document_search_cubit.g.dart';
|
||||
|
||||
class DocumentSearchCubit extends HydratedCubit<DocumentSearchState>
|
||||
with DocumentPagingBlocMixin {
|
||||
class DocumentSearchCubit extends HydratedCubit<DocumentSearchState> with DocumentPagingBlocMixin {
|
||||
@override
|
||||
final PaperlessDocumentsApi api;
|
||||
|
||||
@@ -58,8 +57,7 @@ class DocumentSearchCubit extends HydratedCubit<DocumentSearchState>
|
||||
state.copyWith(
|
||||
searchHistory: [
|
||||
query,
|
||||
...state.searchHistory
|
||||
.whereNot((previousQuery) => previousQuery == query)
|
||||
...state.searchHistory.whereNot((previousQuery) => previousQuery == query)
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -72,9 +70,7 @@ class DocumentSearchCubit extends HydratedCubit<DocumentSearchState>
|
||||
void removeHistoryEntry(String entry) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
searchHistory: state.searchHistory
|
||||
.whereNot((element) => element == entry)
|
||||
.toList(),
|
||||
searchHistory: state.searchHistory.whereNot((element) => element == entry).toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -121,6 +117,5 @@ class DocumentSearchCubit extends HydratedCubit<DocumentSearchState>
|
||||
}
|
||||
|
||||
@override
|
||||
// TODO: implement account
|
||||
UserAccount get account => throw UnimplementedError();
|
||||
Future<void> onFilterUpdated(DocumentFilter filter) async {}
|
||||
}
|
||||
|
||||
@@ -5,10 +5,9 @@ import 'package:paperless_mobile/core/bloc/paperless_server_information_cubit.da
|
||||
import 'package:paperless_mobile/core/bloc/paperless_server_information_state.dart';
|
||||
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
||||
import 'package:paperless_mobile/core/delegate/customizable_sliver_persistent_header_delegate.dart';
|
||||
import 'package:paperless_mobile/core/widgets/material/search/m3_search_bar.dart'
|
||||
as s;
|
||||
import 'package:paperless_mobile/core/widgets/material/search/m3_search_bar.dart' as s;
|
||||
import 'package:paperless_mobile/features/document_search/view/document_search_page.dart';
|
||||
import 'package:paperless_mobile/features/login/model/user_account.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/user_account.dart';
|
||||
import 'package:paperless_mobile/features/settings/view/dialogs/account_settings_dialog.dart';
|
||||
import 'package:paperless_mobile/features/settings/view/manage_accounts_page.dart';
|
||||
import 'package:paperless_mobile/features/settings/view/widgets/global_settings_builder.dart';
|
||||
@@ -46,14 +45,10 @@ class SliverSearchBar extends StatelessWidget {
|
||||
icon: GlobalSettingsBuilder(
|
||||
builder: (context, settings) {
|
||||
return ValueListenableBuilder(
|
||||
valueListenable:
|
||||
Hive.box<UserAccount>(HiveBoxes.userAccount)
|
||||
.listenable(),
|
||||
valueListenable: Hive.box<UserAccount>(HiveBoxes.userAccount).listenable(),
|
||||
builder: (context, box, _) {
|
||||
final account = box.get(settings.currentLoggedInUser!)!;
|
||||
return UserAvatar(
|
||||
userId: settings.currentLoggedInUser!,
|
||||
account: account);
|
||||
return UserAvatar(userId: settings.currentLoggedInUser!, account: account);
|
||||
},
|
||||
);
|
||||
},
|
||||
|
||||
@@ -41,12 +41,10 @@ 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");
|
||||
|
||||
@@ -73,8 +71,7 @@ class _DocumentUploadPreparationPageState
|
||||
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(
|
||||
@@ -95,8 +92,7 @@ class _DocumentUploadPreparationPageState
|
||||
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;
|
||||
@@ -108,22 +104,18 @@ class _DocumentUploadPreparationPageState
|
||||
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);
|
||||
}
|
||||
},
|
||||
),
|
||||
@@ -138,12 +130,10 @@ class _DocumentUploadPreparationPageState
|
||||
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(
|
||||
@@ -153,13 +143,10 @@ class _DocumentUploadPreparationPageState
|
||||
() => _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);
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -184,8 +171,7 @@ class _DocumentUploadPreparationPageState
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: () {
|
||||
_formKey.currentState!
|
||||
.fields[DocumentModel.createdKey]
|
||||
_formKey.currentState!.fields[DocumentModel.createdKey]
|
||||
?.didChange(null);
|
||||
},
|
||||
)
|
||||
@@ -196,8 +182,7 @@ class _DocumentUploadPreparationPageState
|
||||
LabelFormField<Correspondent>(
|
||||
showAnyAssignedOption: false,
|
||||
showNotAssignedOption: false,
|
||||
addLabelPageBuilder: (initialName) =>
|
||||
RepositoryProvider.value(
|
||||
addLabelPageBuilder: (initialName) => RepositoryProvider.value(
|
||||
value: context.read<LabelRepository>(),
|
||||
child: AddCorrespondentPage(initialName: initialName),
|
||||
),
|
||||
@@ -211,8 +196,7 @@ class _DocumentUploadPreparationPageState
|
||||
LabelFormField<DocumentType>(
|
||||
showAnyAssignedOption: false,
|
||||
showNotAssignedOption: false,
|
||||
addLabelPageBuilder: (initialName) =>
|
||||
RepositoryProvider.value(
|
||||
addLabelPageBuilder: (initialName) => RepositoryProvider.value(
|
||||
value: context.read<LabelRepository>(),
|
||||
child: AddDocumentTypePage(initialName: initialName),
|
||||
),
|
||||
@@ -252,10 +236,9 @@ class _DocumentUploadPreparationPageState
|
||||
|
||||
final createdAt = fv[DocumentModel.createdKey] as DateTime?;
|
||||
final title = fv[DocumentModel.titleKey] as String;
|
||||
final docType = fv[DocumentModel.documentTypeKey] as IdQueryParameter;
|
||||
final docType = fv[DocumentModel.documentTypeKey] as SetIdQueryParameter;
|
||||
final tags = fv[DocumentModel.tagsKey] as IdsTagsQuery;
|
||||
final correspondent =
|
||||
fv[DocumentModel.correspondentKey] as IdQueryParameter;
|
||||
final correspondent = fv[DocumentModel.correspondentKey] as SetIdQueryParameter;
|
||||
|
||||
final taskId = await cubit.upload(
|
||||
widget.fileBytes,
|
||||
@@ -266,7 +249,7 @@ class _DocumentUploadPreparationPageState
|
||||
title: title,
|
||||
documentType: docType.id,
|
||||
correspondent: correspondent.id,
|
||||
tags: tags.ids,
|
||||
tags: tags.include,
|
||||
createdAt: createdAt,
|
||||
);
|
||||
showSnackBar(
|
||||
@@ -283,8 +266,7 @@ class _DocumentUploadPreparationPageState
|
||||
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;
|
||||
|
||||
@@ -4,9 +4,10 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:hydrated_bloc/hydrated_bloc.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/user_app_state.dart';
|
||||
import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart';
|
||||
import 'package:paperless_mobile/core/repository/label_repository.dart';
|
||||
import 'package:paperless_mobile/features/login/model/user_account.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/user_account.dart';
|
||||
import 'package:paperless_mobile/features/paged_document_view/cubit/document_paging_bloc_mixin.dart';
|
||||
import 'package:paperless_mobile/features/paged_document_view/cubit/paged_documents_state.dart';
|
||||
import 'package:paperless_mobile/features/settings/model/view_type.dart';
|
||||
@@ -14,8 +15,7 @@ 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;
|
||||
|
||||
@@ -24,24 +24,21 @@ class DocumentsCubit extends HydratedCubit<DocumentsState>
|
||||
@override
|
||||
final DocumentChangedNotifier notifier;
|
||||
|
||||
@override
|
||||
final UserAccount account;
|
||||
final UserAppState _userState;
|
||||
|
||||
DocumentsCubit(
|
||||
this.api,
|
||||
this.notifier,
|
||||
this._labelRepository,
|
||||
this.account,
|
||||
) : super(DocumentsState(filter: account.settings.currentDocumentFilter)) {
|
||||
this._userState,
|
||||
) : super(DocumentsState(filter: _userState.currentDocumentFilter)) {
|
||||
notifier.addListener(
|
||||
this,
|
||||
onUpdated: (document) {
|
||||
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(),
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -49,8 +46,7 @@ class DocumentsCubit extends HydratedCubit<DocumentsState>
|
||||
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(),
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -84,9 +80,7 @@ class DocumentsCubit extends HydratedCubit<DocumentsState>
|
||||
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 {
|
||||
@@ -129,4 +123,10 @@ class DocumentsCubit extends HydratedCubit<DocumentsState>
|
||||
void setViewType(ViewType viewType) {
|
||||
emit(state.copyWith(viewType: viewType));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> onFilterUpdated(DocumentFilter filter) async {
|
||||
_userState.currentDocumentFilter = filter;
|
||||
await _userState.save();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:badges/badges.dart' as b;
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
@@ -41,12 +42,9 @@ class DocumentsPage extends StatefulWidget {
|
||||
State<DocumentsPage> createState() => _DocumentsPageState();
|
||||
}
|
||||
|
||||
class _DocumentsPageState extends State<DocumentsPage>
|
||||
with SingleTickerProviderStateMixin {
|
||||
final SliverOverlapAbsorberHandle searchBarHandle =
|
||||
SliverOverlapAbsorberHandle();
|
||||
final SliverOverlapAbsorberHandle tabBarHandle =
|
||||
SliverOverlapAbsorberHandle();
|
||||
class _DocumentsPageState extends State<DocumentsPage> with SingleTickerProviderStateMixin {
|
||||
final SliverOverlapAbsorberHandle searchBarHandle = SliverOverlapAbsorberHandle();
|
||||
final SliverOverlapAbsorberHandle tabBarHandle = SliverOverlapAbsorberHandle();
|
||||
late final TabController _tabController;
|
||||
|
||||
int _currentTab = 0;
|
||||
@@ -83,8 +81,7 @@ class _DocumentsPageState extends State<DocumentsPage>
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocListener<TaskStatusCubit, TaskStatusState>(
|
||||
listenWhen: (previous, current) =>
|
||||
!previous.isSuccess && current.isSuccess,
|
||||
listenWhen: (previous, current) => !previous.isSuccess && current.isSuccess,
|
||||
listener: (context, state) {
|
||||
showSnackBar(
|
||||
context,
|
||||
@@ -101,8 +98,7 @@ class _DocumentsPageState extends State<DocumentsPage>
|
||||
},
|
||||
child: BlocConsumer<ConnectivityCubit, ConnectivityState>(
|
||||
listenWhen: (previous, current) =>
|
||||
previous != ConnectivityState.connected &&
|
||||
current == ConnectivityState.connected,
|
||||
previous != ConnectivityState.connected && current == ConnectivityState.connected,
|
||||
listener: (context, state) {
|
||||
try {
|
||||
context.read<DocumentsCubit>().reload();
|
||||
@@ -150,11 +146,7 @@ class _DocumentsPageState extends State<DocumentsPage>
|
||||
resizeToAvoidBottomInset: true,
|
||||
body: WillPopScope(
|
||||
onWillPop: () async {
|
||||
if (context
|
||||
.read<DocumentsCubit>()
|
||||
.state
|
||||
.selection
|
||||
.isNotEmpty) {
|
||||
if (context.read<DocumentsCubit>().state.selection.isNotEmpty) {
|
||||
context.read<DocumentsCubit>().resetSelection();
|
||||
}
|
||||
return false;
|
||||
@@ -189,8 +181,7 @@ class _DocumentsPageState extends State<DocumentsPage>
|
||||
}
|
||||
return SliverPersistentHeader(
|
||||
pinned: true,
|
||||
delegate:
|
||||
CustomizableSliverPersistentHeaderDelegate(
|
||||
delegate: CustomizableSliverPersistentHeaderDelegate(
|
||||
minExtent: kTextTabBarHeight,
|
||||
maxExtent: kTextTabBarHeight,
|
||||
child: ColoredTabBar(
|
||||
@@ -214,22 +205,15 @@ class _DocumentsPageState extends State<DocumentsPage>
|
||||
if (metrics.maxScrollExtent == 0) {
|
||||
return true;
|
||||
}
|
||||
final desiredTab =
|
||||
(metrics.pixels / metrics.maxScrollExtent)
|
||||
.round();
|
||||
if (metrics.axis == Axis.horizontal &&
|
||||
_currentTab != desiredTab) {
|
||||
final desiredTab = (metrics.pixels / metrics.maxScrollExtent).round();
|
||||
if (metrics.axis == Axis.horizontal && _currentTab != desiredTab) {
|
||||
setState(() => _currentTab = desiredTab);
|
||||
}
|
||||
return false;
|
||||
},
|
||||
child: TabBarView(
|
||||
controller: _tabController,
|
||||
physics: context
|
||||
.watch<DocumentsCubit>()
|
||||
.state
|
||||
.selection
|
||||
.isNotEmpty
|
||||
physics: context.watch<DocumentsCubit>().state.selection.isNotEmpty
|
||||
? const NeverScrollableScrollPhysics()
|
||||
: null,
|
||||
children: [
|
||||
@@ -297,19 +281,13 @@ class _DocumentsPageState extends State<DocumentsPage>
|
||||
|
||||
final currState = context.read<DocumentsCubit>().state;
|
||||
final max = notification.metrics.maxScrollExtent;
|
||||
if (max == 0 ||
|
||||
_currentTab != 0 ||
|
||||
currState.isLoading ||
|
||||
currState.isLastPageLoaded) {
|
||||
if (max == 0 || _currentTab != 0 || currState.isLoading || currState.isLastPageLoaded) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final offset = notification.metrics.pixels;
|
||||
if (offset >= max * 0.7) {
|
||||
context
|
||||
.read<DocumentsCubit>()
|
||||
.loadMore()
|
||||
.onError<PaperlessServerException>(
|
||||
context.read<DocumentsCubit>().loadMore().onError<PaperlessServerException>(
|
||||
(error, stackTrace) => showErrorMessage(
|
||||
context,
|
||||
error,
|
||||
@@ -344,8 +322,7 @@ class _DocumentsPageState extends State<DocumentsPage>
|
||||
return SliverAdaptiveDocumentsView(
|
||||
viewType: state.viewType,
|
||||
onTap: _openDetails,
|
||||
onSelected:
|
||||
context.read<DocumentsCubit>().toggleDocumentSelection,
|
||||
onSelected: context.read<DocumentsCubit>().toggleDocumentSelection,
|
||||
hasInternetConnection: connectivityState.isConnected,
|
||||
onTagSelected: _addTagToFilter,
|
||||
onCorrespondentSelected: _addCorrespondentToFilter,
|
||||
@@ -436,8 +413,7 @@ class _DocumentsPageState extends State<DocumentsPage>
|
||||
snapSizes: const [0.9, 1],
|
||||
initialChildSize: .9,
|
||||
maxChildSize: 1,
|
||||
builder: (context, controller) =>
|
||||
BlocBuilder<DocumentsCubit, DocumentsState>(
|
||||
builder: (context, controller) => BlocBuilder<DocumentsCubit, DocumentsState>(
|
||||
builder: (context, state) {
|
||||
return DocumentFilterPanel(
|
||||
initialFilter: context.read<DocumentsCubit>().state.filter,
|
||||
@@ -458,9 +434,7 @@ class _DocumentsPageState extends State<DocumentsPage>
|
||||
if (filterIntent.shouldReset) {
|
||||
await context.read<DocumentsCubit>().resetFilter();
|
||||
} else {
|
||||
await context
|
||||
.read<DocumentsCubit>()
|
||||
.updateFilter(filter: filterIntent.filter!);
|
||||
await context.read<DocumentsCubit>().updateFilter(filter: filterIntent.filter!);
|
||||
}
|
||||
} on PaperlessServerException catch (error, stackTrace) {
|
||||
showErrorMessage(context, error, stackTrace);
|
||||
@@ -480,20 +454,21 @@ class _DocumentsPageState extends State<DocumentsPage>
|
||||
|
||||
void _addTagToFilter(int tagId) {
|
||||
try {
|
||||
final tagsQuery =
|
||||
context.read<DocumentsCubit>().state.filter.tags is IdsTagsQuery
|
||||
? context.read<DocumentsCubit>().state.filter.tags as IdsTagsQuery
|
||||
: const IdsTagsQuery();
|
||||
if (tagsQuery.includedIds.contains(tagId)) {
|
||||
final tagsQuery = context.read<DocumentsCubit>().state.filter.tags is IdsTagsQuery
|
||||
? context.read<DocumentsCubit>().state.filter.tags as IdsTagsQuery
|
||||
: const IdsTagsQuery();
|
||||
if (tagsQuery.include.contains(tagId)) {
|
||||
context.read<DocumentsCubit>().updateCurrentFilter(
|
||||
(filter) => filter.copyWith(
|
||||
tags: tagsQuery.withIdsRemoved([tagId]),
|
||||
tags: tagsQuery.copyWith(
|
||||
include: tagsQuery.include.whereNot((id) => id == tagId),
|
||||
exclude: tagsQuery.exclude.whereNot((id) => id == tagId)),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
context.read<DocumentsCubit>().updateCurrentFilter(
|
||||
(filter) => filter.copyWith(
|
||||
tags: tagsQuery.withIdQueriesAdded([IncludeTagIdQuery(tagId)]),
|
||||
tags: tagsQuery.copyWith(include: [...tagsQuery.include, tagId]),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -505,16 +480,17 @@ class _DocumentsPageState extends State<DocumentsPage>
|
||||
void _addCorrespondentToFilter(int? correspondentId) {
|
||||
final cubit = context.read<DocumentsCubit>();
|
||||
try {
|
||||
if (cubit.state.filter.correspondent.id == correspondentId) {
|
||||
cubit.updateCurrentFilter(
|
||||
(filter) =>
|
||||
filter.copyWith(correspondent: const IdQueryParameter.unset()),
|
||||
);
|
||||
} else {
|
||||
cubit.updateCurrentFilter(
|
||||
(filter) => filter.copyWith(
|
||||
correspondent: IdQueryParameter.fromId(correspondentId)),
|
||||
);
|
||||
final correspondent = cubit.state.filter.correspondent;
|
||||
if (correspondent is SetIdQueryParameter) {
|
||||
if (correspondent.id == correspondentId) {
|
||||
cubit.updateCurrentFilter(
|
||||
(filter) => filter.copyWith(correspondent: const IdQueryParameter.unset()),
|
||||
);
|
||||
} else {
|
||||
cubit.updateCurrentFilter(
|
||||
(filter) => filter.copyWith(correspondent: IdQueryParameter.fromId(correspondentId)),
|
||||
);
|
||||
}
|
||||
}
|
||||
} on PaperlessServerException catch (error, stackTrace) {
|
||||
showErrorMessage(context, error, stackTrace);
|
||||
@@ -524,16 +500,17 @@ class _DocumentsPageState extends State<DocumentsPage>
|
||||
void _addDocumentTypeToFilter(int? documentTypeId) {
|
||||
final cubit = context.read<DocumentsCubit>();
|
||||
try {
|
||||
if (cubit.state.filter.documentType.id == documentTypeId) {
|
||||
cubit.updateCurrentFilter(
|
||||
(filter) =>
|
||||
filter.copyWith(documentType: const IdQueryParameter.unset()),
|
||||
);
|
||||
} else {
|
||||
cubit.updateCurrentFilter(
|
||||
(filter) => filter.copyWith(
|
||||
documentType: IdQueryParameter.fromId(documentTypeId)),
|
||||
);
|
||||
final documentType = cubit.state.filter.documentType;
|
||||
if (documentType is SetIdQueryParameter) {
|
||||
if (documentType.id == documentTypeId) {
|
||||
cubit.updateCurrentFilter(
|
||||
(filter) => filter.copyWith(documentType: const IdQueryParameter.unset()),
|
||||
);
|
||||
} else {
|
||||
cubit.updateCurrentFilter(
|
||||
(filter) => filter.copyWith(documentType: IdQueryParameter.fromId(documentTypeId)),
|
||||
);
|
||||
}
|
||||
}
|
||||
} on PaperlessServerException catch (error, stackTrace) {
|
||||
showErrorMessage(context, error, stackTrace);
|
||||
@@ -543,16 +520,17 @@ class _DocumentsPageState extends State<DocumentsPage>
|
||||
void _addStoragePathToFilter(int? pathId) {
|
||||
final cubit = context.read<DocumentsCubit>();
|
||||
try {
|
||||
if (cubit.state.filter.correspondent.id == pathId) {
|
||||
cubit.updateCurrentFilter(
|
||||
(filter) =>
|
||||
filter.copyWith(storagePath: const IdQueryParameter.unset()),
|
||||
);
|
||||
} else {
|
||||
cubit.updateCurrentFilter(
|
||||
(filter) =>
|
||||
filter.copyWith(storagePath: IdQueryParameter.fromId(pathId)),
|
||||
);
|
||||
final path = cubit.state.filter.documentType;
|
||||
if (path is SetIdQueryParameter) {
|
||||
if (path.id == pathId) {
|
||||
cubit.updateCurrentFilter(
|
||||
(filter) => filter.copyWith(storagePath: const IdQueryParameter.unset()),
|
||||
);
|
||||
} else {
|
||||
cubit.updateCurrentFilter(
|
||||
(filter) => filter.copyWith(storagePath: IdQueryParameter.fromId(pathId)),
|
||||
);
|
||||
}
|
||||
}
|
||||
} on PaperlessServerException catch (error, stackTrace) {
|
||||
showErrorMessage(context, error, stackTrace);
|
||||
|
||||
@@ -11,6 +11,7 @@ import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
|
||||
import 'package:paperless_mobile/core/bloc/paperless_server_information_cubit.dart';
|
||||
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/user_app_state.dart';
|
||||
import 'package:paperless_mobile/core/global/constants.dart';
|
||||
import 'package:paperless_mobile/core/repository/label_repository.dart';
|
||||
import 'package:paperless_mobile/core/repository/saved_view_repository.dart';
|
||||
@@ -28,7 +29,7 @@ import 'package:paperless_mobile/features/inbox/view/pages/inbox_page.dart';
|
||||
import 'package:paperless_mobile/features/labels/cubit/label_cubit.dart';
|
||||
import 'package:paperless_mobile/features/labels/view/pages/labels_page.dart';
|
||||
import 'package:paperless_mobile/features/login/cubit/authentication_cubit.dart';
|
||||
import 'package:paperless_mobile/features/login/model/user_account.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/user_account.dart';
|
||||
import 'package:paperless_mobile/features/notifications/services/local_notification_service.dart';
|
||||
import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart';
|
||||
import 'package:paperless_mobile/features/sharing/share_intent_queue.dart';
|
||||
@@ -245,7 +246,7 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
|
||||
context.read(),
|
||||
context.read(),
|
||||
context.read(),
|
||||
Hive.box<UserAccount>(HiveBoxes.userAccount).get(userId)!,
|
||||
Hive.box<UserAppState>(HiveBoxes.userAppState).get(userId)!,
|
||||
)..reload(),
|
||||
),
|
||||
BlocProvider(
|
||||
@@ -280,8 +281,7 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
|
||||
listeners: [
|
||||
BlocListener<ConnectivityCubit, ConnectivityState>(
|
||||
//Only re-initialize data if the connectivity changed from not connected to connected
|
||||
listenWhen: (previous, current) =>
|
||||
current == ConnectivityState.connected,
|
||||
listenWhen: (previous, current) => current == ConnectivityState.connected,
|
||||
listener: (context, state) {
|
||||
_initializeData(context);
|
||||
},
|
||||
@@ -290,9 +290,7 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
|
||||
listener: (context, state) {
|
||||
if (state.task != null) {
|
||||
// Handle local notifications on task change (only when app is running for now).
|
||||
context
|
||||
.read<LocalNotificationService>()
|
||||
.notifyTaskChanged(state.task!);
|
||||
context.read<LocalNotificationService>().notifyTaskChanged(state.task!);
|
||||
}
|
||||
},
|
||||
),
|
||||
@@ -305,9 +303,7 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
|
||||
children: [
|
||||
NavigationRail(
|
||||
labelType: NavigationRailLabelType.all,
|
||||
destinations: destinations
|
||||
.map((e) => e.toNavigationRailDestination())
|
||||
.toList(),
|
||||
destinations: destinations.map((e) => e.toNavigationRailDestination()).toList(),
|
||||
selectedIndex: _currentIndex,
|
||||
onDestinationSelected: _onNavigationChanged,
|
||||
),
|
||||
@@ -325,8 +321,7 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
|
||||
elevation: 4.0,
|
||||
selectedIndex: _currentIndex,
|
||||
onDestinationSelected: _onNavigationChanged,
|
||||
destinations:
|
||||
destinations.map((e) => e.toNavigationDestination()).toList(),
|
||||
destinations: destinations.map((e) => e.toNavigationDestination()).toList(),
|
||||
),
|
||||
body: routes[_currentIndex],
|
||||
);
|
||||
|
||||
@@ -5,7 +5,7 @@ import 'package:paperless_mobile/core/repository/label_repository.dart';
|
||||
import 'package:paperless_mobile/core/repository/saved_view_repository.dart';
|
||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/features/login/cubit/authentication_cubit.dart';
|
||||
import 'package:paperless_mobile/features/settings/model/global_settings.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/global_settings.dart';
|
||||
|
||||
import 'package:paperless_mobile/features/settings/view/widgets/user_settings_builder.dart';
|
||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||
@@ -32,9 +32,7 @@ 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,
|
||||
@@ -56,9 +54,7 @@ class VerifyIdentityPage extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () => context
|
||||
.read<AuthenticationCubit>()
|
||||
.restoreSessionState(),
|
||||
onPressed: () => context.read<AuthenticationCubit>().restoreSessionState(),
|
||||
child: Text(S.of(context)!.verifyIdentity),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -13,8 +13,7 @@ import 'package:paperless_mobile/features/paged_document_view/cubit/document_pag
|
||||
part 'inbox_cubit.g.dart';
|
||||
part 'inbox_state.dart';
|
||||
|
||||
class InboxCubit extends HydratedCubit<InboxState>
|
||||
with DocumentPagingBlocMixin {
|
||||
class InboxCubit extends HydratedCubit<InboxState> with DocumentPagingBlocMixin {
|
||||
final LabelRepository _labelRepository;
|
||||
|
||||
final PaperlessDocumentsApi _documentsApi;
|
||||
@@ -39,10 +38,7 @@ class InboxCubit extends HydratedCubit<InboxState>
|
||||
this,
|
||||
onDeleted: remove,
|
||||
onUpdated: (document) {
|
||||
if (document.tags
|
||||
.toSet()
|
||||
.intersection(state.inboxTags.toSet())
|
||||
.isEmpty) {
|
||||
if (document.tags.toSet().intersection(state.inboxTags.toSet()).isEmpty) {
|
||||
remove(document);
|
||||
emit(state.copyWith(itemsInInboxCount: state.itemsInInboxCount - 1));
|
||||
} else {
|
||||
@@ -101,7 +97,7 @@ class InboxCubit extends HydratedCubit<InboxState>
|
||||
updateFilter(
|
||||
filter: DocumentFilter(
|
||||
sortField: SortField.added,
|
||||
tags: IdsTagsQuery.fromIds(inboxTags),
|
||||
tags: TagsQuery.ids(include: inboxTags),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -131,7 +127,7 @@ class InboxCubit extends HydratedCubit<InboxState>
|
||||
updateFilter(
|
||||
filter: DocumentFilter(
|
||||
sortField: SortField.added,
|
||||
tags: IdsTagsQuery.fromIds(inboxTags),
|
||||
tags: TagsQuery.ids(include: inboxTags),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -141,8 +137,7 @@ class InboxCubit extends HydratedCubit<InboxState>
|
||||
/// from the inbox.
|
||||
///
|
||||
Future<Iterable<int>> removeFromInbox(DocumentModel document) async {
|
||||
final tagsToRemove =
|
||||
document.tags.toSet().intersection(state.inboxTags.toSet());
|
||||
final tagsToRemove = document.tags.toSet().intersection(state.inboxTags.toSet());
|
||||
|
||||
final updatedTags = {...document.tags}..removeAll(tagsToRemove);
|
||||
final updatedDocument = await api.update(
|
||||
@@ -196,8 +191,8 @@ class InboxCubit extends HydratedCubit<InboxState>
|
||||
Future<void> assignAsn(DocumentModel document) async {
|
||||
if (document.archiveSerialNumber == null) {
|
||||
final int asn = await _documentsApi.findNextAsn();
|
||||
final updatedDocument = await _documentsApi
|
||||
.update(document.copyWith(archiveSerialNumber: () => asn));
|
||||
final updatedDocument =
|
||||
await _documentsApi.update(document.copyWith(archiveSerialNumber: () => asn));
|
||||
|
||||
replace(updatedDocument);
|
||||
}
|
||||
@@ -222,4 +217,7 @@ class InboxCubit extends HydratedCubit<InboxState>
|
||||
_labelRepository.removeListener(this);
|
||||
return super.close();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> onFilterUpdated(DocumentFilter filter) async {}
|
||||
}
|
||||
|
||||
@@ -44,12 +44,12 @@ class _FullscreenTagsFormState extends State<FullscreenTagsForm> {
|
||||
_options = widget.options.values.toList();
|
||||
final value = widget.initialValue;
|
||||
if (value is IdsTagsQuery) {
|
||||
_include = value.includedIds.toList();
|
||||
_exclude = value.excludedIds.toList();
|
||||
_include = value.include.toList();
|
||||
_exclude = value.include.toList();
|
||||
} else if (value is AnyAssignedTagsQuery) {
|
||||
_include = value.tagIds.toList();
|
||||
_anyAssigned = true;
|
||||
} else if (value is OnlyNotAssignedTagsQuery) {
|
||||
} else if (value is NotAssignedTagsQuery) {
|
||||
_notAssigned = true;
|
||||
}
|
||||
_textEditingController.addListener(() => setState(() {
|
||||
@@ -113,28 +113,24 @@ class _FullscreenTagsFormState extends State<FullscreenTagsForm> {
|
||||
icon: const Icon(Icons.done),
|
||||
onPressed: () {
|
||||
if (widget.allowOnlySelection) {
|
||||
widget.onSubmit(returnValue: IdsTagsQuery.included(_include));
|
||||
widget.onSubmit(returnValue: TagsQuery.ids(include: _include));
|
||||
return;
|
||||
}
|
||||
late final TagsQuery query;
|
||||
if (_notAssigned) {
|
||||
query = const OnlyNotAssignedTagsQuery();
|
||||
query = const TagsQuery.notAssigned();
|
||||
} else if (_anyAssigned) {
|
||||
query = AnyAssignedTagsQuery(tagIds: _include);
|
||||
query = TagsQuery.anyAssigned(tagIds: _include);
|
||||
} else {
|
||||
query = IdsTagsQuery([
|
||||
for (var id in _include) IncludeTagIdQuery(id),
|
||||
for (var id in _exclude) ExcludeTagIdQuery(id),
|
||||
]);
|
||||
query = TagsQuery.ids(include: _include, exclude: _exclude);
|
||||
}
|
||||
widget.onSubmit(returnValue: query);
|
||||
},
|
||||
),
|
||||
],
|
||||
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),
|
||||
@@ -237,8 +233,7 @@ 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(
|
||||
@@ -304,9 +299,7 @@ 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)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:animations/animations.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
@@ -32,9 +33,9 @@ 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).ids.isEmpty) ||
|
||||
field.value == null;
|
||||
final isEmpty =
|
||||
(field.value is IdsTagsQuery && (field.value as IdsTagsQuery).include.isEmpty) ||
|
||||
field.value == null;
|
||||
bool anyAssigned = field.value is AnyAssignedTagsQuery;
|
||||
return OpenContainer<TagsQuery>(
|
||||
middleColor: Theme.of(context).colorScheme.background,
|
||||
@@ -59,8 +60,7 @@ 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,
|
||||
),
|
||||
@@ -93,33 +93,56 @@ class TagsFormField extends StatelessWidget {
|
||||
) sync* {
|
||||
if (query == null) {
|
||||
yield Container();
|
||||
} else if (query is IdsTagsQuery) {
|
||||
for (final e in query.queries) {
|
||||
yield _buildTagIdQueryWidget(context, e, field);
|
||||
}
|
||||
} else if (query is OnlyNotAssignedTagsQuery) {
|
||||
yield _buildNotAssignedTagWidget(context, field);
|
||||
} else if (query is AnyAssignedTagsQuery) {
|
||||
for (final e in query.tagIds) {
|
||||
yield _buildAnyAssignedTagWidget(context, e, field, query);
|
||||
} 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),
|
||||
],
|
||||
anyAssigned: (value) => [
|
||||
for (var id in value.tagIds) _buildAnyAssignedTagWidget(context, id, field, value),
|
||||
],
|
||||
notAssigned: (value) => [_buildNotAssignedTagWidget(context, field)],
|
||||
);
|
||||
for (var child in widgets) {
|
||||
yield child;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildTagIdQueryWidget(
|
||||
BuildContext context,
|
||||
TagIdQuery e,
|
||||
int id,
|
||||
FormFieldState<TagsQuery?> field,
|
||||
bool exclude,
|
||||
) {
|
||||
assert(field.value is IdsTagsQuery);
|
||||
final formValue = field.value as IdsTagsQuery;
|
||||
final tag = options[e.id]!;
|
||||
final tag = options[id]!;
|
||||
return QueryTagChip(
|
||||
onDeleted: () => field.didChange(formValue.withIdsRemoved([e.id])),
|
||||
onDeleted: () => field.didChange(formValue.copyWith(
|
||||
include: formValue.include.whereNot((element) => element == id),
|
||||
exclude: formValue.exclude.whereNot((element) => element == id),
|
||||
)),
|
||||
onSelected: allowExclude
|
||||
? () => field.didChange(formValue.withIdQueryToggled(e.id))
|
||||
? () {
|
||||
if (formValue.include.contains(id)) {
|
||||
field.didChange(
|
||||
formValue.copyWith(
|
||||
include: formValue.include.whereNot((element) => element == id),
|
||||
exclude: [...formValue.exclude, id],
|
||||
),
|
||||
);
|
||||
} else if (formValue.exclude.contains(id)) {}
|
||||
field.didChange(
|
||||
formValue.copyWith(
|
||||
include: [...formValue.include, id],
|
||||
exclude: formValue.exclude.whereNot((element) => element == id),
|
||||
),
|
||||
);
|
||||
}
|
||||
: null,
|
||||
exclude: e is ExcludeTagIdQuery,
|
||||
exclude: exclude,
|
||||
backgroundColor: tag.color,
|
||||
foregroundColor: tag.textColor,
|
||||
labelText: tag.name,
|
||||
@@ -147,9 +170,11 @@ class TagsFormField extends StatelessWidget {
|
||||
) {
|
||||
return QueryTagChip(
|
||||
onDeleted: () {
|
||||
final updatedQuery = query.withRemoved([e]);
|
||||
final updatedQuery = query.copyWith(
|
||||
tagIds: query.tagIds.whereNot((element) => element == e),
|
||||
);
|
||||
if (updatedQuery.tagIds.isEmpty) {
|
||||
field.didChange(const IdsTagsQuery());
|
||||
field.didChange(const TagsQuery.ids());
|
||||
} else {
|
||||
field.didChange(updatedQuery);
|
||||
}
|
||||
|
||||
@@ -27,12 +27,9 @@ class LabelsPage extends StatefulWidget {
|
||||
State<LabelsPage> createState() => _LabelsPageState();
|
||||
}
|
||||
|
||||
class _LabelsPageState extends State<LabelsPage>
|
||||
with SingleTickerProviderStateMixin {
|
||||
final SliverOverlapAbsorberHandle searchBarHandle =
|
||||
SliverOverlapAbsorberHandle();
|
||||
final SliverOverlapAbsorberHandle tabBarHandle =
|
||||
SliverOverlapAbsorberHandle();
|
||||
class _LabelsPageState extends State<LabelsPage> with SingleTickerProviderStateMixin {
|
||||
final SliverOverlapAbsorberHandle searchBarHandle = SliverOverlapAbsorberHandle();
|
||||
final SliverOverlapAbsorberHandle tabBarHandle = SliverOverlapAbsorberHandle();
|
||||
|
||||
late final TabController _tabController;
|
||||
int _currentIndex = 0;
|
||||
@@ -82,33 +79,25 @@ class _LabelsPageState extends State<LabelsPage>
|
||||
Tab(
|
||||
icon: Icon(
|
||||
Icons.person_outline,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onPrimaryContainer,
|
||||
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||
),
|
||||
),
|
||||
Tab(
|
||||
icon: Icon(
|
||||
Icons.description_outlined,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onPrimaryContainer,
|
||||
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||
),
|
||||
),
|
||||
Tab(
|
||||
icon: Icon(
|
||||
Icons.label_outline,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onPrimaryContainer,
|
||||
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||
),
|
||||
),
|
||||
Tab(
|
||||
icon: Icon(
|
||||
Icons.folder_open,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onPrimaryContainer,
|
||||
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -126,20 +115,17 @@ class _LabelsPageState extends State<LabelsPage>
|
||||
return true;
|
||||
}
|
||||
final desiredTab =
|
||||
((metrics.pixels / metrics.maxScrollExtent) *
|
||||
(_tabController.length - 1))
|
||||
((metrics.pixels / metrics.maxScrollExtent) * (_tabController.length - 1))
|
||||
.round();
|
||||
|
||||
if (metrics.axis == Axis.horizontal &&
|
||||
_currentIndex != desiredTab) {
|
||||
if (metrics.axis == Axis.horizontal && _currentIndex != desiredTab) {
|
||||
setState(() => _currentIndex = desiredTab);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
child: RefreshIndicator(
|
||||
edgeOffset: kTextTabBarHeight,
|
||||
notificationPredicate: (notification) =>
|
||||
connectedState.isConnected,
|
||||
notificationPredicate: (notification) => connectedState.isConnected,
|
||||
onRefresh: () => [
|
||||
context.read<LabelCubit>().reloadCorrespondents,
|
||||
context.read<LabelCubit>().reloadDocumentTypes,
|
||||
@@ -157,20 +143,14 @@ class _LabelsPageState extends State<LabelsPage>
|
||||
SliverOverlapInjector(handle: searchBarHandle),
|
||||
SliverOverlapInjector(handle: tabBarHandle),
|
||||
LabelTabView<Correspondent>(
|
||||
labels: context
|
||||
.watch<LabelCubit>()
|
||||
.state
|
||||
.correspondents,
|
||||
labels: context.watch<LabelCubit>().state.correspondents,
|
||||
filterBuilder: (label) => DocumentFilter(
|
||||
correspondent:
|
||||
IdQueryParameter.fromId(label.id),
|
||||
correspondent: IdQueryParameter.fromId(label.id),
|
||||
pageSize: label.documentCount ?? 0,
|
||||
),
|
||||
onEdit: _openEditCorrespondentPage,
|
||||
emptyStateActionButtonLabel:
|
||||
S.of(context)!.addNewCorrespondent,
|
||||
emptyStateDescription:
|
||||
S.of(context)!.noCorrespondentsSetUp,
|
||||
emptyStateActionButtonLabel: S.of(context)!.addNewCorrespondent,
|
||||
emptyStateDescription: S.of(context)!.noCorrespondentsSetUp,
|
||||
onAddNew: _openAddCorrespondentPage,
|
||||
),
|
||||
],
|
||||
@@ -184,20 +164,14 @@ class _LabelsPageState extends State<LabelsPage>
|
||||
SliverOverlapInjector(handle: searchBarHandle),
|
||||
SliverOverlapInjector(handle: tabBarHandle),
|
||||
LabelTabView<DocumentType>(
|
||||
labels: context
|
||||
.watch<LabelCubit>()
|
||||
.state
|
||||
.documentTypes,
|
||||
labels: context.watch<LabelCubit>().state.documentTypes,
|
||||
filterBuilder: (label) => DocumentFilter(
|
||||
documentType:
|
||||
IdQueryParameter.fromId(label.id),
|
||||
documentType: IdQueryParameter.fromId(label.id),
|
||||
pageSize: label.documentCount ?? 0,
|
||||
),
|
||||
onEdit: _openEditDocumentTypePage,
|
||||
emptyStateActionButtonLabel:
|
||||
S.of(context)!.addNewDocumentType,
|
||||
emptyStateDescription:
|
||||
S.of(context)!.noDocumentTypesSetUp,
|
||||
emptyStateActionButtonLabel: S.of(context)!.addNewDocumentType,
|
||||
emptyStateDescription: S.of(context)!.noDocumentTypesSetUp,
|
||||
onAddNew: _openAddDocumentTypePage,
|
||||
),
|
||||
],
|
||||
@@ -211,10 +185,9 @@ class _LabelsPageState extends State<LabelsPage>
|
||||
SliverOverlapInjector(handle: searchBarHandle),
|
||||
SliverOverlapInjector(handle: tabBarHandle),
|
||||
LabelTabView<Tag>(
|
||||
labels:
|
||||
context.watch<LabelCubit>().state.tags,
|
||||
labels: context.watch<LabelCubit>().state.tags,
|
||||
filterBuilder: (label) => DocumentFilter(
|
||||
tags: IdsTagsQuery.fromIds([label.id!]),
|
||||
tags: TagsQuery.ids(include: [label.id!]),
|
||||
pageSize: label.documentCount ?? 0,
|
||||
),
|
||||
onEdit: _openEditTagPage,
|
||||
@@ -227,10 +200,8 @@ class _LabelsPageState extends State<LabelsPage>
|
||||
)
|
||||
: null,
|
||||
),
|
||||
emptyStateActionButtonLabel:
|
||||
S.of(context)!.addNewTag,
|
||||
emptyStateDescription:
|
||||
S.of(context)!.noTagsSetUp,
|
||||
emptyStateActionButtonLabel: S.of(context)!.addNewTag,
|
||||
emptyStateDescription: S.of(context)!.noTagsSetUp,
|
||||
onAddNew: _openAddTagPage,
|
||||
),
|
||||
],
|
||||
@@ -244,21 +215,15 @@ class _LabelsPageState extends State<LabelsPage>
|
||||
SliverOverlapInjector(handle: searchBarHandle),
|
||||
SliverOverlapInjector(handle: tabBarHandle),
|
||||
LabelTabView<StoragePath>(
|
||||
labels: context
|
||||
.watch<LabelCubit>()
|
||||
.state
|
||||
.storagePaths,
|
||||
labels: context.watch<LabelCubit>().state.storagePaths,
|
||||
onEdit: _openEditStoragePathPage,
|
||||
filterBuilder: (label) => DocumentFilter(
|
||||
storagePath:
|
||||
IdQueryParameter.fromId(label.id),
|
||||
storagePath: IdQueryParameter.fromId(label.id),
|
||||
pageSize: label.documentCount ?? 0,
|
||||
),
|
||||
contentBuilder: (path) => Text(path.path),
|
||||
emptyStateActionButtonLabel:
|
||||
S.of(context)!.addNewStoragePath,
|
||||
emptyStateDescription:
|
||||
S.of(context)!.noStoragePathsSetUp,
|
||||
emptyStateActionButtonLabel: S.of(context)!.addNewStoragePath,
|
||||
emptyStateDescription: S.of(context)!.noStoragePathsSetUp,
|
||||
onAddNew: _openAddStoragePathPage,
|
||||
),
|
||||
],
|
||||
|
||||
@@ -28,10 +28,10 @@ class FullscreenLabelForm<T extends Label> extends StatefulWidget {
|
||||
this.addNewLabelText,
|
||||
this.autofocus = true,
|
||||
}) : assert(
|
||||
!(initialValue?.onlyAssigned ?? false) || showAnyAssignedOption,
|
||||
!(initialValue?.isOnlyAssigned() ?? false) || showAnyAssignedOption,
|
||||
),
|
||||
assert(
|
||||
!(initialValue?.onlyNotAssigned ?? false) || showNotAssignedOption,
|
||||
!(initialValue?.isOnlyNotAssigned() ?? false) || showNotAssignedOption,
|
||||
),
|
||||
assert((addNewLabelText != null) == (onCreateNewLabel != null));
|
||||
|
||||
@@ -39,8 +39,7 @@ 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();
|
||||
@@ -80,7 +79,12 @@ class _FullscreenLabelFormState<T extends Label>
|
||||
FocusScope.of(context).unfocus();
|
||||
final index = AutocompleteHighlightedOption.of(context);
|
||||
final value = index.isNegative ? null : options.elementAt(index);
|
||||
widget.onSubmit(returnValue: IdQueryParameter.fromId(value?.id));
|
||||
widget.onSubmit(
|
||||
returnValue: IdQueryParameter.fromId(
|
||||
value?.whenOrNull(
|
||||
fromId: (id) => id,
|
||||
),
|
||||
));
|
||||
},
|
||||
autofocus: true,
|
||||
style: theme.textTheme.bodyLarge?.apply(
|
||||
@@ -124,11 +128,9 @@ class _FullscreenLabelFormState<T extends Label>
|
||||
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,
|
||||
@@ -183,7 +185,8 @@ class _FullscreenLabelFormState<T extends Label>
|
||||
}
|
||||
for (final option in widget.options.values) {
|
||||
// Don't include the initial value in the selection
|
||||
if (option.id == widget.initialValue?.id) {
|
||||
final initialValue = widget.initialValue;
|
||||
if (initialValue is SetIdQueryParameter && option.id == initialValue.id) {
|
||||
continue;
|
||||
}
|
||||
yield IdQueryParameter.fromId(option.id);
|
||||
@@ -191,8 +194,8 @@ class _FullscreenLabelFormState<T extends Label>
|
||||
}
|
||||
} 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);
|
||||
@@ -218,33 +221,18 @@ class _FullscreenLabelFormState<T extends Label>
|
||||
}
|
||||
|
||||
String? _buildHintText() {
|
||||
if (widget.initialValue?.isSet ?? false) {
|
||||
return widget.options[widget.initialValue!.id]!.name;
|
||||
}
|
||||
if (widget.initialValue?.onlyNotAssigned ?? false) {
|
||||
return S.of(context)!.notAssigned;
|
||||
}
|
||||
if (widget.initialValue?.onlyAssigned ?? false) {
|
||||
return S.of(context)!.anyAssigned;
|
||||
}
|
||||
|
||||
return S.of(context)!.startTyping;
|
||||
return widget.initialValue?.when(
|
||||
unset: () => S.of(context)!.startTyping,
|
||||
notAssigned: () => S.of(context)!.notAssigned,
|
||||
anyAssigned: () => S.of(context)!.anyAssigned,
|
||||
fromId: (id) => widget.options[id]!.name,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildOptionWidget(IdQueryParameter option, bool highlight) {
|
||||
void onTap() => widget.onSubmit(returnValue: option);
|
||||
late final String title;
|
||||
|
||||
if (option.isSet) {
|
||||
title = widget.options[option.id]!.name;
|
||||
}
|
||||
if (option.onlyNotAssigned) {
|
||||
title = S.of(context)!.notAssigned;
|
||||
}
|
||||
if (option.onlyAssigned) {
|
||||
title = S.of(context)!.anyAssigned;
|
||||
}
|
||||
if (option.isUnset) {
|
||||
if (option.isUnset()) {
|
||||
return Center(
|
||||
child: Column(
|
||||
children: [
|
||||
@@ -258,6 +246,12 @@ class _FullscreenLabelFormState<T extends Label>
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final title = option.whenOrNull(
|
||||
notAssigned: () => S.of(context)!.notAssigned,
|
||||
anyAssigned: () => S.of(context)!.anyAssigned,
|
||||
fromId: (id) => widget.options[id]!.name,
|
||||
)!; // Never null, since we already return on unset before
|
||||
return ListTile(
|
||||
selected: highlight,
|
||||
selectedTileColor: Theme.of(context).focusColor,
|
||||
|
||||
@@ -45,20 +45,19 @@ class LabelFormField<T extends Label> extends StatelessWidget {
|
||||
}) : super(key: key);
|
||||
|
||||
String _buildText(BuildContext context, IdQueryParameter? value) {
|
||||
if (value?.isSet ?? false) {
|
||||
return options[value!.id]!.name;
|
||||
} else if (value?.onlyNotAssigned ?? false) {
|
||||
return S.of(context)!.notAssigned;
|
||||
} else if (value?.onlyAssigned ?? false) {
|
||||
return S.of(context)!.anyAssigned;
|
||||
}
|
||||
return '';
|
||||
return value?.when(
|
||||
unset: () => '',
|
||||
notAssigned: () => S.of(context)!.notAssigned,
|
||||
anyAssigned: () => S.of(context)!.anyAssigned,
|
||||
fromId: (id) => options[id]!.name,
|
||||
) ??
|
||||
'';
|
||||
}
|
||||
|
||||
@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,
|
||||
@@ -68,8 +67,9 @@ class LabelFormField<T extends Label> extends StatelessWidget {
|
||||
final controller = TextEditingController(
|
||||
text: _buildText(context, field.value),
|
||||
);
|
||||
final displayedSuggestions =
|
||||
suggestions.whereNot((e) => e.id == field.value?.id).toList();
|
||||
final displayedSuggestions = suggestions
|
||||
.whereNot((e) => e.id == field.value?.maybeWhen(fromId: (id) => id, orElse: () => -1))
|
||||
.toList();
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
@@ -93,8 +93,7 @@ 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,
|
||||
),
|
||||
@@ -107,8 +106,7 @@ class LabelFormField<T extends Label> extends StatelessWidget {
|
||||
? (initialName) {
|
||||
return Navigator.of(context).push<T>(
|
||||
MaterialPageRoute(
|
||||
builder: (context) =>
|
||||
addLabelPageBuilder!(initialName),
|
||||
builder: (context) => addLabelPageBuilder!(initialName),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -139,8 +137,7 @@ 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),
|
||||
|
||||
@@ -5,8 +5,8 @@ import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
||||
import 'package:paperless_mobile/features/linked_documents/cubit/linked_documents_cubit.dart';
|
||||
import 'package:paperless_mobile/features/linked_documents/view/linked_documents_page.dart';
|
||||
import 'package:paperless_mobile/features/login/model/user_account.dart';
|
||||
import 'package:paperless_mobile/features/settings/model/global_settings.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/user_account.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/global_settings.dart';
|
||||
import 'package:paperless_mobile/helpers/format_helpers.dart';
|
||||
|
||||
class LabelItem<T extends Label> extends StatelessWidget {
|
||||
@@ -46,10 +46,9 @@ class LabelItem<T extends Label> extends StatelessWidget {
|
||||
onPressed: (label.documentCount ?? 0) == 0
|
||||
? null
|
||||
: () {
|
||||
final currentUser =
|
||||
Hive.box<GlobalSettings>(HiveBoxes.globalSettings)
|
||||
.getValue()!
|
||||
.currentLoggedInUser!;
|
||||
final currentUser = Hive.box<GlobalSettings>(HiveBoxes.globalSettings)
|
||||
.getValue()!
|
||||
.currentLoggedInUser!;
|
||||
final filter = filterBuilder(label);
|
||||
Navigator.push(
|
||||
context,
|
||||
@@ -60,8 +59,7 @@ class LabelItem<T extends Label> extends StatelessWidget {
|
||||
context.read(),
|
||||
context.read(),
|
||||
context.read(),
|
||||
Hive.box<UserAccount>(HiveBoxes.userAccount)
|
||||
.get(currentUser)!,
|
||||
Hive.box<UserAccount>(HiveBoxes.userAccount).get(currentUser)!,
|
||||
),
|
||||
child: const LinkedDocumentsPage(),
|
||||
),
|
||||
|
||||
@@ -3,7 +3,7 @@ import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart';
|
||||
import 'package:paperless_mobile/core/repository/label_repository.dart';
|
||||
import 'package:paperless_mobile/features/login/model/user_account.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/user_account.dart';
|
||||
import 'package:paperless_mobile/features/paged_document_view/cubit/paged_documents_state.dart';
|
||||
import 'package:paperless_mobile/features/paged_document_view/cubit/document_paging_bloc_mixin.dart';
|
||||
import 'package:paperless_mobile/features/settings/model/view_type.dart';
|
||||
@@ -73,4 +73,7 @@ class LinkedDocumentsCubit extends HydratedCubit<LinkedDocumentsState>
|
||||
Map<String, dynamic>? toJson(LinkedDocumentsState state) {
|
||||
return state.toJson();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> onFilterUpdated(DocumentFilter filter) async {}
|
||||
}
|
||||
|
||||
@@ -7,17 +7,18 @@ import 'package:hive_flutter/adapters.dart';
|
||||
import 'package:hydrated_bloc/hydrated_bloc.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/user_app_state.dart';
|
||||
import 'package:paperless_mobile/core/interceptor/dio_http_error_interceptor.dart';
|
||||
import 'package:paperless_mobile/core/repository/label_repository.dart';
|
||||
import 'package:paperless_mobile/core/repository/saved_view_repository.dart';
|
||||
import 'package:paperless_mobile/core/security/session_manager.dart';
|
||||
import 'package:paperless_mobile/features/login/model/client_certificate.dart';
|
||||
import 'package:paperless_mobile/features/login/model/login_form_credentials.dart';
|
||||
import 'package:paperless_mobile/features/login/model/user_account.dart';
|
||||
import 'package:paperless_mobile/features/login/model/user_credentials.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/user_account.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/user_credentials.dart';
|
||||
import 'package:paperless_mobile/features/login/services/authentication_service.dart';
|
||||
import 'package:paperless_mobile/features/settings/model/global_settings.dart';
|
||||
import 'package:paperless_mobile/features/settings/model/user_settings.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/global_settings.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/user_settings.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
part 'authentication_state.dart';
|
||||
|
||||
@@ -58,33 +59,33 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
||||
clientCertificate: clientCertificate,
|
||||
authToken: token,
|
||||
);
|
||||
final userAccountBox = Hive.box<UserAccount>(HiveBoxes.userAccount);
|
||||
final userStateBox = Hive.box<UserAppState>(HiveBoxes.userAppState);
|
||||
|
||||
final userId = "${credentials.username}@$serverUrl";
|
||||
|
||||
// If it is first time login, create settings for this user.
|
||||
final userAccountBox = Hive.box<UserAccount>(HiveBoxes.userAccount);
|
||||
|
||||
final fullName = await _fetchFullName();
|
||||
if (!userAccountBox.containsKey(userId)) {
|
||||
userAccountBox.put(
|
||||
userId,
|
||||
UserAccount(
|
||||
id: userId,
|
||||
settings: UserSettings(
|
||||
currentDocumentFilter: DocumentFilter(),
|
||||
),
|
||||
serverUrl: serverUrl,
|
||||
username: credentials.username!,
|
||||
fullName: fullName,
|
||||
),
|
||||
);
|
||||
if (userAccountBox.containsKey(userId)) {
|
||||
throw Exception("User with id $userId already exists!");
|
||||
}
|
||||
|
||||
// Mark logged in user as currently active user.
|
||||
final globalSettings =
|
||||
Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!;
|
||||
globalSettings.currentLoggedInUser = userId;
|
||||
globalSettings.save();
|
||||
final fullName = await _fetchFullName();
|
||||
// Create user account
|
||||
await userAccountBox.put(
|
||||
userId,
|
||||
UserAccount(
|
||||
id: userId,
|
||||
settings: UserSettings(),
|
||||
serverUrl: serverUrl,
|
||||
username: credentials.username!,
|
||||
fullName: fullName,
|
||||
),
|
||||
);
|
||||
|
||||
// Create user state
|
||||
await userStateBox.put(
|
||||
userId,
|
||||
UserAppState(userId: userId),
|
||||
);
|
||||
|
||||
// Save credentials in encrypted box
|
||||
final userCredentialsBox = await _getUserCredentialsBox();
|
||||
@@ -96,21 +97,25 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
||||
),
|
||||
);
|
||||
userCredentialsBox.close();
|
||||
|
||||
// Mark logged in user as currently active user.
|
||||
final globalSettings = Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!;
|
||||
globalSettings.currentLoggedInUser = userId;
|
||||
await globalSettings.save();
|
||||
|
||||
emit(
|
||||
AuthenticationState(
|
||||
isAuthenticated: true,
|
||||
username: credentials.username,
|
||||
userId: userId,
|
||||
fullName: fullName,
|
||||
//TODO: Query ui settings with full name and add as parameter here...
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Switches to another account if it exists.
|
||||
Future<void> switchAccount(String userId) async {
|
||||
final globalSettings =
|
||||
Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!;
|
||||
final globalSettings = Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!;
|
||||
if (globalSettings.currentLoggedInUser == userId) {
|
||||
return;
|
||||
}
|
||||
@@ -124,8 +129,8 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
||||
final account = userAccountBox.get(userId)!;
|
||||
|
||||
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;
|
||||
@@ -172,6 +177,7 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
||||
final userId = "${credentials.username}@$serverUrl";
|
||||
|
||||
final userAccountsBox = Hive.box<UserAccount>(HiveBoxes.userAccount);
|
||||
final userStateBox = Hive.box<UserAppState>(HiveBoxes.userAppState);
|
||||
|
||||
if (userAccountsBox.containsKey(userId)) {
|
||||
throw Exception("User already exists");
|
||||
@@ -202,12 +208,18 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
||||
username: credentials.username!,
|
||||
settings: UserSettings(
|
||||
isBiometricAuthenticationEnabled: enableBiometricAuthentication,
|
||||
currentDocumentFilter: DocumentFilter(),
|
||||
),
|
||||
fullName: fullName,
|
||||
),
|
||||
);
|
||||
|
||||
await userStateBox.put(
|
||||
userId,
|
||||
UserAppState(
|
||||
userId: userId,
|
||||
),
|
||||
);
|
||||
|
||||
final userCredentialsBox = await _getUserCredentialsBox();
|
||||
await userCredentialsBox.put(
|
||||
userId,
|
||||
@@ -221,14 +233,16 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
||||
}
|
||||
|
||||
Future<void> removeAccount(String userId) async {
|
||||
final globalSettings =
|
||||
Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!;
|
||||
final currentUser = globalSettings.currentLoggedInUser;
|
||||
final globalSettings = Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!;
|
||||
final userAccountBox = Hive.box<UserAccount>(HiveBoxes.userAccount);
|
||||
final userCredentialsBox = await _getUserCredentialsBox();
|
||||
final userAppStateBox = Hive.box<UserAppState>(HiveBoxes.userAppState);
|
||||
final currentUser = globalSettings.currentLoggedInUser;
|
||||
|
||||
await userAccountBox.delete(userId);
|
||||
await userAppStateBox.delete(userId);
|
||||
await userCredentialsBox.delete(userId);
|
||||
await userAccountBox.close();
|
||||
|
||||
if (currentUser == userId) {
|
||||
return logout();
|
||||
@@ -239,54 +253,49 @@ 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 userId = globalSettings.currentLoggedInUser;
|
||||
if (userId == null) {
|
||||
// If there is nothing to restore, we can quit here.
|
||||
return;
|
||||
}
|
||||
|
||||
final userAccount =
|
||||
Hive.box<UserAccount>(HiveBoxes.userAccount).get(userId)!;
|
||||
final userAccount = Hive.box<UserAccount>(HiveBoxes.userAccount).get(userId)!;
|
||||
|
||||
if (userAccount.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(showBiometricAuthenticationScreen: true));
|
||||
emit(const AuthenticationState(showBiometricAuthenticationScreen: true));
|
||||
return;
|
||||
}
|
||||
}
|
||||
final userCredentialsBox = await _getUserCredentialsBox();
|
||||
final authentication = userCredentialsBox.get(globalSettings.currentLoggedInUser!);
|
||||
|
||||
final authentication =
|
||||
userCredentialsBox.get(globalSettings.currentLoggedInUser!);
|
||||
if (authentication != null) {
|
||||
_dioWrapper.updateSettings(
|
||||
clientCertificate: authentication.clientCertificate,
|
||||
authToken: authentication.token,
|
||||
baseUrl: userAccount.serverUrl,
|
||||
serverInformation: PaperlessServerInformationModel(),
|
||||
);
|
||||
emit(
|
||||
AuthenticationState(
|
||||
isAuthenticated: true,
|
||||
showBiometricAuthenticationScreen: false,
|
||||
username: userAccount.username,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
throw Exception(
|
||||
"User should be authenticated but no authentication information was found.");
|
||||
await userCredentialsBox.close();
|
||||
|
||||
if (authentication == null) {
|
||||
throw Exception("User should be authenticated but no authentication information was found.");
|
||||
}
|
||||
_dioWrapper.updateSettings(
|
||||
clientCertificate: authentication.clientCertificate,
|
||||
authToken: authentication.token,
|
||||
baseUrl: userAccount.serverUrl,
|
||||
serverInformation: PaperlessServerInformationModel(),
|
||||
);
|
||||
emit(
|
||||
AuthenticationState(
|
||||
isAuthenticated: true,
|
||||
showBiometricAuthenticationScreen: false,
|
||||
username: userAccount.username,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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
|
||||
..save();
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
import 'package:hive_flutter/adapters.dart';
|
||||
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
||||
import 'package:paperless_mobile/features/settings/model/user_settings.dart';
|
||||
|
||||
part 'user_account.g.dart';
|
||||
|
||||
@HiveType(typeId: HiveTypeIds.userAccount)
|
||||
class UserAccount extends HiveObject {
|
||||
@HiveField(0)
|
||||
final String serverUrl;
|
||||
|
||||
@HiveField(1)
|
||||
final String username;
|
||||
|
||||
@HiveField(2)
|
||||
final String? fullName;
|
||||
|
||||
@HiveField(3)
|
||||
final String id;
|
||||
|
||||
@HiveField(4)
|
||||
UserSettings settings;
|
||||
|
||||
UserAccount({
|
||||
required this.id,
|
||||
required this.serverUrl,
|
||||
required this.username,
|
||||
required this.settings,
|
||||
this.fullName,
|
||||
});
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
||||
import 'package:paperless_mobile/features/login/model/client_certificate.dart';
|
||||
|
||||
part 'user_credentials.g.dart';
|
||||
|
||||
@HiveType(typeId: HiveTypeIds.userCredentials)
|
||||
class UserCredentials extends HiveObject {
|
||||
@HiveField(0)
|
||||
final String token;
|
||||
@HiveField(1)
|
||||
final ClientCertificate? clientCertificate;
|
||||
|
||||
UserCredentials({
|
||||
required this.token,
|
||||
this.clientCertificate,
|
||||
});
|
||||
}
|
||||
@@ -12,7 +12,7 @@ import 'package:paperless_mobile/features/login/view/widgets/form_fields/client_
|
||||
import 'package:paperless_mobile/features/login/view/widgets/form_fields/server_address_form_field.dart';
|
||||
import 'package:paperless_mobile/features/login/view/widgets/form_fields/user_credentials_form_field.dart';
|
||||
import 'package:paperless_mobile/features/login/view/widgets/login_pages/server_connection_page.dart';
|
||||
import 'package:paperless_mobile/features/settings/model/global_settings.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/global_settings.dart';
|
||||
import 'package:paperless_mobile/helpers/message_helpers.dart';
|
||||
|
||||
import 'widgets/login_pages/server_login_page.dart';
|
||||
|
||||
@@ -2,7 +2,6 @@ import 'package:collection/collection.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart';
|
||||
import 'package:paperless_mobile/features/login/model/user_account.dart';
|
||||
|
||||
import 'paged_documents_state.dart';
|
||||
|
||||
@@ -10,11 +9,11 @@ import 'paged_documents_state.dart';
|
||||
/// Mixin which can be used on cubits that handle documents.
|
||||
/// This implements all paging and filtering logic.
|
||||
///
|
||||
mixin DocumentPagingBlocMixin<State extends DocumentPagingState>
|
||||
on BlocBase<State> {
|
||||
mixin DocumentPagingBlocMixin<State extends DocumentPagingState> on BlocBase<State> {
|
||||
PaperlessDocumentsApi get api;
|
||||
DocumentChangedNotifier get notifier;
|
||||
UserAccount get account;
|
||||
|
||||
Future<void> onFilterUpdated(DocumentFilter filter);
|
||||
|
||||
Future<void> loadMore() async {
|
||||
if (state.isLastPageLoaded) {
|
||||
@@ -30,8 +29,7 @@ mixin DocumentPagingBlocMixin<State extends DocumentPagingState>
|
||||
value: [...state.value, result],
|
||||
));
|
||||
} finally {
|
||||
account.settings.currentDocumentFilter = newFilter;
|
||||
account.save();
|
||||
await onFilterUpdated(newFilter);
|
||||
emit(state.copyWithPaged(isLoading: false));
|
||||
}
|
||||
}
|
||||
@@ -52,8 +50,7 @@ mixin DocumentPagingBlocMixin<State extends DocumentPagingState>
|
||||
hasLoaded: true,
|
||||
));
|
||||
} finally {
|
||||
account.settings.currentDocumentFilter = filter;
|
||||
account.save();
|
||||
await onFilterUpdated(filter);
|
||||
emit(state.copyWithPaged(isLoading: false));
|
||||
}
|
||||
}
|
||||
@@ -66,13 +63,11 @@ mixin DocumentPagingBlocMixin<State extends DocumentPagingState>
|
||||
) async =>
|
||||
updateFilter(filter: transformFn(state.filter));
|
||||
|
||||
Future<void> resetFilter() {
|
||||
Future<void> resetFilter() async {
|
||||
final filter = DocumentFilter.initial.copyWith(
|
||||
sortField: state.filter.sortField,
|
||||
sortOrder: state.filter.sortOrder,
|
||||
);
|
||||
account.settings.currentDocumentFilter = filter;
|
||||
account.save();
|
||||
return updateFilter(filter: filter);
|
||||
}
|
||||
|
||||
@@ -90,8 +85,7 @@ mixin DocumentPagingBlocMixin<State extends DocumentPagingState>
|
||||
));
|
||||
}
|
||||
} finally {
|
||||
account.settings.currentDocumentFilter = filter;
|
||||
account.save();
|
||||
await onFilterUpdated(filter);
|
||||
if (!isClosed) {
|
||||
emit(state.copyWithPaged(isLoading: false));
|
||||
}
|
||||
@@ -132,8 +126,7 @@ mixin DocumentPagingBlocMixin<State extends DocumentPagingState>
|
||||
if (index != -1) {
|
||||
final foundPage = state.value[index];
|
||||
final replacementPage = foundPage.copyWith(
|
||||
results: foundPage.results
|
||||
..removeWhere((element) => element.id == document.id),
|
||||
results: foundPage.results..removeWhere((element) => element.id == document.id),
|
||||
);
|
||||
final newCount = foundPage.count - 1;
|
||||
emit(
|
||||
@@ -141,8 +134,7 @@ mixin DocumentPagingBlocMixin<State extends DocumentPagingState>
|
||||
value: state.value
|
||||
.mapIndexed(
|
||||
(currIndex, element) =>
|
||||
(currIndex == index ? replacementPage : element)
|
||||
.copyWith(count: newCount),
|
||||
(currIndex == index ? replacementPage : element).copyWith(count: newCount),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
@@ -165,14 +157,11 @@ mixin DocumentPagingBlocMixin<State extends DocumentPagingState>
|
||||
if (pageIndex != -1) {
|
||||
final foundPage = state.value[pageIndex];
|
||||
final replacementPage = foundPage.copyWith(
|
||||
results: foundPage.results
|
||||
.map((doc) => doc.id == document.id ? document : doc)
|
||||
.toList(),
|
||||
results: foundPage.results.map((doc) => doc.id == document.id ? document : doc).toList(),
|
||||
);
|
||||
final newState = state.copyWithPaged(
|
||||
value: state.value
|
||||
.mapIndexed((currIndex, element) =>
|
||||
currIndex == pageIndex ? replacementPage : element)
|
||||
.mapIndexed((currIndex, element) => currIndex == pageIndex ? replacementPage : element)
|
||||
.toList(),
|
||||
);
|
||||
emit(newState);
|
||||
|
||||
@@ -69,4 +69,7 @@ class SavedViewDetailsCubit extends HydratedCubit<SavedViewDetailsState>
|
||||
Map<String, dynamic>? toJson(SavedViewDetailsState state) {
|
||||
return state.toJson();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> onFilterUpdated(DocumentFilter filter) async {}
|
||||
}
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
||||
import 'package:paperless_mobile/features/settings/model/color_scheme_option.dart';
|
||||
|
||||
part 'global_settings.g.dart';
|
||||
|
||||
@HiveType(typeId: HiveTypeIds.globalSettings)
|
||||
class GlobalSettings with HiveObjectMixin {
|
||||
@HiveField(0)
|
||||
String preferredLocaleSubtag;
|
||||
|
||||
@HiveField(1)
|
||||
ThemeMode preferredThemeMode;
|
||||
|
||||
@HiveField(2)
|
||||
ColorSchemeOption preferredColorSchemeOption;
|
||||
|
||||
@HiveField(3)
|
||||
bool showOnboarding;
|
||||
|
||||
@HiveField(4)
|
||||
String? currentLoggedInUser;
|
||||
|
||||
GlobalSettings({
|
||||
required this.preferredLocaleSubtag,
|
||||
this.preferredThemeMode = ThemeMode.system,
|
||||
this.preferredColorSchemeOption = ColorSchemeOption.classic,
|
||||
this.showOnboarding = true,
|
||||
this.currentLoggedInUser,
|
||||
});
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
||||
|
||||
part 'user_settings.g.dart';
|
||||
|
||||
@HiveType(typeId: HiveTypeIds.userSettings)
|
||||
class UserSettings with HiveObjectMixin {
|
||||
@HiveField(0)
|
||||
bool isBiometricAuthenticationEnabled;
|
||||
|
||||
@HiveField(1)
|
||||
DocumentFilter currentDocumentFilter;
|
||||
|
||||
UserSettings({
|
||||
this.isBiometricAuthenticationEnabled = false,
|
||||
required this.currentDocumentFilter,
|
||||
});
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
||||
import 'package:paperless_mobile/core/widgets/hint_card.dart';
|
||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/features/login/cubit/authentication_cubit.dart';
|
||||
import 'package:paperless_mobile/features/login/model/user_account.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/user_account.dart';
|
||||
import 'package:paperless_mobile/features/settings/view/widgets/global_settings_builder.dart';
|
||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||
import 'package:paperless_mobile/helpers/message_helpers.dart';
|
||||
|
||||
@@ -9,9 +9,9 @@ import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/features/login/cubit/authentication_cubit.dart';
|
||||
import 'package:paperless_mobile/features/login/model/login_form_credentials.dart';
|
||||
import 'package:paperless_mobile/features/login/model/user_account.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/user_account.dart';
|
||||
import 'package:paperless_mobile/features/login/view/login_page.dart';
|
||||
import 'package:paperless_mobile/features/settings/model/global_settings.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/global_settings.dart';
|
||||
import 'package:paperless_mobile/features/settings/view/dialogs/switch_account_dialog.dart';
|
||||
import 'package:paperless_mobile/features/settings/view/pages/switching_accounts_page.dart';
|
||||
import 'package:paperless_mobile/features/settings/view/widgets/global_settings_builder.dart';
|
||||
@@ -26,13 +26,11 @@ class ManageAccountsPage extends StatelessWidget {
|
||||
return GlobalSettingsBuilder(
|
||||
builder: (context, globalSettings) {
|
||||
return ValueListenableBuilder(
|
||||
valueListenable:
|
||||
Hive.box<UserAccount>(HiveBoxes.userAccount).listenable(),
|
||||
valueListenable: Hive.box<UserAccount>(HiveBoxes.userAccount).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: EdgeInsets.all(24),
|
||||
@@ -51,11 +49,8 @@ class ManageAccountsPage extends StatelessWidget {
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
),
|
||||
children: [
|
||||
_buildAccountTile(
|
||||
context,
|
||||
globalSettings.currentLoggedInUser!,
|
||||
box.get(globalSettings.currentLoggedInUser!)!,
|
||||
globalSettings),
|
||||
_buildAccountTile(context, globalSettings.currentLoggedInUser!,
|
||||
box.get(globalSettings.currentLoggedInUser!)!, globalSettings),
|
||||
// if (otherAccounts.isNotEmpty) Text("Other accounts"),
|
||||
Column(
|
||||
children: [
|
||||
@@ -188,8 +183,7 @@ class ManageAccountsPage extends StatelessWidget {
|
||||
MaterialPageRoute(
|
||||
builder: (context) => LoginPage(
|
||||
titleString: "Add account", //TODO: INTL
|
||||
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,
|
||||
@@ -202,8 +196,8 @@ class ManageAccountsPage extends StatelessWidget {
|
||||
);
|
||||
final shoudSwitch = await showDialog(
|
||||
context: context,
|
||||
builder: (context) => SwitchAccountDialog(
|
||||
username: username, serverUrl: serverUrl),
|
||||
builder: (context) =>
|
||||
SwitchAccountDialog(username: username, serverUrl: serverUrl),
|
||||
) ??
|
||||
false;
|
||||
if (shoudSwitch) {
|
||||
|
||||
@@ -3,8 +3,8 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:hive_flutter/adapters.dart';
|
||||
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
||||
import 'package:paperless_mobile/features/login/services/authentication_service.dart';
|
||||
import 'package:paperless_mobile/features/settings/model/global_settings.dart';
|
||||
import 'package:paperless_mobile/features/settings/model/user_settings.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/global_settings.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/user_settings.dart';
|
||||
import 'package:paperless_mobile/features/settings/view/widgets/user_settings_builder.dart';
|
||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
@@ -7,7 +7,7 @@ import 'package:paperless_mobile/constants.dart';
|
||||
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
||||
import 'package:paperless_mobile/core/translation/color_scheme_option_localization_mapper.dart';
|
||||
import 'package:paperless_mobile/core/widgets/hint_card.dart';
|
||||
import 'package:paperless_mobile/features/settings/model/global_settings.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/global_settings.dart';
|
||||
import 'package:paperless_mobile/features/settings/model/color_scheme_option.dart';
|
||||
import 'package:paperless_mobile/features/settings/view/widgets/global_settings_builder.dart';
|
||||
import 'package:paperless_mobile/features/settings/view/widgets/radio_settings_dialog.dart';
|
||||
|
||||
@@ -3,7 +3,7 @@ import 'package:flutter/src/widgets/framework.dart';
|
||||
import 'package:flutter/src/widgets/placeholder.dart';
|
||||
import 'package:hive_flutter/adapters.dart';
|
||||
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
||||
import 'package:paperless_mobile/features/settings/model/global_settings.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/global_settings.dart';
|
||||
|
||||
class GlobalSettingsBuilder extends StatelessWidget {
|
||||
final Widget Function(BuildContext context, GlobalSettings settings) builder;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:hive_flutter/adapters.dart';
|
||||
import 'package:paperless_mobile/features/settings/model/global_settings.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/global_settings.dart';
|
||||
import 'package:paperless_mobile/features/settings/view/widgets/radio_settings_dialog.dart';
|
||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||
import 'package:paperless_mobile/features/settings/view/widgets/global_settings_builder.dart';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:paperless_mobile/features/login/model/user_account.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/user_account.dart';
|
||||
|
||||
class UserAvatar extends StatelessWidget {
|
||||
final String userId;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hive_flutter/adapters.dart';
|
||||
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
||||
import 'package:paperless_mobile/features/login/model/user_account.dart';
|
||||
import 'package:paperless_mobile/features/settings/model/global_settings.dart';
|
||||
import 'package:paperless_mobile/features/settings/model/user_settings.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/user_account.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/global_settings.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/user_settings.dart';
|
||||
|
||||
class UserAccountBuilder extends StatelessWidget {
|
||||
final Widget Function(
|
||||
@@ -19,12 +19,10 @@ class UserAccountBuilder extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ValueListenableBuilder<Box<UserAccount>>(
|
||||
valueListenable:
|
||||
Hive.box<UserAccount>(HiveBoxes.userAccount).listenable(),
|
||||
valueListenable: Hive.box<UserAccount>(HiveBoxes.userAccount).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);
|
||||
|
||||
@@ -7,8 +7,7 @@ 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
|
||||
@@ -60,4 +59,7 @@ class SimilarDocumentsCubit extends Cubit<SimilarDocumentsState>
|
||||
_labelRepository.removeListener(this);
|
||||
return super.close();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> onFilterUpdated(DocumentFilter filter) async {}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user