Merge pull request #177 from astubenbord/1.14-permissions

Support for 1.14, fix features that broke with API v3
This commit is contained in:
Anton Stubenbord
2023-05-27 18:50:34 +02:00
committed by GitHub
187 changed files with 2879 additions and 9062 deletions

4
.gitignore vendored
View File

@@ -54,7 +54,11 @@ app.*.map.json
# build_runner generated files
lib/**/*.g.dart
lib/**/*.freezed.dart
packages/**/*.g.dart
packages/**/*.freezed.dart
# mockito generated files
*.mocks.dart

View File

@@ -214,4 +214,4 @@ Made with [contrib.rocks](https://contrib.rocks).
## Troubleshooting
#### Suggestions are not selectable in any of the label form fields
This is a known issue and it has to do with accessibility features of Android. Password managers such as Bitwarden often caused this issue. Luckily, this can be resolved by turning off the accessibility features in these apps. This could also be observed with apps that are allowed to display over other apps, such as emulations of the dynamic island on android.
This is a known issue and it has to do with accessibility features of Android. Password managers such as Bitwarden often caused this issue. Luckily, this can be resolved by turning off the accessibility features in these apps. This could also be observed with apps that are allowed to display over other apps, such as emulations of the dynamic island on android.

View File

@@ -0,0 +1,4 @@
Dies ist eine Beta Version und noch work in progress! Diese Version wurde mit paperless-ngx 1.13.0 sowie 1.14.2 getestet.
* Beheben einiger Fehler, die durch API-Update zustande kamen
* Berechtigungs-Checks bei wichtigen Funktionen (noch nicht vollständig)
* Neue Einstellungen für den Standard Dateityp beim Downloaden/Teilen

View File

@@ -0,0 +1,4 @@
This is a beta version and still work in progress! This version was tested with paperless-ngx 1.3.0 as well as 1.14.2.
* Fixes some bugs which were caused by breaking changes to the Paperless API.
* Permission checks for major features (upload, edit, view documents etc., not complete yet)
* Add setting to set default download/share file type

View File

@@ -1,17 +0,0 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/bloc/server_information_state.dart';
class ServerInformationCubit extends Cubit<ServerInformationState> {
final PaperlessServerStatsApi _api;
ServerInformationCubit(this._api) : super(ServerInformationState());
Future<void> updateInformation() async {
final information = await _api.getServerInformation();
emit(ServerInformationState(
isLoaded: true,
information: information,
));
}
}

View File

@@ -1,11 +0,0 @@
import 'package:paperless_api/paperless_api.dart';
class ServerInformationState {
final bool isLoaded;
final PaperlessServerInformationModel? information;
ServerInformationState({
this.isLoaded = false,
this.information,
});
}

View File

@@ -0,0 +1,35 @@
import 'dart:async';
import 'dart:convert';
import 'dart:typed_data';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:hive_flutter/adapters.dart';
///
/// Opens an encrypted box, calls [callback] with the now opened box, awaits
/// [callback] to return and returns the calculated value. Closes the box after.
///
Future<R?> withEncryptedBox<T, R>(String name, FutureOr<R?> Function(Box<T> box) callback) async {
final key = await _getEncryptedBoxKey();
final box = await Hive.openBox<T>(
name,
encryptionCipher: HiveAesCipher(key),
);
final result = await callback(box);
await box.close();
return result;
}
Future<Uint8List> _getEncryptedBoxKey() async {
const secureStorage = FlutterSecureStorage();
if (!await secureStorage.containsKey(key: 'key')) {
final key = Hive.generateSecureKey();
await secureStorage.write(
key: 'key',
value: base64UrlEncode(key),
);
}
final key = (await secureStorage.read(key: 'key'))!;
return base64Decode(key);
}

View File

@@ -1,6 +1,8 @@
import 'package:hive_flutter/adapters.dart';
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
import 'package:paperless_mobile/core/database/tables/global_settings.dart';
import 'package:paperless_mobile/core/database/tables/local_user_settings.dart';
import 'package:paperless_api/paperless_api.dart';
part 'local_user_account.g.dart';
@@ -9,23 +11,22 @@ class LocalUserAccount extends HiveObject {
@HiveField(0)
final String serverUrl;
@HiveField(1)
final String username;
@HiveField(2)
final String? fullName;
@HiveField(3)
final String id;
@HiveField(4)
LocalUserSettings settings;
final LocalUserSettings settings;
@HiveField(7)
UserModel paperlessUser;
LocalUserAccount({
required this.id,
required this.serverUrl,
required this.username,
required this.settings,
this.fullName,
required this.paperlessUser,
});
static LocalUserAccount get current => Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount)
.get(Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!.currentLoggedInUser)!;
}

View File

@@ -1,6 +1,7 @@
import 'package:hive_flutter/adapters.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
import 'package:paperless_mobile/core/database/tables/global_settings.dart';
import 'package:paperless_mobile/features/settings/model/view_type.dart';
part 'local_user_app_state.g.dart';
@@ -37,4 +38,10 @@ class LocalUserAppState extends HiveObject {
this.documentSearchViewType = ViewType.list,
this.savedViewsViewType = ViewType.list,
});
static LocalUserAppState get current {
final currentLocalUserId =
Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!.currentLoggedInUser!;
return Hive.box<LocalUserAppState>(HiveBoxes.localUserAppState).get(currentLocalUserId)!;
}
}

View File

@@ -0,0 +1,5 @@
class ServerMessageException implements Exception {
final String message;
ServerMessageException(this.message);
}

View File

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

View File

@@ -0,0 +1,50 @@
import 'package:dio/dio.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/factory/paperless_api_factory.dart';
import 'package:paperless_mobile/core/security/session_manager.dart';
class PaperlessApiFactoryImpl implements PaperlessApiFactory {
final SessionManager sessionManager;
PaperlessApiFactoryImpl(this.sessionManager);
@override
PaperlessDocumentsApi createDocumentsApi(Dio dio, {required int apiVersion}) {
return PaperlessDocumentsApiImpl(dio);
}
@override
PaperlessLabelsApi createLabelsApi(Dio dio, {required int apiVersion}) {
return PaperlessLabelApiImpl(dio);
}
@override
PaperlessSavedViewsApi createSavedViewsApi(Dio dio, {required int apiVersion}) {
return PaperlessSavedViewsApiImpl(dio);
}
@override
PaperlessServerStatsApi createServerStatsApi(Dio dio, {required int apiVersion}) {
return PaperlessServerStatsApiImpl(dio);
}
@override
PaperlessTasksApi createTasksApi(Dio dio, {required int apiVersion}) {
return PaperlessTasksApiImpl(dio);
}
@override
PaperlessAuthenticationApi createAuthenticationApi(Dio dio) {
return PaperlessAuthenticationApiImpl(dio);
}
@override
PaperlessUserApi createUserApi(Dio dio, {required int apiVersion}) {
if (apiVersion == 3) {
return PaperlessUserApiV3Impl(dio);
} else if (apiVersion < 3) {
return PaperlessUserApiV2Impl(dio);
}
throw Exception("API $apiVersion not supported.");
}
}

View File

@@ -2,6 +2,7 @@ import 'dart:io';
import 'package:dio/dio.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/exception/server_message_exception.dart';
import 'package:paperless_mobile/core/type/types.dart';
class DioHttpErrorInterceptor extends Interceptor {
@@ -15,6 +16,16 @@ class DioHttpErrorInterceptor extends Interceptor {
} else if (data is String) {
return _handlePlainError(data, handler, err);
}
} else if (err.response?.statusCode == 403) {
var data = err.response!.data;
if (data is Map && data.containsKey("detail")) {
handler.reject(DioError(
requestOptions: err.requestOptions,
error: ServerMessageException(data['detail']),
response: err.response,
));
return;
}
} else if (err.error is SocketException) {
final ex = err.error as SocketException;
if (ex.osError?.errorCode == _OsErrorCodes.serverUnreachable.code) {
@@ -67,8 +78,7 @@ class DioHttpErrorInterceptor extends Interceptor {
DioError(
requestOptions: err.requestOptions,
type: DioErrorType.badResponse,
error: const PaperlessServerException(
ErrorCode.missingClientCertificate),
error: const PaperlessServerException(ErrorCode.missingClientCertificate),
),
);
}
@@ -76,8 +86,7 @@ class DioHttpErrorInterceptor extends Interceptor {
}
enum _OsErrorCodes {
serverUnreachable(101),
hostNotFound(7);
serverUnreachable(101);
const _OsErrorCodes(this.code);
final int code;

View File

@@ -0,0 +1,370 @@
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:hive/hive.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
import 'package:paperless_mobile/core/database/tables/global_settings.dart';
import 'package:paperless_mobile/core/database/tables/local_user_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/core/repository/user_repository.dart';
import 'package:paperless_mobile/features/document_bulk_action/cubit/document_bulk_action_cubit.dart';
import 'package:paperless_mobile/features/document_bulk_action/view/widgets/fullscreen_bulk_edit_label_page.dart';
import 'package:paperless_mobile/features/document_bulk_action/view/widgets/fullscreen_bulk_edit_tags_widget.dart';
import 'package:paperless_mobile/features/document_search/cubit/document_search_cubit.dart';
import 'package:paperless_mobile/features/document_search/view/document_search_page.dart';
import 'package:paperless_mobile/features/document_upload/cubit/document_upload_cubit.dart';
import 'package:paperless_mobile/features/document_upload/view/document_upload_preparation_page.dart';
import 'package:paperless_mobile/features/home/view/model/api_version.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/notifications/services/local_notification_service.dart';
import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart';
import 'package:paperless_mobile/features/saved_view/view/add_saved_view_page.dart';
import 'package:paperless_mobile/features/saved_view_details/cubit/saved_view_details_cubit.dart';
import 'package:paperless_mobile/features/saved_view_details/view/saved_view_details_page.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
import 'package:paperless_mobile/routes/document_details_route.dart';
import 'package:provider/provider.dart';
// These are convenience methods for nativating to views without having to pass providers around explicitly.
// Providers unfortunately have to be passed to the routes since they are children of the Navigator, not ancestors.
Future<void> pushDocumentSearchPage(BuildContext context) {
final currentUser =
Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!.currentLoggedInUser;
final userRepo = context.read<UserRepository>();
return Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => MultiProvider(
providers: [
Provider.value(value: context.read<LabelRepository>()),
Provider.value(value: context.read<PaperlessDocumentsApi>()),
Provider.value(value: context.read<DocumentChangedNotifier>()),
Provider.value(value: context.read<CacheManager>()),
Provider.value(value: userRepo),
],
builder: (context, _) {
return BlocProvider(
create: (context) => DocumentSearchCubit(
context.read(),
context.read(),
context.read(),
Hive.box<LocalUserAppState>(HiveBoxes.localUserAppState).get(currentUser)!,
),
child: const DocumentSearchPage(),
);
},
),
),
);
}
Future<void> pushDocumentDetailsRoute(
BuildContext context, {
required DocumentModel document,
bool isLabelClickable = true,
bool allowEdit = true,
String? titleAndContentQueryString,
}) {
return Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => MultiProvider(
providers: [
Provider.value(value: context.read<ApiVersion>()),
Provider.value(value: context.read<LabelRepository>()),
Provider.value(value: context.read<DocumentChangedNotifier>()),
Provider.value(value: context.read<PaperlessDocumentsApi>()),
Provider.value(value: context.read<LocalNotificationService>()),
Provider.value(value: context.read<CacheManager>()),
Provider.value(value: context.read<ConnectivityCubit>()),
if (context.read<ApiVersion>().hasMultiUserSupport)
Provider.value(value: context.read<UserRepository>()),
],
child: DocumentDetailsRoute(
document: document,
isLabelClickable: isLabelClickable,
),
),
),
);
}
Future<void> pushSavedViewDetailsRoute(
BuildContext context, {
required SavedView savedView,
}) {
final apiVersion = context.read<ApiVersion>();
return Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => MultiProvider(
providers: [
Provider.value(value: apiVersion),
if (apiVersion.hasMultiUserSupport) Provider.value(value: context.read<UserRepository>()),
Provider.value(value: context.read<LabelRepository>()),
Provider.value(value: context.read<DocumentChangedNotifier>()),
Provider.value(value: context.read<PaperlessDocumentsApi>()),
Provider.value(value: context.read<CacheManager>()),
Provider.value(value: context.read<ConnectivityCubit>()),
],
builder: (_, child) {
return BlocProvider(
create: (context) => SavedViewDetailsCubit(
context.read(),
context.read(),
context.read(),
LocalUserAppState.current,
savedView: savedView,
),
child: SavedViewDetailsPage(onDelete: context.read<SavedViewCubit>().remove),
);
},
),
),
);
}
Future<SavedView?> pushAddSavedViewRoute(BuildContext context, {required DocumentFilter filter}) {
return Navigator.of(context).push<SavedView?>(
MaterialPageRoute(
builder: (_) => AddSavedViewPage(
currentFilter: filter,
correspondents: context.read<LabelRepository>().state.correspondents,
documentTypes: context.read<LabelRepository>().state.documentTypes,
storagePaths: context.read<LabelRepository>().state.storagePaths,
tags: context.read<LabelRepository>().state.tags,
),
),
);
}
Future<void> pushLinkedDocumentsView(BuildContext context, {required DocumentFilter filter}) {
return Navigator.push(
context,
MaterialPageRoute(
builder: (_) => MultiProvider(
providers: [
Provider.value(value: context.read<ApiVersion>()),
Provider.value(value: context.read<LabelRepository>()),
Provider.value(value: context.read<DocumentChangedNotifier>()),
Provider.value(value: context.read<PaperlessDocumentsApi>()),
Provider.value(value: context.read<LocalNotificationService>()),
Provider.value(value: context.read<CacheManager>()),
Provider.value(value: context.read<ConnectivityCubit>()),
if (context.read<ApiVersion>().hasMultiUserSupport)
Provider.value(value: context.read<UserRepository>()),
],
builder: (context, _) => BlocProvider(
create: (context) => LinkedDocumentsCubit(
filter,
context.read(),
context.read(),
context.read(),
),
child: const LinkedDocumentsPage(),
),
),
),
);
}
Future<void> pushBulkEditCorrespondentRoute(
BuildContext context, {
required List<DocumentModel> selection,
}) {
return Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => MultiProvider(
providers: [
..._getRequiredBulkEditProviders(context),
],
builder: (_, __) => BlocProvider(
create: (_) => DocumentBulkActionCubit(
context.read(),
context.read(),
context.read(),
selection: selection,
),
child: BlocBuilder<DocumentBulkActionCubit, DocumentBulkActionState>(
builder: (context, state) {
return FullscreenBulkEditLabelPage(
options: state.correspondents,
selection: state.selection,
labelMapper: (document) => document.correspondent,
leadingIcon: const Icon(Icons.person_outline),
hintText: S.of(context)!.startTyping,
onSubmit: context.read<DocumentBulkActionCubit>().bulkModifyCorrespondent,
assignMessageBuilder: (int count, String name) {
return S.of(context)!.bulkEditCorrespondentAssignMessage(
name,
count,
);
},
removeMessageBuilder: (int count) {
return S.of(context)!.bulkEditCorrespondentRemoveMessage(count);
},
);
},
),
),
),
),
);
}
Future<void> pushBulkEditStoragePathRoute(
BuildContext context, {
required List<DocumentModel> selection,
}) {
return Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => MultiProvider(
providers: [
..._getRequiredBulkEditProviders(context),
],
builder: (_, __) => BlocProvider(
create: (_) => DocumentBulkActionCubit(
context.read(),
context.read(),
context.read(),
selection: selection,
),
child: BlocBuilder<DocumentBulkActionCubit, DocumentBulkActionState>(
builder: (context, state) {
return FullscreenBulkEditLabelPage(
options: state.storagePaths,
selection: state.selection,
labelMapper: (document) => document.storagePath,
leadingIcon: const Icon(Icons.folder_outlined),
hintText: S.of(context)!.startTyping,
onSubmit: context.read<DocumentBulkActionCubit>().bulkModifyStoragePath,
assignMessageBuilder: (int count, String name) {
return S.of(context)!.bulkEditStoragePathAssignMessage(
count,
name,
);
},
removeMessageBuilder: (int count) {
return S.of(context)!.bulkEditStoragePathRemoveMessage(count);
},
);
},
),
),
),
),
);
}
Future<void> pushBulkEditTagsRoute(
BuildContext context, {
required List<DocumentModel> selection,
}) {
return Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => MultiProvider(
providers: [
..._getRequiredBulkEditProviders(context),
],
builder: (_, __) => BlocProvider(
create: (_) => DocumentBulkActionCubit(
context.read(),
context.read(),
context.read(),
selection: selection,
),
child: Builder(builder: (context) {
return const FullscreenBulkEditTagsWidget();
}),
),
),
),
);
}
Future<void> pushBulkEditDocumentTypeRoute(BuildContext context,
{required List<DocumentModel> selection}) {
return Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => MultiProvider(
providers: [
..._getRequiredBulkEditProviders(context),
],
builder: (_, __) => BlocProvider(
create: (_) => DocumentBulkActionCubit(
context.read(),
context.read(),
context.read(),
selection: selection,
),
child: BlocBuilder<DocumentBulkActionCubit, DocumentBulkActionState>(
builder: (context, state) {
return FullscreenBulkEditLabelPage(
options: state.documentTypes,
selection: state.selection,
labelMapper: (document) => document.documentType,
leadingIcon: const Icon(Icons.description_outlined),
hintText: S.of(context)!.startTyping,
onSubmit: context.read<DocumentBulkActionCubit>().bulkModifyDocumentType,
assignMessageBuilder: (int count, String name) {
return S.of(context)!.bulkEditDocumentTypeAssignMessage(
count,
name,
);
},
removeMessageBuilder: (int count) {
return S.of(context)!.bulkEditDocumentTypeRemoveMessage(count);
},
);
},
),
),
),
),
);
}
Future<DocumentUploadResult?> pushDocumentUploadPreparationPage(
BuildContext context, {
required Uint8List bytes,
String? filename,
String? fileExtension,
String? title,
}) {
final labelRepo = context.read<LabelRepository>();
final docsApi = context.read<PaperlessDocumentsApi>();
return Navigator.of(context).push<DocumentUploadResult>(
MaterialPageRoute(
builder: (_) => MultiProvider(
providers: [
Provider.value(value: labelRepo),
Provider.value(value: docsApi),
],
builder: (_, child) => BlocProvider(
create: (_) => DocumentUploadCubit(
context.read(),
context.read(),
),
child: DocumentUploadPreparationPage(
fileBytes: bytes,
fileExtension: fileExtension,
filename: filename,
title: title,
),
),
),
),
);
}
List<Provider> _getRequiredBulkEditProviders(BuildContext context) {
return [
Provider.value(value: context.read<PaperlessDocumentsApi>()),
Provider.value(value: context.read<LabelRepository>()),
Provider.value(value: context.read<DocumentChangedNotifier>()),
];
}

View File

@@ -1,5 +1,4 @@
import 'dart:async';
import 'dart:developer';
import 'package:flutter/foundation.dart';
import 'package:paperless_api/paperless_api.dart';

View File

@@ -1,29 +1,15 @@
import 'dart:async';
import 'package:flutter/widgets.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/label_repository_state.dart';
import 'package:paperless_mobile/core/repository/persistent_repository.dart';
class LabelRepository extends HydratedCubit<LabelRepositoryState> {
class LabelRepository extends PersistentRepository<LabelRepositoryState> {
final PaperlessLabelsApi _api;
final Map<Object, StreamSubscription> _subscribers = {};
LabelRepository(this._api) : super(const LabelRepositoryState());
void addListener(
Object source, {
required void Function(LabelRepositoryState) onChanged,
}) {
onChanged(state);
_subscribers.putIfAbsent(source, () {
return stream.listen((event) => onChanged(event));
});
}
void removeListener(Object source) async {
await _subscribers[source]?.cancel();
_subscribers.remove(source);
LabelRepository(this._api) : super(const LabelRepositoryState()) {
initialize();
}
Future<void> initialize() {
@@ -195,14 +181,6 @@ class LabelRepository extends HydratedCubit<LabelRepositoryState> {
return updated;
}
@override
Future<void> close() {
_subscribers.forEach((key, subscription) {
subscription.cancel();
});
return super.close();
}
@override
Future<void> clear() async {
await super.clear();

View File

@@ -1,258 +0,0 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'label_repository_state.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods');
LabelRepositoryState _$LabelRepositoryStateFromJson(Map<String, dynamic> json) {
return _LabelRepositoryState.fromJson(json);
}
/// @nodoc
mixin _$LabelRepositoryState {
Map<int, Correspondent> get correspondents =>
throw _privateConstructorUsedError;
Map<int, DocumentType> get documentTypes =>
throw _privateConstructorUsedError;
Map<int, Tag> get tags => throw _privateConstructorUsedError;
Map<int, StoragePath> get storagePaths => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$LabelRepositoryStateCopyWith<LabelRepositoryState> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $LabelRepositoryStateCopyWith<$Res> {
factory $LabelRepositoryStateCopyWith(LabelRepositoryState value,
$Res Function(LabelRepositoryState) then) =
_$LabelRepositoryStateCopyWithImpl<$Res, LabelRepositoryState>;
@useResult
$Res call(
{Map<int, Correspondent> correspondents,
Map<int, DocumentType> documentTypes,
Map<int, Tag> tags,
Map<int, StoragePath> storagePaths});
}
/// @nodoc
class _$LabelRepositoryStateCopyWithImpl<$Res,
$Val extends LabelRepositoryState>
implements $LabelRepositoryStateCopyWith<$Res> {
_$LabelRepositoryStateCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
@pragma('vm:prefer-inline')
@override
$Res call({
Object? correspondents = null,
Object? documentTypes = null,
Object? tags = null,
Object? storagePaths = null,
}) {
return _then(_value.copyWith(
correspondents: null == correspondents
? _value.correspondents
: correspondents // ignore: cast_nullable_to_non_nullable
as Map<int, Correspondent>,
documentTypes: null == documentTypes
? _value.documentTypes
: documentTypes // ignore: cast_nullable_to_non_nullable
as Map<int, DocumentType>,
tags: null == tags
? _value.tags
: tags // ignore: cast_nullable_to_non_nullable
as Map<int, Tag>,
storagePaths: null == storagePaths
? _value.storagePaths
: storagePaths // ignore: cast_nullable_to_non_nullable
as Map<int, StoragePath>,
) as $Val);
}
}
/// @nodoc
abstract class _$$_LabelRepositoryStateCopyWith<$Res>
implements $LabelRepositoryStateCopyWith<$Res> {
factory _$$_LabelRepositoryStateCopyWith(_$_LabelRepositoryState value,
$Res Function(_$_LabelRepositoryState) then) =
__$$_LabelRepositoryStateCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{Map<int, Correspondent> correspondents,
Map<int, DocumentType> documentTypes,
Map<int, Tag> tags,
Map<int, StoragePath> storagePaths});
}
/// @nodoc
class __$$_LabelRepositoryStateCopyWithImpl<$Res>
extends _$LabelRepositoryStateCopyWithImpl<$Res, _$_LabelRepositoryState>
implements _$$_LabelRepositoryStateCopyWith<$Res> {
__$$_LabelRepositoryStateCopyWithImpl(_$_LabelRepositoryState _value,
$Res Function(_$_LabelRepositoryState) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? correspondents = null,
Object? documentTypes = null,
Object? tags = null,
Object? storagePaths = null,
}) {
return _then(_$_LabelRepositoryState(
correspondents: null == correspondents
? _value._correspondents
: correspondents // ignore: cast_nullable_to_non_nullable
as Map<int, Correspondent>,
documentTypes: null == documentTypes
? _value._documentTypes
: documentTypes // ignore: cast_nullable_to_non_nullable
as Map<int, DocumentType>,
tags: null == tags
? _value._tags
: tags // ignore: cast_nullable_to_non_nullable
as Map<int, Tag>,
storagePaths: null == storagePaths
? _value._storagePaths
: storagePaths // ignore: cast_nullable_to_non_nullable
as Map<int, StoragePath>,
));
}
}
/// @nodoc
@JsonSerializable()
class _$_LabelRepositoryState implements _LabelRepositoryState {
const _$_LabelRepositoryState(
{final Map<int, Correspondent> correspondents = const {},
final Map<int, DocumentType> documentTypes = const {},
final Map<int, Tag> tags = const {},
final Map<int, StoragePath> storagePaths = const {}})
: _correspondents = correspondents,
_documentTypes = documentTypes,
_tags = tags,
_storagePaths = storagePaths;
factory _$_LabelRepositoryState.fromJson(Map<String, dynamic> json) =>
_$$_LabelRepositoryStateFromJson(json);
final Map<int, Correspondent> _correspondents;
@override
@JsonKey()
Map<int, Correspondent> get correspondents {
if (_correspondents is EqualUnmodifiableMapView) return _correspondents;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(_correspondents);
}
final Map<int, DocumentType> _documentTypes;
@override
@JsonKey()
Map<int, DocumentType> get documentTypes {
if (_documentTypes is EqualUnmodifiableMapView) return _documentTypes;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(_documentTypes);
}
final Map<int, Tag> _tags;
@override
@JsonKey()
Map<int, Tag> get tags {
if (_tags is EqualUnmodifiableMapView) return _tags;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(_tags);
}
final Map<int, StoragePath> _storagePaths;
@override
@JsonKey()
Map<int, StoragePath> get storagePaths {
if (_storagePaths is EqualUnmodifiableMapView) return _storagePaths;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(_storagePaths);
}
@override
String toString() {
return 'LabelRepositoryState(correspondents: $correspondents, documentTypes: $documentTypes, tags: $tags, storagePaths: $storagePaths)';
}
@override
bool operator ==(dynamic other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$_LabelRepositoryState &&
const DeepCollectionEquality()
.equals(other._correspondents, _correspondents) &&
const DeepCollectionEquality()
.equals(other._documentTypes, _documentTypes) &&
const DeepCollectionEquality().equals(other._tags, _tags) &&
const DeepCollectionEquality()
.equals(other._storagePaths, _storagePaths));
}
@JsonKey(ignore: true)
@override
int get hashCode => Object.hash(
runtimeType,
const DeepCollectionEquality().hash(_correspondents),
const DeepCollectionEquality().hash(_documentTypes),
const DeepCollectionEquality().hash(_tags),
const DeepCollectionEquality().hash(_storagePaths));
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$_LabelRepositoryStateCopyWith<_$_LabelRepositoryState> get copyWith =>
__$$_LabelRepositoryStateCopyWithImpl<_$_LabelRepositoryState>(
this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$_LabelRepositoryStateToJson(
this,
);
}
}
abstract class _LabelRepositoryState implements LabelRepositoryState {
const factory _LabelRepositoryState(
{final Map<int, Correspondent> correspondents,
final Map<int, DocumentType> documentTypes,
final Map<int, Tag> tags,
final Map<int, StoragePath> storagePaths}) = _$_LabelRepositoryState;
factory _LabelRepositoryState.fromJson(Map<String, dynamic> json) =
_$_LabelRepositoryState.fromJson;
@override
Map<int, Correspondent> get correspondents;
@override
Map<int, DocumentType> get documentTypes;
@override
Map<int, Tag> get tags;
@override
Map<int, StoragePath> get storagePaths;
@override
@JsonKey(ignore: true)
_$$_LabelRepositoryStateCopyWith<_$_LabelRepositoryState> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@@ -0,0 +1,32 @@
import 'dart:async';
import 'package:hydrated_bloc/hydrated_bloc.dart';
abstract class PersistentRepository<T> extends HydratedCubit<T> {
final Map<Object, StreamSubscription> _subscribers = {};
PersistentRepository(T initialState) : super(initialState);
void addListener(
Object source, {
required void Function(T) onChanged,
}) {
onChanged(state);
_subscribers.putIfAbsent(source, () {
return stream.listen((event) => onChanged(event));
});
}
void removeListener(Object source) async {
await _subscribers[source]?.cancel();
_subscribers.remove(source);
}
@override
Future<void> close() {
_subscribers.forEach((key, subscription) {
subscription.cancel();
});
return super.close();
}
}

View File

@@ -1,30 +1,20 @@
import 'dart:async';
import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/persistent_repository.dart';
import 'package:paperless_mobile/core/repository/saved_view_repository_state.dart';
class SavedViewRepository extends HydratedCubit<SavedViewRepositoryState> {
class SavedViewRepository extends PersistentRepository<SavedViewRepositoryState> {
final PaperlessSavedViewsApi _api;
final Map<Object, StreamSubscription> _subscribers = {};
void subscribe(
Object source,
void Function(Map<int, SavedView>) onChanged,
) {
_subscribers.putIfAbsent(source, () {
onChanged(state.savedViews);
return stream.listen((event) => onChanged(event.savedViews));
});
SavedViewRepository(this._api) : super(const SavedViewRepositoryState()) {
initialize();
}
void unsubscribe(Object source) async {
await _subscribers[source]?.cancel();
_subscribers.remove(source);
Future<void> initialize() {
return findAll();
}
SavedViewRepository(this._api) : super(const SavedViewRepositoryState());
Future<SavedView> create(SavedView object) async {
final created = await _api.save(object);
final updatedState = {...state.savedViews}..putIfAbsent(created.id!, () => created);
@@ -58,14 +48,6 @@ class SavedViewRepository extends HydratedCubit<SavedViewRepositoryState> {
return found;
}
@override
Future<void> close() {
_subscribers.forEach((key, subscription) {
subscription.cancel();
});
return super.close();
}
@override
Future<void> clear() async {
await super.clear();

View File

@@ -1,167 +0,0 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'saved_view_repository_state.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods');
SavedViewRepositoryState _$SavedViewRepositoryStateFromJson(
Map<String, dynamic> json) {
return _SavedViewRepositoryState.fromJson(json);
}
/// @nodoc
mixin _$SavedViewRepositoryState {
Map<int, SavedView> get savedViews => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$SavedViewRepositoryStateCopyWith<SavedViewRepositoryState> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $SavedViewRepositoryStateCopyWith<$Res> {
factory $SavedViewRepositoryStateCopyWith(SavedViewRepositoryState value,
$Res Function(SavedViewRepositoryState) then) =
_$SavedViewRepositoryStateCopyWithImpl<$Res, SavedViewRepositoryState>;
@useResult
$Res call({Map<int, SavedView> savedViews});
}
/// @nodoc
class _$SavedViewRepositoryStateCopyWithImpl<$Res,
$Val extends SavedViewRepositoryState>
implements $SavedViewRepositoryStateCopyWith<$Res> {
_$SavedViewRepositoryStateCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
@pragma('vm:prefer-inline')
@override
$Res call({
Object? savedViews = null,
}) {
return _then(_value.copyWith(
savedViews: null == savedViews
? _value.savedViews
: savedViews // ignore: cast_nullable_to_non_nullable
as Map<int, SavedView>,
) as $Val);
}
}
/// @nodoc
abstract class _$$_SavedViewRepositoryStateCopyWith<$Res>
implements $SavedViewRepositoryStateCopyWith<$Res> {
factory _$$_SavedViewRepositoryStateCopyWith(
_$_SavedViewRepositoryState value,
$Res Function(_$_SavedViewRepositoryState) then) =
__$$_SavedViewRepositoryStateCopyWithImpl<$Res>;
@override
@useResult
$Res call({Map<int, SavedView> savedViews});
}
/// @nodoc
class __$$_SavedViewRepositoryStateCopyWithImpl<$Res>
extends _$SavedViewRepositoryStateCopyWithImpl<$Res,
_$_SavedViewRepositoryState>
implements _$$_SavedViewRepositoryStateCopyWith<$Res> {
__$$_SavedViewRepositoryStateCopyWithImpl(_$_SavedViewRepositoryState _value,
$Res Function(_$_SavedViewRepositoryState) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? savedViews = null,
}) {
return _then(_$_SavedViewRepositoryState(
savedViews: null == savedViews
? _value._savedViews
: savedViews // ignore: cast_nullable_to_non_nullable
as Map<int, SavedView>,
));
}
}
/// @nodoc
@JsonSerializable()
class _$_SavedViewRepositoryState implements _SavedViewRepositoryState {
const _$_SavedViewRepositoryState(
{final Map<int, SavedView> savedViews = const {}})
: _savedViews = savedViews;
factory _$_SavedViewRepositoryState.fromJson(Map<String, dynamic> json) =>
_$$_SavedViewRepositoryStateFromJson(json);
final Map<int, SavedView> _savedViews;
@override
@JsonKey()
Map<int, SavedView> get savedViews {
if (_savedViews is EqualUnmodifiableMapView) return _savedViews;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(_savedViews);
}
@override
String toString() {
return 'SavedViewRepositoryState(savedViews: $savedViews)';
}
@override
bool operator ==(dynamic other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$_SavedViewRepositoryState &&
const DeepCollectionEquality()
.equals(other._savedViews, _savedViews));
}
@JsonKey(ignore: true)
@override
int get hashCode => Object.hash(
runtimeType, const DeepCollectionEquality().hash(_savedViews));
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$_SavedViewRepositoryStateCopyWith<_$_SavedViewRepositoryState>
get copyWith => __$$_SavedViewRepositoryStateCopyWithImpl<
_$_SavedViewRepositoryState>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$_SavedViewRepositoryStateToJson(
this,
);
}
}
abstract class _SavedViewRepositoryState implements SavedViewRepositoryState {
const factory _SavedViewRepositoryState(
{final Map<int, SavedView> savedViews}) = _$_SavedViewRepositoryState;
factory _SavedViewRepositoryState.fromJson(Map<String, dynamic> json) =
_$_SavedViewRepositoryState.fromJson;
@override
Map<int, SavedView> get savedViews;
@override
@JsonKey(ignore: true)
_$$_SavedViewRepositoryStateCopyWith<_$_SavedViewRepositoryState>
get copyWith => throw _privateConstructorUsedError;
}

View File

@@ -0,0 +1,40 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/persistent_repository.dart';
part 'user_repository_state.dart';
part 'user_repository.freezed.dart';
part 'user_repository.g.dart';
/// Repository for new users (API v3, server version 1.14.2+)
class UserRepository extends PersistentRepository<UserRepositoryState> {
final PaperlessUserApiV3 _userApiV3;
UserRepository(this._userApiV3) : super(const UserRepositoryState());
Future<void> initialize() async {
await findAll();
}
Future<Iterable<UserModel>> findAll() async {
final users = await _userApiV3.findAll();
emit(state.copyWith(users: {for (var e in users) e.id: e}));
return users;
}
Future<UserModel?> find(int id) async {
final user = await _userApiV3.find(id);
emit(state.copyWith(users: state.users..[id] = user));
return user;
}
@override
UserRepositoryState? fromJson(Map<String, dynamic> json) {
return UserRepositoryState.fromJson(json);
}
@override
Map<String, dynamic>? toJson(UserRepositoryState state) {
return state.toJson();
}
}

View File

@@ -0,0 +1,11 @@
part of 'user_repository.dart';
@freezed
class UserRepositoryState with _$UserRepositoryState {
const factory UserRepositoryState({
@Default({}) Map<int, UserModel> users,
}) = _UserRepositoryState;
factory UserRepositoryState.fromJson(Map<String, dynamic> json) =>
_$UserRepositoryStateFromJson(json);
}

View File

@@ -2,7 +2,7 @@ import 'dart:io';
import 'package:dio/dio.dart';
import 'package:dio/io.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:flutter/material.dart';
import 'package:paperless_mobile/core/interceptor/retry_on_connection_change_interceptor.dart';
import 'package:paperless_mobile/features/login/model/client_certificate.dart';
import 'package:pretty_dio_logger/pretty_dio_logger.dart';
@@ -10,15 +10,10 @@ import 'package:pretty_dio_logger/pretty_dio_logger.dart';
/// Manages the security context, authentication and base request URL for
/// an underlying [Dio] client which is injected into all services
/// requiring authenticated access to the Paperless HTTP API.
class SessionManager {
final Dio _client;
PaperlessServerInformationModel _serverInformation;
class SessionManager extends ValueNotifier<Dio> {
Dio get client => value;
Dio get client => _client;
SessionManager([List<Interceptor> interceptors = const []])
: _client = _initDio(interceptors),
_serverInformation = PaperlessServerInformationModel();
SessionManager([List<Interceptor> interceptors = const []]) : super(_initDio(interceptors));
static Dio _initDio(List<Interceptor> interceptors) {
//en- and decoded by utf8 by default
@@ -48,7 +43,6 @@ class SessionManager {
String? baseUrl,
String? authToken,
ClientCertificate? clientCertificate,
PaperlessServerInformationModel? serverInformation,
}) {
if (clientCertificate != null) {
final context = SecurityContext()
@@ -81,15 +75,13 @@ class SessionManager {
});
}
if (serverInformation != null) {
_serverInformation = serverInformation;
}
notifyListeners();
}
void resetSettings() {
client.httpClientAdapter = IOHttpClientAdapter();
client.options.baseUrl = '';
client.options.headers.remove(HttpHeaders.authorizationHeader);
_serverInformation = PaperlessServerInformationModel();
notifyListeners();
}
}

View File

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

View File

@@ -1,16 +1,8 @@
import 'dart:convert';
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/bloc/document_status_cubit.dart';
import 'package:paperless_mobile/core/model/document_processing_status.dart';
import 'package:paperless_mobile/features/login/model/authentication_information.dart';
import 'package:paperless_mobile/constants.dart';
import 'package:paperless_mobile/core/database/tables/user_credentials.dart';
import 'package:web_socket_channel/io.dart';
// import 'package:web_socket_channel/io.dart';
abstract class StatusService {
Future<void> startListeningBeforeDocumentUpload(
@@ -19,7 +11,7 @@ abstract class StatusService {
class WebSocketStatusService implements StatusService {
late WebSocket? socket;
late IOWebSocketChannel? _channel;
// late IOWebSocketChannel? _channel;
WebSocketStatusService();

View File

@@ -1,5 +1,3 @@
import 'package:paperless_api/paperless_api.dart';
import 'package:rxdart/subjects.dart';
typedef JSON = Map<String, dynamic>;
typedef PaperlessValidationErrors = Map<String, String>;

View File

@@ -1,6 +1,4 @@
import 'package:flutter/material.dart';
import 'package:flutter/src/widgets/framework.dart';
import 'package:flutter/src/widgets/placeholder.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
class DialogCancelButton extends StatelessWidget {

View File

@@ -1,6 +1,4 @@
import 'package:flutter/material.dart';
import 'package:flutter/src/widgets/framework.dart';
import 'package:flutter/src/widgets/placeholder.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
enum DialogConfirmButtonStyle {

View File

@@ -181,8 +181,7 @@ class FormBuilderColorPickerField extends FormBuilderField<Color> {
);
@override
FormBuilderColorPickerFieldState createState() =>
FormBuilderColorPickerFieldState();
FormBuilderColorPickerFieldState createState() => FormBuilderColorPickerFieldState();
}
class FormBuilderColorPickerFieldState
@@ -217,8 +216,6 @@ class FormBuilderColorPickerFieldState
final selected = await showDialog<bool>(
context: context,
builder: (BuildContext context) {
final materialLocalizations = S.of(context)!;
return AlertDialog(
// title: null, //const Text('Pick a color!'),
content: _buildColorPicker(),

View File

@@ -1,5 +1,4 @@
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';

View File

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

View File

@@ -1,603 +0,0 @@
//TODO: REMOVE THIS WHEN NATIVE MATERIAL FLUTTER SEARCH IS RELEASED
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
/// Shows a full screen search page and returns the search result selected by
/// the user when the page is closed.
///
/// The search page consists of an app bar with a search field and a body which
/// can either show suggested search queries or the search results.
///
/// The appearance of the search page is determined by the provided
/// `delegate`. The initial query string is given by `query`, which defaults
/// to the empty string. When `query` is set to null, `delegate.query` will
/// be used as the initial query.
///
/// This method returns the selected search result, which can be set in the
/// [SearchDelegate.close] call. If the search page is closed with the system
/// back button, it returns null.
///
/// A given [SearchDelegate] can only be associated with one active [showMaterial3Search]
/// call. Call [SearchDelegate.close] before re-using the same delegate instance
/// for another [showMaterial3Search] call.
///
/// The `useRootNavigator` argument is used to determine whether to push the
/// search page to the [Navigator] furthest from or nearest to the given
/// `context`. By default, `useRootNavigator` is `false` and the search page
/// route created by this method is pushed to the nearest navigator to the
/// given `context`. It can not be `null`.
///
/// The transition to the search page triggered by this method looks best if the
/// screen triggering the transition contains an [AppBar] at the top and the
/// transition is called from an [IconButton] that's part of [AppBar.actions].
/// The animation provided by [SearchDelegate.transitionAnimation] can be used
/// to trigger additional animations in the underlying page while the search
/// page fades in or out. This is commonly used to animate an [AnimatedIcon] in
/// the [AppBar.leading] position e.g. from the hamburger menu to the back arrow
/// used to exit the search page.
///
/// ## Handling emojis and other complex characters
/// {@macro flutter.widgets.EditableText.onChanged}
///
/// See also:
///
/// * [SearchDelegate] to define the content of the search page.
Future<T?> showMaterial3Search<T>({
required BuildContext context,
required SearchDelegate<T> delegate,
String? query = '',
bool useRootNavigator = false,
}) {
delegate.query = query ?? delegate.query;
delegate._currentBody = _SearchBody.suggestions;
return Navigator.of(context, rootNavigator: useRootNavigator)
.push(_SearchPageRoute<T>(
delegate: delegate,
));
}
/// Delegate for [showMaterial3Search] to define the content of the search page.
///
/// The search page always shows an [AppBar] at the top where users can
/// enter their search queries. The buttons shown before and after the search
/// query text field can be customized via [SearchDelegate.buildLeading]
/// and [SearchDelegate.buildActions]. Additionally, a widget can be placed
/// across the bottom of the [AppBar] via [SearchDelegate.buildBottom].
///
/// The body below the [AppBar] can either show suggested queries (returned by
/// [SearchDelegate.buildSuggestions]) or - once the user submits a search - the
/// results of the search as returned by [SearchDelegate.buildResults].
///
/// [SearchDelegate.query] always contains the current query entered by the user
/// and should be used to build the suggestions and results.
///
/// The results can be brought on screen by calling [SearchDelegate.showResults]
/// and you can go back to showing the suggestions by calling
/// [SearchDelegate.showSuggestions].
///
/// Once the user has selected a search result, [SearchDelegate.close] should be
/// called to remove the search page from the top of the navigation stack and
/// to notify the caller of [showMaterial3Search] about the selected search result.
///
/// A given [SearchDelegate] can only be associated with one active [showMaterial3Search]
/// call. Call [SearchDelegate.close] before re-using the same delegate instance
/// for another [showMaterial3Search] call.
///
/// ## Handling emojis and other complex characters
/// {@macro flutter.widgets.EditableText.onChanged}
abstract class SearchDelegate<T> {
/// Constructor to be called by subclasses which may specify
/// [searchFieldLabel], either [searchFieldStyle] or [searchFieldDecorationTheme],
/// [keyboardType] and/or [textInputAction]. Only one of [searchFieldLabel]
/// and [searchFieldDecorationTheme] may be non-null.
///
/// {@tool snippet}
/// ```dart
/// class CustomSearchHintDelegate extends SearchDelegate<String> {
/// CustomSearchHintDelegate({
/// required String hintText,
/// }) : super(
/// searchFieldLabel: hintText,
/// keyboardType: TextInputType.text,
/// textInputAction: TextInputAction.search,
/// );
///
/// @override
/// Widget buildLeading(BuildContext context) => const Text('leading');
///
/// @override
/// PreferredSizeWidget buildBottom(BuildContext context) {
/// return const PreferredSize(
/// preferredSize: Size.fromHeight(56.0),
/// child: Text('bottom'));
/// }
///
/// @override
/// Widget buildSuggestions(BuildContext context) => const Text('suggestions');
///
/// @override
/// Widget buildResults(BuildContext context) => const Text('results');
///
/// @override
/// List<Widget> buildActions(BuildContext context) => <Widget>[];
/// }
/// ```
/// {@end-tool}
SearchDelegate({
this.searchFieldLabel,
this.searchFieldStyle,
this.searchFieldDecorationTheme,
this.keyboardType,
this.textInputAction = TextInputAction.search,
}) : assert(searchFieldStyle == null || searchFieldDecorationTheme == null);
/// Suggestions shown in the body of the search page while the user types a
/// query into the search field.
///
/// The delegate method is called whenever the content of [query] changes.
/// The suggestions should be based on the current [query] string. If the query
/// string is empty, it is good practice to show suggested queries based on
/// past queries or the current context.
///
/// Usually, this method will return a [ListView] with one [ListTile] per
/// suggestion. When [ListTile.onTap] is called, [query] should be updated
/// with the corresponding suggestion and the results page should be shown
/// by calling [showResults].
Widget buildSuggestions(BuildContext context);
/// The results shown after the user submits a search from the search page.
///
/// The current value of [query] can be used to determine what the user
/// searched for.
///
/// This method might be applied more than once to the same query.
/// If your [buildResults] method is computationally expensive, you may want
/// to cache the search results for one or more queries.
///
/// Typically, this method returns a [ListView] with the search results.
/// When the user taps on a particular search result, [close] should be called
/// with the selected result as argument. This will close the search page and
/// communicate the result back to the initial caller of [showMaterial3Search].
Widget buildResults(BuildContext context);
/// A widget to display before the current query in the [AppBar].
///
/// Typically an [IconButton] configured with a [BackButtonIcon] that exits
/// the search with [close]. One can also use an [AnimatedIcon] driven by
/// [transitionAnimation], which animates from e.g. a hamburger menu to the
/// back button as the search overlay fades in.
///
/// Returns null if no widget should be shown.
///
/// See also:
///
/// * [AppBar.leading], the intended use for the return value of this method.
Widget? buildLeading(BuildContext context);
/// Widgets to display after the search query in the [AppBar].
///
/// If the [query] is not empty, this should typically contain a button to
/// clear the query and show the suggestions again (via [showSuggestions]) if
/// the results are currently shown.
///
/// Returns null if no widget should be shown.
///
/// See also:
///
/// * [AppBar.actions], the intended use for the return value of this method.
List<Widget>? buildActions(BuildContext context);
/// Widget to display across the bottom of the [AppBar].
///
/// Returns null by default, i.e. a bottom widget is not included.
///
/// See also:
///
/// * [AppBar.bottom], the intended use for the return value of this method.
///
PreferredSizeWidget? buildBottom(BuildContext context) => null;
/// The theme used to configure the search page.
///
/// The returned [ThemeData] will be used to wrap the entire search page,
/// so it can be used to configure any of its components with the appropriate
/// theme properties.
///
/// Unless overridden, the default theme will configure the AppBar containing
/// the search input text field with a white background and black text on light
/// themes. For dark themes the default is a dark grey background with light
/// color text.
///
/// See also:
///
/// * [AppBarTheme], which configures the AppBar's appearance.
/// * [InputDecorationTheme], which configures the appearance of the search
/// text field.
ThemeData appBarTheme(BuildContext context) {
final ThemeData theme = Theme.of(context);
final ColorScheme colorScheme = theme.colorScheme;
return theme.copyWith(
appBarTheme: AppBarTheme(
systemOverlayStyle: colorScheme.brightness == Brightness.light
? SystemUiOverlayStyle.light
: SystemUiOverlayStyle.dark,
backgroundColor: colorScheme.brightness == Brightness.dark
? Colors.grey[900]
: Colors.white,
iconTheme: theme.primaryIconTheme.copyWith(color: Colors.grey),
),
inputDecorationTheme: searchFieldDecorationTheme ??
InputDecorationTheme(
hintStyle: searchFieldStyle ?? theme.inputDecorationTheme.hintStyle,
border: InputBorder.none,
),
);
}
/// The current query string shown in the [AppBar].
///
/// The user manipulates this string via the keyboard.
///
/// If the user taps on a suggestion provided by [buildSuggestions] this
/// string should be updated to that suggestion via the setter.
String get query => _queryTextController.text;
/// Changes the current query string.
///
/// Setting the query string programmatically moves the cursor to the end of the text field.
set query(String value) {
assert(query != null);
_queryTextController.text = value;
if (_queryTextController.text.isNotEmpty) {
_queryTextController.selection = TextSelection.fromPosition(
TextPosition(offset: _queryTextController.text.length));
}
}
/// Transition from the suggestions returned by [buildSuggestions] to the
/// [query] results returned by [buildResults].
///
/// If the user taps on a suggestion provided by [buildSuggestions] the
/// screen should typically transition to the page showing the search
/// results for the suggested query. This transition can be triggered
/// by calling this method.
///
/// See also:
///
/// * [showSuggestions] to show the search suggestions again.
void showResults(BuildContext context) {
_focusNode?.unfocus();
_currentBody = _SearchBody.results;
}
/// Transition from showing the results returned by [buildResults] to showing
/// the suggestions returned by [buildSuggestions].
///
/// Calling this method will also put the input focus back into the search
/// field of the [AppBar].
///
/// If the results are currently shown this method can be used to go back
/// to showing the search suggestions.
///
/// See also:
///
/// * [showResults] to show the search results.
void showSuggestions(BuildContext context) {
assert(_focusNode != null,
'_focusNode must be set by route before showSuggestions is called.');
_focusNode!.requestFocus();
_currentBody = _SearchBody.suggestions;
}
/// Closes the search page and returns to the underlying route.
///
/// The value provided for `result` is used as the return value of the call
/// to [showMaterial3Search] that launched the search initially.
void close(BuildContext context, T result) {
_currentBody = null;
_focusNode?.unfocus();
Navigator.of(context)
..popUntil((Route<dynamic> route) => route == _route)
..pop(result);
}
/// The hint text that is shown in the search field when it is empty.
///
/// If this value is set to null, the value of
/// `MaterialLocalizationS.of(context)!.searchFieldLabel` will be used instead.
final String? searchFieldLabel;
/// The style of the [searchFieldLabel].
///
/// If this value is set to null, the value of the ambient [Theme]'s
/// [InputDecorationTheme.hintStyle] will be used instead.
///
/// Only one of [searchFieldStyle] or [searchFieldDecorationTheme] can
/// be non-null.
final TextStyle? searchFieldStyle;
/// The [InputDecorationTheme] used to configure the search field's visuals.
///
/// Only one of [searchFieldStyle] or [searchFieldDecorationTheme] can
/// be non-null.
final InputDecorationTheme? searchFieldDecorationTheme;
/// The type of action button to use for the keyboard.
///
/// Defaults to the default value specified in [TextField].
final TextInputType? keyboardType;
/// The text input action configuring the soft keyboard to a particular action
/// button.
///
/// Defaults to [TextInputAction.search].
final TextInputAction textInputAction;
/// [Animation] triggered when the search pages fades in or out.
///
/// This animation is commonly used to animate [AnimatedIcon]s of
/// [IconButton]s returned by [buildLeading] or [buildActions]. It can also be
/// used to animate [IconButton]s contained within the route below the search
/// page.
Animation<double> get transitionAnimation => _proxyAnimation;
// The focus node to use for manipulating focus on the search page. This is
// managed, owned, and set by the _SearchPageRoute using this delegate.
FocusNode? _focusNode;
final TextEditingController _queryTextController = TextEditingController();
final ProxyAnimation _proxyAnimation =
ProxyAnimation(kAlwaysDismissedAnimation);
final ValueNotifier<_SearchBody?> _currentBodyNotifier =
ValueNotifier<_SearchBody?>(null);
_SearchBody? get _currentBody => _currentBodyNotifier.value;
set _currentBody(_SearchBody? value) {
_currentBodyNotifier.value = value;
}
_SearchPageRoute<T>? _route;
}
/// Describes the body that is currently shown under the [AppBar] in the
/// search page.
enum _SearchBody {
/// Suggested queries are shown in the body.
///
/// The suggested queries are generated by [SearchDelegate.buildSuggestions].
suggestions,
/// Search results are currently shown in the body.
///
/// The search results are generated by [SearchDelegate.buildResults].
results,
}
class _SearchPageRoute<T> extends PageRoute<T> {
_SearchPageRoute({
required this.delegate,
}) {
assert(
delegate._route == null,
'The ${delegate.runtimeType} instance is currently used by another active '
'search. Please close that search by calling close() on the SearchDelegate '
'before opening another search with the same delegate instance.',
);
delegate._route = this;
}
final SearchDelegate<T> delegate;
@override
Color? get barrierColor => null;
@override
String? get barrierLabel => null;
@override
Duration get transitionDuration => const Duration(milliseconds: 300);
@override
bool get maintainState => false;
@override
Widget buildTransitions(
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child,
) {
return FadeTransition(
opacity: animation,
child: child,
);
}
@override
Animation<double> createAnimation() {
final Animation<double> animation = super.createAnimation();
delegate._proxyAnimation.parent = animation;
return animation;
}
@override
Widget buildPage(
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
) {
return _SearchPage<T>(
delegate: delegate,
animation: animation,
);
}
@override
void didComplete(T? result) {
super.didComplete(result);
assert(delegate._route == this);
delegate._route = null;
delegate._currentBody = null;
}
}
class _SearchPage<T> extends StatefulWidget {
const _SearchPage({
required this.delegate,
required this.animation,
});
final SearchDelegate<T> delegate;
final Animation<double> animation;
@override
State<StatefulWidget> createState() => _SearchPageState<T>();
}
class _SearchPageState<T> extends State<_SearchPage<T>> {
// This node is owned, but not hosted by, the search page. Hosting is done by
// the text field.
FocusNode focusNode = FocusNode();
@override
void initState() {
super.initState();
widget.delegate._queryTextController.addListener(_onQueryChanged);
widget.animation.addStatusListener(_onAnimationStatusChanged);
widget.delegate._currentBodyNotifier.addListener(_onSearchBodyChanged);
focusNode.addListener(_onFocusChanged);
widget.delegate._focusNode = focusNode;
}
@override
void dispose() {
super.dispose();
widget.delegate._queryTextController.removeListener(_onQueryChanged);
widget.animation.removeStatusListener(_onAnimationStatusChanged);
widget.delegate._currentBodyNotifier.removeListener(_onSearchBodyChanged);
widget.delegate._focusNode = null;
focusNode.dispose();
}
void _onAnimationStatusChanged(AnimationStatus status) {
if (status != AnimationStatus.completed) {
return;
}
widget.animation.removeStatusListener(_onAnimationStatusChanged);
if (widget.delegate._currentBody == _SearchBody.suggestions) {
focusNode.requestFocus();
}
}
@override
void didUpdateWidget(_SearchPage<T> oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.delegate != oldWidget.delegate) {
oldWidget.delegate._queryTextController.removeListener(_onQueryChanged);
widget.delegate._queryTextController.addListener(_onQueryChanged);
oldWidget.delegate._currentBodyNotifier
.removeListener(_onSearchBodyChanged);
widget.delegate._currentBodyNotifier.addListener(_onSearchBodyChanged);
oldWidget.delegate._focusNode = null;
widget.delegate._focusNode = focusNode;
}
}
void _onFocusChanged() {
if (focusNode.hasFocus &&
widget.delegate._currentBody != _SearchBody.suggestions) {
widget.delegate.showSuggestions(context);
}
}
void _onQueryChanged() {
setState(() {
// rebuild ourselves because query changed.
});
}
void _onSearchBodyChanged() {
setState(() {
// rebuild ourselves because search body changed.
});
}
@override
Widget build(BuildContext context) {
assert(debugCheckHasMaterialLocalizations(context));
final ThemeData theme = widget.delegate.appBarTheme(context);
final String searchFieldLabel =
widget.delegate.searchFieldLabel ?? S.of(context)!.search;
Widget? body;
switch (widget.delegate._currentBody) {
case _SearchBody.suggestions:
body = KeyedSubtree(
key: const ValueKey<_SearchBody>(_SearchBody.suggestions),
child: widget.delegate.buildSuggestions(context),
);
break;
case _SearchBody.results:
body = KeyedSubtree(
key: const ValueKey<_SearchBody>(_SearchBody.results),
child: widget.delegate.buildResults(context),
);
break;
case null:
break;
}
late final String routeName;
switch (theme.platform) {
case TargetPlatform.iOS:
case TargetPlatform.macOS:
routeName = '';
break;
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows:
routeName = searchFieldLabel;
}
return Semantics(
explicitChildNodes: true,
scopesRoute: true,
namesRoute: true,
label: routeName,
child: Theme(
data: theme,
child: Scaffold(
appBar: AppBar(
toolbarHeight: 72,
leading: widget.delegate.buildLeading(context),
title: TextField(
controller: widget.delegate._queryTextController,
focusNode: focusNode,
style: widget.delegate.searchFieldStyle ??
theme.textTheme.titleLarge,
textInputAction: widget.delegate.textInputAction,
keyboardType: widget.delegate.keyboardType,
onSubmitted: (String _) {
widget.delegate.showResults(context);
},
decoration: InputDecoration(hintText: searchFieldLabel),
),
actions: widget.delegate.buildActions(context),
bottom: widget.delegate.buildBottom(context),
),
body: AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: body,
),
),
),
);
}
}

View File

@@ -1,79 +0,0 @@
import 'dart:math';
import 'package:flutter/material.dart';
class SearchBar extends StatelessWidget {
const SearchBar({
Key? key,
this.height = 56,
required this.leadingIcon,
this.trailingIcon,
required this.supportingText,
required this.onTap,
}) : super(key: key);
final double height;
double get effectiveHeight {
return max(height, 48);
}
final VoidCallback onTap;
final Widget leadingIcon;
final Widget? trailingIcon;
final String supportingText;
@override
Widget build(BuildContext context) {
final ColorScheme colorScheme = Theme.of(context).colorScheme;
final TextTheme textTheme = Theme.of(context).textTheme;
return Container(
constraints: const BoxConstraints(minWidth: 360, maxWidth: 720),
width: double.infinity,
height: effectiveHeight,
child: Material(
elevation: 1,
color: colorScheme.surface,
shadowColor: colorScheme.shadow,
surfaceTintColor: colorScheme.surfaceTint,
borderRadius: BorderRadius.circular(effectiveHeight / 2),
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(effectiveHeight / 2),
highlightColor: Colors.transparent,
splashFactory: InkRipple.splashFactory,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Row(children: [
leadingIcon,
Expanded(
child: Padding(
padding: const EdgeInsets.only(right: 8),
child: TextField(
onTap: onTap,
readOnly: true,
enabled: false,
cursorColor: colorScheme.primary,
style: textTheme.bodyLarge,
textAlignVertical: TextAlignVertical.center,
decoration: InputDecoration(
isCollapsed: true,
border: InputBorder.none,
contentPadding: const EdgeInsets.symmetric(horizontal: 8),
hintText: supportingText,
hintStyle: textTheme.bodyLarge?.apply(
color: colorScheme.onSurfaceVariant,
),
),
),
),
),
if (trailingIcon != null) trailingIcon!,
]),
),
),
),
);
}
}

View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,16 +1,16 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/constants.dart';
import 'package:paperless_mobile/core/bloc/server_information_cubit.dart';
import 'package:paperless_mobile/core/widgets/paperless_logo.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/home/view/model/api_version.dart';
import 'package:paperless_mobile/features/settings/view/settings_page.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
import 'package:provider/provider.dart';
import 'package:url_launcher/link.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:url_launcher/url_launcher_string.dart';
class AppDrawer extends StatelessWidget {
@@ -19,7 +19,6 @@ class AppDrawer extends StatelessWidget {
@override
Widget build(BuildContext context) {
return SafeArea(
top: true,
child: Drawer(
child: Column(
children: [
@@ -42,7 +41,16 @@ class AppDrawer extends StatelessWidget {
ListTile(
dense: true,
leading: const Icon(Icons.bug_report_outlined),
title: Text(S.of(context)!.reportABug),
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(S.of(context)!.reportABug),
const Icon(
Icons.open_in_new,
size: 16,
)
],
),
onTap: () {
launchUrlString(
'https://github.com/astubenbord/paperless-mobile/issues/new',
@@ -64,7 +72,7 @@ class AppDrawer extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(S.of(context)!.donateCoffee),
Icon(
const Icon(
Icons.open_in_new,
size: 16,
)
@@ -85,8 +93,11 @@ class AppDrawer extends StatelessWidget {
),
onTap: () => Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => BlocProvider.value(
value: context.read<ServerInformationCubit>(),
builder: (_) => MultiProvider(
providers: [
Provider.value(value: context.read<PaperlessServerStatsApi>()),
Provider.value(value: context.read<ApiVersion>()),
],
child: const SettingsPage(),
),
),
@@ -99,6 +110,8 @@ class AppDrawer extends StatelessWidget {
}
void _showAboutDialog(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
showAboutDialog(
context: context,
applicationIcon: const ImageIcon(
@@ -111,10 +124,11 @@ class AppDrawer extends StatelessWidget {
const SizedBox(height: 16),
Text(
"Source Code",
style: Theme.of(context).textTheme.titleMedium,
style: theme.textTheme.titleMedium,
),
RichText(
text: TextSpan(
style: theme.textTheme.bodyMedium?.copyWith(color: colorScheme.onSurface),
children: [
TextSpan(
text: S.of(context)!.findTheSourceCodeOn,
@@ -137,9 +151,30 @@ class AppDrawer extends StatelessWidget {
const SizedBox(height: 16),
Text(
'Credits',
style: Theme.of(context).textTheme.titleMedium,
style: theme.textTheme.titleMedium?.copyWith(color: colorScheme.onSurface),
),
RichText(
text: TextSpan(
style: theme.textTheme.bodyMedium?.copyWith(color: colorScheme.onSurface),
children: [
const TextSpan(
text: 'Onboarding images by ',
),
TextSpan(
text: 'pch.vector',
style: const TextStyle(color: Colors.blue),
recognizer: TapGestureRecognizer()
..onTap = () {
launchUrlString(
'https://www.freepik.com/free-vector/business-team-working-cogwheel-mechanism-together_8270974.htm#query=setting&position=4&from_view=author');
},
),
const TextSpan(
text: ' on Freepik.',
),
],
),
),
_buildOnboardingImageCredits(),
],
);
}
@@ -148,19 +183,19 @@ class AppDrawer extends StatelessWidget {
return RichText(
text: TextSpan(
children: [
TextSpan(
const TextSpan(
text: 'Onboarding images by ',
),
TextSpan(
text: 'pch.vector',
style: TextStyle(color: Colors.blue),
style: const TextStyle(color: Colors.blue),
recognizer: TapGestureRecognizer()
..onTap = () {
launchUrlString(
'https://www.freepik.com/free-vector/business-team-working-cogwheel-mechanism-together_8270974.htm#query=setting&position=4&from_view=author');
},
),
TextSpan(
const TextSpan(
text: ' on Freepik.',
),
],

View File

@@ -50,7 +50,7 @@ class _ApplicationIntroSlideshowState extends State<ApplicationIntroSlideshow> {
image: AssetImages.organizeDocuments.image,
),
),
bodyWidget: Column(
bodyWidget: const Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
@@ -70,7 +70,7 @@ class _ApplicationIntroSlideshowState extends State<ApplicationIntroSlideshow> {
padding: const EdgeInsets.all(8.0),
child: Image(image: AssetImages.secureDocuments.image),
),
bodyWidget: Column(
bodyWidget: const Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
@@ -90,8 +90,8 @@ class _ApplicationIntroSlideshowState extends State<ApplicationIntroSlideshow> {
padding: const EdgeInsets.all(8.0),
child: Image(image: AssetImages.success.image),
),
bodyWidget: Column(
children: const [
bodyWidget: const Column(
children: [
BiometricAuthenticationSetting(),
LanguageSelectionSetting(),
ThemeModeSetting(),

View File

@@ -2,7 +2,6 @@ import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:collection/collection.dart';
import 'package:equatable/equatable.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';

View File

@@ -1,269 +0,0 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'document_bulk_action_cubit.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods');
/// @nodoc
mixin _$DocumentBulkActionState {
List<DocumentModel> get selection => throw _privateConstructorUsedError;
Map<int, Correspondent> get correspondents =>
throw _privateConstructorUsedError;
Map<int, DocumentType> get documentTypes =>
throw _privateConstructorUsedError;
Map<int, Tag> get tags => throw _privateConstructorUsedError;
Map<int, StoragePath> get storagePaths => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$DocumentBulkActionStateCopyWith<DocumentBulkActionState> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $DocumentBulkActionStateCopyWith<$Res> {
factory $DocumentBulkActionStateCopyWith(DocumentBulkActionState value,
$Res Function(DocumentBulkActionState) then) =
_$DocumentBulkActionStateCopyWithImpl<$Res, DocumentBulkActionState>;
@useResult
$Res call(
{List<DocumentModel> selection,
Map<int, Correspondent> correspondents,
Map<int, DocumentType> documentTypes,
Map<int, Tag> tags,
Map<int, StoragePath> storagePaths});
}
/// @nodoc
class _$DocumentBulkActionStateCopyWithImpl<$Res,
$Val extends DocumentBulkActionState>
implements $DocumentBulkActionStateCopyWith<$Res> {
_$DocumentBulkActionStateCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
@pragma('vm:prefer-inline')
@override
$Res call({
Object? selection = null,
Object? correspondents = null,
Object? documentTypes = null,
Object? tags = null,
Object? storagePaths = null,
}) {
return _then(_value.copyWith(
selection: null == selection
? _value.selection
: selection // ignore: cast_nullable_to_non_nullable
as List<DocumentModel>,
correspondents: null == correspondents
? _value.correspondents
: correspondents // ignore: cast_nullable_to_non_nullable
as Map<int, Correspondent>,
documentTypes: null == documentTypes
? _value.documentTypes
: documentTypes // ignore: cast_nullable_to_non_nullable
as Map<int, DocumentType>,
tags: null == tags
? _value.tags
: tags // ignore: cast_nullable_to_non_nullable
as Map<int, Tag>,
storagePaths: null == storagePaths
? _value.storagePaths
: storagePaths // ignore: cast_nullable_to_non_nullable
as Map<int, StoragePath>,
) as $Val);
}
}
/// @nodoc
abstract class _$$_DocumentBulkActionStateCopyWith<$Res>
implements $DocumentBulkActionStateCopyWith<$Res> {
factory _$$_DocumentBulkActionStateCopyWith(_$_DocumentBulkActionState value,
$Res Function(_$_DocumentBulkActionState) then) =
__$$_DocumentBulkActionStateCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{List<DocumentModel> selection,
Map<int, Correspondent> correspondents,
Map<int, DocumentType> documentTypes,
Map<int, Tag> tags,
Map<int, StoragePath> storagePaths});
}
/// @nodoc
class __$$_DocumentBulkActionStateCopyWithImpl<$Res>
extends _$DocumentBulkActionStateCopyWithImpl<$Res,
_$_DocumentBulkActionState>
implements _$$_DocumentBulkActionStateCopyWith<$Res> {
__$$_DocumentBulkActionStateCopyWithImpl(_$_DocumentBulkActionState _value,
$Res Function(_$_DocumentBulkActionState) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? selection = null,
Object? correspondents = null,
Object? documentTypes = null,
Object? tags = null,
Object? storagePaths = null,
}) {
return _then(_$_DocumentBulkActionState(
selection: null == selection
? _value._selection
: selection // ignore: cast_nullable_to_non_nullable
as List<DocumentModel>,
correspondents: null == correspondents
? _value._correspondents
: correspondents // ignore: cast_nullable_to_non_nullable
as Map<int, Correspondent>,
documentTypes: null == documentTypes
? _value._documentTypes
: documentTypes // ignore: cast_nullable_to_non_nullable
as Map<int, DocumentType>,
tags: null == tags
? _value._tags
: tags // ignore: cast_nullable_to_non_nullable
as Map<int, Tag>,
storagePaths: null == storagePaths
? _value._storagePaths
: storagePaths // ignore: cast_nullable_to_non_nullable
as Map<int, StoragePath>,
));
}
}
/// @nodoc
class _$_DocumentBulkActionState extends _DocumentBulkActionState {
const _$_DocumentBulkActionState(
{required final List<DocumentModel> selection,
required final Map<int, Correspondent> correspondents,
required final Map<int, DocumentType> documentTypes,
required final Map<int, Tag> tags,
required final Map<int, StoragePath> storagePaths})
: _selection = selection,
_correspondents = correspondents,
_documentTypes = documentTypes,
_tags = tags,
_storagePaths = storagePaths,
super._();
final List<DocumentModel> _selection;
@override
List<DocumentModel> get selection {
if (_selection is EqualUnmodifiableListView) return _selection;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_selection);
}
final Map<int, Correspondent> _correspondents;
@override
Map<int, Correspondent> get correspondents {
if (_correspondents is EqualUnmodifiableMapView) return _correspondents;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(_correspondents);
}
final Map<int, DocumentType> _documentTypes;
@override
Map<int, DocumentType> get documentTypes {
if (_documentTypes is EqualUnmodifiableMapView) return _documentTypes;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(_documentTypes);
}
final Map<int, Tag> _tags;
@override
Map<int, Tag> get tags {
if (_tags is EqualUnmodifiableMapView) return _tags;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(_tags);
}
final Map<int, StoragePath> _storagePaths;
@override
Map<int, StoragePath> get storagePaths {
if (_storagePaths is EqualUnmodifiableMapView) return _storagePaths;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(_storagePaths);
}
@override
String toString() {
return 'DocumentBulkActionState(selection: $selection, correspondents: $correspondents, documentTypes: $documentTypes, tags: $tags, storagePaths: $storagePaths)';
}
@override
bool operator ==(dynamic other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$_DocumentBulkActionState &&
const DeepCollectionEquality()
.equals(other._selection, _selection) &&
const DeepCollectionEquality()
.equals(other._correspondents, _correspondents) &&
const DeepCollectionEquality()
.equals(other._documentTypes, _documentTypes) &&
const DeepCollectionEquality().equals(other._tags, _tags) &&
const DeepCollectionEquality()
.equals(other._storagePaths, _storagePaths));
}
@override
int get hashCode => Object.hash(
runtimeType,
const DeepCollectionEquality().hash(_selection),
const DeepCollectionEquality().hash(_correspondents),
const DeepCollectionEquality().hash(_documentTypes),
const DeepCollectionEquality().hash(_tags),
const DeepCollectionEquality().hash(_storagePaths));
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$_DocumentBulkActionStateCopyWith<_$_DocumentBulkActionState>
get copyWith =>
__$$_DocumentBulkActionStateCopyWithImpl<_$_DocumentBulkActionState>(
this, _$identity);
}
abstract class _DocumentBulkActionState extends DocumentBulkActionState {
const factory _DocumentBulkActionState(
{required final List<DocumentModel> selection,
required final Map<int, Correspondent> correspondents,
required final Map<int, DocumentType> documentTypes,
required final Map<int, Tag> tags,
required final Map<int, StoragePath> storagePaths}) =
_$_DocumentBulkActionState;
const _DocumentBulkActionState._() : super._();
@override
List<DocumentModel> get selection;
@override
Map<int, Correspondent> get correspondents;
@override
Map<int, DocumentType> get documentTypes;
@override
Map<int, Tag> get tags;
@override
Map<int, StoragePath> get storagePaths;
@override
@JsonKey(ignore: true)
_$$_DocumentBulkActionStateCopyWith<_$_DocumentBulkActionState>
get copyWith => throw _privateConstructorUsedError;
}

View File

@@ -17,6 +17,7 @@ class BulkEditLabelBottomSheet<T extends Label> extends StatefulWidget {
final LabelOptionsSelector<T> availableOptionsSelector;
final void Function(int? selectedId) onSubmit;
final int? initialValue;
final bool canCreateNewLabel;
const BulkEditLabelBottomSheet({
super.key,
@@ -26,6 +27,7 @@ class BulkEditLabelBottomSheet<T extends Label> extends StatefulWidget {
required this.availableOptionsSelector,
required this.onSubmit,
this.initialValue,
required this.canCreateNewLabel,
});
@override
@@ -58,6 +60,7 @@ class _BulkEditLabelBottomSheetState<T extends Label> extends State<BulkEditLabe
initialValue: widget.initialValue != null
? IdQueryParameter.fromId(widget.initialValue!)
: const IdQueryParameter.unset(),
canCreateNewLabel: widget.canCreateNewLabel,
name: "labelFormField",
options: widget.availableOptionsSelector(state),
labelText: widget.formFieldLabel,

View File

@@ -1,6 +1,4 @@
import 'package:flutter/material.dart';
import 'package:flutter/src/widgets/framework.dart';
import 'package:flutter/src/widgets/placeholder.dart';
import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_cancel_button.dart';
import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_confirm_button.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';

View File

@@ -26,8 +26,8 @@ class _FullscreenBulkEditTagsWidgetState
/// Tags not assigned to at least one document in the selection
late final List<int> _nonSharedTags;
List<int> _addTags = [];
List<int> _removeTags = [];
final List<int> _addTags = [];
final List<int> _removeTags = [];
late List<int> _filteredTags;
@override

View File

@@ -12,6 +12,7 @@ import 'package:paperless_mobile/core/service/file_description.dart';
import 'package:paperless_mobile/core/service/file_service.dart';
import 'package:paperless_mobile/features/notifications/services/local_notification_service.dart';
import 'package:share_plus/share_plus.dart';
import 'package:cross_file/cross_file.dart';
part 'document_details_cubit.freezed.dart';
part 'document_details_state.dart';

View File

@@ -1,350 +0,0 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'document_details_cubit.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods');
/// @nodoc
mixin _$DocumentDetailsState {
DocumentModel get document => throw _privateConstructorUsedError;
DocumentMetaData? get metaData => throw _privateConstructorUsedError;
bool get isFullContentLoaded => throw _privateConstructorUsedError;
String? get fullContent => throw _privateConstructorUsedError;
FieldSuggestions? get suggestions => throw _privateConstructorUsedError;
Map<int, Correspondent> get correspondents =>
throw _privateConstructorUsedError;
Map<int, DocumentType> get documentTypes =>
throw _privateConstructorUsedError;
Map<int, Tag> get tags => throw _privateConstructorUsedError;
Map<int, StoragePath> get storagePaths => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$DocumentDetailsStateCopyWith<DocumentDetailsState> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $DocumentDetailsStateCopyWith<$Res> {
factory $DocumentDetailsStateCopyWith(DocumentDetailsState value,
$Res Function(DocumentDetailsState) then) =
_$DocumentDetailsStateCopyWithImpl<$Res, DocumentDetailsState>;
@useResult
$Res call(
{DocumentModel document,
DocumentMetaData? metaData,
bool isFullContentLoaded,
String? fullContent,
FieldSuggestions? suggestions,
Map<int, Correspondent> correspondents,
Map<int, DocumentType> documentTypes,
Map<int, Tag> tags,
Map<int, StoragePath> storagePaths});
}
/// @nodoc
class _$DocumentDetailsStateCopyWithImpl<$Res,
$Val extends DocumentDetailsState>
implements $DocumentDetailsStateCopyWith<$Res> {
_$DocumentDetailsStateCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
@pragma('vm:prefer-inline')
@override
$Res call({
Object? document = null,
Object? metaData = freezed,
Object? isFullContentLoaded = null,
Object? fullContent = freezed,
Object? suggestions = freezed,
Object? correspondents = null,
Object? documentTypes = null,
Object? tags = null,
Object? storagePaths = null,
}) {
return _then(_value.copyWith(
document: null == document
? _value.document
: document // ignore: cast_nullable_to_non_nullable
as DocumentModel,
metaData: freezed == metaData
? _value.metaData
: metaData // ignore: cast_nullable_to_non_nullable
as DocumentMetaData?,
isFullContentLoaded: null == isFullContentLoaded
? _value.isFullContentLoaded
: isFullContentLoaded // ignore: cast_nullable_to_non_nullable
as bool,
fullContent: freezed == fullContent
? _value.fullContent
: fullContent // ignore: cast_nullable_to_non_nullable
as String?,
suggestions: freezed == suggestions
? _value.suggestions
: suggestions // ignore: cast_nullable_to_non_nullable
as FieldSuggestions?,
correspondents: null == correspondents
? _value.correspondents
: correspondents // ignore: cast_nullable_to_non_nullable
as Map<int, Correspondent>,
documentTypes: null == documentTypes
? _value.documentTypes
: documentTypes // ignore: cast_nullable_to_non_nullable
as Map<int, DocumentType>,
tags: null == tags
? _value.tags
: tags // ignore: cast_nullable_to_non_nullable
as Map<int, Tag>,
storagePaths: null == storagePaths
? _value.storagePaths
: storagePaths // ignore: cast_nullable_to_non_nullable
as Map<int, StoragePath>,
) as $Val);
}
}
/// @nodoc
abstract class _$$_DocumentDetailsStateCopyWith<$Res>
implements $DocumentDetailsStateCopyWith<$Res> {
factory _$$_DocumentDetailsStateCopyWith(_$_DocumentDetailsState value,
$Res Function(_$_DocumentDetailsState) then) =
__$$_DocumentDetailsStateCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{DocumentModel document,
DocumentMetaData? metaData,
bool isFullContentLoaded,
String? fullContent,
FieldSuggestions? suggestions,
Map<int, Correspondent> correspondents,
Map<int, DocumentType> documentTypes,
Map<int, Tag> tags,
Map<int, StoragePath> storagePaths});
}
/// @nodoc
class __$$_DocumentDetailsStateCopyWithImpl<$Res>
extends _$DocumentDetailsStateCopyWithImpl<$Res, _$_DocumentDetailsState>
implements _$$_DocumentDetailsStateCopyWith<$Res> {
__$$_DocumentDetailsStateCopyWithImpl(_$_DocumentDetailsState _value,
$Res Function(_$_DocumentDetailsState) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? document = null,
Object? metaData = freezed,
Object? isFullContentLoaded = null,
Object? fullContent = freezed,
Object? suggestions = freezed,
Object? correspondents = null,
Object? documentTypes = null,
Object? tags = null,
Object? storagePaths = null,
}) {
return _then(_$_DocumentDetailsState(
document: null == document
? _value.document
: document // ignore: cast_nullable_to_non_nullable
as DocumentModel,
metaData: freezed == metaData
? _value.metaData
: metaData // ignore: cast_nullable_to_non_nullable
as DocumentMetaData?,
isFullContentLoaded: null == isFullContentLoaded
? _value.isFullContentLoaded
: isFullContentLoaded // ignore: cast_nullable_to_non_nullable
as bool,
fullContent: freezed == fullContent
? _value.fullContent
: fullContent // ignore: cast_nullable_to_non_nullable
as String?,
suggestions: freezed == suggestions
? _value.suggestions
: suggestions // ignore: cast_nullable_to_non_nullable
as FieldSuggestions?,
correspondents: null == correspondents
? _value._correspondents
: correspondents // ignore: cast_nullable_to_non_nullable
as Map<int, Correspondent>,
documentTypes: null == documentTypes
? _value._documentTypes
: documentTypes // ignore: cast_nullable_to_non_nullable
as Map<int, DocumentType>,
tags: null == tags
? _value._tags
: tags // ignore: cast_nullable_to_non_nullable
as Map<int, Tag>,
storagePaths: null == storagePaths
? _value._storagePaths
: storagePaths // ignore: cast_nullable_to_non_nullable
as Map<int, StoragePath>,
));
}
}
/// @nodoc
class _$_DocumentDetailsState implements _DocumentDetailsState {
const _$_DocumentDetailsState(
{required this.document,
this.metaData,
this.isFullContentLoaded = false,
this.fullContent,
this.suggestions,
final Map<int, Correspondent> correspondents = const {},
final Map<int, DocumentType> documentTypes = const {},
final Map<int, Tag> tags = const {},
final Map<int, StoragePath> storagePaths = const {}})
: _correspondents = correspondents,
_documentTypes = documentTypes,
_tags = tags,
_storagePaths = storagePaths;
@override
final DocumentModel document;
@override
final DocumentMetaData? metaData;
@override
@JsonKey()
final bool isFullContentLoaded;
@override
final String? fullContent;
@override
final FieldSuggestions? suggestions;
final Map<int, Correspondent> _correspondents;
@override
@JsonKey()
Map<int, Correspondent> get correspondents {
if (_correspondents is EqualUnmodifiableMapView) return _correspondents;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(_correspondents);
}
final Map<int, DocumentType> _documentTypes;
@override
@JsonKey()
Map<int, DocumentType> get documentTypes {
if (_documentTypes is EqualUnmodifiableMapView) return _documentTypes;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(_documentTypes);
}
final Map<int, Tag> _tags;
@override
@JsonKey()
Map<int, Tag> get tags {
if (_tags is EqualUnmodifiableMapView) return _tags;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(_tags);
}
final Map<int, StoragePath> _storagePaths;
@override
@JsonKey()
Map<int, StoragePath> get storagePaths {
if (_storagePaths is EqualUnmodifiableMapView) return _storagePaths;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(_storagePaths);
}
@override
String toString() {
return 'DocumentDetailsState(document: $document, metaData: $metaData, isFullContentLoaded: $isFullContentLoaded, fullContent: $fullContent, suggestions: $suggestions, correspondents: $correspondents, documentTypes: $documentTypes, tags: $tags, storagePaths: $storagePaths)';
}
@override
bool operator ==(dynamic other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$_DocumentDetailsState &&
(identical(other.document, document) ||
other.document == document) &&
(identical(other.metaData, metaData) ||
other.metaData == metaData) &&
(identical(other.isFullContentLoaded, isFullContentLoaded) ||
other.isFullContentLoaded == isFullContentLoaded) &&
(identical(other.fullContent, fullContent) ||
other.fullContent == fullContent) &&
(identical(other.suggestions, suggestions) ||
other.suggestions == suggestions) &&
const DeepCollectionEquality()
.equals(other._correspondents, _correspondents) &&
const DeepCollectionEquality()
.equals(other._documentTypes, _documentTypes) &&
const DeepCollectionEquality().equals(other._tags, _tags) &&
const DeepCollectionEquality()
.equals(other._storagePaths, _storagePaths));
}
@override
int get hashCode => Object.hash(
runtimeType,
document,
metaData,
isFullContentLoaded,
fullContent,
suggestions,
const DeepCollectionEquality().hash(_correspondents),
const DeepCollectionEquality().hash(_documentTypes),
const DeepCollectionEquality().hash(_tags),
const DeepCollectionEquality().hash(_storagePaths));
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$_DocumentDetailsStateCopyWith<_$_DocumentDetailsState> get copyWith =>
__$$_DocumentDetailsStateCopyWithImpl<_$_DocumentDetailsState>(
this, _$identity);
}
abstract class _DocumentDetailsState implements DocumentDetailsState {
const factory _DocumentDetailsState(
{required final DocumentModel document,
final DocumentMetaData? metaData,
final bool isFullContentLoaded,
final String? fullContent,
final FieldSuggestions? suggestions,
final Map<int, Correspondent> correspondents,
final Map<int, DocumentType> documentTypes,
final Map<int, Tag> tags,
final Map<int, StoragePath> storagePaths}) = _$_DocumentDetailsState;
@override
DocumentModel get document;
@override
DocumentMetaData? get metaData;
@override
bool get isFullContentLoaded;
@override
String? get fullContent;
@override
FieldSuggestions? get suggestions;
@override
Map<int, Correspondent> get correspondents;
@override
Map<int, DocumentType> get documentTypes;
@override
Map<int, Tag> get tags;
@override
Map<int, StoragePath> get storagePaths;
@override
@JsonKey(ignore: true)
_$$_DocumentDetailsStateCopyWith<_$_DocumentDetailsState> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@@ -41,7 +41,7 @@ class _SelectFileTypeDialogState extends State<SelectFileTypeDialog> {
},
title: Text(S.of(context)!.archivedPdf),
),
Divider(),
const Divider(),
CheckboxListTile(
controlAffinity: ListTileControlAffinity.leading,
value: _rememberSelection,

View File

@@ -3,6 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:open_filex/open_filex.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
import 'package:paperless_mobile/core/translation/error_code_localization_mapper.dart';
import 'package:paperless_mobile/core/widgets/material/colored_tab_bar.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
@@ -11,19 +12,20 @@ import 'package:paperless_mobile/features/document_details/view/widgets/document
import 'package:paperless_mobile/features/document_details/view/widgets/document_download_button.dart';
import 'package:paperless_mobile/features/document_details/view/widgets/document_meta_data_widget.dart';
import 'package:paperless_mobile/features/document_details/view/widgets/document_overview_widget.dart';
import 'package:paperless_mobile/features/document_details/view/widgets/document_permissions_widget.dart';
import 'package:paperless_mobile/features/document_details/view/widgets/document_share_button.dart';
import 'package:paperless_mobile/features/document_edit/cubit/document_edit_cubit.dart';
import 'package:paperless_mobile/features/document_edit/view/document_edit_page.dart';
import 'package:paperless_mobile/features/documents/view/pages/document_view.dart';
import 'package:paperless_mobile/features/documents/view/widgets/delete_document_confirmation_dialog.dart';
import 'package:paperless_mobile/features/documents/view/widgets/document_preview.dart';
import 'package:paperless_mobile/features/home/view/model/api_version.dart';
import 'package:paperless_mobile/features/similar_documents/cubit/similar_documents_cubit.dart';
import 'package:paperless_mobile/features/similar_documents/view/similar_documents_view.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
import 'package:paperless_mobile/helpers/message_helpers.dart';
class DocumentDetailsPage extends StatefulWidget {
final bool allowEdit;
final bool isLabelClickable;
final String? titleAndContentQueryString;
@@ -31,7 +33,6 @@ class DocumentDetailsPage extends StatefulWidget {
Key? key,
this.isLabelClickable = true,
this.titleAndContentQueryString,
this.allowEdit = true,
}) : super(key: key);
@override
@@ -39,41 +40,36 @@ class DocumentDetailsPage extends StatefulWidget {
}
class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
late Future<DocumentMetaData> _metaData;
static const double _itemSpacing = 24;
final _pagingScrollController = ScrollController();
@override
void initState() {
super.initState();
_loadMetaData();
}
void _loadMetaData() {
_metaData = context
.read<PaperlessDocumentsApi>()
.getMetaData(context.read<DocumentDetailsCubit>().state.document);
@override
void didChangeDependencies() {
super.didChangeDependencies();
}
@override
Widget build(BuildContext context) {
final apiVersion = context.watch<ApiVersion>();
final tabLength = 4 + (apiVersion.hasMultiUserSupport ? 1 : 0);
return WillPopScope(
onWillPop: () async {
Navigator.of(context).pop(context.read<DocumentDetailsCubit>().state.document);
return false;
},
child: DefaultTabController(
length: 4,
length: tabLength,
child: BlocListener<ConnectivityCubit, ConnectivityState>(
listenWhen: (previous, current) => !previous.isConnected && current.isConnected,
listener: (context, state) {
_loadMetaData();
setState(() {});
context.read<DocumentDetailsCubit>().loadMetaData();
},
child: Scaffold(
extendBodyBehindAppBar: false,
floatingActionButtonLocation: FloatingActionButtonLocation.endDocked,
floatingActionButton: widget.allowEdit ? _buildEditButton() : null,
floatingActionButton: _buildEditButton(),
bottomNavigationBar: _buildBottomAppBar(),
body: NestedScrollView(
headerSliverBuilder: (context, innerBoxIsScrolled) => [
@@ -155,6 +151,15 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
),
),
),
if (apiVersion.hasMultiUserSupport)
Tab(
child: Text(
"Permissions",
style: TextStyle(
color: Theme.of(context).colorScheme.onPrimaryContainer,
),
),
),
],
),
),
@@ -228,6 +233,18 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
),
],
),
if (apiVersion.hasMultiUserSupport)
CustomScrollView(
controller: _pagingScrollController,
slivers: [
SliverOverlapInjector(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
),
DocumentPermissionsWidget(
document: state.document,
),
],
),
],
),
),
@@ -242,25 +259,25 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
}
Widget _buildEditButton() {
bool canEdit = context.watchInternetConnection &&
LocalUserAccount.current.paperlessUser
.hasPermission(PermissionAction.change, PermissionTarget.document);
if (!canEdit) {
return const SizedBox.shrink();
}
return BlocBuilder<DocumentDetailsCubit, DocumentDetailsState>(
builder: (context, state) {
// final _filteredSuggestions =
// state.suggestions?.documentDifference(state.document);
return BlocBuilder<ConnectivityCubit, ConnectivityState>(
builder: (context, connectivityState) {
if (!connectivityState.isConnected) {
return const SizedBox.shrink();
}
return Tooltip(
message: S.of(context)!.editDocumentTooltip,
preferBelow: false,
verticalOffset: 40,
child: FloatingActionButton(
child: const Icon(Icons.edit),
onPressed: () => _onEdit(state.document),
),
);
},
return Tooltip(
message: S.of(context)!.editDocumentTooltip,
preferBelow: false,
verticalOffset: 40,
child: FloatingActionButton(
child: const Icon(Icons.edit),
onPressed: () => _onEdit(state.document),
),
);
},
);
@@ -273,24 +290,27 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
child: BlocBuilder<ConnectivityCubit, ConnectivityState>(
builder: (context, connectivityState) {
final isConnected = connectivityState.isConnected;
final canDelete = isConnected &&
LocalUserAccount.current.paperlessUser
.hasPermission(PermissionAction.delete, PermissionTarget.document);
return Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
IconButton(
tooltip: S.of(context)!.deleteDocumentTooltip,
icon: const Icon(Icons.delete),
onPressed:
widget.allowEdit && isConnected ? () => _onDelete(state.document) : null,
onPressed: canDelete ? () => _onDelete(state.document) : null,
).paddedSymmetrically(horizontal: 4),
DocumentDownloadButton(
document: state.document,
enabled: isConnected,
metaData: _metaData,
),
//TODO: Enable again, need new pdf viewer package...
IconButton(
tooltip: S.of(context)!.previewTooltip,
icon: const Icon(Icons.visibility),
onPressed: isConnected ? () => _onOpen(state.document) : null,
onPressed: (isConnected && false) ? () => _onOpen(state.document) : null,
).paddedOnly(right: 4.0),
IconButton(
tooltip: S.of(context)!.openInSystemViewer,
@@ -383,7 +403,7 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
Future<void> _onOpen(DocumentModel document) async {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => DocumentView(
builder: (_) => DocumentView(
documentBytes: context.read<PaperlessDocumentsApi>().getPreview(document.id),
),
),

View File

@@ -3,6 +3,7 @@ import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
import 'package:paperless_mobile/core/type/types.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/document_details/cubit/document_details_cubit.dart';
@@ -17,8 +18,7 @@ class ArchiveSerialNumberField extends StatefulWidget {
});
@override
State<ArchiveSerialNumberField> createState() =>
_ArchiveSerialNumberFieldState();
State<ArchiveSerialNumberField> createState() => _ArchiveSerialNumberFieldState();
}
class _ArchiveSerialNumberFieldState extends State<ArchiveSerialNumberField> {
@@ -39,20 +39,21 @@ class _ArchiveSerialNumberFieldState extends State<ArchiveSerialNumberField> {
void _clearButtonListener() {
setState(() {
_showClearButton = _asnEditingController.text.isNotEmpty;
_canUpdate = int.tryParse(_asnEditingController.text) !=
widget.document.archiveSerialNumber;
_canUpdate = int.tryParse(_asnEditingController.text) != widget.document.archiveSerialNumber;
});
}
@override
Widget build(BuildContext context) {
final userCanEditDocument = LocalUserAccount.current.paperlessUser.hasPermission(
PermissionAction.change,
PermissionTarget.document,
);
return BlocListener<DocumentDetailsCubit, DocumentDetailsState>(
listenWhen: (previous, current) =>
previous.document.archiveSerialNumber !=
current.document.archiveSerialNumber,
previous.document.archiveSerialNumber != current.document.archiveSerialNumber,
listener: (context, state) {
_asnEditingController.text =
state.document.archiveSerialNumber?.toString() ?? '';
_asnEditingController.text = state.document.archiveSerialNumber?.toString() ?? '';
setState(() {
_canUpdate = false;
});
@@ -61,6 +62,7 @@ class _ArchiveSerialNumberFieldState extends State<ArchiveSerialNumberField> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextFormField(
enabled: userCanEditDocument,
controller: _asnEditingController,
keyboardType: TextInputType.number,
onChanged: (value) {
@@ -78,15 +80,13 @@ class _ArchiveSerialNumberFieldState extends State<ArchiveSerialNumberField> {
IconButton(
icon: const Icon(Icons.clear),
color: Theme.of(context).colorScheme.primary,
onPressed: _asnEditingController.clear,
onPressed: userCanEditDocument ? _asnEditingController.clear : null,
),
IconButton(
icon: const Icon(Icons.plus_one_rounded),
color: Theme.of(context).colorScheme.primary,
onPressed:
context.watchInternetConnection && !_showClearButton
? _onAutoAssign
: null,
context.watchInternetConnection && !_showClearButton ? _onAutoAssign : null,
).paddedOnly(right: 8),
],
),
@@ -97,9 +97,7 @@ class _ArchiveSerialNumberFieldState extends State<ArchiveSerialNumberField> {
),
TextButton.icon(
icon: const Icon(Icons.done),
onPressed: context.watchInternetConnection && _canUpdate
? _onSubmitted
: null,
onPressed: context.watchInternetConnection && _canUpdate ? _onSubmitted : null,
label: Text(S.of(context)!.save),
).padded(),
],

View File

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

View File

@@ -20,12 +20,10 @@ import 'package:permission_handler/permission_handler.dart';
class DocumentDownloadButton extends StatefulWidget {
final DocumentModel? document;
final bool enabled;
final Future<DocumentMetaData> metaData;
const DocumentDownloadButton({
super.key,
required this.document,
this.enabled = true,
required this.metaData,
});
@override
@@ -81,7 +79,7 @@ class _DocumentDownloadButtonState extends State<DocumentDownloadButton> {
break;
}
if (Platform.isAndroid && androidInfo!.version.sdkInt! <= 29) {
if (Platform.isAndroid && androidInfo!.version.sdkInt <= 29) {
final isGranted = await askForPermission(Permission.storage);
if (!isGranted) {
return;

View File

@@ -29,7 +29,11 @@ class _DocumentMetaDataWidgetState extends State<DocumentMetaDataWidget> {
builder: (context, state) {
debugPrint("Building state...");
if (state.metaData == null) {
return const Center(child: CircularProgressIndicator());
return const SliverToBoxAdapter(
child: Center(
child: CircularProgressIndicator(),
),
);
}
return SliverList(
delegate: SliverChildListDelegate(

View File

@@ -4,7 +4,6 @@ import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/widgets/highlighted_text.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/document_details/view/widgets/details_item.dart';
import 'package:paperless_mobile/features/labels/storage_path/view/widgets/storage_path_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/app_localizations.dart';

View File

@@ -0,0 +1,19 @@
import 'package:flutter/material.dart';
import 'package:paperless_api/paperless_api.dart';
class DocumentPermissionsWidget extends StatefulWidget {
final DocumentModel document;
const DocumentPermissionsWidget({super.key, required this.document});
@override
State<DocumentPermissionsWidget> createState() => _DocumentPermissionsWidgetState();
}
class _DocumentPermissionsWidgetState extends State<DocumentPermissionsWidget> {
@override
Widget build(BuildContext context) {
return const SliverToBoxAdapter(
child: Placeholder(),
);
}
}

View File

@@ -78,7 +78,7 @@ class _DocumentShareButtonState extends State<DocumentShareButton> {
break;
}
if (Platform.isAndroid && androidInfo!.version.sdkInt! < 30) {
if (Platform.isAndroid && androidInfo!.version.sdkInt < 30) {
final isGranted = await askForPermission(Permission.storage);
if (!isGranted) {
return;

View File

@@ -1,260 +0,0 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'document_edit_cubit.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods');
/// @nodoc
mixin _$DocumentEditState {
DocumentModel get document => throw _privateConstructorUsedError;
Map<int, Correspondent> get correspondents =>
throw _privateConstructorUsedError;
Map<int, DocumentType> get documentTypes =>
throw _privateConstructorUsedError;
Map<int, StoragePath> get storagePaths => throw _privateConstructorUsedError;
Map<int, Tag> get tags => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$DocumentEditStateCopyWith<DocumentEditState> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $DocumentEditStateCopyWith<$Res> {
factory $DocumentEditStateCopyWith(
DocumentEditState value, $Res Function(DocumentEditState) then) =
_$DocumentEditStateCopyWithImpl<$Res, DocumentEditState>;
@useResult
$Res call(
{DocumentModel document,
Map<int, Correspondent> correspondents,
Map<int, DocumentType> documentTypes,
Map<int, StoragePath> storagePaths,
Map<int, Tag> tags});
}
/// @nodoc
class _$DocumentEditStateCopyWithImpl<$Res, $Val extends DocumentEditState>
implements $DocumentEditStateCopyWith<$Res> {
_$DocumentEditStateCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
@pragma('vm:prefer-inline')
@override
$Res call({
Object? document = null,
Object? correspondents = null,
Object? documentTypes = null,
Object? storagePaths = null,
Object? tags = null,
}) {
return _then(_value.copyWith(
document: null == document
? _value.document
: document // ignore: cast_nullable_to_non_nullable
as DocumentModel,
correspondents: null == correspondents
? _value.correspondents
: correspondents // ignore: cast_nullable_to_non_nullable
as Map<int, Correspondent>,
documentTypes: null == documentTypes
? _value.documentTypes
: documentTypes // ignore: cast_nullable_to_non_nullable
as Map<int, DocumentType>,
storagePaths: null == storagePaths
? _value.storagePaths
: storagePaths // ignore: cast_nullable_to_non_nullable
as Map<int, StoragePath>,
tags: null == tags
? _value.tags
: tags // ignore: cast_nullable_to_non_nullable
as Map<int, Tag>,
) as $Val);
}
}
/// @nodoc
abstract class _$$_DocumentEditStateCopyWith<$Res>
implements $DocumentEditStateCopyWith<$Res> {
factory _$$_DocumentEditStateCopyWith(_$_DocumentEditState value,
$Res Function(_$_DocumentEditState) then) =
__$$_DocumentEditStateCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{DocumentModel document,
Map<int, Correspondent> correspondents,
Map<int, DocumentType> documentTypes,
Map<int, StoragePath> storagePaths,
Map<int, Tag> tags});
}
/// @nodoc
class __$$_DocumentEditStateCopyWithImpl<$Res>
extends _$DocumentEditStateCopyWithImpl<$Res, _$_DocumentEditState>
implements _$$_DocumentEditStateCopyWith<$Res> {
__$$_DocumentEditStateCopyWithImpl(
_$_DocumentEditState _value, $Res Function(_$_DocumentEditState) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? document = null,
Object? correspondents = null,
Object? documentTypes = null,
Object? storagePaths = null,
Object? tags = null,
}) {
return _then(_$_DocumentEditState(
document: null == document
? _value.document
: document // ignore: cast_nullable_to_non_nullable
as DocumentModel,
correspondents: null == correspondents
? _value._correspondents
: correspondents // ignore: cast_nullable_to_non_nullable
as Map<int, Correspondent>,
documentTypes: null == documentTypes
? _value._documentTypes
: documentTypes // ignore: cast_nullable_to_non_nullable
as Map<int, DocumentType>,
storagePaths: null == storagePaths
? _value._storagePaths
: storagePaths // ignore: cast_nullable_to_non_nullable
as Map<int, StoragePath>,
tags: null == tags
? _value._tags
: tags // ignore: cast_nullable_to_non_nullable
as Map<int, Tag>,
));
}
}
/// @nodoc
class _$_DocumentEditState implements _DocumentEditState {
const _$_DocumentEditState(
{required this.document,
final Map<int, Correspondent> correspondents = const {},
final Map<int, DocumentType> documentTypes = const {},
final Map<int, StoragePath> storagePaths = const {},
final Map<int, Tag> tags = const {}})
: _correspondents = correspondents,
_documentTypes = documentTypes,
_storagePaths = storagePaths,
_tags = tags;
@override
final DocumentModel document;
final Map<int, Correspondent> _correspondents;
@override
@JsonKey()
Map<int, Correspondent> get correspondents {
if (_correspondents is EqualUnmodifiableMapView) return _correspondents;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(_correspondents);
}
final Map<int, DocumentType> _documentTypes;
@override
@JsonKey()
Map<int, DocumentType> get documentTypes {
if (_documentTypes is EqualUnmodifiableMapView) return _documentTypes;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(_documentTypes);
}
final Map<int, StoragePath> _storagePaths;
@override
@JsonKey()
Map<int, StoragePath> get storagePaths {
if (_storagePaths is EqualUnmodifiableMapView) return _storagePaths;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(_storagePaths);
}
final Map<int, Tag> _tags;
@override
@JsonKey()
Map<int, Tag> get tags {
if (_tags is EqualUnmodifiableMapView) return _tags;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(_tags);
}
@override
String toString() {
return 'DocumentEditState(document: $document, correspondents: $correspondents, documentTypes: $documentTypes, storagePaths: $storagePaths, tags: $tags)';
}
@override
bool operator ==(dynamic other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$_DocumentEditState &&
(identical(other.document, document) ||
other.document == document) &&
const DeepCollectionEquality()
.equals(other._correspondents, _correspondents) &&
const DeepCollectionEquality()
.equals(other._documentTypes, _documentTypes) &&
const DeepCollectionEquality()
.equals(other._storagePaths, _storagePaths) &&
const DeepCollectionEquality().equals(other._tags, _tags));
}
@override
int get hashCode => Object.hash(
runtimeType,
document,
const DeepCollectionEquality().hash(_correspondents),
const DeepCollectionEquality().hash(_documentTypes),
const DeepCollectionEquality().hash(_storagePaths),
const DeepCollectionEquality().hash(_tags));
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$_DocumentEditStateCopyWith<_$_DocumentEditState> get copyWith =>
__$$_DocumentEditStateCopyWithImpl<_$_DocumentEditState>(
this, _$identity);
}
abstract class _DocumentEditState implements DocumentEditState {
const factory _DocumentEditState(
{required final DocumentModel document,
final Map<int, Correspondent> correspondents,
final Map<int, DocumentType> documentTypes,
final Map<int, StoragePath> storagePaths,
final Map<int, Tag> tags}) = _$_DocumentEditState;
@override
DocumentModel get document;
@override
Map<int, Correspondent> get correspondents;
@override
Map<int, DocumentType> get documentTypes;
@override
Map<int, StoragePath> get storagePaths;
@override
Map<int, Tag> get tags;
@override
@JsonKey(ignore: true)
_$$_DocumentEditStateCopyWith<_$_DocumentEditState> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@@ -1,5 +1,4 @@
import 'dart:async';
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -8,6 +7,7 @@ import 'package:flutter_typeahead/flutter_typeahead.dart';
import 'package:intl/intl.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/workarounds/colored_chip.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
@@ -16,7 +16,6 @@ import 'package:paperless_mobile/features/edit_label/view/impl/add_correspondent
import 'package:paperless_mobile/features/edit_label/view/impl/add_document_type_page.dart';
import 'package:paperless_mobile/features/edit_label/view/impl/add_storage_path_page.dart';
import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_form_field.dart';
import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_form_field.dart';
import 'package:paperless_mobile/features/labels/view/widgets/label_form_field.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
@@ -117,6 +116,11 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
name: fkCorrespondent,
prefixIcon: const Icon(Icons.person_outlined),
allowSelectUnassigned: true,
canCreateNewLabel:
LocalUserAccount.current.paperlessUser.hasPermission(
PermissionAction.add,
PermissionTarget.correspondent,
),
),
if (_filteredSuggestions?.hasSuggestedCorrespondents ?? false)
_buildSuggestionsSkeleton<int>(
@@ -144,6 +148,11 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
initialName: currentInput,
),
),
canCreateNewLabel:
LocalUserAccount.current.paperlessUser.hasPermission(
PermissionAction.add,
PermissionTarget.documentType,
),
addLabelText: S.of(context)!.addDocumentType,
labelText: S.of(context)!.documentType,
initialValue: state.document.documentType != null
@@ -177,6 +186,11 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
value: context.read<LabelRepository>(),
child: AddStoragePathPage(initalName: initialValue),
),
canCreateNewLabel:
LocalUserAccount.current.paperlessUser.hasPermission(
PermissionAction.add,
PermissionTarget.storagePath,
),
addLabelText: S.of(context)!.addStoragePath,
labelText: S.of(context)!.storagePath,
options: state.storagePaths,
@@ -264,9 +278,12 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
var mergedDocument = document.copyWith(
title: values[fkTitle],
created: values[fkCreatedDate],
documentType: () => (values[fkDocumentType] as SetIdQueryParameter).id,
correspondent: () => (values[fkCorrespondent] as SetIdQueryParameter).id,
storagePath: () => (values[fkStoragePath] as SetIdQueryParameter).id,
documentType: () =>
(values[fkDocumentType] as IdQueryParameter).whenOrNull(fromId: (id) => id),
correspondent: () =>
(values[fkCorrespondent] as IdQueryParameter).whenOrNull(fromId: (id) => id),
storagePath: () =>
(values[fkStoragePath] as IdQueryParameter).whenOrNull(fromId: (id) => id),
tags: (values[fkTags] as IdsTagsQuery).include,
content: values[fkContent],
);

View File

@@ -11,19 +11,16 @@ import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
import 'package:paperless_mobile/core/delegate/customizable_sliver_persistent_header_delegate.dart';
import 'package:paperless_mobile/core/global/constants.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/navigation/push_routes.dart';
import 'package:paperless_mobile/core/service/file_description.dart';
import 'package:paperless_mobile/core/service/file_service.dart';
import 'package:paperless_mobile/features/app_drawer/view/app_drawer.dart';
import 'package:paperless_mobile/features/document_scan/cubit/document_scanner_cubit.dart';
import 'package:paperless_mobile/features/document_scan/view/widgets/scanned_image_item.dart';
import 'package:paperless_mobile/features/document_search/view/sliver_search_bar.dart';
import 'package:paperless_mobile/features/document_upload/cubit/document_upload_cubit.dart';
import 'package:paperless_mobile/features/document_upload/view/document_upload_preparation_page.dart';
import 'package:paperless_mobile/features/documents/view/pages/document_view.dart';
import 'package:paperless_mobile/features/tasks/cubit/task_status_cubit.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
import 'package:paperless_mobile/helpers/file_helpers.dart';
import 'package:paperless_mobile/helpers/message_helpers.dart';
import 'package:paperless_mobile/helpers/permission_helpers.dart';
import 'package:path/path.dart' as p;
@@ -38,12 +35,9 @@ class ScannerPage extends StatefulWidget {
State<ScannerPage> createState() => _ScannerPageState();
}
class _ScannerPageState extends State<ScannerPage>
with SingleTickerProviderStateMixin {
final SliverOverlapAbsorberHandle searchBarHandle =
SliverOverlapAbsorberHandle();
final SliverOverlapAbsorberHandle actionsHandle =
SliverOverlapAbsorberHandle();
class _ScannerPageState extends State<ScannerPage> with SingleTickerProviderStateMixin {
final SliverOverlapAbsorberHandle searchBarHandle = SliverOverlapAbsorberHandle();
final SliverOverlapAbsorberHandle actionsHandle = SliverOverlapAbsorberHandle();
@override
Widget build(BuildContext context) {
@@ -180,8 +174,7 @@ class _ScannerPageState extends State<ScannerPage>
final success = await EdgeDetection.detectEdge(file.path);
if (!success) {
if (kDebugMode) {
dev.log(
'[ScannerPage] Scan either not successful or canceled by user.');
dev.log('[ScannerPage] Scan either not successful or canceled by user.');
}
return;
}
@@ -195,26 +188,15 @@ class _ScannerPageState extends State<ScannerPage>
final file = await _assembleFileBytes(
context.read<DocumentScannerCubit>().state,
);
final uploadResult = await Navigator.of(context).push<DocumentUploadResult>(
MaterialPageRoute(
builder: (_) => BlocProvider(
create: (context) => DocumentUploadCubit(
context.read(),
context.read(),
),
child: DocumentUploadPreparationPage(
fileBytes: file.bytes,
fileExtension: file.extension,
),
),
),
final uploadResult = await pushDocumentUploadPreparationPage(
context,
bytes: file.bytes,
fileExtension: file.extension,
);
if ((uploadResult?.success ?? false) && uploadResult?.taskId != null) {
// For paperless version older than 1.11.3, task id will always be null!
context.read<DocumentScannerCubit>().reset();
context
.read<TaskStatusCubit>()
.listenToTaskChanges(uploadResult!.taskId!);
context.read<TaskStatusCubit>().listenToTaskChanges(uploadResult!.taskId!);
}
}
@@ -307,21 +289,12 @@ class _ScannerPageState extends State<ScannerPage>
);
return;
}
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => BlocProvider(
create: (context) => DocumentUploadCubit(
context.read(),
context.read(),
),
child: DocumentUploadPreparationPage(
fileBytes: file.readAsBytesSync(),
filename: fileDescription.filename,
title: fileDescription.filename,
fileExtension: fileDescription.extension,
),
),
),
pushDocumentUploadPreparationPage(
context,
bytes: file.readAsBytesSync(),
filename: fileDescription.filename,
title: fileDescription.filename,
fileExtension: fileDescription.extension,
);
}
}

View File

@@ -98,7 +98,7 @@ class _ScannedImageItemState extends State<ScannedImageItem> {
alignment: Alignment.bottomCenter,
child: TextButton(
onPressed: widget.onDelete,
child: Text("Remove"),
child: const Text("Remove"),
),
),
],

View File

@@ -56,7 +56,7 @@ class DocumentSearchCubit extends Cubit<DocumentSearchState> with DocumentPaging
final searchFilter = DocumentFilter(
query: TextQuery.extended(query),
);
await updateFilter(filter: searchFilter);
emit(
state.copyWith(
@@ -87,6 +87,10 @@ class DocumentSearchCubit extends Cubit<DocumentSearchState> with DocumentPaging
}
Future<void> suggest(String query) async {
final normalizedQuery = query.trim();
if (normalizedQuery.isEmpty) {
return;
}
emit(
state.copyWith(
isLoading: true,
@@ -96,10 +100,13 @@ class DocumentSearchCubit extends Cubit<DocumentSearchState> with DocumentPaging
),
);
final suggestions = await api.autocomplete(query);
emit(state.copyWith(
suggestions: suggestions,
isLoading: false,
));
print("Suggestions found: $suggestions");
emit(
state.copyWith(
suggestions: suggestions,
isLoading: false,
),
);
}
void reset() {

View File

@@ -7,18 +7,12 @@ enum SearchView {
@JsonSerializable(ignoreUnannotated: true)
class DocumentSearchState extends DocumentPagingState {
@JsonKey()
final List<String> searchHistory;
final SearchView view;
final List<String> suggestions;
@JsonKey()
final ViewType viewType;
final Map<int, Correspondent> correspondents;
final Map<int, DocumentType> documentTypes;
final Map<int, Tag> tags;
final Map<int, StoragePath> storagePaths;
const DocumentSearchState({
this.view = SearchView.suggestions,
this.searchHistory = const [],
@@ -28,10 +22,6 @@ class DocumentSearchState extends DocumentPagingState {
super.hasLoaded,
super.isLoading,
super.value,
this.correspondents = const {},
this.documentTypes = const {},
this.tags = const {},
this.storagePaths = const {},
});
@override
@@ -41,10 +31,6 @@ class DocumentSearchState extends DocumentPagingState {
suggestions,
view,
viewType,
correspondents,
documentTypes,
tags,
storagePaths,
];
@override
@@ -85,10 +71,6 @@ class DocumentSearchState extends DocumentPagingState {
view: view ?? this.view,
suggestions: suggestions ?? this.suggestions,
viewType: viewType ?? this.viewType,
correspondents: correspondents ?? this.correspondents,
documentTypes: documentTypes ?? this.documentTypes,
tags: tags ?? this.tags,
storagePaths: storagePaths ?? this.storagePaths,
);
}

View File

@@ -0,0 +1,139 @@
import 'package:animations/animations.dart';
import 'package:flutter/material.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:hive_flutter/adapters.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/local_user_account.dart';
import 'package:paperless_mobile/core/database/tables/local_user_app_state.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/repository/user_repository.dart';
import 'package:paperless_mobile/features/document_search/cubit/document_search_cubit.dart';
import 'package:paperless_mobile/features/document_search/view/document_search_page.dart';
import 'package:paperless_mobile/features/home/view/model/api_version.dart';
import 'package:paperless_mobile/features/settings/view/manage_accounts_page.dart';
import 'package:paperless_mobile/features/settings/view/widgets/global_settings_builder.dart';
import 'package:paperless_mobile/features/settings/view/widgets/user_avatar.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
import 'package:provider/provider.dart';
class DocumentSearchBar extends StatefulWidget {
const DocumentSearchBar({super.key});
@override
State<DocumentSearchBar> createState() => _DocumentSearchBarState();
}
class _DocumentSearchBarState extends State<DocumentSearchBar> {
@override
Widget build(BuildContext context) {
return Container(
margin: EdgeInsets.only(top: 8),
child: OpenContainer(
transitionDuration: const Duration(milliseconds: 200),
transitionType: ContainerTransitionType.fadeThrough,
closedElevation: 1,
middleColor: Theme.of(context).colorScheme.surfaceVariant,
openColor: Theme.of(context).colorScheme.background,
closedColor: Theme.of(context).colorScheme.surfaceVariant,
closedShape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(56),
),
closedBuilder: (_, action) {
return InkWell(
onTap: action,
child: ConstrainedBox(
constraints: const BoxConstraints(
maxWidth: 720,
minWidth: 360,
maxHeight: 56,
minHeight: 48,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
IconButton(
icon: const Icon(Icons.menu),
onPressed: Scaffold.of(context).openDrawer,
),
Flexible(
child: Text(
S.of(context)!.searchDocuments,
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.w500,
color: Theme.of(context).hintColor,
),
),
),
],
),
),
),
_buildUserAvatar(context),
],
),
),
);
},
openBuilder: (_, action) {
return MultiProvider(
providers: [
Provider.value(value: context.read<LabelRepository>()),
Provider.value(value: context.read<PaperlessDocumentsApi>()),
Provider.value(value: context.read<CacheManager>()),
Provider.value(value: context.read<ApiVersion>()),
if (context.read<ApiVersion>().hasMultiUserSupport)
Provider.value(value: context.read<UserRepository>()),
],
child: Provider(
create: (_) => DocumentSearchCubit(
context.read(),
context.read(),
context.read(),
Hive.box<LocalUserAppState>(HiveBoxes.localUserAppState)
.get(LocalUserAccount.current.id)!,
),
builder: (_, __) => const DocumentSearchPage(),
),
);
},
),
);
}
IconButton _buildUserAvatar(BuildContext context) {
return IconButton(
padding: const EdgeInsets.all(6),
icon: GlobalSettingsBuilder(
builder: (context, settings) {
return ValueListenableBuilder(
valueListenable: Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount).listenable(),
builder: (context, box, _) {
final account = box.get(settings.currentLoggedInUser!)!;
return UserAvatar(
userId: settings.currentLoggedInUser!,
account: account,
);
},
);
},
),
onPressed: () {
final apiVersion = context.read<ApiVersion>();
showDialog(
context: context,
builder: (context) => Provider.value(
value: apiVersion,
child: const ManageAccountsPage(),
),
);
},
);
}
}

View File

@@ -3,10 +3,7 @@ import 'dart:async';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:hive/hive.dart';
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
import 'package:paperless_mobile/core/database/tables/global_settings.dart';
import 'package:paperless_mobile/core/database/tables/local_user_app_state.dart';
import 'package:paperless_mobile/core/navigation/push_routes.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/document_search/cubit/document_search_cubit.dart';
import 'package:paperless_mobile/features/document_search/view/remove_history_entry_dialog.dart';
@@ -14,27 +11,8 @@ import 'package:paperless_mobile/features/documents/view/widgets/adaptive_docume
import 'package:paperless_mobile/features/documents/view/widgets/selection/view_type_selection_widget.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
import 'package:paperless_mobile/routes/document_details_route.dart';
import 'dart:math' as math;
Future<void> showDocumentSearchPage(BuildContext context) {
final currentUser =
Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!.currentLoggedInUser;
return Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => BlocProvider(
create: (context) => DocumentSearchCubit(
context.read(),
context.read(),
context.read(),
Hive.box<LocalUserAppState>(HiveBoxes.localUserAppState).get(currentUser)!,
),
child: const DocumentSearchPage(),
),
),
);
}
class DocumentSearchPage extends StatefulWidget {
const DocumentSearchPage({super.key});
@@ -54,38 +32,38 @@ class _DocumentSearchPageState extends State<DocumentSearchPage> {
final theme = Theme.of(context);
return Scaffold(
appBar: AppBar(
backgroundColor: theme.colorScheme.surface,
backgroundColor: theme.colorScheme.surfaceVariant,
toolbarHeight: 72,
leading: BackButton(
color: theme.colorScheme.onSurface,
color: theme.colorScheme.onSurfaceVariant,
),
title: TextField(
autofocus: true,
style: theme.textTheme.bodyLarge?.apply(
color: theme.colorScheme.onSurface,
),
focusNode: _queryFocusNode,
decoration: InputDecoration(
contentPadding: EdgeInsets.zero,
hintStyle: theme.textTheme.bodyLarge?.apply(
color: theme.colorScheme.onSurfaceVariant,
title: Hero(
tag: "search_hero_tag",
child: TextField(
autofocus: true,
// style: theme.textTheme.bodyLarge?.apply(
// color: theme.colorScheme.onSurface,
// ),
focusNode: _queryFocusNode,
decoration: InputDecoration(
contentPadding: EdgeInsets.zero,
hintText: S.of(context)!.searchDocuments,
border: InputBorder.none,
),
hintText: S.of(context)!.searchDocuments,
border: InputBorder.none,
controller: _queryController,
onChanged: (query) {
_debounceTimer?.cancel();
_debounceTimer = Timer(const Duration(milliseconds: 500), () {
context.read<DocumentSearchCubit>().suggest(query);
});
},
textInputAction: TextInputAction.search,
onSubmitted: (query) {
FocusScope.of(context).unfocus();
_debounceTimer?.cancel();
context.read<DocumentSearchCubit>().search(query);
},
),
controller: _queryController,
onChanged: (query) {
_debounceTimer?.cancel();
_debounceTimer = Timer(const Duration(milliseconds: 500), () {
context.read<DocumentSearchCubit>().suggest(query);
});
},
textInputAction: TextInputAction.search,
onSubmitted: (query) {
FocusScope.of(context).unfocus();
_debounceTimer?.cancel();
context.read<DocumentSearchCubit>().search(query);
},
),
actions: [
IconButton(
@@ -97,22 +75,22 @@ class _DocumentSearchPageState extends State<DocumentSearchPage> {
},
).padded(),
],
bottom: PreferredSize(
preferredSize: const Size.fromHeight(1),
child: Divider(
color: theme.colorScheme.outline,
),
),
),
body: BlocBuilder<DocumentSearchCubit, DocumentSearchState>(
builder: (context, state) {
switch (state.view) {
case SearchView.suggestions:
return _buildSuggestionsView(state);
case SearchView.results:
return _buildResultsView(state);
}
},
body: Column(
children: [
Expanded(
child: BlocBuilder<DocumentSearchCubit, DocumentSearchState>(
builder: (context, state) {
switch (state.view) {
case SearchView.suggestions:
return _buildSuggestionsView(state);
case SearchView.results:
return _buildResultsView(state);
}
},
),
),
],
),
);
}
@@ -156,7 +134,7 @@ class _DocumentSearchPageState extends State<DocumentSearchPage> {
),
childCount: suggestions.length,
),
)
),
],
);
}
@@ -226,19 +204,12 @@ class _DocumentSearchPageState extends State<DocumentSearchPage> {
hasLoaded: state.hasLoaded,
enableHeroAnimation: false,
onTap: (document) {
Navigator.pushNamed(
pushDocumentDetailsRoute(
context,
DocumentDetailsRoute.routeName,
arguments: DocumentDetailsRouteArguments(
document: document,
isLabelClickable: false,
),
document: document,
isLabelClickable: false,
);
},
correspondents: state.correspondents,
documentTypes: state.documentTypes,
tags: state.tags,
storagePaths: state.storagePaths,
)
],
);

View File

@@ -1 +0,0 @@

View File

@@ -1,16 +1,12 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:hive_flutter/adapters.dart';
import 'package:paperless_mobile/core/bloc/server_information_cubit.dart';
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
import 'package:paperless_mobile/core/database/tables/global_settings.dart';
import 'package:paperless_mobile/core/database/tables/local_user_app_state.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/features/document_search/view/document_search_page.dart';
import 'package:paperless_mobile/features/settings/view/manage_accounts_page.dart';
import 'package:paperless_mobile/features/settings/view/widgets/global_settings_builder.dart';
import 'package:paperless_mobile/features/settings/view/widgets/user_avatar.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
import 'package:paperless_mobile/features/document_search/cubit/document_search_cubit.dart';
import 'package:paperless_mobile/features/document_search/view/document_search_bar.dart';
class SliverSearchBar extends StatelessWidget {
final bool floating;
@@ -23,6 +19,9 @@ class SliverSearchBar extends StatelessWidget {
@override
Widget build(BuildContext context) {
final currentUser =
Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!.currentLoggedInUser;
return SliverPersistentHeader(
floating: floating,
pinned: pinned,
@@ -30,38 +29,15 @@ class SliverSearchBar extends StatelessWidget {
minExtent: kToolbarHeight,
maxExtent: kToolbarHeight,
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 8.0),
child: s.SearchBar(
height: kToolbarHeight,
supportingText: S.of(context)!.searchDocuments,
onTap: () => showDocumentSearchPage(context),
leadingIcon: IconButton(
icon: const Icon(Icons.menu),
onPressed: Scaffold.of(context).openDrawer,
),
trailingIcon: IconButton(
icon: GlobalSettingsBuilder(
builder: (context, settings) {
return ValueListenableBuilder(
valueListenable:
Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount).listenable(),
builder: (context, box, _) {
final account = box.get(settings.currentLoggedInUser!)!;
return UserAvatar(userId: settings.currentLoggedInUser!, account: account);
},
);
},
),
onPressed: () {
showDialog(
context: context,
builder: (_) => BlocProvider.value(
value: context.read<ServerInformationCubit>(),
child: const ManageAccountsPage(),
),
);
},
margin: const EdgeInsets.symmetric(horizontal: 16.0),
child: BlocProvider(
create: (context) => DocumentSearchCubit(
context.read(),
context.read(),
context.read(),
Hive.box<LocalUserAppState>(HiveBoxes.localUserAppState).get(currentUser)!,
),
child: const DocumentSearchBar(),
),
),
),

View File

@@ -35,6 +35,7 @@ class DocumentUploadCubit extends Cubit<DocumentUploadState> {
int? correspondent,
Iterable<int> tags = const [],
DateTime? createdAt,
int? asn,
}) async {
return await _documentApi.create(
bytes,
@@ -44,6 +45,7 @@ class DocumentUploadCubit extends Cubit<DocumentUploadState> {
documentType: documentType,
tags: tags,
createdAt: createdAt,
asn: asn,
);
}

View File

@@ -7,6 +7,7 @@ import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:intl/date_symbol_data_local.dart';
import 'package:intl/intl.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/type/types.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
@@ -192,6 +193,10 @@ class _DocumentUploadPreparationPageState extends State<DocumentUploadPreparatio
options: state.correspondents,
prefixIcon: const Icon(Icons.person_outline),
allowSelectUnassigned: true,
canCreateNewLabel: LocalUserAccount.current.paperlessUser.hasPermission(
PermissionAction.add,
PermissionTarget.correspondent,
),
),
// Document type
LabelFormField<DocumentType>(
@@ -207,6 +212,10 @@ class _DocumentUploadPreparationPageState extends State<DocumentUploadPreparatio
options: state.documentTypes,
prefixIcon: const Icon(Icons.description_outlined),
allowSelectUnassigned: true,
canCreateNewLabel: LocalUserAccount.current.paperlessUser.hasPermission(
PermissionAction.add,
PermissionTarget.documentType,
),
),
TagsFormField(
name: DocumentModel.tagsKey,
@@ -238,10 +247,14 @@ class _DocumentUploadPreparationPageState extends State<DocumentUploadPreparatio
final createdAt = fv[DocumentModel.createdKey] as DateTime?;
final title = fv[DocumentModel.titleKey] as String;
final docType = fv[DocumentModel.documentTypeKey] as SetIdQueryParameter;
final tags = fv[DocumentModel.tagsKey] as IdsTagsQuery;
final correspondent = fv[DocumentModel.correspondentKey] as SetIdQueryParameter;
final docType = (fv[DocumentModel.documentTypeKey] as IdQueryParameter?)
?.whenOrNull(fromId: (id) => id);
final tags = (fv[DocumentModel.tagsKey] as TagsQuery?)
?.whenOrNull(ids: (include, exclude) => include) ??
[];
final correspondent = (fv[DocumentModel.correspondentKey] as IdQueryParameter?)
?.whenOrNull(fromId: (id) => id);
final asn = fv[DocumentModel.asnKey] as int?;
final taskId = await cubit.upload(
widget.fileBytes,
filename: _padWithExtension(
@@ -249,10 +262,11 @@ class _DocumentUploadPreparationPageState extends State<DocumentUploadPreparatio
widget.fileExtension,
),
title: title,
documentType: docType.id,
correspondent: correspondent.id,
tags: tags.include,
documentType: docType,
correspondent: correspondent,
tags: tags,
createdAt: createdAt,
asn: asn,
);
showSnackBar(
context,

View File

@@ -14,7 +14,7 @@ import 'package:paperless_mobile/features/settings/model/view_type.dart';
part 'documents_cubit.g.dart';
part 'documents_state.dart';
class DocumentsCubit extends Cubit<DocumentsState> with DocumentPagingBlocMixin {
class DocumentsCubit extends HydratedCubit<DocumentsState> with DocumentPagingBlocMixin {
@override
final PaperlessDocumentsApi api;
@@ -114,8 +114,9 @@ class DocumentsCubit extends Cubit<DocumentsState> with DocumentPagingBlocMixin
void setViewType(ViewType viewType) {
emit(state.copyWith(viewType: viewType));
_userState.documentsPageViewType = viewType;
_userState.save();
_userState
..documentsPageViewType = viewType
..save();
}
@override
@@ -123,4 +124,14 @@ class DocumentsCubit extends Cubit<DocumentsState> with DocumentPagingBlocMixin
_userState.currentDocumentFilter = filter;
await _userState.save();
}
@override
DocumentsState? fromJson(Map<String, dynamic> json) {
return DocumentsState.fromJson(json);
}
@override
Map<String, dynamic>? toJson(DocumentsState state) {
return state.toJson();
}
}

View File

@@ -83,8 +83,7 @@ class DocumentsState extends DocumentPagingState {
);
}
factory DocumentsState.fromJson(Map<String, dynamic> json) =>
_$DocumentsStateFromJson(json);
factory DocumentsState.fromJson(Map<String, dynamic> json) => _$DocumentsStateFromJson(json);
Map<String, dynamic> toJson() => _$DocumentsStateToJson(this);
}

View File

@@ -1,8 +1,5 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
import 'package:pdfx/pdfx.dart';
class DocumentView extends StatefulWidget {
final Future<Uint8List> documentBytes;
@@ -17,36 +14,37 @@ class DocumentView extends StatefulWidget {
}
class _DocumentViewState extends State<DocumentView> {
late PdfController _pdfController;
// late PdfController _pdfController;
@override
void initState() {
super.initState();
_pdfController = PdfController(
document: PdfDocument.openData(
widget.documentBytes,
),
);
// _pdfController = PdfController(
// document: PdfDocument.openData(
// widget.documentBytes,
// ),
// );
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(S.of(context)!.preview),
),
body: PdfView(
builders: PdfViewBuilders<DefaultBuilderOptions>(
options: const DefaultBuilderOptions(
loaderSwitchDuration: Duration(milliseconds: 500),
),
pageLoaderBuilder: (context) => const Center(
child: CircularProgressIndicator(),
),
),
scrollDirection: Axis.vertical,
controller: _pdfController,
),
);
return Container();
// return Scaffold(
// appBar: AppBar(
// title: Text(S.of(context)!.preview),
// ),
// body: PdfView(
// builders: PdfViewBuilders<DefaultBuilderOptions>(
// options: const DefaultBuilderOptions(
// loaderSwitchDuration: Duration(milliseconds: 500),
// ),
// pageLoaderBuilder: (context) => const Center(
// child: CircularProgressIndicator(),
// ),
// ),
// scrollDirection: Axis.vertical,
// controller: _pdfController,
// ),
// );
}
}

View File

@@ -5,6 +5,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
import 'package:paperless_mobile/core/delegate/customizable_sliver_persistent_header_delegate.dart';
import 'package:paperless_mobile/core/navigation/push_routes.dart';
import 'package:paperless_mobile/core/widgets/material/colored_tab_bar.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/app_drawer/view/app_drawer.dart';
@@ -17,12 +18,11 @@ import 'package:paperless_mobile/features/documents/view/widgets/selection/docum
import 'package:paperless_mobile/features/documents/view/widgets/selection/view_type_selection_widget.dart';
import 'package:paperless_mobile/features/documents/view/widgets/sort_documents_button.dart';
import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart';
import 'package:paperless_mobile/features/saved_view/view/add_saved_view_page.dart';
import 'package:paperless_mobile/features/saved_view/view/saved_view_list.dart';
import 'package:paperless_mobile/features/tasks/cubit/task_status_cubit.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
import 'package:paperless_mobile/helpers/message_helpers.dart';
import 'package:paperless_mobile/routes/document_details_route.dart';
import 'package:sliver_tools/sliver_tools.dart';
class DocumentFilterIntent {
final DocumentFilter? filter;
@@ -108,7 +108,7 @@ class _DocumentsPageState extends State<DocumentsPage> with SingleTickerProvider
},
builder: (context, connectivityState) {
return SafeArea(
top: context.read<DocumentsCubit>().state.selection.isEmpty,
top: true,
child: Scaffold(
drawer: const AppDrawer(),
floatingActionButton: BlocBuilder<DocumentsCubit, DocumentsState>(
@@ -148,8 +148,9 @@ class _DocumentsPageState extends State<DocumentsPage> with SingleTickerProvider
onWillPop: () async {
if (context.read<DocumentsCubit>().state.selection.isNotEmpty) {
context.read<DocumentsCubit>().resetSelection();
return false;
}
return false;
return true;
},
child: Stack(
children: [
@@ -160,13 +161,18 @@ class _DocumentsPageState extends State<DocumentsPage> with SingleTickerProvider
handle: searchBarHandle,
sliver: BlocBuilder<DocumentsCubit, DocumentsState>(
builder: (context, state) {
if (state.selection.isNotEmpty) {
// Show selection app bar when selection mode is active
return DocumentSelectionSliverAppBar(
state: state,
);
}
return const SliverSearchBar(floating: true);
return AnimatedSwitcher(
layoutBuilder: SliverAnimatedSwitcher.defaultLayoutBuilder,
transitionBuilder: SliverAnimatedSwitcher.defaultTransitionBuilder,
child: state.selection.isEmpty
? const SliverSearchBar(floating: true)
: DocumentSelectionSliverAppBar(
state: state,
),
duration: const Duration(
milliseconds: 250,
),
);
},
),
),
@@ -333,10 +339,6 @@ class _DocumentsPageState extends State<DocumentsPage> with SingleTickerProvider
isLabelClickable: true,
isLoading: state.isLoading,
selectedDocumentIds: state.selectedIds,
correspondents: state.correspondents,
documentTypes: state.documentTypes,
tags: state.tags,
storagePaths: state.storagePaths,
);
},
),
@@ -368,21 +370,7 @@ class _DocumentsPageState extends State<DocumentsPage> with SingleTickerProvider
}
void _onCreateSavedView(DocumentFilter filter) async {
final newView = await Navigator.of(context).push<SavedView?>(
MaterialPageRoute(
builder: (context) => BlocBuilder<SavedViewCubit, SavedViewState>(
builder: (context, state) {
return AddSavedViewPage(
currentFilter: filter,
correspondents: state.correspondents,
documentTypes: state.documentTypes,
storagePaths: state.storagePaths,
tags: state.tags,
);
},
),
),
);
final newView = await pushAddSavedViewRoute(context, filter: filter);
if (newView != null) {
try {
await context.read<SavedViewCubit>().add(newView);
@@ -443,12 +431,9 @@ class _DocumentsPageState extends State<DocumentsPage> with SingleTickerProvider
}
void _openDetails(DocumentModel document) {
Navigator.pushNamed(
pushDocumentDetailsRoute(
context,
DocumentDetailsRoute.routeName,
arguments: DocumentDetailsRouteArguments(
document: document,
),
document: document,
);
}

View File

@@ -24,11 +24,6 @@ abstract class AdaptiveDocumentsView extends StatelessWidget {
final void Function(int? id)? onDocumentTypeSelected;
final void Function(int? id)? onStoragePathSelected;
final Map<int, Correspondent> correspondents;
final Map<int, DocumentType> documentTypes;
final Map<int, Tag> tags;
final Map<int, StoragePath> storagePaths;
bool get showLoadingPlaceholder => !hasLoaded && isLoading;
const AdaptiveDocumentsView({
@@ -47,10 +42,6 @@ abstract class AdaptiveDocumentsView extends StatelessWidget {
required this.isLoading,
required this.hasLoaded,
this.enableHeroAnimation = true,
required this.correspondents,
required this.documentTypes,
required this.tags,
required this.storagePaths,
});
AdaptiveDocumentsView.fromPagedState(
@@ -67,10 +58,6 @@ abstract class AdaptiveDocumentsView extends StatelessWidget {
required this.hasInternetConnection,
this.viewType = ViewType.list,
this.selectedDocumentIds = const [],
required this.correspondents,
required this.documentTypes,
required this.tags,
required this.storagePaths,
}) : documents = state.documents,
isLoading = state.isLoading,
hasLoaded = state.hasLoaded;
@@ -93,10 +80,6 @@ class SliverAdaptiveDocumentsView extends AdaptiveDocumentsView {
super.enableHeroAnimation,
required super.isLoading,
required super.hasLoaded,
required super.correspondents,
required super.documentTypes,
required super.tags,
required super.storagePaths,
});
@override
@@ -132,10 +115,6 @@ class SliverAdaptiveDocumentsView extends AdaptiveDocumentsView {
onDocumentTypeSelected: onDocumentTypeSelected,
onStoragePathSelected: onStoragePathSelected,
enableHeroAnimation: enableHeroAnimation,
correspondents: correspondents,
documentTypes: documentTypes,
storagePaths: storagePaths,
tags: tags,
);
},
),
@@ -165,10 +144,6 @@ class SliverAdaptiveDocumentsView extends AdaptiveDocumentsView {
onStoragePathSelected: onStoragePathSelected,
enableHeroAnimation: enableHeroAnimation,
highlights: document.searchHit?.highlights,
correspondents: correspondents,
documentTypes: documentTypes,
storagePaths: storagePaths,
tags: tags,
);
},
),
@@ -201,10 +176,6 @@ class SliverAdaptiveDocumentsView extends AdaptiveDocumentsView {
onDocumentTypeSelected: onDocumentTypeSelected,
onStoragePathSelected: onStoragePathSelected,
enableHeroAnimation: enableHeroAnimation,
correspondents: correspondents,
documentTypes: documentTypes,
storagePaths: storagePaths,
tags: tags,
);
},
);
@@ -230,10 +201,6 @@ class DefaultAdaptiveDocumentsView extends AdaptiveDocumentsView {
super.selectedDocumentIds,
super.viewType,
super.enableHeroAnimation = true,
required super.correspondents,
required super.documentTypes,
required super.tags,
required super.storagePaths,
});
@override
@@ -272,10 +239,6 @@ class DefaultAdaptiveDocumentsView extends AdaptiveDocumentsView {
onDocumentTypeSelected: onDocumentTypeSelected,
onStoragePathSelected: onStoragePathSelected,
enableHeroAnimation: enableHeroAnimation,
correspondents: correspondents,
documentTypes: documentTypes,
storagePaths: storagePaths,
tags: tags,
);
},
);
@@ -306,10 +269,6 @@ class DefaultAdaptiveDocumentsView extends AdaptiveDocumentsView {
onDocumentTypeSelected: onDocumentTypeSelected,
onStoragePathSelected: onStoragePathSelected,
enableHeroAnimation: enableHeroAnimation,
correspondents: correspondents,
documentTypes: documentTypes,
storagePaths: storagePaths,
tags: tags,
);
},
);
@@ -344,10 +303,6 @@ class DefaultAdaptiveDocumentsView extends AdaptiveDocumentsView {
onDocumentTypeSelected: onDocumentTypeSelected,
onStoragePathSelected: onStoragePathSelected,
enableHeroAnimation: enableHeroAnimation,
correspondents: correspondents,
documentTypes: documentTypes,
storagePaths: storagePaths,
tags: tags,
);
},
);

View File

@@ -2,7 +2,6 @@ import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/features/documents/view/pages/document_view.dart';
import 'package:provider/provider.dart';
import 'package:shimmer/shimmer.dart';
@@ -44,9 +43,7 @@ class DocumentPreview extends StatelessWidget {
fit: fit,
alignment: alignment,
cacheKey: "thumb_${document.id}",
imageUrl: context
.read<PaperlessDocumentsApi>()
.getThumbnailUrl(document.id),
imageUrl: context.read<PaperlessDocumentsApi>().getThumbnailUrl(document.id),
errorWidget: (ctxt, msg, __) => Text(msg),
placeholder: (context, value) => Shimmer.fromColors(
baseColor: Colors.grey[300]!,

View File

@@ -3,12 +3,14 @@ import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_html/flutter_html.dart';
import 'package:intl/intl.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/documents/view/widgets/document_preview.dart';
import 'package:paperless_mobile/features/documents/view/widgets/items/document_item.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:provider/provider.dart';
class DocumentDetailedItem extends DocumentItem {
final String? highlights;
@@ -26,10 +28,6 @@ class DocumentDetailedItem extends DocumentItem {
super.onStoragePathSelected,
super.onTagSelected,
super.onTap,
required super.tags,
required super.correspondents,
required super.documentTypes,
required super.storagePaths,
});
@override
@@ -116,7 +114,8 @@ class DocumentDetailedItem extends DocumentItem {
textStyle: Theme.of(context).textTheme.titleSmall?.apply(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
correspondent: correspondents[document.correspondent],
correspondent:
context.watch<LabelRepository>().state.correspondents[document.correspondent],
),
],
).paddedLTRB(8, 0, 8, 4),
@@ -131,13 +130,16 @@ class DocumentDetailedItem extends DocumentItem {
textStyle: Theme.of(context).textTheme.titleSmall?.apply(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
documentType: documentTypes[document.documentType],
documentType:
context.watch<LabelRepository>().state.documentTypes[document.documentType],
),
],
).paddedLTRB(8, 0, 8, 4),
TagsWidget(
isMultiLine: false,
tags: document.tags.map((e) => tags[e]!).toList(),
tags: document.tags
.map((e) => context.watch<LabelRepository>().state.tags[e]!)
.toList(),
).padded(),
if (highlights != null)
Html(

View File

@@ -1,11 +1,12 @@
import 'package:flutter/material.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/features/documents/view/widgets/document_preview.dart';
import 'package:paperless_mobile/features/documents/view/widgets/items/document_item.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:intl/intl.dart';
import 'package:provider/provider.dart';
class DocumentGridItem extends DocumentItem {
const DocumentGridItem({
@@ -21,10 +22,6 @@ class DocumentGridItem extends DocumentItem {
super.onTagSelected,
super.onTap,
required super.enableHeroAnimation,
required super.tags,
required super.correspondents,
required super.documentTypes,
required super.storagePaths,
});
@override
@@ -33,9 +30,8 @@ class DocumentGridItem extends DocumentItem {
padding: const EdgeInsets.all(8.0),
child: Card(
elevation: 1.0,
color: isSelected
? Theme.of(context).colorScheme.inversePrimary
: Theme.of(context).cardColor,
color:
isSelected ? Theme.of(context).colorScheme.inversePrimary : Theme.of(context).cardColor,
child: InkWell(
borderRadius: BorderRadius.circular(12),
onTap: _onTap,
@@ -58,10 +54,16 @@ class DocumentGridItem extends DocumentItem {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CorrespondentWidget(
correspondent: correspondents[document.correspondent],
correspondent: context
.watch<LabelRepository>()
.state
.correspondents[document.correspondent],
),
DocumentTypeWidget(
documentType: documentTypes[document.documentType],
documentType: context
.watch<LabelRepository>()
.state
.documentTypes[document.documentType],
),
Text(
document.title,
@@ -71,7 +73,9 @@ class DocumentGridItem extends DocumentItem {
),
const Spacer(),
TagsWidget(
tags: document.tags.map((e) => tags[e]!).toList(),
tags: document.tags
.map((e) => context.watch<LabelRepository>().state.tags[e]!)
.toList(),
isMultiLine: false,
onTagSelected: onTagSelected,
),

View File

@@ -10,11 +10,6 @@ abstract class DocumentItem extends StatelessWidget {
final bool isLabelClickable;
final bool enableHeroAnimation;
final Map<int, Tag> tags;
final Map<int, Correspondent> correspondents;
final Map<int, DocumentType> documentTypes;
final Map<int, StoragePath> storagePaths;
final void Function(int tagId)? onTagSelected;
final void Function(int? correspondentId)? onCorrespondentSelected;
final void Function(int? documentTypeId)? onDocumentTypeSelected;
@@ -33,9 +28,5 @@ abstract class DocumentItem extends StatelessWidget {
this.onDocumentTypeSelected,
this.onStoragePathSelected,
required this.enableHeroAnimation,
required this.tags,
required this.correspondents,
required this.documentTypes,
required this.storagePaths,
});
}

View File

@@ -1,9 +1,11 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/features/documents/view/widgets/document_preview.dart';
import 'package:paperless_mobile/features/documents/view/widgets/items/document_item.dart';
import 'package:paperless_mobile/features/labels/correspondent/view/widgets/correspondent_widget.dart';
import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_widget.dart';
import 'package:provider/provider.dart';
class DocumentListItem extends DocumentItem {
static const _a4AspectRatio = 1 / 1.4142;
@@ -21,14 +23,11 @@ class DocumentListItem extends DocumentItem {
super.onTagSelected,
super.onTap,
super.enableHeroAnimation = true,
required super.tags,
required super.correspondents,
required super.documentTypes,
required super.storagePaths,
});
@override
Widget build(BuildContext context) {
final labels = context.watch<LabelRepository>().state;
return Material(
child: ListTile(
dense: true,
@@ -46,7 +45,10 @@ class DocumentListItem extends DocumentItem {
absorbing: isSelectionActive,
child: CorrespondentWidget(
isClickable: isLabelClickable,
correspondent: correspondents[document.correspondent],
correspondent: context
.watch<LabelRepository>()
.state
.correspondents[document.correspondent],
onSelected: onCorrespondentSelected,
),
),
@@ -62,8 +64,8 @@ class DocumentListItem extends DocumentItem {
child: TagsWidget(
isClickable: isLabelClickable,
tags: document.tags
.where((e) => tags.containsKey(e))
.map((e) => tags[e]!)
.where((e) => labels.tags.containsKey(e))
.map((e) => labels.tags[e]!)
.toList(),
isMultiLine: false,
onTagSelected: (id) => onTagSelected?.call(id),
@@ -78,15 +80,12 @@ class DocumentListItem extends DocumentItem {
overflow: TextOverflow.ellipsis,
text: TextSpan(
text: DateFormat.yMMMd().format(document.created),
style: Theme.of(context)
.textTheme
.labelSmall
?.apply(color: Colors.grey),
style: Theme.of(context).textTheme.labelSmall?.apply(color: Colors.grey),
children: document.documentType != null
? [
const TextSpan(text: '\u30FB'),
TextSpan(
text: documentTypes[document.documentType]?.name,
text: labels.documentTypes[document.documentType]?.name,
),
]
: null,

View File

@@ -61,8 +61,8 @@ class DocumentsListLoadingWidget extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
TagsPlaceholder(count: 2, dense: true),
SizedBox(height: 2),
const TagsPlaceholder(count: 2, dense: true),
const SizedBox(height: 2),
TextPlaceholder(
length: 250,
fontSize: Theme.of(context).textTheme.labelSmall!.fontSize!,

View File

@@ -1,10 +1,8 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
import 'package:paperless_mobile/core/widgets/form_builder_fields/extended_date_range_form_field/form_builder_extended_date_range_picker.dart';
import 'package:paperless_mobile/features/labels/cubit/label_cubit.dart';
import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_form_field.dart';
import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_form_field.dart';
import 'package:paperless_mobile/features/labels/view/widgets/label_form_field.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
@@ -156,6 +154,10 @@ class _DocumentFilterFormState extends State<DocumentFilterForm> {
initialValue: widget.initialFilter.documentType,
prefixIcon: const Icon(Icons.description_outlined),
allowSelectUnassigned: false,
canCreateNewLabel: LocalUserAccount.current.paperlessUser.hasPermission(
PermissionAction.add,
PermissionTarget.documentType,
),
);
}
@@ -167,6 +169,10 @@ class _DocumentFilterFormState extends State<DocumentFilterForm> {
initialValue: widget.initialFilter.correspondent,
prefixIcon: const Icon(Icons.person_outline),
allowSelectUnassigned: false,
canCreateNewLabel: LocalUserAccount.current.paperlessUser.hasPermission(
PermissionAction.add,
PermissionTarget.correspondent,
),
);
}
@@ -178,6 +184,10 @@ class _DocumentFilterFormState extends State<DocumentFilterForm> {
initialValue: widget.initialFilter.storagePath,
prefixIcon: const Icon(Icons.folder_outlined),
allowSelectUnassigned: false,
canCreateNewLabel: LocalUserAccount.current.paperlessUser.hasPermission(
PermissionAction.add,
PermissionTarget.storagePath,
),
);
}

View File

@@ -1,10 +1,8 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/translation/sort_field_localization_mapper.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/labels/cubit/label_cubit.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
class SortFieldSelectionBottomSheet extends StatefulWidget {
@@ -29,12 +27,10 @@ class SortFieldSelectionBottomSheet extends StatefulWidget {
});
@override
State<SortFieldSelectionBottomSheet> createState() =>
_SortFieldSelectionBottomSheetState();
State<SortFieldSelectionBottomSheet> createState() => _SortFieldSelectionBottomSheetState();
}
class _SortFieldSelectionBottomSheetState
extends State<SortFieldSelectionBottomSheet> {
class _SortFieldSelectionBottomSheetState extends State<SortFieldSelectionBottomSheet> {
late SortField? _currentSortField;
late SortOrder _currentSortOrder;
@@ -62,8 +58,8 @@ class _SortFieldSelectionBottomSheetState
),
TextButton(
child: Text(S.of(context)!.apply),
onPressed: () {
widget.onSubmit(
onPressed: () async {
await widget.onSubmit(
_currentSortField,
_currentSortOrder,
);
@@ -131,7 +127,9 @@ class _SortFieldSelectionBottomSheetState
contentPadding: const EdgeInsets.only(left: 32, right: 16),
title: Text(translateSortField(context, field)),
trailing: _currentSortField == field ? const Icon(Icons.done) : null,
onTap: () => setState(() => _currentSortField = field),
onTap: () {
setState(() => _currentSortField = field);
},
);
}
}

View File

@@ -1,7 +1,6 @@
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/cubit/documents_cubit.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';

View File

@@ -1,10 +1,8 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/navigation/push_routes.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/document_bulk_action/cubit/document_bulk_action_cubit.dart';
import 'package:paperless_mobile/features/document_bulk_action/view/widgets/fullscreen_bulk_edit_label_page.dart';
import 'package:paperless_mobile/features/document_bulk_action/view/widgets/fullscreen_bulk_edit_tags_widget.dart';
import 'package:paperless_mobile/features/documents/cubit/documents_cubit.dart';
import 'package:paperless_mobile/features/documents/view/widgets/selection/bulk_delete_confirmation_dialog.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
@@ -35,15 +33,12 @@ class DocumentSelectionSliverAppBar extends StatelessWidget {
onPressed: () async {
final shouldDelete = await showDialog<bool>(
context: context,
builder: (context) =>
BulkDeleteConfirmationDialog(state: state),
builder: (context) => BulkDeleteConfirmationDialog(state: state),
) ??
false;
if (shouldDelete) {
try {
await context
.read<DocumentsCubit>()
.bulkDelete(state.selection);
await context.read<DocumentsCubit>().bulkDelete(state.selection);
showSnackBar(
context,
S.of(context)!.documentsSuccessfullyDeleted,
@@ -66,140 +61,22 @@ class DocumentSelectionSliverAppBar extends StatelessWidget {
ActionChip(
label: Text(S.of(context)!.correspondent),
avatar: const Icon(Icons.edit),
onPressed: () async {
await Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => BlocProvider(
create: (context) => DocumentBulkActionCubit(
context.read(),
context.read(),
context.read(),
selection: state.selection,
),
child: BlocBuilder<DocumentBulkActionCubit,
DocumentBulkActionState>(
builder: (context, state) {
return FullscreenBulkEditLabelPage(
options: state.correspondents,
selection: state.selection,
labelMapper: (document) => document.correspondent,
leadingIcon: const Icon(Icons.person_outline),
hintText: S.of(context)!.startTyping,
onSubmit: context
.read<DocumentBulkActionCubit>()
.bulkModifyCorrespondent,
assignMessageBuilder: (int count, String name) {
return S
.of(context)!
.bulkEditCorrespondentAssignMessage(
name,
count,
);
},
removeMessageBuilder: (int count) {
return S
.of(context)!
.bulkEditCorrespondentRemoveMessage(count);
},
);
},
),
),
),
);
onPressed: () {
pushBulkEditCorrespondentRoute(context, selection: state.selection);
},
).paddedOnly(left: 8, right: 4),
ActionChip(
label: Text(S.of(context)!.documentType),
avatar: const Icon(Icons.edit),
onPressed: () async {
await Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => BlocProvider(
create: (context) => DocumentBulkActionCubit(
context.read(),
context.read(),
context.read(),
selection: state.selection,
),
child: BlocBuilder<DocumentBulkActionCubit,
DocumentBulkActionState>(
builder: (context, state) {
return FullscreenBulkEditLabelPage(
options: state.documentTypes,
selection: state.selection,
labelMapper: (document) => document.documentType,
leadingIcon:
const Icon(Icons.description_outlined),
hintText: S.of(context)!.startTyping,
onSubmit: context
.read<DocumentBulkActionCubit>()
.bulkModifyDocumentType,
assignMessageBuilder: (int count, String name) {
return S
.of(context)!
.bulkEditDocumentTypeAssignMessage(
count,
name,
);
},
removeMessageBuilder: (int count) {
return S
.of(context)!
.bulkEditDocumentTypeRemoveMessage(count);
},
);
},
),
),
),
);
pushBulkEditDocumentTypeRoute(context, selection: state.selection);
},
).paddedOnly(left: 8, right: 4),
ActionChip(
label: Text(S.of(context)!.storagePath),
avatar: const Icon(Icons.edit),
onPressed: () async {
await Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => BlocProvider(
create: (context) => DocumentBulkActionCubit(
context.read(),
context.read(),
context.read(),
selection: state.selection,
),
child: BlocBuilder<DocumentBulkActionCubit,
DocumentBulkActionState>(
builder: (context, state) {
return FullscreenBulkEditLabelPage(
options: state.storagePaths,
selection: state.selection,
labelMapper: (document) => document.storagePath,
leadingIcon: const Icon(Icons.folder_outlined),
hintText: S.of(context)!.startTyping,
onSubmit: context
.read<DocumentBulkActionCubit>()
.bulkModifyStoragePath,
assignMessageBuilder: (int count, String name) {
return S
.of(context)!
.bulkEditStoragePathAssignMessage(
count,
name,
);
},
removeMessageBuilder: (int count) {
return S
.of(context)!
.bulkEditStoragePathRemoveMessage(count);
},
);
},
),
),
),
);
pushBulkEditStoragePathRoute(context, selection: state.selection);
},
).paddedOnly(left: 8, right: 4),
_buildBulkEditTagsChip(context).paddedOnly(left: 4, right: 4),
@@ -215,21 +92,7 @@ class DocumentSelectionSliverAppBar extends StatelessWidget {
label: Text(S.of(context)!.tags),
avatar: const Icon(Icons.edit),
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => BlocProvider(
create: (context) => DocumentBulkActionCubit(
context.read(),
context.read(),
context.read(),
selection: state.selection,
),
child: Builder(builder: (context) {
return const FullscreenBulkEditTagsWidget();
}),
),
),
);
pushBulkEditTagsRoute(context, selection: state.selection);
},
);
}

View File

@@ -35,7 +35,7 @@ class ViewTypeSelectionWidget extends StatelessWidget {
), // Ensures text is not split into two lines
position: PopupMenuPosition.under,
initialValue: viewType,
icon: Icon(icon),
icon: Icon(icon, color: Theme.of(context).colorScheme.primary),
itemBuilder: (context) => [
_buildViewTypeOption(
context,

View File

@@ -1,7 +1,6 @@
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/translation/sort_field_localization_mapper.dart';
import 'package:paperless_mobile/features/documents/cubit/documents_cubit.dart';
import 'package:paperless_mobile/features/documents/view/widgets/search/sort_field_selection_bottom_sheet.dart';
@@ -21,6 +20,7 @@ class SortDocumentsButton extends StatelessWidget {
if (state.filter.sortField == null) {
return const SizedBox.shrink();
}
print(state.filter.sortField);
return TextButton.icon(
icon: Icon(state.filter.sortOrder == SortOrder.ascending
? Icons.arrow_upward
@@ -49,14 +49,14 @@ class SortDocumentsButton extends StatelessWidget {
child: SortFieldSelectionBottomSheet(
initialSortField: state.filter.sortField,
initialSortOrder: state.filter.sortOrder,
onSubmit: (field, order) => context
.read<DocumentsCubit>()
.updateCurrentFilter(
(filter) => filter.copyWith(
sortField: field,
sortOrder: order,
),
),
onSubmit: (field, order) {
return context.read<DocumentsCubit>().updateCurrentFilter(
(filter) => filter.copyWith(
sortField: field,
sortOrder: order,
),
);
},
correspondents: state.correspondents,
documentTypes: state.documentTypes,
storagePaths: state.storagePaths,

View File

@@ -1,237 +0,0 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'edit_label_cubit.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods');
/// @nodoc
mixin _$EditLabelState {
Map<int, Correspondent> get correspondents =>
throw _privateConstructorUsedError;
Map<int, DocumentType> get documentTypes =>
throw _privateConstructorUsedError;
Map<int, Tag> get tags => throw _privateConstructorUsedError;
Map<int, StoragePath> get storagePaths => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$EditLabelStateCopyWith<EditLabelState> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $EditLabelStateCopyWith<$Res> {
factory $EditLabelStateCopyWith(
EditLabelState value, $Res Function(EditLabelState) then) =
_$EditLabelStateCopyWithImpl<$Res, EditLabelState>;
@useResult
$Res call(
{Map<int, Correspondent> correspondents,
Map<int, DocumentType> documentTypes,
Map<int, Tag> tags,
Map<int, StoragePath> storagePaths});
}
/// @nodoc
class _$EditLabelStateCopyWithImpl<$Res, $Val extends EditLabelState>
implements $EditLabelStateCopyWith<$Res> {
_$EditLabelStateCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
@pragma('vm:prefer-inline')
@override
$Res call({
Object? correspondents = null,
Object? documentTypes = null,
Object? tags = null,
Object? storagePaths = null,
}) {
return _then(_value.copyWith(
correspondents: null == correspondents
? _value.correspondents
: correspondents // ignore: cast_nullable_to_non_nullable
as Map<int, Correspondent>,
documentTypes: null == documentTypes
? _value.documentTypes
: documentTypes // ignore: cast_nullable_to_non_nullable
as Map<int, DocumentType>,
tags: null == tags
? _value.tags
: tags // ignore: cast_nullable_to_non_nullable
as Map<int, Tag>,
storagePaths: null == storagePaths
? _value.storagePaths
: storagePaths // ignore: cast_nullable_to_non_nullable
as Map<int, StoragePath>,
) as $Val);
}
}
/// @nodoc
abstract class _$$_EditLabelStateCopyWith<$Res>
implements $EditLabelStateCopyWith<$Res> {
factory _$$_EditLabelStateCopyWith(
_$_EditLabelState value, $Res Function(_$_EditLabelState) then) =
__$$_EditLabelStateCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{Map<int, Correspondent> correspondents,
Map<int, DocumentType> documentTypes,
Map<int, Tag> tags,
Map<int, StoragePath> storagePaths});
}
/// @nodoc
class __$$_EditLabelStateCopyWithImpl<$Res>
extends _$EditLabelStateCopyWithImpl<$Res, _$_EditLabelState>
implements _$$_EditLabelStateCopyWith<$Res> {
__$$_EditLabelStateCopyWithImpl(
_$_EditLabelState _value, $Res Function(_$_EditLabelState) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? correspondents = null,
Object? documentTypes = null,
Object? tags = null,
Object? storagePaths = null,
}) {
return _then(_$_EditLabelState(
correspondents: null == correspondents
? _value._correspondents
: correspondents // ignore: cast_nullable_to_non_nullable
as Map<int, Correspondent>,
documentTypes: null == documentTypes
? _value._documentTypes
: documentTypes // ignore: cast_nullable_to_non_nullable
as Map<int, DocumentType>,
tags: null == tags
? _value._tags
: tags // ignore: cast_nullable_to_non_nullable
as Map<int, Tag>,
storagePaths: null == storagePaths
? _value._storagePaths
: storagePaths // ignore: cast_nullable_to_non_nullable
as Map<int, StoragePath>,
));
}
}
/// @nodoc
class _$_EditLabelState implements _EditLabelState {
const _$_EditLabelState(
{final Map<int, Correspondent> correspondents = const {},
final Map<int, DocumentType> documentTypes = const {},
final Map<int, Tag> tags = const {},
final Map<int, StoragePath> storagePaths = const {}})
: _correspondents = correspondents,
_documentTypes = documentTypes,
_tags = tags,
_storagePaths = storagePaths;
final Map<int, Correspondent> _correspondents;
@override
@JsonKey()
Map<int, Correspondent> get correspondents {
if (_correspondents is EqualUnmodifiableMapView) return _correspondents;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(_correspondents);
}
final Map<int, DocumentType> _documentTypes;
@override
@JsonKey()
Map<int, DocumentType> get documentTypes {
if (_documentTypes is EqualUnmodifiableMapView) return _documentTypes;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(_documentTypes);
}
final Map<int, Tag> _tags;
@override
@JsonKey()
Map<int, Tag> get tags {
if (_tags is EqualUnmodifiableMapView) return _tags;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(_tags);
}
final Map<int, StoragePath> _storagePaths;
@override
@JsonKey()
Map<int, StoragePath> get storagePaths {
if (_storagePaths is EqualUnmodifiableMapView) return _storagePaths;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(_storagePaths);
}
@override
String toString() {
return 'EditLabelState(correspondents: $correspondents, documentTypes: $documentTypes, tags: $tags, storagePaths: $storagePaths)';
}
@override
bool operator ==(dynamic other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$_EditLabelState &&
const DeepCollectionEquality()
.equals(other._correspondents, _correspondents) &&
const DeepCollectionEquality()
.equals(other._documentTypes, _documentTypes) &&
const DeepCollectionEquality().equals(other._tags, _tags) &&
const DeepCollectionEquality()
.equals(other._storagePaths, _storagePaths));
}
@override
int get hashCode => Object.hash(
runtimeType,
const DeepCollectionEquality().hash(_correspondents),
const DeepCollectionEquality().hash(_documentTypes),
const DeepCollectionEquality().hash(_tags),
const DeepCollectionEquality().hash(_storagePaths));
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$_EditLabelStateCopyWith<_$_EditLabelState> get copyWith =>
__$$_EditLabelStateCopyWithImpl<_$_EditLabelState>(this, _$identity);
}
abstract class _EditLabelState implements EditLabelState {
const factory _EditLabelState(
{final Map<int, Correspondent> correspondents,
final Map<int, DocumentType> documentTypes,
final Map<int, Tag> tags,
final Map<int, StoragePath> storagePaths}) = _$_EditLabelState;
@override
Map<int, Correspondent> get correspondents;
@override
Map<int, DocumentType> get documentTypes;
@override
Map<int, Tag> get tags;
@override
Map<int, StoragePath> get storagePaths;
@override
@JsonKey(ignore: true)
_$$_EditLabelStateCopyWith<_$_EditLabelState> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@@ -18,6 +18,7 @@ class EditLabelPage<T extends Label> extends StatelessWidget {
final List<Widget> additionalFields;
final Future<T> Function(BuildContext context, T label) onSubmit;
final Future<void> Function(BuildContext context, T label) onDelete;
final bool canDelete;
const EditLabelPage({
super.key,
@@ -26,6 +27,7 @@ class EditLabelPage<T extends Label> extends StatelessWidget {
this.additionalFields = const [],
required this.onSubmit,
required this.onDelete,
required this.canDelete,
});
@override
@@ -40,6 +42,7 @@ class EditLabelPage<T extends Label> extends StatelessWidget {
fromJsonT: fromJsonT,
onSubmit: onSubmit,
onDelete: onDelete,
canDelete: canDelete,
),
);
}
@@ -51,6 +54,7 @@ class EditLabelForm<T extends Label> extends StatelessWidget {
final List<Widget> additionalFields;
final Future<T> Function(BuildContext context, T label) onSubmit;
final Future<void> Function(BuildContext context, T label) onDelete;
final bool canDelete;
const EditLabelForm({
super.key,
@@ -59,6 +63,7 @@ class EditLabelForm<T extends Label> extends StatelessWidget {
required this.additionalFields,
required this.onSubmit,
required this.onDelete,
required this.canDelete,
});
@override
@@ -68,7 +73,7 @@ class EditLabelForm<T extends Label> extends StatelessWidget {
title: Text(S.of(context)!.edit),
actions: [
IconButton(
onPressed: () => _onDelete(context),
onPressed: canDelete ? () => _onDelete(context) : null,
icon: const Icon(Icons.delete),
),
],

View File

@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
import 'package:paperless_mobile/features/edit_label/cubit/edit_label_cubit.dart';
import 'package:paperless_mobile/features/edit_label/view/edit_label_page.dart';
@@ -19,10 +20,12 @@ class EditCorrespondentPage extends StatelessWidget {
return EditLabelPage<Correspondent>(
label: correspondent,
fromJsonT: Correspondent.fromJson,
onSubmit: (context, label) =>
context.read<EditLabelCubit>().replaceCorrespondent(label),
onDelete: (context, label) =>
context.read<EditLabelCubit>().removeCorrespondent(label),
onSubmit: (context, label) => context.read<EditLabelCubit>().replaceCorrespondent(label),
onDelete: (context, label) => context.read<EditLabelCubit>().removeCorrespondent(label),
canDelete: LocalUserAccount.current.paperlessUser.hasPermission(
PermissionAction.delete,
PermissionTarget.correspondent,
),
);
}),
);

View File

@@ -1,7 +1,7 @@
import 'package:flutter/widgets.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/database/tables/local_user_account.dart';
import 'package:paperless_mobile/features/edit_label/cubit/edit_label_cubit.dart';
import 'package:paperless_mobile/features/edit_label/view/edit_label_page.dart';
@@ -18,10 +18,12 @@ class EditDocumentTypePage extends StatelessWidget {
child: EditLabelPage<DocumentType>(
label: documentType,
fromJsonT: DocumentType.fromJson,
onSubmit: (context, label) =>
context.read<EditLabelCubit>().replaceDocumentType(label),
onDelete: (context, label) =>
context.read<EditLabelCubit>().removeDocumentType(label),
onSubmit: (context, label) => context.read<EditLabelCubit>().replaceDocumentType(label),
onDelete: (context, label) => context.read<EditLabelCubit>().removeDocumentType(label),
canDelete: LocalUserAccount.current.paperlessUser.hasPermission(
PermissionAction.delete,
PermissionTarget.documentType,
),
),
);
}

View File

@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
import 'package:paperless_mobile/features/edit_label/cubit/edit_label_cubit.dart';
import 'package:paperless_mobile/features/edit_label/view/edit_label_page.dart';
import 'package:paperless_mobile/features/labels/storage_path/view/widgets/storage_path_autofill_form_builder_field.dart';
@@ -18,10 +19,12 @@ class EditStoragePathPage extends StatelessWidget {
child: EditLabelPage<StoragePath>(
label: storagePath,
fromJsonT: StoragePath.fromJson,
onSubmit: (context, label) =>
context.read<EditLabelCubit>().replaceStoragePath(label),
onDelete: (context, label) =>
context.read<EditLabelCubit>().removeStoragePath(label),
onSubmit: (context, label) => context.read<EditLabelCubit>().replaceStoragePath(label),
onDelete: (context, label) => context.read<EditLabelCubit>().removeStoragePath(label),
canDelete: LocalUserAccount.current.paperlessUser.hasPermission(
PermissionAction.delete,
PermissionTarget.storagePath,
),
additionalFields: [
StoragePathAutofillFormBuilderField(
name: StoragePath.pathKey,

View File

@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
import 'package:paperless_mobile/core/widgets/form_builder_fields/form_builder_color_picker.dart';
import 'package:paperless_mobile/features/edit_label/cubit/edit_label_cubit.dart';
import 'package:paperless_mobile/features/edit_label/view/edit_label_page.dart';
@@ -21,10 +22,12 @@ class EditTagPage extends StatelessWidget {
child: EditLabelPage<Tag>(
label: tag,
fromJsonT: Tag.fromJson,
onSubmit: (context, label) =>
context.read<EditLabelCubit>().replaceTag(label),
onDelete: (context, label) =>
context.read<EditLabelCubit>().removeTag(label),
onSubmit: (context, label) => context.read<EditLabelCubit>().replaceTag(label),
onDelete: (context, label) => context.read<EditLabelCubit>().removeTag(label),
canDelete: LocalUserAccount.current.paperlessUser.hasPermission(
PermissionAction.delete,
PermissionTarget.tag,
),
additionalFields: [
FormBuilderColorPickerField(
initialValue: tag.color,

View File

@@ -8,7 +8,6 @@ import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
import 'package:paperless_mobile/helpers/message_helpers.dart';
import 'package:paperless_mobile/constants.dart';
class SubmitButtonConfig<T extends Label> {
final Widget icon;

View File

@@ -6,45 +6,33 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:hive/hive.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
import 'package:paperless_mobile/core/bloc/server_information_cubit.dart';
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
import 'package:paperless_mobile/core/database/tables/global_settings.dart';
import 'package:paperless_mobile/core/database/tables/local_user_app_state.dart';
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
import 'package:paperless_mobile/core/global/constants.dart';
import 'package:paperless_mobile/core/navigation/push_routes.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/repository/saved_view_repository.dart';
import 'package:paperless_mobile/core/service/file_description.dart';
import 'package:paperless_mobile/core/translation/error_code_localization_mapper.dart';
import 'package:paperless_mobile/features/document_scan/cubit/document_scanner_cubit.dart';
import 'package:paperless_mobile/features/document_scan/view/scanner_page.dart';
import 'package:paperless_mobile/features/document_upload/cubit/document_upload_cubit.dart';
import 'package:paperless_mobile/features/document_upload/view/document_upload_preparation_page.dart';
import 'package:paperless_mobile/features/documents/cubit/documents_cubit.dart';
import 'package:paperless_mobile/features/documents/view/pages/documents_page.dart';
import 'package:paperless_mobile/features/home/view/route_description.dart';
import 'package:paperless_mobile/features/inbox/cubit/inbox_cubit.dart';
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/core/database/tables/local_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';
import 'package:paperless_mobile/features/tasks/cubit/task_status_cubit.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
import 'package:paperless_mobile/helpers/message_helpers.dart';
import 'package:receive_sharing_intent/receive_sharing_intent.dart';
import 'package:responsive_builder/responsive_builder.dart';
/// Wrapper around all functionality for a logged in user.
/// Performs initialization logic.
class HomePage extends StatefulWidget {
const HomePage({Key? key}) : super(key: key);
final int paperlessApiVersion;
const HomePage({Key? key, required this.paperlessApiVersion}) : super(key: key);
@override
_HomePageState createState() => _HomePageState();
@@ -52,35 +40,12 @@ class HomePage extends StatefulWidget {
class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
int _currentIndex = 0;
final DocumentScannerCubit _scannerCubit = DocumentScannerCubit();
late final DocumentsCubit _documentsCubit;
late final InboxCubit _inboxCubit;
late final SavedViewCubit _savedViewCubit;
late Timer _inboxTimer;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
_initializeData(context);
final userId =
Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!.currentLoggedInUser;
_documentsCubit = DocumentsCubit(
context.read(),
context.read(),
context.read(),
Hive.box<LocalUserAppState>(HiveBoxes.localUserAppState).get(userId)!,
)..reload();
_savedViewCubit = SavedViewCubit(
context.read(),
context.read(),
)..reload();
_inboxCubit = InboxCubit(
context.read(),
context.read(),
context.read(),
context.read(),
);
_listenToInboxChanges();
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
_listenForReceivedFiles();
@@ -97,7 +62,7 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
if (!mounted) {
timer.cancel();
} else {
_inboxCubit.refreshItemsInInboxCount();
context.read<InboxCubit>().refreshItemsInInboxCount();
}
});
}
@@ -127,9 +92,6 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
void dispose() {
WidgetsBinding.instance.removeObserver(this);
_inboxTimer.cancel();
_inboxCubit.close();
_documentsCubit.close();
_savedViewCubit.close();
super.dispose();
}
@@ -176,25 +138,23 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
}
return;
}
if (!LocalUserAccount.current.paperlessUser
.hasPermission(PermissionAction.add, PermissionTarget.document)) {
Fluttertoast.showToast(
msg: "You do not have the permissions to upload documents.",
);
return;
}
final fileDescription = FileDescription.fromPath(mediaFile.path);
if (await File(mediaFile.path).exists()) {
final bytes = File(mediaFile.path).readAsBytesSync();
final result = await Navigator.push<DocumentUploadResult>(
final result = await pushDocumentUploadPreparationPage(
context,
MaterialPageRoute(
builder: (context) => BlocProvider.value(
value: DocumentUploadCubit(
context.read(),
context.read(),
),
child: DocumentUploadPreparationPage(
fileBytes: bytes,
filename: fileDescription.filename,
title: fileDescription.filename,
fileExtension: fileDescription.extension,
),
),
),
bytes: bytes,
filename: fileDescription.filename,
title: fileDescription.filename,
fileExtension: fileDescription.extension,
);
if (result?.success ?? false) {
await Fluttertoast.showToast(
@@ -212,8 +172,6 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
@override
Widget build(BuildContext context) {
final userId =
Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!.currentLoggedInUser!;
final destinations = [
RouteDescription(
icon: const Icon(Icons.description_outlined),
@@ -223,14 +181,16 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
),
label: S.of(context)!.documents,
),
RouteDescription(
icon: const Icon(Icons.document_scanner_outlined),
selectedIcon: Icon(
Icons.document_scanner,
color: Theme.of(context).colorScheme.primary,
if (LocalUserAccount.current.paperlessUser
.hasPermission(PermissionAction.add, PermissionTarget.document))
RouteDescription(
icon: const Icon(Icons.document_scanner_outlined),
selectedIcon: Icon(
Icons.document_scanner,
color: Theme.of(context).colorScheme.primary,
),
label: S.of(context)!.scanner,
),
label: S.of(context)!.scanner,
),
RouteDescription(
icon: const Icon(Icons.sell_outlined),
selectedIcon: Icon(
@@ -247,54 +207,32 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
),
label: S.of(context)!.inbox,
badgeBuilder: (icon) => BlocBuilder<InboxCubit, InboxState>(
bloc: _inboxCubit,
builder: (context, state) {
if (state.itemsInInboxCount > 0) {
return Badge.count(
count: state.itemsInInboxCount,
child: icon,
);
}
return icon;
return Badge.count(
isLabelVisible: state.itemsInInboxCount > 0,
count: state.itemsInInboxCount,
child: icon,
);
},
),
),
];
final routes = <Widget>[
MultiBlocProvider(
// key: ValueKey(userId),
providers: [
BlocProvider.value(value: _documentsCubit),
BlocProvider.value(value: _savedViewCubit),
],
child: const DocumentsPage(),
),
BlocProvider.value(
value: _scannerCubit,
child: const ScannerPage(),
),
MultiBlocProvider(
// key: ValueKey(userId),
providers: [
BlocProvider(
create: (context) => LabelCubit(context.read()),
)
],
child: const LabelsPage(),
),
BlocProvider<InboxCubit>.value(
value: _inboxCubit,
child: const InboxPage(),
),
const DocumentsPage(),
if (LocalUserAccount.current.paperlessUser
.hasPermission(PermissionAction.add, PermissionTarget.document))
const ScannerPage(),
const LabelsPage(),
const InboxPage(),
];
return MultiBlocListener(
listeners: [
BlocListener<ConnectivityCubit, ConnectivityState>(
//Only re-initialize data if the connectivity changed from not connected to connected
// If app was started offline, load data once it comes back online.
listenWhen: (previous, current) => current == ConnectivityState.connected,
listener: (context, state) {
_initializeData(context);
context.read<LabelRepository>().initialize();
context.read<SavedViewRepository>().initialize();
},
),
BlocListener<TaskStatusCubit, TaskStatusState>(
@@ -346,15 +284,4 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
setState(() => _currentIndex = index);
}
}
void _initializeData(BuildContext context) {
Future.wait([
context.read<LabelRepository>().initialize(),
context.read<SavedViewRepository>().findAll(),
context.read<ServerInformationCubit>().updateInformation(),
]).onError<PaperlessServerException>((error, stackTrace) {
showErrorMessage(context, error, stackTrace);
throw error;
});
}
}

View File

@@ -1,29 +1,156 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/src/widgets/framework.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_mobile/core/bloc/server_information_cubit.dart';
import 'package:flutter/material.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:hive_flutter/adapters.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/local_user_app_state.dart';
import 'package:paperless_mobile/core/factory/paperless_api_factory.dart';
import 'package:paperless_mobile/core/notifier/document_changed_notifier.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/repository/user_repository.dart';
import 'package:paperless_mobile/core/security/session_manager.dart';
import 'package:paperless_mobile/core/service/dio_file_service.dart';
import 'package:paperless_mobile/features/document_scan/cubit/document_scanner_cubit.dart';
import 'package:paperless_mobile/features/documents/cubit/documents_cubit.dart';
import 'package:paperless_mobile/features/home/view/home_page.dart';
import 'package:paperless_mobile/features/home/view/model/api_version.dart';
import 'package:paperless_mobile/features/inbox/cubit/inbox_cubit.dart';
import 'package:paperless_mobile/features/labels/cubit/label_cubit.dart';
import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart';
import 'package:paperless_mobile/features/settings/view/widgets/global_settings_builder.dart';
import 'package:paperless_mobile/features/tasks/cubit/task_status_cubit.dart';
import 'package:provider/provider.dart';
class HomeRoute extends StatelessWidget {
const HomeRoute({super.key});
/// The id of the currently authenticated user (e.g. demo@paperless.example.com)
final String localUserId;
/// The Paperless API version of the currently connected instance
final int paperlessApiVersion;
// A factory providing the API implementations given an API version
final PaperlessApiFactory paperlessProviderFactory;
const HomeRoute({
super.key,
required this.paperlessApiVersion,
required this.paperlessProviderFactory,
required this.localUserId,
});
@override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider(
create: (context) => TaskStatusCubit(
context.read(),
),
),
BlocProvider<ServerInformationCubit>(
create: (context) => ServerInformationCubit(
context.read(),
)..updateInformation(),
),
],
child: HomePage(),
return GlobalSettingsBuilder(
builder: (context, settings) {
final currentLocalUserId = settings.currentLoggedInUser!;
final apiVersion = ApiVersion(paperlessApiVersion);
return MultiProvider(
providers: [
Provider.value(value: apiVersion),
Provider<CacheManager>(
create: (context) => CacheManager(
Config(
// Isolated cache per user.
localUserId,
fileService: DioFileService(context.read<SessionManager>().client),
),
),
),
ProxyProvider<SessionManager, PaperlessDocumentsApi>(
update: (context, value, previous) => paperlessProviderFactory.createDocumentsApi(
value.client,
apiVersion: paperlessApiVersion,
),
),
ProxyProvider<SessionManager, PaperlessLabelsApi>(
update: (context, value, previous) => paperlessProviderFactory.createLabelsApi(
value.client,
apiVersion: paperlessApiVersion,
),
),
ProxyProvider<SessionManager, PaperlessSavedViewsApi>(
update: (context, value, previous) => paperlessProviderFactory.createSavedViewsApi(
value.client,
apiVersion: paperlessApiVersion,
),
),
ProxyProvider<SessionManager, PaperlessServerStatsApi>(
update: (context, value, previous) => paperlessProviderFactory.createServerStatsApi(
value.client,
apiVersion: paperlessApiVersion,
),
),
ProxyProvider<SessionManager, PaperlessTasksApi>(
update: (context, value, previous) => paperlessProviderFactory.createTasksApi(
value.client,
apiVersion: paperlessApiVersion,
),
),
if (apiVersion.hasMultiUserSupport)
ProxyProvider<SessionManager, PaperlessUserApiV3>(
update: (context, value, previous) => PaperlessUserApiV3Impl(
value.client,
),
),
],
builder: (context, child) {
return MultiProvider(
providers: [
ProxyProvider<PaperlessLabelsApi, LabelRepository>(
update: (context, value, previous) => LabelRepository(value),
),
ProxyProvider<PaperlessSavedViewsApi, SavedViewRepository>(
update: (context, value, previous) => SavedViewRepository(value)..initialize(),
),
],
builder: (context, child) {
return MultiProvider(
providers: [
ProxyProvider3<PaperlessDocumentsApi, DocumentChangedNotifier, LabelRepository,
DocumentsCubit>(
update: (context, docApi, notifier, labelRepo, previous) => DocumentsCubit(
docApi,
notifier,
labelRepo,
Hive.box<LocalUserAppState>(HiveBoxes.localUserAppState)
.get(currentLocalUserId)!,
)..reload(),
),
Provider(create: (context) => DocumentScannerCubit()),
ProxyProvider4<PaperlessDocumentsApi, PaperlessServerStatsApi, LabelRepository,
DocumentChangedNotifier, InboxCubit>(
update: (context, docApi, statsApi, labelRepo, notifier, previous) =>
InboxCubit(
docApi,
statsApi,
labelRepo,
notifier,
)..initialize(),
),
ProxyProvider<SavedViewRepository, SavedViewCubit>(
update: (context, savedViewRepo, previous) => SavedViewCubit(
savedViewRepo,
)..initialize(),
),
ProxyProvider<LabelRepository, LabelCubit>(
update: (context, value, previous) => LabelCubit(value),
),
ProxyProvider<PaperlessTasksApi, TaskStatusCubit>(
update: (context, value, previous) => TaskStatusCubit(value),
),
if (paperlessApiVersion >= 3)
ProxyProvider<PaperlessUserApiV3, UserRepository>(
update: (context, value, previous) => UserRepository(value)..initialize(),
),
],
child: HomePage(paperlessApiVersion: paperlessApiVersion),
);
},
);
},
);
},
);
}
}

View File

@@ -0,0 +1,7 @@
class ApiVersion {
final int version;
ApiVersion(this.version);
bool get hasMultiUserSupport => version >= 3;
}

View File

@@ -5,12 +5,14 @@ class RouteDescription {
final Icon icon;
final Icon selectedIcon;
final Widget Function(Widget icon)? badgeBuilder;
final bool enabled;
RouteDescription({
required this.label,
required this.icon,
required this.selectedIcon,
this.badgeBuilder,
this.enabled = true,
});
NavigationDestination toNavigationDestination() {

View File

@@ -1,11 +1,9 @@
import 'package:flutter/material.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
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/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';

View File

@@ -52,16 +52,18 @@ class InboxCubit extends HydratedCubit<InboxState> with DocumentPagingBlocMixin
emit(state.copyWith(labels: labels));
},
);
refreshItemsInInboxCount(false);
loadInbox();
}
void refreshItemsInInboxCount([bool shouldLoadInbox = true]) async {
Future<void> initialize() async {
await refreshItemsInInboxCount(false);
await loadInbox();
}
Future<void> refreshItemsInInboxCount([bool shouldLoadInbox = true]) async {
final stats = await _statsApi.getServerStatistics();
if (stats.documentsInInbox != state.itemsInInboxCount && shouldLoadInbox) {
loadInbox();
await loadInbox();
}
emit(
state.copyWith(

View File

@@ -3,6 +3,8 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:intl/intl.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
import 'package:paperless_mobile/core/exception/server_message_exception.dart';
import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_cancel_button.dart';
import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_confirm_button.dart';
import 'package:paperless_mobile/core/widgets/hint_card.dart';
@@ -10,7 +12,6 @@ import 'package:paperless_mobile/extensions/dart_extensions.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/app_drawer/view/app_drawer.dart';
import 'package:paperless_mobile/features/document_search/view/sliver_search_bar.dart';
import 'package:paperless_mobile/features/documents/view/widgets/placeholder/documents_list_loading_widget.dart';
import 'package:paperless_mobile/features/inbox/cubit/inbox_cubit.dart';
import 'package:paperless_mobile/features/inbox/view/widgets/inbox_empty_widget.dart';
import 'package:paperless_mobile/features/inbox/view/widgets/inbox_item.dart';
@@ -26,10 +27,8 @@ class InboxPage extends StatefulWidget {
State<InboxPage> createState() => _InboxPageState();
}
class _InboxPageState extends State<InboxPage>
with DocumentPagingViewMixin<InboxPage, InboxCubit> {
final SliverOverlapAbsorberHandle searchBarHandle =
SliverOverlapAbsorberHandle();
class _InboxPageState extends State<InboxPage> with DocumentPagingViewMixin<InboxPage, InboxCubit> {
final SliverOverlapAbsorberHandle searchBarHandle = SliverOverlapAbsorberHandle();
@override
final pagingScrollController = ScrollController();
@@ -43,11 +42,13 @@ class _InboxPageState extends State<InboxPage>
@override
Widget build(BuildContext context) {
final canEditDocument = LocalUserAccount.current.paperlessUser
.hasPermission(PermissionAction.change, PermissionTarget.document);
return Scaffold(
drawer: const AppDrawer(),
floatingActionButton: BlocBuilder<InboxCubit, InboxState>(
builder: (context, state) {
if (!state.hasLoaded || state.documents.isEmpty) {
if (!state.hasLoaded || state.documents.isEmpty || !canEditDocument) {
return const SizedBox.shrink();
}
return FloatingActionButton.extended(
@@ -80,8 +81,7 @@ class _InboxPageState extends State<InboxPage>
} else if (state.documents.isEmpty) {
return Center(
child: InboxEmptyWidget(
emptyStateRefreshIndicatorKey:
_emptyStateRefreshIndicatorKey,
emptyStateRefreshIndicatorKey: _emptyStateRefreshIndicatorKey,
),
);
} else {
@@ -92,8 +92,7 @@ class _InboxPageState extends State<InboxPage>
SliverToBoxAdapter(
child: HintCard(
show: !state.isHintAcknowledged,
hintText:
S.of(context)!.swipeLeftToMarkADocumentAsSeen,
hintText: S.of(context)!.swipeLeftToMarkADocumentAsSeen,
onHintAcknowledged: () =>
context.read<InboxCubit>().acknowledgeHint(),
),
@@ -108,13 +107,10 @@ class _InboxPageState extends State<InboxPage>
child: Align(
alignment: Alignment.centerLeft,
child: ClipRRect(
borderRadius:
BorderRadius.circular(32.0),
borderRadius: BorderRadius.circular(32.0),
child: Text(
entry.key,
style: Theme.of(context)
.textTheme
.bodySmall,
style: Theme.of(context).textTheme.bodySmall,
textAlign: TextAlign.center,
).padded(),
),
@@ -182,7 +178,7 @@ class _InboxPageState extends State<InboxPage>
],
).padded(),
confirmDismiss: (_) => _onItemDismissed(doc),
key: UniqueKey(),
key: ValueKey(doc.id),
child: InboxItem(document: doc),
);
}
@@ -227,14 +223,15 @@ class _InboxPageState extends State<InboxPage>
return true;
} on PaperlessServerException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);
return false;
} on ServerMessageException catch (error) {
showGenericError(context, error.message);
} catch (error) {
showErrorMessage(
context,
const PaperlessServerException.unknown(),
);
return false;
}
return false;
}
Future<void> _onUndoMarkAsSeen(
@@ -242,9 +239,7 @@ class _InboxPageState extends State<InboxPage>
Iterable<int> removedTags,
) async {
try {
await context
.read<InboxCubit>()
.undoRemoveFromInbox(document, removedTags);
await context.read<InboxCubit>().undoRemoveFromInbox(document, removedTags);
} on PaperlessServerException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);
}

View File

@@ -2,6 +2,8 @@ import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
import 'package:paperless_mobile/core/navigation/push_routes.dart';
import 'package:paperless_mobile/core/workarounds/colored_chip.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/documents/view/widgets/delete_document_confirmation_dialog.dart';
@@ -11,8 +13,6 @@ import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_widget.d
import 'package:paperless_mobile/features/labels/view/widgets/label_text.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
import 'package:paperless_mobile/routes/document_details_route.dart';
class InboxItem extends StatefulWidget {
static const a4AspectRatio = 1 / 1.4142;
@@ -37,14 +37,11 @@ class _InboxItemState extends State<InboxItem> {
builder: (context, state) {
return GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () async {
Navigator.pushNamed(
onTap: () {
pushDocumentDetailsRoute(
context,
DocumentDetailsRoute.routeName,
arguments: DocumentDetailsRouteArguments(
document: widget.document,
isLabelClickable: false,
),
document: widget.document,
isLabelClickable: false,
);
},
child: SizedBox(
@@ -110,8 +107,8 @@ class _InboxItemState extends State<InboxItem> {
],
),
),
SizedBox(
height: 56,
LimitedBox(
maxHeight: 56,
child: _buildActions(context),
),
],
@@ -123,58 +120,46 @@ class _InboxItemState extends State<InboxItem> {
}
Widget _buildActions(BuildContext context) {
final canEdit = LocalUserAccount.current.paperlessUser
.hasPermission(PermissionAction.change, PermissionTarget.document);
final canDelete = LocalUserAccount.current.paperlessUser
.hasPermission(PermissionAction.delete, PermissionTarget.document);
final chipShape = RoundedRectangleBorder(
borderRadius: BorderRadius.circular(32),
);
final actions = [
_buildAssignAsnAction(chipShape, context),
const SizedBox(width: 8.0),
ColoredChipWrapper(
child: ActionChip(
avatar: const Icon(Icons.delete_outline),
shape: chipShape,
label: Text(S.of(context)!.deleteDocument),
onPressed: () async {
final shouldDelete = await showDialog<bool>(
context: context,
builder: (context) => DeleteDocumentConfirmationDialog(document: widget.document),
) ??
false;
if (shouldDelete) {
context.read<InboxCubit>().delete(widget.document);
}
},
if (canEdit) _buildAssignAsnAction(chipShape, context),
if (canEdit && canDelete) const SizedBox(width: 8.0),
if (canDelete)
ColoredChipWrapper(
child: ActionChip(
avatar: const Icon(Icons.delete_outline),
shape: chipShape,
label: Text(S.of(context)!.deleteDocument),
onPressed: () async {
final shouldDelete = await showDialog<bool>(
context: context,
builder: (context) =>
DeleteDocumentConfirmationDialog(document: widget.document),
) ??
false;
if (shouldDelete) {
context.read<InboxCubit>().delete(widget.document);
}
},
),
),
),
];
// return FutureBuilder<FieldSuggestions>(
// future: _fieldSuggestions,
// builder: (context, snapshot) {
// List<Widget>? suggestions;
// if (!snapshot.hasData) {
// suggestions = [
// const SizedBox(width: 4),
// ];
// } else {
// if (snapshot.data!.hasSuggestions) {
// suggestions = [
// const SizedBox(width: 4),
// ..._buildSuggestionChips(
// chipShape,
// snapshot.data!,
// context.watch<InboxCubit>().state,
// ),
// ];
// }
// }
if (actions.isEmpty) {
return const SizedBox.shrink();
}
return Row(
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.bolt_outlined),
const Icon(Icons.auto_awesome),
ConstrainedBox(
constraints: const BoxConstraints(
maxWidth: 50,
@@ -231,6 +216,7 @@ class _InboxItemState extends State<InboxItem> {
setState(() {
_isAsnAssignLoading = true;
});
context.read<InboxCubit>().assignAsn(widget.document).whenComplete(
() => setState(() => _isAsnAssignLoading = false),
);

View File

@@ -3,7 +3,6 @@ import 'package:paperless_mobile/core/widgets/shimmer_placeholder.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/documents/view/widgets/placeholder/tags_placeholder.dart';
import 'package:paperless_mobile/features/documents/view/widgets/placeholder/text_placeholder.dart';
import 'package:paperless_mobile/features/inbox/view/widgets/inbox_item.dart';
class InboxListLoadingWidget extends StatelessWidget {
const InboxListLoadingWidget({super.key});
@@ -48,10 +47,10 @@ class InboxListLoadingWidget extends StatelessWidget {
),
),
const SizedBox(width: 8),
Flexible(
const Flexible(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: const [
children: [
Spacer(),
TextPlaceholder(length: 200, fontSize: 14),
Spacer(),

View File

@@ -1,237 +0,0 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'label_cubit.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods');
/// @nodoc
mixin _$LabelState {
Map<int, Correspondent> get correspondents =>
throw _privateConstructorUsedError;
Map<int, DocumentType> get documentTypes =>
throw _privateConstructorUsedError;
Map<int, Tag> get tags => throw _privateConstructorUsedError;
Map<int, StoragePath> get storagePaths => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$LabelStateCopyWith<LabelState> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $LabelStateCopyWith<$Res> {
factory $LabelStateCopyWith(
LabelState value, $Res Function(LabelState) then) =
_$LabelStateCopyWithImpl<$Res, LabelState>;
@useResult
$Res call(
{Map<int, Correspondent> correspondents,
Map<int, DocumentType> documentTypes,
Map<int, Tag> tags,
Map<int, StoragePath> storagePaths});
}
/// @nodoc
class _$LabelStateCopyWithImpl<$Res, $Val extends LabelState>
implements $LabelStateCopyWith<$Res> {
_$LabelStateCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
@pragma('vm:prefer-inline')
@override
$Res call({
Object? correspondents = null,
Object? documentTypes = null,
Object? tags = null,
Object? storagePaths = null,
}) {
return _then(_value.copyWith(
correspondents: null == correspondents
? _value.correspondents
: correspondents // ignore: cast_nullable_to_non_nullable
as Map<int, Correspondent>,
documentTypes: null == documentTypes
? _value.documentTypes
: documentTypes // ignore: cast_nullable_to_non_nullable
as Map<int, DocumentType>,
tags: null == tags
? _value.tags
: tags // ignore: cast_nullable_to_non_nullable
as Map<int, Tag>,
storagePaths: null == storagePaths
? _value.storagePaths
: storagePaths // ignore: cast_nullable_to_non_nullable
as Map<int, StoragePath>,
) as $Val);
}
}
/// @nodoc
abstract class _$$_LabelStateCopyWith<$Res>
implements $LabelStateCopyWith<$Res> {
factory _$$_LabelStateCopyWith(
_$_LabelState value, $Res Function(_$_LabelState) then) =
__$$_LabelStateCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{Map<int, Correspondent> correspondents,
Map<int, DocumentType> documentTypes,
Map<int, Tag> tags,
Map<int, StoragePath> storagePaths});
}
/// @nodoc
class __$$_LabelStateCopyWithImpl<$Res>
extends _$LabelStateCopyWithImpl<$Res, _$_LabelState>
implements _$$_LabelStateCopyWith<$Res> {
__$$_LabelStateCopyWithImpl(
_$_LabelState _value, $Res Function(_$_LabelState) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? correspondents = null,
Object? documentTypes = null,
Object? tags = null,
Object? storagePaths = null,
}) {
return _then(_$_LabelState(
correspondents: null == correspondents
? _value._correspondents
: correspondents // ignore: cast_nullable_to_non_nullable
as Map<int, Correspondent>,
documentTypes: null == documentTypes
? _value._documentTypes
: documentTypes // ignore: cast_nullable_to_non_nullable
as Map<int, DocumentType>,
tags: null == tags
? _value._tags
: tags // ignore: cast_nullable_to_non_nullable
as Map<int, Tag>,
storagePaths: null == storagePaths
? _value._storagePaths
: storagePaths // ignore: cast_nullable_to_non_nullable
as Map<int, StoragePath>,
));
}
}
/// @nodoc
class _$_LabelState implements _LabelState {
const _$_LabelState(
{final Map<int, Correspondent> correspondents = const {},
final Map<int, DocumentType> documentTypes = const {},
final Map<int, Tag> tags = const {},
final Map<int, StoragePath> storagePaths = const {}})
: _correspondents = correspondents,
_documentTypes = documentTypes,
_tags = tags,
_storagePaths = storagePaths;
final Map<int, Correspondent> _correspondents;
@override
@JsonKey()
Map<int, Correspondent> get correspondents {
if (_correspondents is EqualUnmodifiableMapView) return _correspondents;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(_correspondents);
}
final Map<int, DocumentType> _documentTypes;
@override
@JsonKey()
Map<int, DocumentType> get documentTypes {
if (_documentTypes is EqualUnmodifiableMapView) return _documentTypes;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(_documentTypes);
}
final Map<int, Tag> _tags;
@override
@JsonKey()
Map<int, Tag> get tags {
if (_tags is EqualUnmodifiableMapView) return _tags;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(_tags);
}
final Map<int, StoragePath> _storagePaths;
@override
@JsonKey()
Map<int, StoragePath> get storagePaths {
if (_storagePaths is EqualUnmodifiableMapView) return _storagePaths;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(_storagePaths);
}
@override
String toString() {
return 'LabelState(correspondents: $correspondents, documentTypes: $documentTypes, tags: $tags, storagePaths: $storagePaths)';
}
@override
bool operator ==(dynamic other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$_LabelState &&
const DeepCollectionEquality()
.equals(other._correspondents, _correspondents) &&
const DeepCollectionEquality()
.equals(other._documentTypes, _documentTypes) &&
const DeepCollectionEquality().equals(other._tags, _tags) &&
const DeepCollectionEquality()
.equals(other._storagePaths, _storagePaths));
}
@override
int get hashCode => Object.hash(
runtimeType,
const DeepCollectionEquality().hash(_correspondents),
const DeepCollectionEquality().hash(_documentTypes),
const DeepCollectionEquality().hash(_tags),
const DeepCollectionEquality().hash(_storagePaths));
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$_LabelStateCopyWith<_$_LabelState> get copyWith =>
__$$_LabelStateCopyWithImpl<_$_LabelState>(this, _$identity);
}
abstract class _LabelState implements LabelState {
const factory _LabelState(
{final Map<int, Correspondent> correspondents,
final Map<int, DocumentType> documentTypes,
final Map<int, Tag> tags,
final Map<int, StoragePath> storagePaths}) = _$_LabelState;
@override
Map<int, Correspondent> get correspondents;
@override
Map<int, DocumentType> get documentTypes;
@override
Map<int, Tag> get tags;
@override
Map<int, StoragePath> get storagePaths;
@override
@JsonKey(ignore: true)
_$$_LabelStateCopyWith<_$_LabelState> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@@ -1,4 +1,3 @@
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';

View File

@@ -1,10 +1,9 @@
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';
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
import 'package:paperless_mobile/core/workarounds/colored_chip.dart';
import 'package:paperless_mobile/features/labels/tags/view/widgets/fullscreen_tags_form.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
@@ -72,7 +71,11 @@ class TagsFormField extends StatelessWidget {
onSubmit: closeForm,
initialValue: field.value,
allowOnlySelection: allowOnlySelection,
allowCreation: allowCreation,
allowCreation: allowCreation &&
LocalUserAccount.current.paperlessUser.hasPermission(
PermissionAction.add,
PermissionTarget.tag,
),
allowExclude: allowExclude,
),
onClosed: (data) {

Some files were not shown because too many files have changed in this diff Show More