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

@@ -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,15 +259,17 @@ 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,
@@ -262,8 +281,6 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
);
},
);
},
);
}
BlocBuilder<DocumentDetailsCubit, DocumentDetailsState> _buildBottomAppBar() {
@@ -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,
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(),
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

@@ -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(
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,22 +32,21 @@ 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(
title: Hero(
tag: "search_hero_tag",
child: TextField(
autofocus: true,
style: theme.textTheme.bodyLarge?.apply(
color: theme.colorScheme.onSurface,
),
// 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,
),
hintText: S.of(context)!.searchDocuments,
border: InputBorder.none,
),
@@ -87,6 +64,7 @@ class _DocumentSearchPageState extends State<DocumentSearchPage> {
context.read<DocumentSearchCubit>().search(query);
},
),
),
actions: [
IconButton(
color: theme.colorScheme.onSurfaceVariant,
@@ -97,14 +75,11 @@ class _DocumentSearchPageState extends State<DocumentSearchPage> {
},
).padded(),
],
bottom: PreferredSize(
preferredSize: const Size.fromHeight(1),
child: Divider(
color: theme.colorScheme.outline,
),
),
),
body: BlocBuilder<DocumentSearchCubit, DocumentSearchState>(
body: Column(
children: [
Expanded(
child: BlocBuilder<DocumentSearchCubit, DocumentSearchState>(
builder: (context, state) {
switch (state.view) {
case SearchView.suggestions:
@@ -114,6 +89,9 @@ class _DocumentSearchPageState extends State<DocumentSearchPage> {
}
},
),
),
],
),
);
}
@@ -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,
),
);
},
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 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(
return AnimatedSwitcher(
layoutBuilder: SliverAnimatedSwitcher.defaultLayoutBuilder,
transitionBuilder: SliverAnimatedSwitcher.defaultTransitionBuilder,
child: state.selection.isEmpty
? const SliverSearchBar(floating: true)
: DocumentSelectionSliverAppBar(
state: state,
),
duration: const Duration(
milliseconds: 250,
),
);
}
return const SliverSearchBar(floating: true);
},
),
),
@@ -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,
),
);
}

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(
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,
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,6 +181,8 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
),
label: S.of(context)!.documents,
),
if (LocalUserAccount.current.paperlessUser
.hasPermission(PermissionAction.add, PermissionTarget.document))
RouteDescription(
icon: const Icon(Icons.document_scanner_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(
isLabelVisible: state.itemsInInboxCount > 0,
count: state.itemsInInboxCount,
child: icon,
);
}
return 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(
return GlobalSettingsBuilder(
builder: (context, settings) {
final currentLocalUserId = settings.currentLoggedInUser!;
final apiVersion = ApiVersion(paperlessApiVersion);
return MultiProvider(
providers: [
BlocProvider(
create: (context) => TaskStatusCubit(
context.read(),
Provider.value(value: apiVersion),
Provider<CacheManager>(
create: (context) => CacheManager(
Config(
// Isolated cache per user.
localUserId,
fileService: DioFileService(context.read<SessionManager>().client),
),
),
BlocProvider<ServerInformationCubit>(
create: (context) => ServerInformationCubit(
context.read(),
)..updateInformation(),
),
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,
),
),
],
child: HomePage(),
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,
),
);
},
child: SizedBox(
@@ -110,8 +107,8 @@ class _InboxItemState extends State<InboxItem> {
],
),
),
SizedBox(
height: 56,
LimitedBox(
maxHeight: 56,
child: _buildActions(context),
),
],
@@ -123,12 +120,17 @@ 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),
if (canEdit) _buildAssignAsnAction(chipShape, context),
if (canEdit && canDelete) const SizedBox(width: 8.0),
if (canDelete)
ColoredChipWrapper(
child: ActionChip(
avatar: const Icon(Icons.delete_outline),
@@ -137,7 +139,8 @@ class _InboxItemState extends State<InboxItem> {
onPressed: () async {
final shouldDelete = await showDialog<bool>(
context: context,
builder: (context) => DeleteDocumentConfirmationDialog(document: widget.document),
builder: (context) =>
DeleteDocumentConfirmationDialog(document: widget.document),
) ??
false;
if (shouldDelete) {
@@ -147,34 +150,16 @@ class _InboxItemState extends State<InboxItem> {
),
),
];
// 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) {

View File

@@ -2,6 +2,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/bloc/connectivity_cubit.dart';
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
import 'package:paperless_mobile/core/delegate/customizable_sliver_persistent_header_delegate.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/widgets/material/colored_tab_bar.dart';
@@ -16,7 +17,6 @@ import 'package:paperless_mobile/features/edit_label/view/impl/edit_document_typ
import 'package:paperless_mobile/features/edit_label/view/impl/edit_storage_path_page.dart';
import 'package:paperless_mobile/features/edit_label/view/impl/edit_tag_page.dart';
import 'package:paperless_mobile/features/labels/cubit/label_cubit.dart';
import 'package:paperless_mobile/features/labels/cubit/label_cubit_mixin.dart';
import 'package:paperless_mobile/features/labels/view/widgets/label_tab_view.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
@@ -146,8 +146,11 @@ class _LabelsPageState extends State<LabelsPage> with SingleTickerProviderStateM
labels: context.watch<LabelCubit>().state.correspondents,
filterBuilder: (label) => DocumentFilter(
correspondent: IdQueryParameter.fromId(label.id!),
pageSize: label.documentCount ?? 0,
),
canEdit: LocalUserAccount.current.paperlessUser.hasPermission(
PermissionAction.change, PermissionTarget.correspondent),
canAddNew: LocalUserAccount.current.paperlessUser.hasPermission(
PermissionAction.add, PermissionTarget.correspondent),
onEdit: _openEditCorrespondentPage,
emptyStateActionButtonLabel: S.of(context)!.addNewCorrespondent,
emptyStateDescription: S.of(context)!.noCorrespondentsSetUp,
@@ -167,8 +170,11 @@ class _LabelsPageState extends State<LabelsPage> with SingleTickerProviderStateM
labels: context.watch<LabelCubit>().state.documentTypes,
filterBuilder: (label) => DocumentFilter(
documentType: IdQueryParameter.fromId(label.id!),
pageSize: label.documentCount ?? 0,
),
canEdit: LocalUserAccount.current.paperlessUser.hasPermission(
PermissionAction.change, PermissionTarget.documentType),
canAddNew: LocalUserAccount.current.paperlessUser.hasPermission(
PermissionAction.add, PermissionTarget.documentType),
onEdit: _openEditDocumentTypePage,
emptyStateActionButtonLabel: S.of(context)!.addNewDocumentType,
emptyStateDescription: S.of(context)!.noDocumentTypesSetUp,
@@ -188,8 +194,11 @@ class _LabelsPageState extends State<LabelsPage> with SingleTickerProviderStateM
labels: context.watch<LabelCubit>().state.tags,
filterBuilder: (label) => DocumentFilter(
tags: TagsQuery.ids(include: [label.id!]),
pageSize: label.documentCount ?? 0,
),
canEdit: LocalUserAccount.current.paperlessUser
.hasPermission(PermissionAction.change, PermissionTarget.tag),
canAddNew: LocalUserAccount.current.paperlessUser
.hasPermission(PermissionAction.add, PermissionTarget.tag),
onEdit: _openEditTagPage,
leadingBuilder: (t) => CircleAvatar(
backgroundColor: t.color,
@@ -219,8 +228,11 @@ class _LabelsPageState extends State<LabelsPage> with SingleTickerProviderStateM
onEdit: _openEditStoragePathPage,
filterBuilder: (label) => DocumentFilter(
storagePath: IdQueryParameter.fromId(label.id!),
pageSize: label.documentCount ?? 0,
),
canEdit: LocalUserAccount.current.paperlessUser.hasPermission(
PermissionAction.change, PermissionTarget.storagePath),
canAddNew: LocalUserAccount.current.paperlessUser.hasPermission(
PermissionAction.add, PermissionTarget.storagePath),
contentBuilder: (path) => Text(path.path),
emptyStateActionButtonLabel: S.of(context)!.addNewStoragePath,
emptyStateDescription: S.of(context)!.noStoragePathsSetUp,

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