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
**/dot_images/*
docker/

View File

@@ -82,10 +82,13 @@ flutter {
dependencies {
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'
androidTestImplementation 'androidx.test:runner:1.2.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
// Required for flutter_local_notifications
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"
android:name="${applicationName}"
android:icon="@mipmap/launcher_icon">
@@ -11,10 +12,14 @@
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while 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" />
the
Android process has started. This theme is visible to the user
while
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>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
@@ -34,14 +39,16 @@
</activity>
<!-- 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" />
</application>
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="29" />
android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="29" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
</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,
_correspondentRepository = correspondentRepository,
_documentTypeRepository = documentTypeRepository,
super(
const DocumentUploadState(
tags: {},
correspondents: {},
documentTypes: {},
),
) {
super(const DocumentUploadState()) {
_subs.add(_tagRepository.values.listen(
(tags) => emit(state.copyWith(tags: tags?.values)),
));

View File

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

View File

@@ -236,8 +236,10 @@ class _DocumentUploadPreparationPageState
final taskId = await cubit.upload(
widget.fileBytes,
filename:
_padWithPdfExtension(_formKey.currentState?.value[fkFileName]),
filename: _padWithExtension(
_formKey.currentState?.value[fkFileName],
widget.fileExtension,
),
title: title,
documentType: docType.id,
correspondent: correspondent.id,
@@ -261,11 +263,12 @@ class _DocumentUploadPreparationPageState
}
}
String _padWithPdfExtension(String source) {
return source.endsWith(".pdf") ? source : '$source.pdf';
String _padWithExtension(String source, [String? extension]) {
final ext = extension ?? '.pdf';
return source.endsWith(ext) ? source : '$source$ext';
}
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() {
emit(state.copyWith(selectedSavedViewId: null));
}

View File

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

View File

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

View File

@@ -36,62 +36,60 @@ class DocumentListItem extends StatelessWidget {
@override
Widget build(BuildContext context) {
return SizedBox(
child: ListTile(
dense: true,
selected: isSelected,
onTap: () => _onTap(),
selectedTileColor: Theme.of(context).colorScheme.inversePrimary,
onLongPress: () => onSelected?.call(document),
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Row(
children: [
AbsorbPointer(
absorbing: isAtLeastOneSelected,
child: CorrespondentWidget(
isClickable: isLabelClickable,
correspondentId: document.correspondent,
onSelected: onCorrespondentSelected,
),
return ListTile(
dense: true,
selected: isSelected,
onTap: () => _onTap(),
selectedTileColor: Theme.of(context).colorScheme.inversePrimary,
onLongPress: () => onSelected?.call(document),
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Row(
children: [
AbsorbPointer(
absorbing: isAtLeastOneSelected,
child: CorrespondentWidget(
isClickable: isLabelClickable,
correspondentId: document.correspondent,
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),
),
),
],
),
),
isThreeLine: document.tags.isNotEmpty,
leading: AspectRatio(
aspectRatio: _a4AspectRatio,
child: GestureDetector(
child: DocumentPreview(
id: document.id,
fit: BoxFit.cover,
alignment: Alignment.topCenter,
),
Text(
document.title,
overflow: TextOverflow.ellipsis,
maxLines: document.tags.isEmpty ? 2 : 1,
),
),
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_form_builder/flutter_form_builder.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:provider/provider.dart';
class TextQueryFormField extends StatelessWidget {
final String name;
@@ -21,57 +24,69 @@ class TextQueryFormField extends StatelessWidget {
name: name,
initialValue: initialValue,
builder: (field) {
return TextFormField(
initialValue: initialValue?.queryText,
textInputAction: TextInputAction.done,
decoration: InputDecoration(
prefixIcon: const Icon(Icons.search_outlined),
labelText: _buildLabelText(context, field.value!.queryType),
suffixIcon: 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));
return Autocomplete(
optionsBuilder: (value) =>
context.read<DocumentsCubit>().autocomplete(value.text),
initialValue: initialValue?.queryText != null
? TextEditingValue(text: initialValue!.queryText!)
: null,
fieldViewBuilder:
(context, textEditingController, focusNode, onFieldSubmitted) {
return TextFormField(
controller: textEditingController,
focusNode: focusNode,
decoration: InputDecoration(
prefixIcon: const Icon(Icons.search_outlined),
labelText: _buildLabelText(context, field.value!.queryType),
suffixIcon: _buildQueryTypeMenu(context, field),
),
onChanged: (value) {
field.didChange(field.value?.copyWith(queryText: value));
},
),
),
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) {
switch (queryType) {
case QueryType.title:

View File

@@ -1,14 +1,14 @@
import 'package:flutter/material.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/generated/l10n.dart';
class BulkDeleteConfirmationDialog extends StatelessWidget {
static const _bulletPoint = "\u2022";
final DocumentsState state;
const BulkDeleteConfirmationDialog({Key? key, required this.state})
: super(key: key);
const BulkDeleteConfirmationDialog({
Key? key,
required this.state,
}) : super(key: key);
@override
Widget build(BuildContext context) {
@@ -29,13 +29,7 @@ class BulkDeleteConfirmationDialog extends StatelessWidget {
.documentsPageSelectionBulkDeleteDialogWarningTextMany,
),
const SizedBox(height: 16),
ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 150),
child: ListView(
shrinkWrap: true,
children: state.selection.map(_buildBulletPoint).toList(),
),
),
...state.selection.map(_buildBulletPoint).toList(),
const SizedBox(height: 16),
Text(
S.of(context).documentsPageSelectionBulkDeleteDialogContinueText),
@@ -61,12 +55,15 @@ class BulkDeleteConfirmationDialog extends StatelessWidget {
}
Widget _buildBulletPoint(DocumentModel doc) {
return Text(
"\t$_bulletPoint ${doc.title}",
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontWeight: FontWeight.w700,
return ListTile(
dense: true,
title: Text(
doc.title,
maxLines: 1,
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:receive_sharing_intent/receive_sharing_intent.dart';
import 'package:path/path.dart' as p;
import 'package:responsive_builder/responsive_builder.dart';
class HomePage extends StatefulWidget {
const HomePage({Key? key}) : super(key: key);
@@ -165,44 +166,114 @@ class _HomePageState extends State<HomePage> {
},
),
],
child: Scaffold(
key: rootScaffoldKey,
bottomNavigationBar: BottomNavBar(
selectedIndex: _currentIndex,
onNavigationChanged: (index) {
if (_currentIndex != index) {
setState(() => _currentIndex = index);
}
},
),
drawer: const InfoDrawer(),
body: [
MultiBlocProvider(
providers: [
BlocProvider(
create: (context) => DocumentsCubit(
context.read<PaperlessDocumentsApi>(),
context.read<SavedViewRepository>(),
child: ResponsiveBuilder(
builder: (context, sizingInformation) {
if (!sizingInformation.isMobile) {
return Scaffold(
key: rootScaffoldKey,
drawer: const InfoDrawer(),
body: Row(children: [
NavigationRail(
labelType: NavigationRailLabelType.all,
destinations: [
NavigationRailDestination(
icon: const Icon(Icons.description_outlined),
selectedIcon: Icon(
Icons.description,
color: Theme.of(context).colorScheme.primary,
),
label: Text(S.of(context).bottomNavDocumentsPageLabel),
),
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(
create: (context) => SavedViewCubit(
context.read<SavedViewRepository>(),
),
BlocProvider.value(
value: _scannerCubit,
child: const ScannerPage(),
),
],
child: const DocumentsPage(),
),
BlocProvider.value(
value: _scannerCubit,
child: const ScannerPage(),
),
const LabelsPage(),
][_currentIndex],
const LabelsPage(),
][_currentIndex],
);
},
),
);
}
void _onNavigationChanged(index) {
if (_currentIndex != index) {
setState(() => _currentIndex = index);
}
}
void _initializeData(BuildContext context) {
try {
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() {
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:paperless_api/paperless_api.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/view/pages/document_details_page.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/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/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 {
static const _a4AspectRatio = 1 / 1.4142;
@@ -23,7 +32,31 @@ class InboxItem extends StatelessWidget {
@override
Widget build(BuildContext context) {
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,
leading: AspectRatio(
aspectRatio: _a4AspectRatio,
@@ -37,16 +70,54 @@ class InboxItem extends StatelessWidget {
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
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(
tagIds: document.tags,
isMultiLine: false,
isClickable: 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 {
final returnedDocument = await Navigator.push<DocumentModel?>(
context,

View File

@@ -7,6 +7,8 @@ class TagWidget extends StatelessWidget {
final VoidCallback onSelected;
final bool isSelected;
final bool isClickable;
final bool showShortName;
final bool dense;
const TagWidget({
super.key,
@@ -15,6 +17,8 @@ class TagWidget extends StatelessWidget {
this.isClickable = true,
required this.onSelected,
required this.isSelected,
this.showShortName = false,
this.dense = false,
});
@override
@@ -24,13 +28,18 @@ class TagWidget extends StatelessWidget {
child: AbsorbPointer(
absorbing: !isClickable,
child: FilterChip(
labelPadding:
dense ? const EdgeInsets.symmetric(horizontal: 2) : null,
padding: dense ? const EdgeInsets.all(4) : null,
selected: isSelected,
selectedColor: tag.color,
onSelected: (_) => onSelected(),
visualDensity: const VisualDensity(vertical: -2),
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
label: Text(
tag.name,
showShortName && tag.name.length > 6
? '${tag.name.substring(0, 6)}...'
: tag.name,
style: TextStyle(color: 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/tags/view/widgets/tag_widget.dart';
class TagsWidget extends StatefulWidget {
class TagsWidget extends StatelessWidget {
final Iterable<int> tagIds;
final bool isMultiLine;
final VoidCallback? afterTagTapped;
final void Function(int tagId)? onTagSelected;
final bool isClickable;
final bool Function(int id) isSelectedPredicate;
final bool showShortNames;
final bool dense;
const TagsWidget({
Key? key,
@@ -21,32 +23,31 @@ class TagsWidget extends StatefulWidget {
this.isMultiLine = true,
this.isClickable = true,
required this.isSelectedPredicate,
required this.onTagSelected,
this.onTagSelected,
this.showShortNames = false,
this.dense = false,
}) : super(key: key);
@override
State<TagsWidget> createState() => _TagsWidgetState();
}
class _TagsWidgetState extends State<TagsWidget> {
@override
Widget build(BuildContext context) {
return TagBlocProvider(
child: BlocBuilder<LabelCubit<Tag>, LabelState<Tag>>(
builder: (context, state) {
final children = widget.tagIds
final children = tagIds
.where((id) => state.labels.containsKey(id))
.map(
(id) => TagWidget(
tag: state.getLabel(id)!,
afterTagTapped: widget.afterTagTapped,
isClickable: widget.isClickable,
isSelected: widget.isSelectedPredicate(id),
onSelected: () => widget.onTagSelected?.call(id),
afterTagTapped: afterTagTapped,
isClickable: isClickable,
isSelected: isSelectedPredicate(id),
onSelected: () => onTagSelected?.call(id),
showShortName: showShortNames,
dense: dense,
),
)
.toList();
if (widget.isMultiLine) {
if (isMultiLine) {
return Wrap(
runAlignment: WrapAlignment.start,
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_form_builder/flutter_form_builder.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 {
static const String fkServerAddress = "serverAddress";
final void Function(String address) onDone;
final void Function(String? address) onDone;
const ServerAddressFormField({
Key? key,
required this.onDone,
@@ -31,7 +33,7 @@ class _ServerAddressFormFieldState extends State<ServerAddressFormField> {
});
}
final TextEditingController _textEditingController = TextEditingController();
final _textEditingController = TextEditingController();
@override
Widget build(BuildContext context) {
@@ -56,7 +58,7 @@ class _ServerAddressFormFieldState extends State<ServerAddressFormField> {
labelText: S.of(context).loginPageServerUrlFieldLabel,
suffixIcon: _canClear
? IconButton(
icon: Icon(Icons.clear),
icon: const Icon(Icons.clear),
color: Theme.of(context).iconTheme.color,
onPressed: () {
_textEditingController.clear();
@@ -64,18 +66,14 @@ class _ServerAddressFormFieldState extends State<ServerAddressFormField> {
)
: null,
),
onSubmitted: (value) {
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);
},
onSubmitted: (_) => _formatInput(),
);
}
String _replaceTrailingSlashes(String src) {
return src.replaceAll(RegExp(r'^\/+|\/+$'), '');
void _formatInput() {
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(
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
TextButton(
child: Text("Test connection"),
onPressed: _updateReachability,
),
FilledButton(
child: Text(S.of(context).loginPageContinueLabel),
onPressed: _reachabilityStatus == ReachabilityStatus.reachable
@@ -76,6 +80,7 @@ class _ServerConnectionPageState extends State<ServerConnectionPage> {
setState(() {
_isCheckingConnection = true;
});
final status = await context
.read<ConnectivityStatusService>()
.isPaperlessServerReachable(

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,3 +1,4 @@
import 'package:equatable/equatable.dart';
import 'package:json_annotation/json_annotation.dart';
import 'query_type.dart';
@@ -5,7 +6,7 @@ import 'query_type.dart';
part 'text_query.g.dart';
@JsonSerializable()
class TextQuery {
class TextQuery extends Equatable {
final QueryType queryType;
final String? queryText;
@@ -61,4 +62,7 @@ class TextQuery {
factory TextQuery.fromJson(Map<String, dynamic> 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(
'/api/search/autocomplete/',
queryParameters: {
'query': query,
'term': query,
'limit': limit,
},
);
if (response.statusCode == 200) {
return response.data as List<String>;
return (response.data as List).cast<String>();
}
throw const PaperlessServerException(ErrorCode.autocompleteQueryError);
} on DioError catch (err) {

View File

@@ -1292,6 +1292,14 @@ packages:
url: "https://pub.dev"
source: hosted
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:
dependency: "direct main"
description:

View File

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