Added dependencies which fix crash on android 12L/13, improved list layout in inbox

This commit is contained in:
Anton Stubenbord
2023-01-14 19:33:00 +01:00
parent 0eb8e4954c
commit 21462c0463
31 changed files with 492 additions and 234 deletions

2
.gitignore vendored
View File

@@ -60,3 +60,5 @@ untranslated_messages.txt
#lakos generated files #lakos generated files
**/dot_images/* **/dot_images/*
docker/

View File

@@ -82,10 +82,13 @@ flutter {
dependencies { dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'androidx.window:window:1.0.0'
implementation 'androidx.window:window-java:1.0.0'
testImplementation 'junit:junit:4.12' testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test:runner:1.2.0' androidTestImplementation 'androidx.test:runner:1.2.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
// Required for flutter_local_notifications // Required for flutter_local_notifications
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
} }

View File

@@ -1,4 +1,5 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.paperless_mobile"> <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.paperless_mobile">
<application android:label="Paperless Mobile" <application android:label="Paperless Mobile"
android:name="${applicationName}" android:name="${applicationName}"
android:icon="@mipmap/launcher_icon"> android:icon="@mipmap/launcher_icon">
@@ -11,10 +12,14 @@
android:hardwareAccelerated="true" android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize"> android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as <!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user the
while the Flutter UI initializes. After that, this theme continues Android process has started. This theme is visible to the user
to determine the Window background behind the Flutter UI. --> while
<meta-data android:name="io.flutter.embedding.android.NormalTheme" android:resource="@style/NormalTheme" /> the Flutter UI initializes. After that, this theme continues
to
determine the Window background behind the Flutter UI. -->
<meta-data android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme" />
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
@@ -34,14 +39,16 @@
</activity> </activity>
<!-- Don't delete the meta-data below. <!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java --> This is used by the Flutter tool to
generate GeneratedPluginRegistrant.java -->
<meta-data android:name="flutterEmbedding" android:value="2" /> <meta-data android:name="flutterEmbedding" android:value="2" />
</application> </application>
<uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" /> <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" /> android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="29" /> android:maxSdkVersion="29" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
</manifest> </manifest>

View File

@@ -0,0 +1,7 @@
extension SizeLimitedString on String {
String withLengthLimitedTo(int length, [String overflow = "..."]) {
return this.length > length
? '${substring(0, length - overflow.length)}$overflow'
: this;
}
}

View File

@@ -35,13 +35,7 @@ class DocumentUploadCubit extends Cubit<DocumentUploadState> {
_tagRepository = tagRepository, _tagRepository = tagRepository,
_correspondentRepository = correspondentRepository, _correspondentRepository = correspondentRepository,
_documentTypeRepository = documentTypeRepository, _documentTypeRepository = documentTypeRepository,
super( super(const DocumentUploadState()) {
const DocumentUploadState(
tags: {},
correspondents: {},
documentTypes: {},
),
) {
_subs.add(_tagRepository.values.listen( _subs.add(_tagRepository.values.listen(
(tags) => emit(state.copyWith(tags: tags?.values)), (tags) => emit(state.copyWith(tags: tags?.values)),
)); ));

View File

@@ -7,9 +7,9 @@ class DocumentUploadState extends Equatable {
final Map<int, DocumentType> documentTypes; final Map<int, DocumentType> documentTypes;
const DocumentUploadState({ const DocumentUploadState({
required this.tags, this.tags = const {},
required this.correspondents, this.correspondents = const {},
required this.documentTypes, this.documentTypes = const {},
}); });
@override @override

View File

@@ -236,8 +236,10 @@ class _DocumentUploadPreparationPageState
final taskId = await cubit.upload( final taskId = await cubit.upload(
widget.fileBytes, widget.fileBytes,
filename: filename: _padWithExtension(
_padWithPdfExtension(_formKey.currentState?.value[fkFileName]), _formKey.currentState?.value[fkFileName],
widget.fileExtension,
),
title: title, title: title,
documentType: docType.id, documentType: docType.id,
correspondent: correspondent.id, correspondent: correspondent.id,
@@ -261,11 +263,12 @@ class _DocumentUploadPreparationPageState
} }
} }
String _padWithPdfExtension(String source) { String _padWithExtension(String source, [String? extension]) {
return source.endsWith(".pdf") ? source : '$source.pdf'; final ext = extension ?? '.pdf';
return source.endsWith(ext) ? source : '$source$ext';
} }
String _formatFilename(String source) { String _formatFilename(String source) {
return source.replaceAll(RegExp(r"[\W_]"), "_"); return source.replaceAll(RegExp(r"[\W_]"), "_").toLowerCase();
} }
} }

View File

@@ -219,6 +219,11 @@ class DocumentsCubit extends Cubit<DocumentsState> with HydratedMixin {
} }
} }
Future<Iterable<String>> autocomplete(String query) async {
final res = await _api.autocomplete(query);
return res;
}
void unselectView() { void unselectView() {
emit(state.copyWith(selectedSavedViewId: null)); emit(state.copyWith(selectedSavedViewId: null));
} }

View File

@@ -309,7 +309,7 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
"Suggestions: ", S.of(context).documentEditPageSuggestionsLabel,
style: Theme.of(context).textTheme.bodySmall, style: Theme.of(context).textTheme.bodySmall,
), ),
SizedBox( SizedBox(

View File

@@ -50,7 +50,7 @@ class _DocumentsPageState extends State<DocumentsPage> {
double _offset = 0; double _offset = 0;
double _last = 0; double _last = 0;
static const double _savedViewWidgetHeight = 78 + 16; static const double _savedViewWidgetHeight = 80 + 16;
@override @override
void initState() { void initState() {

View File

@@ -36,62 +36,60 @@ class DocumentListItem extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SizedBox( return ListTile(
child: ListTile( dense: true,
dense: true, selected: isSelected,
selected: isSelected, onTap: () => _onTap(),
onTap: () => _onTap(), selectedTileColor: Theme.of(context).colorScheme.inversePrimary,
selectedTileColor: Theme.of(context).colorScheme.inversePrimary, onLongPress: () => onSelected?.call(document),
onLongPress: () => onSelected?.call(document), title: Column(
title: Column( crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.spaceAround,
mainAxisAlignment: MainAxisAlignment.spaceAround, children: [
children: [ Row(
Row( children: [
children: [ AbsorbPointer(
AbsorbPointer( absorbing: isAtLeastOneSelected,
absorbing: isAtLeastOneSelected, child: CorrespondentWidget(
child: CorrespondentWidget( isClickable: isLabelClickable,
isClickable: isLabelClickable, correspondentId: document.correspondent,
correspondentId: document.correspondent, onSelected: onCorrespondentSelected,
onSelected: onCorrespondentSelected,
),
), ),
], ),
), ],
Text(
document.title,
overflow: TextOverflow.ellipsis,
maxLines: document.tags.isEmpty ? 2 : 1,
),
],
),
subtitle: Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: AbsorbPointer(
absorbing: isAtLeastOneSelected,
child: TagsWidget(
isClickable: isLabelClickable,
tagIds: document.tags,
isMultiLine: false,
isSelectedPredicate: isTagSelectedPredicate,
onTagSelected: (id) => onTagSelected?.call(id),
),
), ),
), Text(
isThreeLine: document.tags.isNotEmpty, document.title,
leading: AspectRatio( overflow: TextOverflow.ellipsis,
aspectRatio: _a4AspectRatio, maxLines: document.tags.isEmpty ? 2 : 1,
child: GestureDetector(
child: DocumentPreview(
id: document.id,
fit: BoxFit.cover,
alignment: Alignment.topCenter,
),
), ),
), ],
contentPadding: const EdgeInsets.all(8.0),
), ),
subtitle: Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: AbsorbPointer(
absorbing: isAtLeastOneSelected,
child: TagsWidget(
isClickable: isLabelClickable,
tagIds: document.tags,
isMultiLine: false,
isSelectedPredicate: isTagSelectedPredicate,
onTagSelected: (id) => onTagSelected?.call(id),
),
),
),
isThreeLine: document.tags.isNotEmpty,
leading: AspectRatio(
aspectRatio: _a4AspectRatio,
child: GestureDetector(
child: DocumentPreview(
id: document.id,
fit: BoxFit.cover,
alignment: Alignment.topCenter,
),
),
),
contentPadding: const EdgeInsets.all(8.0),
); );
} }

View File

@@ -1,7 +1,10 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/widgets/form_builder_fields/form_builder_type_ahead.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart';
import 'package:paperless_mobile/generated/l10n.dart'; import 'package:paperless_mobile/generated/l10n.dart';
import 'package:provider/provider.dart';
class TextQueryFormField extends StatelessWidget { class TextQueryFormField extends StatelessWidget {
final String name; final String name;
@@ -21,57 +24,69 @@ class TextQueryFormField extends StatelessWidget {
name: name, name: name,
initialValue: initialValue, initialValue: initialValue,
builder: (field) { builder: (field) {
return TextFormField( return Autocomplete(
initialValue: initialValue?.queryText, optionsBuilder: (value) =>
textInputAction: TextInputAction.done, context.read<DocumentsCubit>().autocomplete(value.text),
decoration: InputDecoration( initialValue: initialValue?.queryText != null
prefixIcon: const Icon(Icons.search_outlined), ? TextEditingValue(text: initialValue!.queryText!)
labelText: _buildLabelText(context, field.value!.queryType), : null,
suffixIcon: PopupMenuButton<QueryType>( fieldViewBuilder:
icon: onlyExtendedQueryAllowed (context, textEditingController, focusNode, onFieldSubmitted) {
? Icon( return TextFormField(
Icons.more_vert, controller: textEditingController,
color: Theme.of(context).disabledColor, focusNode: focusNode,
) decoration: InputDecoration(
: null, prefixIcon: const Icon(Icons.search_outlined),
enabled: !onlyExtendedQueryAllowed, labelText: _buildLabelText(context, field.value!.queryType),
itemBuilder: (context) => [ suffixIcon: _buildQueryTypeMenu(context, field),
PopupMenuItem( ),
child: ListTile( onChanged: (value) {
title: Text(S field.didChange(field.value?.copyWith(queryText: value));
.of(context)
.documentFilterQueryOptionsTitleAndContentLabel),
),
value: QueryType.titleAndContent,
),
PopupMenuItem(
child: ListTile(
title: Text(
S.of(context).documentFilterQueryOptionsTitleLabel),
),
value: QueryType.title,
),
PopupMenuItem(
child: ListTile(
title: Text(
S.of(context).documentFilterQueryOptionsExtendedLabel),
),
value: QueryType.extended,
),
],
onSelected: (selection) {
field.didChange(field.value?.copyWith(queryType: selection));
}, },
), );
),
onChanged: (value) {
field.didChange(field.value?.copyWith(queryText: value));
}, },
); );
}, },
); );
} }
PopupMenuButton<QueryType> _buildQueryTypeMenu(
BuildContext context, FormFieldState<TextQuery> field) {
return PopupMenuButton<QueryType>(
icon: onlyExtendedQueryAllowed
? Icon(
Icons.more_vert,
color: Theme.of(context).disabledColor,
)
: null,
enabled: !onlyExtendedQueryAllowed,
itemBuilder: (context) => [
PopupMenuItem(
child: ListTile(
title: Text(
S.of(context).documentFilterQueryOptionsTitleAndContentLabel),
),
value: QueryType.titleAndContent,
),
PopupMenuItem(
child: ListTile(
title: Text(S.of(context).documentFilterQueryOptionsTitleLabel),
),
value: QueryType.title,
),
PopupMenuItem(
child: ListTile(
title: Text(S.of(context).documentFilterQueryOptionsExtendedLabel),
),
value: QueryType.extended,
),
],
onSelected: (selection) {
field.didChange(field.value?.copyWith(queryType: selection));
},
);
}
String _buildLabelText(BuildContext context, QueryType queryType) { String _buildLabelText(BuildContext context, QueryType queryType) {
switch (queryType) { switch (queryType) {
case QueryType.title: case QueryType.title:

View File

@@ -1,14 +1,14 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_state.dart'; import 'package:paperless_mobile/features/documents/bloc/documents_state.dart';
import 'package:paperless_mobile/generated/l10n.dart'; import 'package:paperless_mobile/generated/l10n.dart';
class BulkDeleteConfirmationDialog extends StatelessWidget { class BulkDeleteConfirmationDialog extends StatelessWidget {
static const _bulletPoint = "\u2022";
final DocumentsState state; final DocumentsState state;
const BulkDeleteConfirmationDialog({Key? key, required this.state}) const BulkDeleteConfirmationDialog({
: super(key: key); Key? key,
required this.state,
}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -29,13 +29,7 @@ class BulkDeleteConfirmationDialog extends StatelessWidget {
.documentsPageSelectionBulkDeleteDialogWarningTextMany, .documentsPageSelectionBulkDeleteDialogWarningTextMany,
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
ConstrainedBox( ...state.selection.map(_buildBulletPoint).toList(),
constraints: const BoxConstraints(maxHeight: 150),
child: ListView(
shrinkWrap: true,
children: state.selection.map(_buildBulletPoint).toList(),
),
),
const SizedBox(height: 16), const SizedBox(height: 16),
Text( Text(
S.of(context).documentsPageSelectionBulkDeleteDialogContinueText), S.of(context).documentsPageSelectionBulkDeleteDialogContinueText),
@@ -61,12 +55,15 @@ class BulkDeleteConfirmationDialog extends StatelessWidget {
} }
Widget _buildBulletPoint(DocumentModel doc) { Widget _buildBulletPoint(DocumentModel doc) {
return Text( return ListTile(
"\t$_bulletPoint ${doc.title}", dense: true,
maxLines: 1, title: Text(
overflow: TextOverflow.ellipsis, doc.title,
style: const TextStyle( maxLines: 1,
fontWeight: FontWeight.w700, overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontWeight: FontWeight.w700,
),
), ),
); );
} }

View File

@@ -32,6 +32,7 @@ import 'package:paperless_mobile/generated/l10n.dart';
import 'package:paperless_mobile/util.dart'; import 'package:paperless_mobile/util.dart';
import 'package:receive_sharing_intent/receive_sharing_intent.dart'; import 'package:receive_sharing_intent/receive_sharing_intent.dart';
import 'package:path/path.dart' as p; import 'package:path/path.dart' as p;
import 'package:responsive_builder/responsive_builder.dart';
class HomePage extends StatefulWidget { class HomePage extends StatefulWidget {
const HomePage({Key? key}) : super(key: key); const HomePage({Key? key}) : super(key: key);
@@ -165,44 +166,114 @@ class _HomePageState extends State<HomePage> {
}, },
), ),
], ],
child: Scaffold( child: ResponsiveBuilder(
key: rootScaffoldKey, builder: (context, sizingInformation) {
bottomNavigationBar: BottomNavBar( if (!sizingInformation.isMobile) {
selectedIndex: _currentIndex, return Scaffold(
onNavigationChanged: (index) { key: rootScaffoldKey,
if (_currentIndex != index) { drawer: const InfoDrawer(),
setState(() => _currentIndex = index); body: Row(children: [
} NavigationRail(
}, labelType: NavigationRailLabelType.all,
), destinations: [
drawer: const InfoDrawer(), NavigationRailDestination(
body: [ icon: const Icon(Icons.description_outlined),
MultiBlocProvider( selectedIcon: Icon(
providers: [ Icons.description,
BlocProvider( color: Theme.of(context).colorScheme.primary,
create: (context) => DocumentsCubit( ),
context.read<PaperlessDocumentsApi>(), label: Text(S.of(context).bottomNavDocumentsPageLabel),
context.read<SavedViewRepository>(), ),
NavigationRailDestination(
icon: const Icon(Icons.document_scanner_outlined),
selectedIcon: Icon(
Icons.document_scanner,
color: Theme.of(context).colorScheme.primary,
),
label: Text(S.of(context).bottomNavScannerPageLabel),
),
NavigationRailDestination(
icon: const Icon(Icons.sell_outlined),
selectedIcon: Icon(
Icons.sell,
color: Theme.of(context).colorScheme.primary,
),
label: Text(S.of(context).bottomNavLabelsPageLabel),
),
],
selectedIndex: _currentIndex,
onDestinationSelected: _onNavigationChanged,
), ),
const VerticalDivider(thickness: 1, width: 1),
Expanded(
child: [
MultiBlocProvider(
providers: [
BlocProvider(
create: (context) => DocumentsCubit(
context.read<PaperlessDocumentsApi>(),
context.read<SavedViewRepository>(),
),
),
BlocProvider(
create: (context) => SavedViewCubit(
context.read<SavedViewRepository>(),
),
),
],
child: const DocumentsPage(),
),
BlocProvider.value(
value: _scannerCubit,
child: const ScannerPage(),
),
const LabelsPage(),
][_currentIndex]),
]),
);
}
return Scaffold(
key: rootScaffoldKey,
bottomNavigationBar: BottomNavBar(
selectedIndex: _currentIndex,
onNavigationChanged: _onNavigationChanged,
),
drawer: const InfoDrawer(),
body: [
MultiBlocProvider(
providers: [
BlocProvider(
create: (context) => DocumentsCubit(
context.read<PaperlessDocumentsApi>(),
context.read<SavedViewRepository>(),
),
),
BlocProvider(
create: (context) => SavedViewCubit(
context.read<SavedViewRepository>(),
),
),
],
child: const DocumentsPage(),
), ),
BlocProvider( BlocProvider.value(
create: (context) => SavedViewCubit( value: _scannerCubit,
context.read<SavedViewRepository>(), child: const ScannerPage(),
),
), ),
], const LabelsPage(),
child: const DocumentsPage(), ][_currentIndex],
), );
BlocProvider.value( },
value: _scannerCubit,
child: const ScannerPage(),
),
const LabelsPage(),
][_currentIndex],
), ),
); );
} }
void _onNavigationChanged(index) {
if (_currentIndex != index) {
setState(() => _currentIndex = index);
}
}
void _initializeData(BuildContext context) { void _initializeData(BuildContext context) {
try { try {
context.read<LabelRepository<Tag, TagRepositoryState>>().findAll(); context.read<LabelRepository<Tag, TagRepositoryState>>().findAll();

View File

@@ -122,6 +122,19 @@ 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));
emit(
state.copyWith(
inboxItems: state.inboxItems
.map((e) => e.id == document.id ? updatedDocument : e)),
);
}
}
void acknowledgeHint() { void acknowledgeHint() {
emit(state.copyWith(isHintAcknowledged: true)); emit(state.copyWith(isHintAcknowledged: true));
} }

View File

@@ -3,11 +3,20 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/provider/label_repositories_provider.dart'; import 'package:paperless_mobile/core/repository/provider/label_repositories_provider.dart';
import 'package:paperless_mobile/core/repository/state/impl/correspondent_repository_state.dart';
import 'package:paperless_mobile/core/repository/state/impl/document_type_repository_state.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/document_details/bloc/document_details_cubit.dart'; import 'package:paperless_mobile/features/document_details/bloc/document_details_cubit.dart';
import 'package:paperless_mobile/features/document_details/view/pages/document_details_page.dart'; import 'package:paperless_mobile/features/document_details/view/pages/document_details_page.dart';
import 'package:paperless_mobile/features/documents/view/widgets/document_preview.dart'; import 'package:paperless_mobile/features/documents/view/widgets/document_preview.dart';
import 'package:paperless_mobile/features/inbox/bloc/inbox_cubit.dart'; import 'package:paperless_mobile/features/inbox/bloc/inbox_cubit.dart';
import 'package:paperless_mobile/features/labels/correspondent/view/widgets/correspondent_widget.dart';
import 'package:paperless_mobile/features/labels/document_type/view/widgets/document_type_widget.dart';
import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_widget.dart'; import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_widget.dart';
import 'package:paperless_mobile/features/labels/view/widgets/label_text.dart';
import 'package:paperless_mobile/generated/l10n.dart';
import 'package:badges/badges.dart' as b;
import 'package:paperless_mobile/extensions/string_extensions.dart';
class InboxItem extends StatelessWidget { class InboxItem extends StatelessWidget {
static const _a4AspectRatio = 1 / 1.4142; static const _a4AspectRatio = 1 / 1.4142;
@@ -23,7 +32,31 @@ class InboxItem extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ListTile( return ListTile(
title: Text(document.title), title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
IntrinsicHeight(
child: Wrap(
direction: Axis.horizontal,
children: [
Row(
children: [
Text(
document.title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodyLarge,
)
],
),
Row(
children: [],
),
],
),
),
],
),
isThreeLine: true, isThreeLine: true,
leading: AspectRatio( leading: AspectRatio(
aspectRatio: _a4AspectRatio, aspectRatio: _a4AspectRatio,
@@ -37,16 +70,54 @@ class InboxItem extends StatelessWidget {
subtitle: Column( subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text(DateFormat().format(document.added)), Row(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.person_outline,
size: Theme.of(context).textTheme.bodySmall?.fontSize,
),
Flexible(
child: LabelText<Correspondent, CorrespondentRepositoryState>(
id: document.correspondent,
style: Theme.of(context).textTheme.bodySmall,
),
),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.description_outlined,
size: Theme.of(context).textTheme.bodySmall?.fontSize,
),
Flexible(
child: LabelText<DocumentType, DocumentTypeRepositoryState>(
id: document.documentType,
style: Theme.of(context).textTheme.bodySmall,
),
),
],
),
TagsWidget( TagsWidget(
tagIds: document.tags, tagIds: document.tags,
isMultiLine: false, isMultiLine: false,
isClickable: false, isClickable: false,
isSelectedPredicate: (_) => false, isSelectedPredicate: (_) => false,
onTagSelected: (_) {}, showShortNames: true,
dense: true,
), ),
], ],
), ),
trailing: document.archiveSerialNumber != null
? Text(
document.archiveSerialNumber!.toString(),
style: Theme.of(context).textTheme.bodySmall,
)
: null,
onTap: () async { onTap: () async {
final returnedDocument = await Navigator.push<DocumentModel?>( final returnedDocument = await Navigator.push<DocumentModel?>(
context, context,

View File

@@ -7,6 +7,8 @@ class TagWidget extends StatelessWidget {
final VoidCallback onSelected; final VoidCallback onSelected;
final bool isSelected; final bool isSelected;
final bool isClickable; final bool isClickable;
final bool showShortName;
final bool dense;
const TagWidget({ const TagWidget({
super.key, super.key,
@@ -15,6 +17,8 @@ class TagWidget extends StatelessWidget {
this.isClickable = true, this.isClickable = true,
required this.onSelected, required this.onSelected,
required this.isSelected, required this.isSelected,
this.showShortName = false,
this.dense = false,
}); });
@override @override
@@ -24,13 +28,18 @@ class TagWidget extends StatelessWidget {
child: AbsorbPointer( child: AbsorbPointer(
absorbing: !isClickable, absorbing: !isClickable,
child: FilterChip( child: FilterChip(
labelPadding:
dense ? const EdgeInsets.symmetric(horizontal: 2) : null,
padding: dense ? const EdgeInsets.all(4) : null,
selected: isSelected, selected: isSelected,
selectedColor: tag.color, selectedColor: tag.color,
onSelected: (_) => onSelected(), onSelected: (_) => onSelected(),
visualDensity: const VisualDensity(vertical: -2), visualDensity: const VisualDensity(vertical: -2),
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
label: Text( label: Text(
tag.name, showShortName && tag.name.length > 6
? '${tag.name.substring(0, 6)}...'
: tag.name,
style: TextStyle(color: tag.textColor), style: TextStyle(color: tag.textColor),
), ),
checkmarkColor: tag.textColor, checkmarkColor: tag.textColor,

View File

@@ -6,13 +6,15 @@ import 'package:paperless_mobile/features/labels/bloc/label_state.dart';
import 'package:paperless_mobile/features/labels/bloc/providers/tag_bloc_provider.dart'; import 'package:paperless_mobile/features/labels/bloc/providers/tag_bloc_provider.dart';
import 'package:paperless_mobile/features/labels/tags/view/widgets/tag_widget.dart'; import 'package:paperless_mobile/features/labels/tags/view/widgets/tag_widget.dart';
class TagsWidget extends StatefulWidget { class TagsWidget extends StatelessWidget {
final Iterable<int> tagIds; final Iterable<int> tagIds;
final bool isMultiLine; final bool isMultiLine;
final VoidCallback? afterTagTapped; final VoidCallback? afterTagTapped;
final void Function(int tagId)? onTagSelected; final void Function(int tagId)? onTagSelected;
final bool isClickable; final bool isClickable;
final bool Function(int id) isSelectedPredicate; final bool Function(int id) isSelectedPredicate;
final bool showShortNames;
final bool dense;
const TagsWidget({ const TagsWidget({
Key? key, Key? key,
@@ -21,32 +23,31 @@ class TagsWidget extends StatefulWidget {
this.isMultiLine = true, this.isMultiLine = true,
this.isClickable = true, this.isClickable = true,
required this.isSelectedPredicate, required this.isSelectedPredicate,
required this.onTagSelected, this.onTagSelected,
this.showShortNames = false,
this.dense = false,
}) : super(key: key); }) : super(key: key);
@override
State<TagsWidget> createState() => _TagsWidgetState();
}
class _TagsWidgetState extends State<TagsWidget> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return TagBlocProvider( return TagBlocProvider(
child: BlocBuilder<LabelCubit<Tag>, LabelState<Tag>>( child: BlocBuilder<LabelCubit<Tag>, LabelState<Tag>>(
builder: (context, state) { builder: (context, state) {
final children = widget.tagIds final children = tagIds
.where((id) => state.labels.containsKey(id)) .where((id) => state.labels.containsKey(id))
.map( .map(
(id) => TagWidget( (id) => TagWidget(
tag: state.getLabel(id)!, tag: state.getLabel(id)!,
afterTagTapped: widget.afterTagTapped, afterTagTapped: afterTagTapped,
isClickable: widget.isClickable, isClickable: isClickable,
isSelected: widget.isSelectedPredicate(id), isSelected: isSelectedPredicate(id),
onSelected: () => widget.onTagSelected?.call(id), onSelected: () => onTagSelected?.call(id),
showShortName: showShortNames,
dense: dense,
), ),
) )
.toList(); .toList();
if (widget.isMultiLine) { if (isMultiLine) {
return Wrap( return Wrap(
runAlignment: WrapAlignment.start, runAlignment: WrapAlignment.start,
children: children, children: children,

View File

@@ -0,0 +1,40 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/repository/state/repository_state.dart';
import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart';
import 'package:paperless_mobile/features/labels/bloc/label_state.dart';
import 'package:paperless_mobile/features/labels/bloc/providers/document_type_bloc_provider.dart';
class LabelText<T extends Label, State extends RepositoryState>
extends StatelessWidget {
final int? id;
final String placeholder;
final TextStyle? style;
const LabelText({
super.key,
this.style,
this.id,
this.placeholder = "",
});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => LabelCubit<T>(
context.read<LabelRepository<T, State>>(),
),
child: BlocBuilder<LabelCubit<T>, LabelState<T>>(
builder: (context, state) {
return Text(
state.labels[id]?.toString() ?? placeholder,
style: style,
);
},
),
);
;
}
}

View File

@@ -1,3 +1,5 @@
import 'dart:developer';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:form_builder_validators/form_builder_validators.dart'; import 'package:form_builder_validators/form_builder_validators.dart';
@@ -6,7 +8,7 @@ import 'package:paperless_mobile/generated/l10n.dart';
class ServerAddressFormField extends StatefulWidget { class ServerAddressFormField extends StatefulWidget {
static const String fkServerAddress = "serverAddress"; static const String fkServerAddress = "serverAddress";
final void Function(String address) onDone; final void Function(String? address) onDone;
const ServerAddressFormField({ const ServerAddressFormField({
Key? key, Key? key,
required this.onDone, required this.onDone,
@@ -31,7 +33,7 @@ class _ServerAddressFormFieldState extends State<ServerAddressFormField> {
}); });
} }
final TextEditingController _textEditingController = TextEditingController(); final _textEditingController = TextEditingController();
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -56,7 +58,7 @@ class _ServerAddressFormFieldState extends State<ServerAddressFormField> {
labelText: S.of(context).loginPageServerUrlFieldLabel, labelText: S.of(context).loginPageServerUrlFieldLabel,
suffixIcon: _canClear suffixIcon: _canClear
? IconButton( ? IconButton(
icon: Icon(Icons.clear), icon: const Icon(Icons.clear),
color: Theme.of(context).iconTheme.color, color: Theme.of(context).iconTheme.color,
onPressed: () { onPressed: () {
_textEditingController.clear(); _textEditingController.clear();
@@ -64,18 +66,14 @@ class _ServerAddressFormFieldState extends State<ServerAddressFormField> {
) )
: null, : null,
), ),
onSubmitted: (value) { onSubmitted: (_) => _formatInput(),
if (value == null) return;
// Remove trailing slash if it is a valid address.
String address = value.trim();
address = _replaceTrailingSlashes(address);
_textEditingController.text = address;
widget.onDone(address);
},
); );
} }
String _replaceTrailingSlashes(String src) { void _formatInput() {
return src.replaceAll(RegExp(r'^\/+|\/+$'), ''); String address = _textEditingController.text.trim();
address = address.replaceAll(RegExp(r'^\/+|\/+$'), '');
_textEditingController.text = address;
widget.onDone(address);
} }
} }

View File

@@ -58,8 +58,12 @@ class _ServerConnectionPageState extends State<ServerConnectionPage> {
), ),
bottomNavigationBar: BottomAppBar( bottomNavigationBar: BottomAppBar(
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
TextButton(
child: Text("Test connection"),
onPressed: _updateReachability,
),
FilledButton( FilledButton(
child: Text(S.of(context).loginPageContinueLabel), child: Text(S.of(context).loginPageContinueLabel),
onPressed: _reachabilityStatus == ReachabilityStatus.reachable onPressed: _reachabilityStatus == ReachabilityStatus.reachable
@@ -76,6 +80,7 @@ class _ServerConnectionPageState extends State<ServerConnectionPage> {
setState(() { setState(() {
_isCheckingConnection = true; _isCheckingConnection = true;
}); });
final status = await context final status = await context
.read<ConnectivityStatusService>() .read<ConnectivityStatusService>()
.isPaperlessServerReachable( .isPaperlessServerReachable(

View File

@@ -32,6 +32,10 @@ class LocalNotificationService {
initializationSettings, initializationSettings,
onDidReceiveNotificationResponse: onDidReceiveNotificationResponse, onDidReceiveNotificationResponse: onDidReceiveNotificationResponse,
); );
await _plugin
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin>()
?.requestPermission();
} }
//TODO: INTL //TODO: INTL

View File

@@ -142,31 +142,29 @@ class _ScannerPageState extends State<ScannerPage>
final file = await _assembleFileBytes( final file = await _assembleFileBytes(
context.read<DocumentScannerCubit>().state, context.read<DocumentScannerCubit>().state,
); );
final taskId = await Navigator.of(context).push( final taskId = await Navigator.of(context).push<String?>(
MaterialPageRoute( MaterialPageRoute(
builder: (_) => LabelRepositoriesProvider( builder: (_) => LabelRepositoriesProvider(
child: BlocProvider( child: BlocProvider(
create: (context) => DocumentUploadCubit( create: (context) => DocumentUploadCubit(
localVault: context.read<LocalVault>(), localVault: context.read<LocalVault>(),
documentApi: context.read<PaperlessDocumentsApi>(), documentApi: context.read<PaperlessDocumentsApi>(),
correspondentRepository: context.read< correspondentRepository: context.read<
LabelRepository<Correspondent, LabelRepository<Correspondent,
CorrespondentRepositoryState>>(), CorrespondentRepositoryState>>(),
documentTypeRepository: context.read< documentTypeRepository: context.read<
LabelRepository<DocumentType, LabelRepository<DocumentType, DocumentTypeRepositoryState>>(),
DocumentTypeRepositoryState>>(), tagRepository:
tagRepository: context.read<LabelRepository<Tag, TagRepositoryState>>(),
context.read<LabelRepository<Tag, TagRepositoryState>>(), ),
), child: DocumentUploadPreparationPage(
child: DocumentUploadPreparationPage( fileBytes: file.bytes,
fileBytes: file.bytes, fileExtension: file.extension,
fileExtension: file.extension,
),
),
), ),
), ),
) ?? ),
false; ),
);
if (taskId != null) { if (taskId != null) {
// For paperless version older than 1.11.3, task id will always be null! // For paperless version older than 1.11.3, task id will always be null!
context.read<DocumentScannerCubit>().reset(); context.read<DocumentScannerCubit>().reset();

View File

@@ -563,5 +563,6 @@
"verifyIdentityPageTitle": "", "verifyIdentityPageTitle": "",
"@verifyIdentityPageTitle": {}, "@verifyIdentityPageTitle": {},
"verifyIdentityPageVerifyIdentityButtonLabel": "", "verifyIdentityPageVerifyIdentityButtonLabel": "",
"@verifyIdentityPageVerifyIdentityButtonLabel": {} "@verifyIdentityPageVerifyIdentityButtonLabel": {},
"inboxPageAssignAsnLabel": "Assign ASN"
} }

View File

@@ -563,5 +563,6 @@
"verifyIdentityPageTitle": "Verifiziere deine Identität", "verifyIdentityPageTitle": "Verifiziere deine Identität",
"@verifyIdentityPageTitle": {}, "@verifyIdentityPageTitle": {},
"verifyIdentityPageVerifyIdentityButtonLabel": "Identität verifizieren", "verifyIdentityPageVerifyIdentityButtonLabel": "Identität verifizieren",
"@verifyIdentityPageVerifyIdentityButtonLabel": {} "@verifyIdentityPageVerifyIdentityButtonLabel": {},
"inboxPageAssignAsnLabel": "Assign ASN"
} }

View File

@@ -563,5 +563,6 @@
"verifyIdentityPageTitle": "Verify your identity", "verifyIdentityPageTitle": "Verify your identity",
"@verifyIdentityPageTitle": {}, "@verifyIdentityPageTitle": {},
"verifyIdentityPageVerifyIdentityButtonLabel": "Verify Identity", "verifyIdentityPageVerifyIdentityButtonLabel": "Verify Identity",
"@verifyIdentityPageVerifyIdentityButtonLabel": {} "@verifyIdentityPageVerifyIdentityButtonLabel": {},
"inboxPageAssignAsnLabel": "Assign ASN"
} }

View File

@@ -113,8 +113,9 @@ void main() async {
authApi, authApi,
sessionManager, sessionManager,
); );
await authCubit await authCubit.restoreSessionState(
.restoreSessionState(appSettingsCubit.state.isLocalAuthenticationEnabled); appSettingsCubit.state.isLocalAuthenticationEnabled,
);
if (authCubit.state.isAuthenticated) { if (authCubit.state.isAuthenticated) {
final auth = authCubit.state.authentication!; final auth = authCubit.state.authentication!;

View File

@@ -1,3 +1,4 @@
import 'package:equatable/equatable.dart';
import 'package:json_annotation/json_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
import 'query_type.dart'; import 'query_type.dart';
@@ -5,7 +6,7 @@ import 'query_type.dart';
part 'text_query.g.dart'; part 'text_query.g.dart';
@JsonSerializable() @JsonSerializable()
class TextQuery { class TextQuery extends Equatable {
final QueryType queryType; final QueryType queryType;
final String? queryText; final String? queryText;
@@ -61,4 +62,7 @@ class TextQuery {
factory TextQuery.fromJson(Map<String, dynamic> json) => factory TextQuery.fromJson(Map<String, dynamic> json) =>
_$TextQueryFromJson(json); _$TextQueryFromJson(json);
@override
List<Object?> get props => [queryType, queryText];
} }

View File

@@ -219,12 +219,12 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi {
final response = await client.get( final response = await client.get(
'/api/search/autocomplete/', '/api/search/autocomplete/',
queryParameters: { queryParameters: {
'query': query, 'term': query,
'limit': limit, 'limit': limit,
}, },
); );
if (response.statusCode == 200) { if (response.statusCode == 200) {
return response.data as List<String>; return (response.data as List).cast<String>();
} }
throw const PaperlessServerException(ErrorCode.autocompleteQueryError); throw const PaperlessServerException(ErrorCode.autocompleteQueryError);
} on DioError catch (err) { } on DioError catch (err) {

View File

@@ -1292,6 +1292,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.4.5" version: "1.4.5"
responsive_builder:
dependency: "direct main"
description:
name: responsive_builder
sha256: f01bc341c73b6db7bd6319e22d2c160f28f924399ae46e6699ecc8160ba2765c
url: "https://pub.dev"
source: hosted
version: "0.4.3"
rxdart: rxdart:
dependency: "direct main" dependency: "direct main"
description: description:

View File

@@ -85,6 +85,7 @@ dependencies:
device_info_plus: ^4.1.3 device_info_plus: ^4.1.3
flutter_local_notifications: ^13.0.0 flutter_local_notifications: ^13.0.0
flutter_staggered_grid_view: ^0.6.2 flutter_staggered_grid_view: ^0.6.2
responsive_builder: ^0.4.3
dev_dependencies: dev_dependencies:
integration_test: integration_test: