mirror of
https://github.com/Xevion/paperless-mobile.git
synced 2025-12-07 11:15:49 -06:00
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:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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,
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
35
lib/core/config/hive/hive_extensions.dart
Normal file
35
lib/core/config/hive/hive_extensions.dart
Normal 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);
|
||||
}
|
||||
@@ -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)!;
|
||||
}
|
||||
|
||||
@@ -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)!;
|
||||
}
|
||||
}
|
||||
|
||||
5
lib/core/exception/server_message_exception.dart
Normal file
5
lib/core/exception/server_message_exception.dart
Normal file
@@ -0,0 +1,5 @@
|
||||
class ServerMessageException implements Exception {
|
||||
final String message;
|
||||
|
||||
ServerMessageException(this.message);
|
||||
}
|
||||
12
lib/core/factory/paperless_api_factory.dart
Normal file
12
lib/core/factory/paperless_api_factory.dart
Normal 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});
|
||||
}
|
||||
50
lib/core/factory/paperless_api_factory_impl.dart
Normal file
50
lib/core/factory/paperless_api_factory_impl.dart
Normal 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.");
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
370
lib/core/navigation/push_routes.dart
Normal file
370
lib/core/navigation/push_routes.dart
Normal 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>()),
|
||||
];
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
32
lib/core/repository/persistent_repository.dart
Normal file
32
lib/core/repository/persistent_repository.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
40
lib/core/repository/user_repository.dart
Normal file
40
lib/core/repository/user_repository.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
11
lib/core/repository/user_repository_state.dart
Normal file
11
lib/core/repository/user_repository_state.dart
Normal 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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:paperless_mobile/core/model/github_error_report.model.dart';
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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!,
|
||||
]),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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.',
|
||||
),
|
||||
],
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -41,7 +41,7 @@ class _SelectFileTypeDialogState extends State<SelectFileTypeDialog> {
|
||||
},
|
||||
title: Text(S.of(context)!.archivedPdf),
|
||||
),
|
||||
Divider(),
|
||||
const Divider(),
|
||||
CheckboxListTile(
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
value: _rememberSelection,
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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(),
|
||||
],
|
||||
|
||||
@@ -24,7 +24,7 @@ class DetailsItem extends StatelessWidget {
|
||||
}
|
||||
|
||||
DetailsItem.text(
|
||||
String text, {
|
||||
String text, {super.key,
|
||||
required this.label,
|
||||
required BuildContext context,
|
||||
}) : content = Text(
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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],
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,7 +98,7 @@ class _ScannedImageItemState extends State<ScannedImageItem> {
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: TextButton(
|
||||
onPressed: widget.onDelete,
|
||||
child: Text("Remove"),
|
||||
child: const Text("Remove"),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
139
lib/features/document_search/view/document_search_bar.dart
Normal file
139
lib/features/document_search/view/document_search_bar.dart
Normal 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(),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
],
|
||||
);
|
||||
|
||||
@@ -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(),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
// ),
|
||||
// );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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]!,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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!,
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
7
lib/features/home/view/model/api_version.dart
Normal file
7
lib/features/home/view/model/api_version.dart
Normal file
@@ -0,0 +1,7 @@
|
||||
class ApiVersion {
|
||||
final int version;
|
||||
|
||||
ApiVersion(this.version);
|
||||
|
||||
bool get hasMultiUserSupport => version >= 3;
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user