feat: Migrate to go_router

This commit is contained in:
Anton Stubenbord
2023-07-30 23:51:00 +02:00
parent 61336d9527
commit f1398e6d4c
78 changed files with 2206 additions and 1756 deletions
@@ -0,0 +1,20 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
class GoRouterRefreshStream extends ChangeNotifier {
GoRouterRefreshStream(Stream<dynamic> stream) {
notifyListeners();
_subscription = stream.asBroadcastStream().listen(
(dynamic _) => notifyListeners(),
);
}
late final StreamSubscription<dynamic> _subscription;
@override
void dispose() {
_subscription.cancel();
super.dispose();
}
}
@@ -21,7 +21,7 @@ class GlobalSettings with HiveObjectMixin {
bool showOnboarding; bool showOnboarding;
@HiveField(4) @HiveField(4)
String? currentLoggedInUser; String? loggedInUserId;
@HiveField(5) @HiveField(5)
FileDownloadType defaultDownloadType; FileDownloadType defaultDownloadType;
@@ -37,7 +37,7 @@ class GlobalSettings with HiveObjectMixin {
this.preferredThemeMode = ThemeMode.system, this.preferredThemeMode = ThemeMode.system,
this.preferredColorSchemeOption = ColorSchemeOption.classic, this.preferredColorSchemeOption = ColorSchemeOption.classic,
this.showOnboarding = true, this.showOnboarding = true,
this.currentLoggedInUser, this.loggedInUserId,
this.defaultDownloadType = FileDownloadType.alwaysAsk, this.defaultDownloadType = FileDownloadType.alwaysAsk,
this.defaultShareType = FileDownloadType.alwaysAsk, this.defaultShareType = FileDownloadType.alwaysAsk,
this.enforceSinglePagePdfUpload = false, this.enforceSinglePagePdfUpload = false,
@@ -20,16 +20,16 @@ class LocalUserAccount extends HiveObject {
@HiveField(7) @HiveField(7)
UserModel paperlessUser; UserModel paperlessUser;
@HiveField(8, defaultValue: 2)
int apiVersion;
LocalUserAccount({ LocalUserAccount({
required this.id, required this.id,
required this.serverUrl, required this.serverUrl,
required this.settings, required this.settings,
required this.paperlessUser, required this.paperlessUser,
required this.apiVersion,
}); });
static LocalUserAccount get current => bool get hasMultiUserSupport => apiVersion >= 3;
Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount).get(
Hive.box<GlobalSettings>(HiveBoxes.globalSettings)
.getValue()!
.currentLoggedInUser)!;
} }
@@ -43,7 +43,7 @@ class LocalUserAppState extends HiveObject {
final currentLocalUserId = final currentLocalUserId =
Hive.box<GlobalSettings>(HiveBoxes.globalSettings) Hive.box<GlobalSettings>(HiveBoxes.globalSettings)
.getValue()! .getValue()!
.currentLoggedInUser!; .loggedInUserId!;
return Hive.box<LocalUserAppState>(HiveBoxes.localUserAppState) return Hive.box<LocalUserAppState>(HiveBoxes.localUserAppState)
.get(currentLocalUserId)!; .get(currentLocalUserId)!;
} }
+10 -48
View File
@@ -4,11 +4,13 @@ import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:go_router/go_router.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/bloc/connectivity_cubit.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/config/hive/hive_config.dart';
import 'package:paperless_mobile/core/database/tables/global_settings.dart'; import 'package:paperless_mobile/core/database/tables/global_settings.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/database/tables/local_user_app_state.dart';
import 'package:paperless_mobile/core/notifier/document_changed_notifier.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/label_repository.dart';
@@ -29,7 +31,6 @@ import 'package:paperless_mobile/features/saved_view/view/add_saved_view_page.da
import 'package:paperless_mobile/features/saved_view_details/cubit/saved_view_details_cubit.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/features/saved_view_details/view/saved_view_details_page.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
import 'package:paperless_mobile/routes/document_details_route.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
// These are convenience methods for nativating to views without having to pass providers around explicitly. // These are convenience methods for nativating to views without having to pass providers around explicitly.
@@ -38,20 +39,11 @@ import 'package:provider/provider.dart';
Future<void> pushDocumentSearchPage(BuildContext context) { Future<void> pushDocumentSearchPage(BuildContext context) {
final currentUser = Hive.box<GlobalSettings>(HiveBoxes.globalSettings) final currentUser = Hive.box<GlobalSettings>(HiveBoxes.globalSettings)
.getValue()! .getValue()!
.currentLoggedInUser; .loggedInUserId;
final userRepo = context.read<UserRepository>(); final userRepo = context.read<UserRepository>();
return Navigator.of(context).push( return Navigator.of(context).push(
MaterialPageRoute( MaterialPageRoute(
builder: (_) => MultiProvider( builder: (_) => BlocProvider(
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( create: (context) => DocumentSearchCubit(
context.read(), context.read(),
context.read(), context.read(),
@@ -59,38 +51,6 @@ Future<void> pushDocumentSearchPage(BuildContext context) {
.get(currentUser)!, .get(currentUser)!,
), ),
child: const DocumentSearchPage(), 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,
),
), ),
), ),
); );
@@ -106,7 +66,7 @@ Future<void> pushSavedViewDetailsRoute(
builder: (_) => MultiProvider( builder: (_) => MultiProvider(
providers: [ providers: [
Provider.value(value: apiVersion), Provider.value(value: apiVersion),
if (apiVersion.hasMultiUserSupport) if (context.watch<LocalUserAccount>().hasMultiUserSupport)
Provider.value(value: context.read<UserRepository>()), Provider.value(value: context.read<UserRepository>()),
Provider.value(value: context.read<LabelRepository>()), Provider.value(value: context.read<LabelRepository>()),
Provider.value(value: context.read<DocumentChangedNotifier>()), Provider.value(value: context.read<DocumentChangedNotifier>()),
@@ -147,8 +107,10 @@ Future<SavedView?> pushAddSavedViewRoute(BuildContext context,
); );
} }
Future<void> pushLinkedDocumentsView(BuildContext context, Future<void> pushLinkedDocumentsView(
{required DocumentFilter filter}) { BuildContext context, {
required DocumentFilter filter,
}) {
return Navigator.push( return Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
@@ -161,7 +123,7 @@ Future<void> pushLinkedDocumentsView(BuildContext context,
Provider.value(value: context.read<LocalNotificationService>()), Provider.value(value: context.read<LocalNotificationService>()),
Provider.value(value: context.read<CacheManager>()), Provider.value(value: context.read<CacheManager>()),
Provider.value(value: context.read<ConnectivityCubit>()), Provider.value(value: context.read<ConnectivityCubit>()),
if (context.read<ApiVersion>().hasMultiUserSupport) if (context.watch<LocalUserAccount>().hasMultiUserSupport)
Provider.value(value: context.read<UserRepository>()), Provider.value(value: context.read<UserRepository>()),
], ],
builder: (context, _) => BlocProvider( builder: (context, _) => BlocProvider(
+2 -12
View File
@@ -9,6 +9,7 @@ import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/home/view/model/api_version.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/features/settings/view/settings_page.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
import 'package:paperless_mobile/routes/typed/top_level/settings_route.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:url_launcher/url_launcher_string.dart'; import 'package:url_launcher/url_launcher_string.dart';
@@ -91,18 +92,7 @@ class AppDrawer extends StatelessWidget {
title: Text( title: Text(
S.of(context)!.settings, S.of(context)!.settings,
), ),
onTap: () => Navigator.of(context).push( onTap: () => SettingsRoute().push(context),
MaterialPageRoute(
builder: (_) => MultiProvider(
providers: [
Provider.value(
value: context.read<PaperlessServerStatsApi>()),
Provider.value(value: context.read<ApiVersion>()),
],
child: const SettingsPage(),
),
),
),
), ),
], ],
), ),
@@ -45,7 +45,6 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
), ),
), ),
); );
loadSuggestions();
loadMetaData(); loadMetaData();
} }
@@ -54,13 +53,6 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
_notifier.notifyDeleted(document); _notifier.notifyDeleted(document);
} }
Future<void> loadSuggestions() async {
final suggestions = await _api.findSuggestions(state.document);
if (!isClosed) {
emit(state.copyWith(suggestions: suggestions));
}
}
Future<void> loadMetaData() async { Future<void> loadMetaData() async {
final metaData = await _api.getMetaData(state.document); final metaData = await _api.getMetaData(state.document);
if (!isClosed) { if (!isClosed) {
@@ -7,7 +7,6 @@ class DocumentDetailsState with _$DocumentDetailsState {
DocumentMetaData? metaData, DocumentMetaData? metaData,
@Default(false) bool isFullContentLoaded, @Default(false) bool isFullContentLoaded,
String? fullContent, String? fullContent,
FieldSuggestions? suggestions,
@Default({}) Map<int, Correspondent> correspondents, @Default({}) Map<int, Correspondent> correspondents,
@Default({}) Map<int, DocumentType> documentTypes, @Default({}) Map<int, DocumentType> documentTypes,
@Default({}) Map<int, Tag> tags, @Default({}) Map<int, Tag> tags,
@@ -14,8 +14,6 @@ import 'package:paperless_mobile/features/document_details/view/widgets/document
import 'package:paperless_mobile/features/document_details/view/widgets/document_overview_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_permissions_widget.dart';
import 'package:paperless_mobile/features/document_details/view/widgets/document_share_button.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/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/delete_document_confirmation_dialog.dart';
import 'package:paperless_mobile/features/documents/view/widgets/document_preview.dart'; import 'package:paperless_mobile/features/documents/view/widgets/document_preview.dart';
@@ -24,6 +22,7 @@ import 'package:paperless_mobile/features/similar_documents/cubit/similar_docume
import 'package:paperless_mobile/features/similar_documents/view/similar_documents_view.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/generated/l10n/app_localizations.dart';
import 'package:paperless_mobile/helpers/message_helpers.dart'; import 'package:paperless_mobile/helpers/message_helpers.dart';
import 'package:paperless_mobile/routes/typed/branches/documents_route.dart';
class DocumentDetailsPage extends StatefulWidget { class DocumentDetailsPage extends StatefulWidget {
final bool isLabelClickable; final bool isLabelClickable;
@@ -46,9 +45,9 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final apiVersion = context.watch<ApiVersion>(); final hasMultiUserSupport =
context.watch<LocalUserAccount>().hasMultiUserSupport;
final tabLength = 4 + (apiVersion.hasMultiUserSupport ? 1 : 0); final tabLength = 4 + (hasMultiUserSupport ? 1 : 0);
return WillPopScope( return WillPopScope(
onWillPop: () async { onWillPop: () async {
Navigator.of(context) Navigator.of(context)
@@ -171,7 +170,7 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
), ),
), ),
), ),
if (apiVersion.hasMultiUserSupport) if (hasMultiUserSupport)
Tab( Tab(
child: Text( child: Text(
"Permissions", "Permissions",
@@ -259,7 +258,7 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
), ),
], ],
), ),
if (apiVersion.hasMultiUserSupport) if (hasMultiUserSupport)
CustomScrollView( CustomScrollView(
controller: _pagingScrollController, controller: _pagingScrollController,
slivers: [ slivers: [
@@ -286,8 +285,10 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
} }
Widget _buildEditButton() { Widget _buildEditButton() {
final currentUser = context.watch<LocalUserAccount>();
bool canEdit = context.watchInternetConnection && bool canEdit = context.watchInternetConnection &&
LocalUserAccount.current.paperlessUser.canEditDocuments; currentUser.paperlessUser.canEditDocuments;
if (!canEdit) { if (!canEdit) {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
@@ -302,7 +303,7 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
verticalOffset: 40, verticalOffset: 40,
child: FloatingActionButton( child: FloatingActionButton(
child: const Icon(Icons.edit), child: const Icon(Icons.edit),
onPressed: () => _onEdit(state.document), onPressed: () => EditDocumentRoute(state.document).push(context),
), ),
); );
}, },
@@ -316,9 +317,9 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
child: BlocBuilder<ConnectivityCubit, ConnectivityState>( child: BlocBuilder<ConnectivityCubit, ConnectivityState>(
builder: (context, connectivityState) { builder: (context, connectivityState) {
final isConnected = connectivityState.isConnected; final isConnected = connectivityState.isConnected;
final currentUser = context.watch<LocalUserAccount>();
final canDelete = isConnected && final canDelete =
LocalUserAccount.current.paperlessUser.canDeleteDocuments; isConnected && currentUser.paperlessUser.canDeleteDocuments;
return Row( return Row(
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
children: [ children: [
@@ -360,47 +361,6 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
); );
} }
Future<void> _onEdit(DocumentModel document) async {
{
final cubit = context.read<DocumentDetailsCubit>();
Navigator.push<bool>(
context,
MaterialPageRoute(
builder: (_) => MultiBlocProvider(
providers: [
BlocProvider.value(
value: DocumentEditCubit(
context.read(),
context.read(),
context.read(),
document: document,
),
),
BlocProvider<DocumentDetailsCubit>.value(
value: cubit,
),
],
child: BlocListener<DocumentEditCubit, DocumentEditState>(
listenWhen: (previous, current) =>
previous.document != current.document,
listener: (context, state) {
cubit.replace(state.document);
},
child: BlocBuilder<DocumentDetailsCubit, DocumentDetailsState>(
builder: (context, state) {
return DocumentEditPage(
suggestions: state.suggestions,
);
},
),
),
),
maintainState: true,
),
);
}
}
void _onOpenFileInSystemViewer() async { void _onOpenFileInSystemViewer() async {
final status = final status =
await context.read<DocumentDetailsCubit>().openDocumentInSystemViewer(); await context.read<DocumentDetailsCubit>().openDocumentInSystemViewer();
@@ -47,7 +47,7 @@ class _ArchiveSerialNumberFieldState extends State<ArchiveSerialNumberField> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final userCanEditDocument = final userCanEditDocument =
LocalUserAccount.current.paperlessUser.canEditDocuments; context.watch<LocalUserAccount>().paperlessUser.canEditDocuments;
return BlocListener<DocumentDetailsCubit, DocumentDetailsState>( return BlocListener<DocumentDetailsCubit, DocumentDetailsState>(
listenWhen: (previous, current) => listenWhen: (previous, current) =>
previous.document.archiveSerialNumber != previous.document.archiveSerialNumber !=
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/database/tables/local_user_account.dart'; import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
@@ -47,7 +48,10 @@ class DocumentOverviewWidget extends StatelessWidget {
label: S.of(context)!.createdAt, label: S.of(context)!.createdAt,
).paddedOnly(bottom: itemSpacing), ).paddedOnly(bottom: itemSpacing),
if (document.documentType != null && if (document.documentType != null &&
LocalUserAccount.current.paperlessUser.canViewDocumentTypes) context
.watch<LocalUserAccount>()
.paperlessUser
.canViewDocumentTypes)
DetailsItem( DetailsItem(
label: S.of(context)!.documentType, label: S.of(context)!.documentType,
content: LabelText<DocumentType>( content: LabelText<DocumentType>(
@@ -56,7 +60,10 @@ class DocumentOverviewWidget extends StatelessWidget {
), ),
).paddedOnly(bottom: itemSpacing), ).paddedOnly(bottom: itemSpacing),
if (document.correspondent != null && if (document.correspondent != null &&
LocalUserAccount.current.paperlessUser.canViewCorrespondents) context
.watch<LocalUserAccount>()
.paperlessUser
.canViewCorrespondents)
DetailsItem( DetailsItem(
label: S.of(context)!.correspondent, label: S.of(context)!.correspondent,
content: LabelText<Correspondent>( content: LabelText<Correspondent>(
@@ -65,7 +72,10 @@ class DocumentOverviewWidget extends StatelessWidget {
), ),
).paddedOnly(bottom: itemSpacing), ).paddedOnly(bottom: itemSpacing),
if (document.storagePath != null && if (document.storagePath != null &&
LocalUserAccount.current.paperlessUser.canViewStoragePaths) context
.watch<LocalUserAccount>()
.paperlessUser
.canViewStoragePaths)
DetailsItem( DetailsItem(
label: S.of(context)!.storagePath, label: S.of(context)!.storagePath,
content: LabelText<StoragePath>( content: LabelText<StoragePath>(
@@ -73,7 +83,7 @@ class DocumentOverviewWidget extends StatelessWidget {
), ),
).paddedOnly(bottom: itemSpacing), ).paddedOnly(bottom: itemSpacing),
if (document.tags.isNotEmpty && if (document.tags.isNotEmpty &&
LocalUserAccount.current.paperlessUser.canViewTags) context.watch<LocalUserAccount>().paperlessUser.canViewTags)
DetailsItem( DetailsItem(
label: S.of(context)!.tags, label: S.of(context)!.tags,
content: Padding( content: Padding(
@@ -57,6 +57,11 @@ class DocumentEditCubit extends Cubit<DocumentEditState> {
} }
} }
Future<void> loadFieldSuggestions() async {
final suggestions = await _docsApi.findSuggestions(state.document);
emit(state.copyWith(suggestions: suggestions));
}
void replace(DocumentModel document) { void replace(DocumentModel document) {
emit(state.copyWith(document: document)); emit(state.copyWith(document: document));
} }
@@ -4,6 +4,7 @@ part of 'document_edit_cubit.dart';
class DocumentEditState with _$DocumentEditState { class DocumentEditState with _$DocumentEditState {
const factory DocumentEditState({ const factory DocumentEditState({
required DocumentModel document, required DocumentModel document,
FieldSuggestions? suggestions,
@Default({}) Map<int, Correspondent> correspondents, @Default({}) Map<int, Correspondent> correspondents,
@Default({}) Map<int, DocumentType> documentTypes, @Default({}) Map<int, DocumentType> documentTypes,
@Default({}) Map<int, StoragePath> storagePaths, @Default({}) Map<int, StoragePath> storagePaths,
@@ -22,10 +22,8 @@ import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
import 'package:paperless_mobile/helpers/message_helpers.dart'; import 'package:paperless_mobile/helpers/message_helpers.dart';
class DocumentEditPage extends StatefulWidget { class DocumentEditPage extends StatefulWidget {
final FieldSuggestions? suggestions;
const DocumentEditPage({ const DocumentEditPage({
Key? key, Key? key,
required this.suggestions,
}) : super(key: key); }) : super(key: key);
@override @override
@@ -44,19 +42,12 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
final GlobalKey<FormBuilderState> _formKey = GlobalKey(); final GlobalKey<FormBuilderState> _formKey = GlobalKey();
bool _isSubmitLoading = false; bool _isSubmitLoading = false;
late final FieldSuggestions? _filteredSuggestions;
@override
void initState() {
super.initState();
_filteredSuggestions = widget.suggestions
?.documentDifference(context.read<DocumentEditCubit>().state.document);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocBuilder<DocumentEditCubit, DocumentEditState>( return BlocBuilder<DocumentEditCubit, DocumentEditState>(
builder: (context, state) { builder: (context, state) {
final filteredSuggestions = state.suggestions?.documentDifference(
context.read<DocumentEditCubit>().state.document);
return DefaultTabController( return DefaultTabController(
length: 2, length: 2,
child: Scaffold( child: Scaffold(
@@ -94,8 +85,10 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
ListView( ListView(
children: [ children: [
_buildTitleFormField(state.document.title).padded(), _buildTitleFormField(state.document.title).padded(),
_buildCreatedAtFormField(state.document.created) _buildCreatedAtFormField(
.padded(), state.document.created,
filteredSuggestions,
).padded(),
// Correspondent form field // Correspondent form field
Column( Column(
children: [ children: [
@@ -123,15 +116,17 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
name: fkCorrespondent, name: fkCorrespondent,
prefixIcon: const Icon(Icons.person_outlined), prefixIcon: const Icon(Icons.person_outlined),
allowSelectUnassigned: true, allowSelectUnassigned: true,
canCreateNewLabel: LocalUserAccount.current canCreateNewLabel: context
.paperlessUser.canCreateCorrespondents, .watch<LocalUserAccount>()
.paperlessUser
.canCreateCorrespondents,
), ),
if (_filteredSuggestions if (filteredSuggestions
?.hasSuggestedCorrespondents ?? ?.hasSuggestedCorrespondents ??
false) false)
_buildSuggestionsSkeleton<int>( _buildSuggestionsSkeleton<int>(
suggestions: suggestions:
_filteredSuggestions!.correspondents, filteredSuggestions!.correspondents,
itemBuilder: (context, itemData) => itemBuilder: (context, itemData) =>
ActionChip( ActionChip(
label: Text( label: Text(
@@ -160,8 +155,10 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
initialName: currentInput, initialName: currentInput,
), ),
), ),
canCreateNewLabel: LocalUserAccount.current canCreateNewLabel: context
.paperlessUser.canCreateDocumentTypes, .watch<LocalUserAccount>()
.paperlessUser
.canCreateDocumentTypes,
addLabelText: S.of(context)!.addDocumentType, addLabelText: S.of(context)!.addDocumentType,
labelText: S.of(context)!.documentType, labelText: S.of(context)!.documentType,
initialValue: initialValue:
@@ -175,12 +172,12 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
const Icon(Icons.description_outlined), const Icon(Icons.description_outlined),
allowSelectUnassigned: true, allowSelectUnassigned: true,
), ),
if (_filteredSuggestions if (filteredSuggestions
?.hasSuggestedDocumentTypes ?? ?.hasSuggestedDocumentTypes ??
false) false)
_buildSuggestionsSkeleton<int>( _buildSuggestionsSkeleton<int>(
suggestions: suggestions:
_filteredSuggestions!.documentTypes, filteredSuggestions!.documentTypes,
itemBuilder: (context, itemData) => itemBuilder: (context, itemData) =>
ActionChip( ActionChip(
label: Text( label: Text(
@@ -204,10 +201,12 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
RepositoryProvider.value( RepositoryProvider.value(
value: context.read<LabelRepository>(), value: context.read<LabelRepository>(),
child: AddStoragePathPage( child: AddStoragePathPage(
initalName: initialValue), initialName: initialValue),
), ),
canCreateNewLabel: LocalUserAccount.current canCreateNewLabel: context
.paperlessUser.canCreateStoragePaths, .watch<LocalUserAccount>()
.paperlessUser
.canCreateStoragePaths,
addLabelText: S.of(context)!.addStoragePath, addLabelText: S.of(context)!.addStoragePath,
labelText: S.of(context)!.storagePath, labelText: S.of(context)!.storagePath,
options: state.storagePaths, options: state.storagePaths,
@@ -232,14 +231,14 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
include: state.document.tags.toList(), include: state.document.tags.toList(),
), ),
).padded(), ).padded(),
if (_filteredSuggestions?.tags if (filteredSuggestions?.tags
.toSet() .toSet()
.difference(state.document.tags.toSet()) .difference(state.document.tags.toSet())
.isNotEmpty ?? .isNotEmpty ??
false) false)
_buildSuggestionsSkeleton<int>( _buildSuggestionsSkeleton<int>(
suggestions: suggestions:
(_filteredSuggestions?.tags.toSet() ?? {}), (filteredSuggestions?.tags.toSet() ?? {}),
itemBuilder: (context, itemData) { itemBuilder: (context, itemData) {
final tag = state.tags[itemData]!; final tag = state.tags[itemData]!;
return ActionChip( return ActionChip(
@@ -343,7 +342,8 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
); );
} }
Widget _buildCreatedAtFormField(DateTime? initialCreatedAtDate) { Widget _buildCreatedAtFormField(
DateTime? initialCreatedAtDate, FieldSuggestions? filteredSuggestions) {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@@ -358,9 +358,9 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
format: DateFormat.yMMMMd(), format: DateFormat.yMMMMd(),
initialEntryMode: DatePickerEntryMode.calendar, initialEntryMode: DatePickerEntryMode.calendar,
), ),
if (_filteredSuggestions?.hasSuggestedDates ?? false) if (filteredSuggestions?.hasSuggestedDates ?? false)
_buildSuggestionsSkeleton<DateTime>( _buildSuggestionsSkeleton<DateTime>(
suggestions: _filteredSuggestions!.dates, suggestions: filteredSuggestions!.dates,
itemBuilder: (context, itemData) => ActionChip( itemBuilder: (context, itemData) => ActionChip(
label: Text(DateFormat.yMMMd().format(itemData)), label: Text(DateFormat.yMMMd().format(itemData)),
onPressed: () => _formKey.currentState?.fields[fkCreatedDate] onPressed: () => _formKey.currentState?.fields[fkCreatedDate]
@@ -91,7 +91,7 @@ class _DocumentSearchBarState extends State<DocumentSearchBar> {
Provider.value(value: context.read<PaperlessDocumentsApi>()), Provider.value(value: context.read<PaperlessDocumentsApi>()),
Provider.value(value: context.read<CacheManager>()), Provider.value(value: context.read<CacheManager>()),
Provider.value(value: context.read<ApiVersion>()), Provider.value(value: context.read<ApiVersion>()),
if (context.read<ApiVersion>().hasMultiUserSupport) if (context.watch<LocalUserAccount>().hasMultiUserSupport)
Provider.value(value: context.read<UserRepository>()), Provider.value(value: context.read<UserRepository>()),
], ],
child: Provider( child: Provider(
@@ -99,7 +99,7 @@ class _DocumentSearchBarState extends State<DocumentSearchBar> {
context.read(), context.read(),
context.read(), context.read(),
Hive.box<LocalUserAppState>(HiveBoxes.localUserAppState) Hive.box<LocalUserAppState>(HiveBoxes.localUserAppState)
.get(LocalUserAccount.current.id)!, .get(context.watch<LocalUserAccount>().id)!,
), ),
builder: (_, __) => const DocumentSearchPage(), builder: (_, __) => const DocumentSearchPage(),
), ),
@@ -112,19 +112,7 @@ class _DocumentSearchBarState extends State<DocumentSearchBar> {
IconButton _buildUserAvatar(BuildContext context) { IconButton _buildUserAvatar(BuildContext context) {
return IconButton( return IconButton(
padding: const EdgeInsets.all(6), padding: const EdgeInsets.all(6),
icon: GlobalSettingsBuilder( icon: UserAvatar(account: context.watch<LocalUserAccount>()),
builder: (context, settings) {
return ValueListenableBuilder(
valueListenable:
Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount)
.listenable(),
builder: (context, box, _) {
final account = box.get(settings.currentLoggedInUser!)!;
return UserAvatar(account: account);
},
);
},
),
onPressed: () { onPressed: () {
final apiVersion = context.read<ApiVersion>(); final apiVersion = context.read<ApiVersion>();
showDialog( showDialog(
@@ -4,6 +4,7 @@ import 'dart:math' as math;
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'package:paperless_mobile/core/navigation/push_routes.dart'; import 'package:paperless_mobile/core/navigation/push_routes.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.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/cubit/document_search_cubit.dart';
@@ -11,6 +12,7 @@ import 'package:paperless_mobile/features/document_search/view/remove_history_en
import 'package:paperless_mobile/features/documents/view/widgets/adaptive_documents_view.dart'; import 'package:paperless_mobile/features/documents/view/widgets/adaptive_documents_view.dart';
import 'package:paperless_mobile/features/documents/view/widgets/selection/view_type_selection_widget.dart'; 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/generated/l10n/app_localizations.dart';
import 'package:paperless_mobile/routes/typed/branches/documents_route.dart';
class DocumentSearchPage extends StatefulWidget { class DocumentSearchPage extends StatefulWidget {
const DocumentSearchPage({super.key}); const DocumentSearchPage({super.key});
@@ -218,11 +220,8 @@ class _DocumentSearchPageState extends State<DocumentSearchPage> {
hasLoaded: state.hasLoaded, hasLoaded: state.hasLoaded,
enableHeroAnimation: false, enableHeroAnimation: false,
onTap: (document) { onTap: (document) {
pushDocumentDetailsRoute( DocumentDetailsRoute($extra: document, isLabelClickable: false)
context, .push(context);
document: document,
isLabelClickable: false,
);
}, },
) )
], ],
@@ -25,7 +25,7 @@ class SliverSearchBar extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (LocalUserAccount.current.paperlessUser.canViewDocuments) { if (context.watch<LocalUserAccount>().paperlessUser.canViewDocuments) {
return SliverAppBar( return SliverAppBar(
toolbarHeight: kToolbarHeight, toolbarHeight: kToolbarHeight,
flexibleSpace: Container( flexibleSpace: Container(
@@ -49,7 +49,7 @@ class SliverSearchBar extends StatelessWidget {
Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount) Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount)
.listenable(), .listenable(),
builder: (context, box, _) { builder: (context, box, _) {
final account = box.get(settings.currentLoggedInUser!)!; final account = box.get(settings.loggedInUserId!)!;
return UserAvatar(account: account); return UserAvatar(account: account);
}, },
); );
@@ -198,8 +198,10 @@ class _DocumentUploadPreparationPageState
), ),
), ),
// Correspondent // Correspondent
if (LocalUserAccount if (context
.current.paperlessUser.canViewCorrespondents) .watch<LocalUserAccount>()
.paperlessUser
.canViewCorrespondents)
LabelFormField<Correspondent>( LabelFormField<Correspondent>(
showAnyAssignedOption: false, showAnyAssignedOption: false,
showNotAssignedOption: false, showNotAssignedOption: false,
@@ -220,11 +222,16 @@ class _DocumentUploadPreparationPageState
options: state.correspondents, options: state.correspondents,
prefixIcon: const Icon(Icons.person_outline), prefixIcon: const Icon(Icons.person_outline),
allowSelectUnassigned: true, allowSelectUnassigned: true,
canCreateNewLabel: LocalUserAccount canCreateNewLabel: context
.current.paperlessUser.canCreateCorrespondents, .watch<LocalUserAccount>()
.paperlessUser
.canCreateCorrespondents,
), ),
// Document type // Document type
if (LocalUserAccount.current.paperlessUser.canViewDocumentTypes) if (context
.watch<LocalUserAccount>()
.paperlessUser
.canViewDocumentTypes)
LabelFormField<DocumentType>( LabelFormField<DocumentType>(
showAnyAssignedOption: false, showAnyAssignedOption: false,
showNotAssignedOption: false, showNotAssignedOption: false,
@@ -245,10 +252,12 @@ class _DocumentUploadPreparationPageState
options: state.documentTypes, options: state.documentTypes,
prefixIcon: const Icon(Icons.description_outlined), prefixIcon: const Icon(Icons.description_outlined),
allowSelectUnassigned: true, allowSelectUnassigned: true,
canCreateNewLabel: LocalUserAccount canCreateNewLabel: context
.current.paperlessUser.canCreateDocumentTypes, .watch<LocalUserAccount>()
.paperlessUser
.canCreateDocumentTypes,
), ),
if (LocalUserAccount.current.paperlessUser.canViewTags) if (context.watch<LocalUserAccount>().paperlessUser.canViewTags)
TagsFormField( TagsFormField(
name: DocumentModel.tagsKey, name: DocumentModel.tagsKey,
allowCreation: true, allowCreation: true,
@@ -296,7 +305,7 @@ class _DocumentUploadPreparationPageState
), ),
userId: Hive.box<GlobalSettings>(HiveBoxes.globalSettings) userId: Hive.box<GlobalSettings>(HiveBoxes.globalSettings)
.getValue()! .getValue()!
.currentLoggedInUser!, .loggedInUserId!,
title: title, title: title,
documentType: docType, documentType: docType,
correspondent: correspondent, correspondent: correspondent,
@@ -23,6 +23,7 @@ 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/features/tasks/cubit/task_status_cubit.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
import 'package:paperless_mobile/helpers/message_helpers.dart'; import 'package:paperless_mobile/helpers/message_helpers.dart';
import 'package:paperless_mobile/routes/typed/branches/documents_route.dart';
class DocumentFilterIntent { class DocumentFilterIntent {
final DocumentFilter? filter; final DocumentFilter? filter;
@@ -55,7 +56,7 @@ class _DocumentsPageState extends State<DocumentsPage>
void initState() { void initState() {
super.initState(); super.initState();
final showSavedViews = final showSavedViews =
LocalUserAccount.current.paperlessUser.canViewSavedViews; context.read<LocalUserAccount>().paperlessUser.canViewSavedViews;
_tabController = TabController( _tabController = TabController(
length: showSavedViews ? 2 : 1, length: showSavedViews ? 2 : 1,
vsync: this, vsync: this,
@@ -116,7 +117,7 @@ class _DocumentsPageState extends State<DocumentsPage>
return SafeArea( return SafeArea(
top: true, top: true,
child: Scaffold( child: Scaffold(
drawer: const AppDrawer(), drawer: AppDrawer(),
floatingActionButton: BlocBuilder<DocumentsCubit, DocumentsState>( floatingActionButton: BlocBuilder<DocumentsCubit, DocumentsState>(
builder: (context, state) { builder: (context, state) {
final appliedFiltersCount = state.filter.appliedFiltersCount; final appliedFiltersCount = state.filter.appliedFiltersCount;
@@ -232,7 +233,9 @@ class _DocumentsPageState extends State<DocumentsPage>
controller: _tabController, controller: _tabController,
tabs: [ tabs: [
Tab(text: S.of(context)!.documents), Tab(text: S.of(context)!.documents),
if (LocalUserAccount.current.paperlessUser if (context
.watch<LocalUserAccount>()
.paperlessUser
.canViewSavedViews) .canViewSavedViews)
Tab(text: S.of(context)!.views), Tab(text: S.of(context)!.views),
], ],
@@ -276,8 +279,10 @@ class _DocumentsPageState extends State<DocumentsPage>
); );
}, },
), ),
if (LocalUserAccount if (context
.current.paperlessUser.canViewSavedViews) .watch<LocalUserAccount>()
.paperlessUser
.canViewSavedViews)
Builder( Builder(
builder: (context) { builder: (context) {
return _buildSavedViewsTab( return _buildSavedViewsTab(
@@ -378,7 +383,9 @@ class _DocumentsPageState extends State<DocumentsPage>
final allowToggleFilter = state.selection.isEmpty; final allowToggleFilter = state.selection.isEmpty;
return SliverAdaptiveDocumentsView( return SliverAdaptiveDocumentsView(
viewType: state.viewType, viewType: state.viewType,
onTap: _openDetails, onTap: (document) {
DocumentDetailsRoute($extra: document).push(context);
},
onSelected: onSelected:
context.read<DocumentsCubit>().toggleDocumentSelection, context.read<DocumentsCubit>().toggleDocumentSelection,
hasInternetConnection: connectivityState.isConnected, hasInternetConnection: connectivityState.isConnected,
@@ -488,13 +495,6 @@ class _DocumentsPageState extends State<DocumentsPage>
} }
} }
void _openDetails(DocumentModel document) {
pushDocumentDetailsRoute(
context,
document: document,
);
}
void _addTagToFilter(int tagId) { void _addTagToFilter(int tagId) {
final cubit = context.read<DocumentsCubit>(); final cubit = context.read<DocumentsCubit>();
try { try {
@@ -2,7 +2,12 @@ import 'dart:math';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_html/flutter_html.dart'; import 'package:flutter_html/flutter_html.dart';
import 'package:hive_flutter/adapters.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.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/core/database/tables/local_user_account.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart'; import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.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/document_preview.dart';
@@ -32,6 +37,12 @@ class DocumentDetailedItem extends DocumentItem {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final currentUserId = Hive.box<GlobalSettings>(HiveBoxes.globalSettings)
.getValue()!
.loggedInUserId;
final paperlessUser = Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount)
.get(currentUserId)!
.paperlessUser;
final size = MediaQuery.of(context).size; final size = MediaQuery.of(context).size;
final insets = MediaQuery.of(context).viewInsets; final insets = MediaQuery.of(context).viewInsets;
final padding = MediaQuery.of(context).viewPadding; final padding = MediaQuery.of(context).viewPadding;
@@ -104,6 +115,7 @@ class DocumentDetailedItem extends DocumentItem {
maxLines: 2, maxLines: 2,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
).paddedLTRB(8, 0, 8, 4), ).paddedLTRB(8, 0, 8, 4),
if (paperlessUser.canViewCorrespondents)
Row( Row(
children: [ children: [
const Icon( const Icon(
@@ -122,6 +134,7 @@ class DocumentDetailedItem extends DocumentItem {
), ),
], ],
).paddedLTRB(8, 0, 8, 4), ).paddedLTRB(8, 0, 8, 4),
if (paperlessUser.canViewDocumentTypes)
Row( Row(
children: [ children: [
const Icon( const Icon(
@@ -140,6 +153,7 @@ class DocumentDetailedItem extends DocumentItem {
), ),
], ],
).paddedLTRB(8, 0, 8, 4), ).paddedLTRB(8, 0, 8, 4),
if (paperlessUser.canViewTags)
TagsWidget( TagsWidget(
tags: document.tags tags: document.tags
.map((e) => context.watch<LabelRepository>().state.tags[e]!) .map((e) => context.watch<LabelRepository>().state.tags[e]!)
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/database/tables/local_user_account.dart'; import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
@@ -160,8 +161,10 @@ class _DocumentFilterFormState extends State<DocumentFilterForm> {
initialValue: widget.initialFilter.documentType, initialValue: widget.initialFilter.documentType,
prefixIcon: const Icon(Icons.description_outlined), prefixIcon: const Icon(Icons.description_outlined),
allowSelectUnassigned: false, allowSelectUnassigned: false,
canCreateNewLabel: canCreateNewLabel: context
LocalUserAccount.current.paperlessUser.canCreateDocumentTypes, .watch<LocalUserAccount>()
.paperlessUser
.canCreateDocumentTypes,
); );
} }
@@ -173,8 +176,10 @@ class _DocumentFilterFormState extends State<DocumentFilterForm> {
initialValue: widget.initialFilter.correspondent, initialValue: widget.initialFilter.correspondent,
prefixIcon: const Icon(Icons.person_outline), prefixIcon: const Icon(Icons.person_outline),
allowSelectUnassigned: false, allowSelectUnassigned: false,
canCreateNewLabel: canCreateNewLabel: context
LocalUserAccount.current.paperlessUser.canCreateCorrespondents, .watch<LocalUserAccount>()
.paperlessUser
.canCreateCorrespondents,
); );
} }
@@ -187,7 +192,7 @@ class _DocumentFilterFormState extends State<DocumentFilterForm> {
prefixIcon: const Icon(Icons.folder_outlined), prefixIcon: const Icon(Icons.folder_outlined),
allowSelectUnassigned: false, allowSelectUnassigned: false,
canCreateNewLabel: canCreateNewLabel:
LocalUserAccount.current.paperlessUser.canCreateStoragePaths, context.watch<LocalUserAccount>().paperlessUser.canCreateStoragePaths,
); );
} }
@@ -7,8 +7,8 @@ import 'package:paperless_mobile/features/labels/storage_path/view/widgets/stora
import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
class AddStoragePathPage extends StatelessWidget { class AddStoragePathPage extends StatelessWidget {
final String? initalName; final String? initialName;
const AddStoragePathPage({Key? key, this.initalName}) : super(key: key); const AddStoragePathPage({Key? key, this.initialName}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -19,7 +19,7 @@ class AddStoragePathPage extends StatelessWidget {
child: AddLabelPage<StoragePath>( child: AddLabelPage<StoragePath>(
pageTitle: Text(S.of(context)!.addStoragePath), pageTitle: Text(S.of(context)!.addStoragePath),
fromJsonT: StoragePath.fromJson, fromJsonT: StoragePath.fromJson,
initialName: initalName, initialName: initialName,
onSubmit: (context, label) => onSubmit: (context, label) =>
context.read<EditLabelCubit>().addStoragePath(label), context.read<EditLabelCubit>().addStoragePath(label),
additionalFields: const [ additionalFields: const [
@@ -10,8 +10,8 @@ import 'package:paperless_mobile/features/edit_label/view/add_label_page.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
class AddTagPage extends StatelessWidget { class AddTagPage extends StatelessWidget {
final String? initialValue; final String? initialName;
const AddTagPage({Key? key, this.initialValue}) : super(key: key); const AddTagPage({Key? key, this.initialName}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -22,7 +22,7 @@ class AddTagPage extends StatelessWidget {
child: AddLabelPage<Tag>( child: AddLabelPage<Tag>(
pageTitle: Text(S.of(context)!.addTag), pageTitle: Text(S.of(context)!.addTag),
fromJsonT: Tag.fromJson, fromJsonT: Tag.fromJson,
initialName: initialValue, initialName: initialName,
onSubmit: (context, label) => onSubmit: (context, label) =>
context.read<EditLabelCubit>().addTag(label), context.read<EditLabelCubit>().addTag(label),
additionalFields: [ additionalFields: [
@@ -24,8 +24,10 @@ class EditCorrespondentPage extends StatelessWidget {
context.read<EditLabelCubit>().replaceCorrespondent(label), context.read<EditLabelCubit>().replaceCorrespondent(label),
onDelete: (context, label) => onDelete: (context, label) =>
context.read<EditLabelCubit>().removeCorrespondent(label), context.read<EditLabelCubit>().removeCorrespondent(label),
canDelete: canDelete: context
LocalUserAccount.current.paperlessUser.canDeleteCorrespondents, .watch<LocalUserAccount>()
.paperlessUser
.canDeleteCorrespondents,
); );
}), }),
); );
@@ -22,8 +22,10 @@ class EditDocumentTypePage extends StatelessWidget {
context.read<EditLabelCubit>().replaceDocumentType(label), context.read<EditLabelCubit>().replaceDocumentType(label),
onDelete: (context, label) => onDelete: (context, label) =>
context.read<EditLabelCubit>().removeDocumentType(label), context.read<EditLabelCubit>().removeDocumentType(label),
canDelete: canDelete: context
LocalUserAccount.current.paperlessUser.canDeleteDocumentTypes, .watch<LocalUserAccount>()
.paperlessUser
.canDeleteDocumentTypes,
), ),
); );
} }
@@ -23,7 +23,10 @@ class EditStoragePathPage extends StatelessWidget {
context.read<EditLabelCubit>().replaceStoragePath(label), context.read<EditLabelCubit>().replaceStoragePath(label),
onDelete: (context, label) => onDelete: (context, label) =>
context.read<EditLabelCubit>().removeStoragePath(label), context.read<EditLabelCubit>().removeStoragePath(label),
canDelete: LocalUserAccount.current.paperlessUser.canDeleteStoragePaths, canDelete: context
.watch<LocalUserAccount>()
.paperlessUser
.canDeleteStoragePaths,
additionalFields: [ additionalFields: [
StoragePathAutofillFormBuilderField( StoragePathAutofillFormBuilderField(
name: StoragePath.pathKey, name: StoragePath.pathKey,
@@ -26,7 +26,8 @@ class EditTagPage extends StatelessWidget {
context.read<EditLabelCubit>().replaceTag(label), context.read<EditLabelCubit>().replaceTag(label),
onDelete: (context, label) => onDelete: (context, label) =>
context.read<EditLabelCubit>().removeTag(label), context.read<EditLabelCubit>().removeTag(label),
canDelete: LocalUserAccount.current.paperlessUser.canDeleteTags, canDelete:
context.watch<LocalUserAccount>().paperlessUser.canDeleteTags,
additionalFields: [ additionalFields: [
FormBuilderColorPickerField( FormBuilderColorPickerField(
initialValue: tag.color, initialValue: tag.color,
+2 -1
View File
@@ -3,6 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
import 'package:paperless_mobile/core/translation/matching_algorithm_localization_mapper.dart'; import 'package:paperless_mobile/core/translation/matching_algorithm_localization_mapper.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.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/home/view/model/api_version.dart';
@@ -68,7 +69,7 @@ class _LabelFormState<T extends Label> extends State<LabelForm<T>> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
List<MatchingAlgorithm> selectableMatchingAlgorithmValues = List<MatchingAlgorithm> selectableMatchingAlgorithmValues =
getSelectableMatchingAlgorithmValues( getSelectableMatchingAlgorithmValues(
context.watch<ApiVersion>().hasMultiUserSupport, context.watch<LocalUserAccount>().hasMultiUserSupport,
); );
return Scaffold( return Scaffold(
resizeToAvoidBottomInset: false, resizeToAvoidBottomInset: false,
-330
View File
@@ -1,330 +0,0 @@
import 'dart:async';
import 'dart:developer';
import 'dart:io';
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/config/hive/hive_config.dart';
import 'package:paperless_mobile/core/database/tables/global_settings.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/view/scanner_page.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/view/pages/labels_page.dart';
import 'package:paperless_mobile/features/notifications/services/local_notification_service.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: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 {
final int paperlessApiVersion;
const HomePage({Key? key, required this.paperlessApiVersion})
: super(key: key);
@override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
int _currentIndex = 0;
Timer? _inboxTimer;
late final StreamSubscription _shareMediaSubscription;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
final currentUser = Hive.box<GlobalSettings>(HiveBoxes.globalSettings)
.getValue()!
.currentLoggedInUser!;
// For sharing files coming from outside the app while the app is still opened
_shareMediaSubscription = ReceiveSharingIntent.getMediaStream().listen(
(files) =>
ShareIntentQueue.instance.addAll(files, userId: currentUser));
// For sharing files coming from outside the app while the app is closed
ReceiveSharingIntent.getInitialMedia().then((files) =>
ShareIntentQueue.instance.addAll(files, userId: currentUser));
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
_listenForReceivedFiles();
});
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
}
void _listenToInboxChanges() {
if (LocalUserAccount.current.paperlessUser.canViewTags) {
_inboxTimer = Timer.periodic(const Duration(seconds: 60), (timer) {
if (!mounted) {
timer.cancel();
} else {
context.read<InboxCubit>().refreshItemsInInboxCount();
}
});
}
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
switch (state) {
case AppLifecycleState.resumed:
log('App is now in foreground');
context.read<ConnectivityCubit>().reload();
log("Reloaded device connectivity state");
if (!(_inboxTimer?.isActive ?? true)) {
_listenToInboxChanges();
}
break;
case AppLifecycleState.inactive:
case AppLifecycleState.paused:
case AppLifecycleState.detached:
default:
log('App is now in background');
_inboxTimer?.cancel();
break;
}
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
_inboxTimer?.cancel();
_shareMediaSubscription.cancel();
super.dispose();
}
void _listenForReceivedFiles() async {
final currentUser = Hive.box<GlobalSettings>(HiveBoxes.globalSettings)
.getValue()!
.currentLoggedInUser!;
if (ShareIntentQueue.instance.userHasUnhandlesFiles(currentUser)) {
await _handleReceivedFile(ShareIntentQueue.instance.pop(currentUser)!);
}
ShareIntentQueue.instance.addListener(() async {
final queue = ShareIntentQueue.instance;
while (queue.userHasUnhandlesFiles(currentUser)) {
final file = queue.pop(currentUser)!;
await _handleReceivedFile(file);
}
});
}
bool _isFileTypeSupported(SharedMediaFile file) {
return supportedFileExtensions.contains(
file.path.split('.').last.toLowerCase(),
);
}
Future<void> _handleReceivedFile(final SharedMediaFile file) async {
SharedMediaFile mediaFile;
if (Platform.isIOS) {
// Workaround for file not found on iOS: https://stackoverflow.com/a/72813212
mediaFile = SharedMediaFile(
file.path.replaceAll('file://', ''),
file.thumbnail,
file.duration,
file.type,
);
} else {
mediaFile = file;
}
debugPrint("Consuming media file: ${mediaFile.path}");
if (!_isFileTypeSupported(mediaFile)) {
Fluttertoast.showToast(
msg: translateError(context, ErrorCode.unsupportedFileFormat),
);
if (Platform.isAndroid) {
// As stated in the docs, SystemNavigator.pop() is ignored on IOS to comply with HCI guidelines.
await SystemNavigator.pop();
}
return;
}
if (!LocalUserAccount.current.paperlessUser.canCreateDocuments) {
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 = await File(mediaFile.path).readAsBytes();
final result = await pushDocumentUploadPreparationPage(
context,
bytes: bytes,
filename: fileDescription.filename,
title: fileDescription.filename,
fileExtension: fileDescription.extension,
);
if (result?.success ?? false) {
await Fluttertoast.showToast(
msg: S.of(context)!.documentSuccessfullyUploadedProcessing,
);
SystemNavigator.pop();
}
} else {
Fluttertoast.showToast(
msg: S.of(context)!.couldNotAccessReceivedFile,
toastLength: Toast.LENGTH_LONG,
);
}
}
@override
Widget build(BuildContext context) {
final destinations = [
RouteDescription(
icon: const Icon(Icons.description_outlined),
selectedIcon: Icon(
Icons.description,
color: Theme.of(context).colorScheme.primary,
),
label: S.of(context)!.documents,
),
if (LocalUserAccount.current.paperlessUser.canCreateDocuments)
RouteDescription(
icon: const Icon(Icons.document_scanner_outlined),
selectedIcon: Icon(
Icons.document_scanner,
color: Theme.of(context).colorScheme.primary,
),
label: S.of(context)!.scanner,
),
RouteDescription(
icon: const Icon(Icons.sell_outlined),
selectedIcon: Icon(
Icons.sell,
color: Theme.of(context).colorScheme.primary,
),
label: S.of(context)!.labels,
),
if (LocalUserAccount.current.paperlessUser.canViewTags)
RouteDescription(
icon: const Icon(Icons.inbox_outlined),
selectedIcon: Icon(
Icons.inbox,
color: Theme.of(context).colorScheme.primary,
),
label: S.of(context)!.inbox,
badgeBuilder: (icon) => BlocBuilder<InboxCubit, InboxState>(
builder: (context, state) {
return Badge.count(
isLabelVisible: state.itemsInInboxCount > 0,
count: state.itemsInInboxCount,
child: icon,
);
},
),
),
];
final routes = <Widget>[
const DocumentsPage(),
if (LocalUserAccount.current.paperlessUser.canCreateDocuments)
const ScannerPage(),
const LabelsPage(),
if (LocalUserAccount.current.paperlessUser.canViewTags) const InboxPage(),
];
return MultiBlocListener(
listeners: [
BlocListener<ConnectivityCubit, ConnectivityState>(
// If app was started offline, load data once it comes back online.
listenWhen: (previous, current) =>
previous != ConnectivityState.connected &&
current == ConnectivityState.connected,
listener: (context, state) async {
try {
debugPrint(
"[HomePage] BlocListener#listener: "
"Loading saved views and labels...",
);
await Future.wait([
context.read<LabelRepository>().initialize(),
context.read<SavedViewRepository>().initialize(),
]);
debugPrint("[HomePage] BlocListener#listener: "
"Saved views and labels successfully loaded.");
} catch (error, stackTrace) {
debugPrint(
'[HomePage] BlocListener.listener: '
'An error occurred while loading saved views and labels.\n'
'${error.toString()}',
);
debugPrintStack(stackTrace: stackTrace);
}
},
),
BlocListener<TaskStatusCubit, TaskStatusState>(
listener: (context, state) {
if (state.task != null) {
// Handle local notifications on task change (only when app is running for now).
context
.read<LocalNotificationService>()
.notifyTaskChanged(state.task!);
}
},
),
],
child: ResponsiveBuilder(
builder: (context, sizingInformation) {
if (!sizingInformation.isMobile) {
return Scaffold(
body: Row(
children: [
NavigationRail(
labelType: NavigationRailLabelType.all,
destinations: destinations
.map((e) => e.toNavigationRailDestination())
.toList(),
selectedIndex: _currentIndex,
onDestinationSelected: _onNavigationChanged,
),
const VerticalDivider(thickness: 1, width: 1),
Expanded(
child: routes[_currentIndex],
),
],
),
);
}
return Scaffold(
bottomNavigationBar: NavigationBar(
labelBehavior: NavigationDestinationLabelBehavior.alwaysShow,
elevation: 4.0,
selectedIndex: _currentIndex,
onDestinationSelected: _onNavigationChanged,
destinations:
destinations.map((e) => e.toNavigationDestination()).toList(),
),
body: routes[_currentIndex],
);
},
),
);
}
void _onNavigationChanged(index) {
if (_currentIndex != index) {
setState(() => _currentIndex = index);
}
}
}
-204
View File
@@ -1,204 +0,0 @@
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/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 {
/// 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 GlobalSettingsBuilder(
builder: (context, settings) {
final currentLocalUserId = settings.currentLoggedInUser;
if (currentLocalUserId == null) {
// This is the case when the current user logs out of the app.
return SizedBox.shrink();
}
final currentUser =
Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount)
.get(currentLocalUserId)!;
final apiVersion = ApiVersion(paperlessApiVersion);
return MultiProvider(
providers: [
Provider.value(value: apiVersion),
Provider<CacheManager>(
create: (context) => CacheManager(
Config(
// Isolated cache per user.
localUserId,
fileService:
DioFileService(context.read<SessionManager>().client),
),
),
),
ProxyProvider<SessionManager, PaperlessDocumentsApi>(
update: (context, value, previous) =>
paperlessProviderFactory.createDocumentsApi(
value.client,
apiVersion: paperlessApiVersion,
),
),
ProxyProvider<SessionManager, PaperlessLabelsApi>(
update: (context, value, previous) =>
paperlessProviderFactory.createLabelsApi(
value.client,
apiVersion: paperlessApiVersion,
),
),
ProxyProvider<SessionManager, PaperlessSavedViewsApi>(
update: (context, value, previous) =>
paperlessProviderFactory.createSavedViewsApi(
value.client,
apiVersion: paperlessApiVersion,
),
),
ProxyProvider<SessionManager, PaperlessServerStatsApi>(
update: (context, value, previous) =>
paperlessProviderFactory.createServerStatsApi(
value.client,
apiVersion: paperlessApiVersion,
),
),
ProxyProvider<SessionManager, PaperlessTasksApi>(
update: (context, value, previous) =>
paperlessProviderFactory.createTasksApi(
value.client,
apiVersion: paperlessApiVersion,
),
),
if (apiVersion.hasMultiUserSupport)
ProxyProvider<SessionManager, PaperlessUserApiV3>(
update: (context, value, previous) => PaperlessUserApiV3Impl(
value.client,
),
),
],
builder: (context, child) {
return MultiProvider(
providers: [
ProxyProvider<PaperlessLabelsApi, LabelRepository>(
update: (context, value, previous) {
final repo = LabelRepository(value);
if (currentUser.paperlessUser.canViewCorrespondents) {
repo.findAllCorrespondents();
}
if (currentUser.paperlessUser.canViewDocumentTypes) {
repo.findAllDocumentTypes();
}
if (currentUser.paperlessUser.canViewTags) {
repo.findAllTags();
}
if (currentUser.paperlessUser.canViewStoragePaths) {
repo.findAllStoragePaths();
}
return repo;
},
),
ProxyProvider<PaperlessSavedViewsApi, SavedViewRepository>(
update: (context, value, previous) {
final repo = SavedViewRepository(value);
if (currentUser.paperlessUser.canViewSavedViews) {
repo.initialize();
}
return repo;
},
),
],
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)!,
)..initialize(),
),
Provider(
create: (context) =>
DocumentScannerCubit(context.read())),
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),
),
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),
);
},
);
},
);
},
);
}
}
@@ -0,0 +1,208 @@
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/factory/paperless_api_factory.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/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 HomeShellWidget extends StatelessWidget {
/// 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;
final Widget child;
const HomeShellWidget({
super.key,
required this.paperlessApiVersion,
required this.paperlessProviderFactory,
required this.localUserId,
required this.child,
});
@override
Widget build(BuildContext context) {
return GlobalSettingsBuilder(
builder: (context, settings) {
final currentUserId = settings.loggedInUserId;
if (currentUserId == null) {
// This is the case when the current user logs out of the app.
return SizedBox.shrink();
}
final apiVersion = ApiVersion(paperlessApiVersion);
return ValueListenableBuilder(
valueListenable:
Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount)
.listenable(keys: [currentUserId]),
builder: (context, box, _) {
final currentLocalUser = box.get(currentUserId)!;
return MultiProvider(
providers: [
Provider.value(value: currentLocalUser),
Provider.value(value: apiVersion),
Provider(
create: (context) => CacheManager(
Config(
// Isolated cache per user.
localUserId,
fileService:
DioFileService(context.read<SessionManager>().client),
),
),
),
Provider(
create: (context) =>
paperlessProviderFactory.createDocumentsApi(
context.read<SessionManager>().client,
apiVersion: paperlessApiVersion,
),
),
Provider(
create: (context) => paperlessProviderFactory.createLabelsApi(
context.read<SessionManager>().client,
apiVersion: paperlessApiVersion,
),
),
Provider(
create: (context) =>
paperlessProviderFactory.createSavedViewsApi(
context.read<SessionManager>().client,
apiVersion: paperlessApiVersion,
),
),
Provider(
create: (context) =>
paperlessProviderFactory.createServerStatsApi(
context.read<SessionManager>().client,
apiVersion: paperlessApiVersion,
),
),
Provider(
create: (context) => paperlessProviderFactory.createTasksApi(
context.read<SessionManager>().client,
apiVersion: paperlessApiVersion,
),
),
if (currentLocalUser.hasMultiUserSupport)
Provider(
create: (context) => PaperlessUserApiV3Impl(
context.read<SessionManager>().client,
),
),
],
builder: (context, _) {
return MultiProvider(
providers: [
Provider(
create: (context) {
final repo = LabelRepository(context.read());
if (currentLocalUser
.paperlessUser.canViewCorrespondents) {
repo.findAllCorrespondents();
}
if (currentLocalUser
.paperlessUser.canViewDocumentTypes) {
repo.findAllDocumentTypes();
}
if (currentLocalUser.paperlessUser.canViewTags) {
repo.findAllTags();
}
if (currentLocalUser
.paperlessUser.canViewStoragePaths) {
repo.findAllStoragePaths();
}
return repo;
},
),
Provider(
create: (context) {
final repo = SavedViewRepository(context.read());
if (currentLocalUser.paperlessUser.canViewSavedViews) {
repo.initialize();
}
return repo;
},
),
],
builder: (context, _) {
return MultiProvider(
providers: [
Provider(
create: (context) => DocumentsCubit(
context.read(),
context.read(),
context.read(),
Hive.box<LocalUserAppState>(
HiveBoxes.localUserAppState)
.get(currentUserId)!,
)..initialize(),
),
Provider(
create: (context) =>
DocumentScannerCubit(context.read()),
),
if (currentLocalUser.paperlessUser.canViewDocuments &&
currentLocalUser.paperlessUser.canViewTags)
Provider(
create: (context) => InboxCubit(
context.read(),
context.read(),
context.read(),
context.read(),
).initialize(),
),
Provider(
create: (context) => SavedViewCubit(
context.read(),
),
),
Provider(
create: (context) => LabelCubit(
context.read(),
),
),
Provider(
create: (context) => TaskStatusCubit(
context.read(),
),
),
if (currentLocalUser.hasMultiUserSupport)
Provider(
create: (context) => UserRepository(
context.read(),
)..initialize(),
),
],
child: child,
);
},
);
},
);
},
);
},
);
}
}
@@ -1,7 +1,7 @@
class ApiVersion { class ApiVersion {
final int version; final int version;
ApiVersion(this.version); const ApiVersion(this.version);
bool get hasMultiUserSupport => version >= 3;
} }
@@ -0,0 +1,168 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/features/app_drawer/view/app_drawer.dart';
import 'package:paperless_mobile/features/inbox/cubit/inbox_cubit.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
import 'package:paperless_mobile/helpers/message_helpers.dart';
const _landingPage = 0;
const _documentsIndex = 1;
const _scannerIndex = 2;
const _labelsIndex = 3;
const _inboxIndex = 4;
class ScaffoldWithNavigationBar extends StatefulWidget {
final UserModel authenticatedUser;
final StatefulNavigationShell navigationShell;
const ScaffoldWithNavigationBar({
super.key,
required this.authenticatedUser,
required this.navigationShell,
});
@override
State<ScaffoldWithNavigationBar> createState() =>
ScaffoldWithNavigationBarState();
}
class ScaffoldWithNavigationBarState extends State<ScaffoldWithNavigationBar> {
@override
Widget build(BuildContext context) {
final disabledColor = Theme.of(context).disabledColor;
final primaryColor = Theme.of(context).colorScheme.primary;
return Scaffold(
drawer: const AppDrawer(),
bottomNavigationBar: NavigationBar(
selectedIndex: widget.navigationShell.currentIndex,
onDestinationSelected: (index) {
switch (index) {
case _landingPage:
widget.navigationShell.goBranch(index);
break;
case _documentsIndex:
if (widget.authenticatedUser.canViewDocuments) {
widget.navigationShell.goBranch(index);
} else {
showSnackBar(
context, "You do not have permission to access this page.");
}
break;
case _scannerIndex:
if (widget.authenticatedUser.canCreateDocuments) {
widget.navigationShell.goBranch(index);
} else {
showSnackBar(
context, "You do not have permission to access this page.");
}
break;
case _labelsIndex:
if (widget.authenticatedUser.canViewAnyLabel) {
widget.navigationShell.goBranch(index);
} else {
showSnackBar(
context, "You do not have permission to access this page.");
}
break;
case _inboxIndex:
if (widget.authenticatedUser.canViewDocuments &&
widget.authenticatedUser.canViewTags) {
widget.navigationShell.goBranch(index);
} else {
showSnackBar(
context, "You do not have permission to access this page.");
}
break;
default:
break;
}
},
destinations: [
NavigationDestination(
icon: Icon(Icons.home_outlined),
selectedIcon: Icon(
Icons.home,
color: primaryColor,
),
label: "Home", //TODO: INTL
),
NavigationDestination(
icon: Icon(
Icons.description_outlined,
color: !widget.authenticatedUser.canViewDocuments
? disabledColor
: null,
),
selectedIcon: Icon(
Icons.description,
color: primaryColor,
),
label: S.of(context)!.documents,
),
NavigationDestination(
icon: Icon(
Icons.document_scanner_outlined,
color: !widget.authenticatedUser.canCreateDocuments
? disabledColor
: null,
),
selectedIcon: Icon(
Icons.document_scanner,
color: primaryColor,
),
label: S.of(context)!.scanner,
),
NavigationDestination(
icon: Icon(
Icons.sell_outlined,
color: !widget.authenticatedUser.canViewAnyLabel
? disabledColor
: null,
),
selectedIcon: Icon(
Icons.sell,
color: primaryColor,
),
label: S.of(context)!.labels,
),
NavigationDestination(
icon: Builder(builder: (context) {
if (!(widget.authenticatedUser.canViewDocuments &&
widget.authenticatedUser.canViewTags)) {
return Icon(
Icons.close,
color: disabledColor,
);
}
return BlocBuilder<InboxCubit, InboxState>(
builder: (context, state) {
return Badge.count(
isLabelVisible: state.itemsInInboxCount > 0,
count: state.itemsInInboxCount,
child: const Icon(Icons.inbox_outlined),
);
},
);
}),
selectedIcon: BlocBuilder<InboxCubit, InboxState>(
builder: (context, state) {
return Badge.count(
isLabelVisible: state.itemsInInboxCount > 0,
count: state.itemsInInboxCount,
child: Icon(
Icons.inbox,
color: primaryColor,
),
);
},
),
label: S.of(context)!.inbox,
),
],
),
body: widget.navigationShell,
);
}
}
@@ -61,6 +61,7 @@ class InboxCubit extends HydratedCubit<InboxState>
Future<void> initialize() async { Future<void> initialize() async {
await refreshItemsInInboxCount(false); await refreshItemsInInboxCount(false);
await loadInbox(); await loadInbox();
super.initialize();
} }
Future<void> refreshItemsInInboxCount([bool shouldLoadInbox = true]) async { Future<void> refreshItemsInInboxCount([bool shouldLoadInbox = true]) async {
@@ -39,7 +39,7 @@ class _InboxPageState extends State<InboxPage>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final canEditDocument = final canEditDocument =
LocalUserAccount.current.paperlessUser.canEditDocuments; context.watch<LocalUserAccount>().paperlessUser.canEditDocuments;
return Scaffold( return Scaffold(
drawer: const AppDrawer(), drawer: const AppDrawer(),
floatingActionButton: BlocBuilder<InboxCubit, InboxState>( floatingActionButton: BlocBuilder<InboxCubit, InboxState>(
@@ -1,6 +1,7 @@
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/database/tables/local_user_account.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/navigation/push_routes.dart';
@@ -15,6 +16,7 @@ import 'package:paperless_mobile/features/inbox/cubit/inbox_cubit.dart';
import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_widget.dart'; import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_widget.dart';
import 'package:paperless_mobile/features/labels/view/widgets/label_text.dart'; import 'package:paperless_mobile/features/labels/view/widgets/label_text.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
import 'package:paperless_mobile/routes/typed/branches/documents_route.dart';
class InboxItemPlaceholder extends StatelessWidget { class InboxItemPlaceholder extends StatelessWidget {
const InboxItemPlaceholder({super.key}); const InboxItemPlaceholder({super.key});
@@ -150,11 +152,10 @@ class _InboxItemState extends State<InboxItem> {
return GestureDetector( return GestureDetector(
behavior: HitTestBehavior.translucent, behavior: HitTestBehavior.translucent,
onTap: () { onTap: () {
pushDocumentDetailsRoute( DocumentDetailsRoute(
context, $extra: widget.document,
document: widget.document,
isLabelClickable: false, isLabelClickable: false,
); ).push(context);
}, },
child: SizedBox( child: SizedBox(
height: 200, height: 200,
@@ -238,8 +239,9 @@ class _InboxItemState extends State<InboxItem> {
} }
Widget _buildActions(BuildContext context) { Widget _buildActions(BuildContext context) {
final canEdit = LocalUserAccount.current.paperlessUser.canEditDocuments; final currentUser = context.watch<LocalUserAccount>().paperlessUser;
final canDelete = LocalUserAccount.current.paperlessUser.canDeleteDocuments; final canEdit = currentUser.canEditDocuments;
final canDelete = currentUser.canDeleteDocuments;
final chipShape = RoundedRectangleBorder( final chipShape = RoundedRectangleBorder(
borderRadius: BorderRadius.circular(32), borderRadius: BorderRadius.circular(32),
); );
@@ -191,7 +191,7 @@ class _FullscreenTagsFormState extends State<FullscreenTagsForm> {
final createdTag = await Navigator.of(context).push<Tag?>( final createdTag = await Navigator.of(context).push<Tag?>(
MaterialPageRoute( MaterialPageRoute(
builder: (context) => AddTagPage( builder: (context) => AddTagPage(
initialValue: _textEditingController.text, initialName: _textEditingController.text,
), ),
), ),
); );
@@ -1,6 +1,7 @@
import 'package:animations/animations.dart'; import 'package:animations/animations.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/database/tables/local_user_account.dart'; import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
@@ -73,7 +74,7 @@ class TagsFormField extends StatelessWidget {
initialValue: field.value, initialValue: field.value,
allowOnlySelection: allowOnlySelection, allowOnlySelection: allowOnlySelection,
allowCreation: allowCreation && allowCreation: allowCreation &&
LocalUserAccount.current.paperlessUser.canCreateTags, context.watch<LocalUserAccount>().paperlessUser.canCreateTags,
allowExclude: allowExclude, allowExclude: allowExclude,
), ),
onClosed: (data) { onClosed: (data) {
+129 -218
View File
@@ -7,23 +7,13 @@ 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/global_settings.dart';
import 'package:paperless_mobile/core/database/tables/local_user_account.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/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'; import 'package:paperless_mobile/core/widgets/material/colored_tab_bar.dart';
import 'package:paperless_mobile/features/app_drawer/view/app_drawer.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/document_search/view/sliver_search_bar.dart';
import 'package:paperless_mobile/features/edit_label/view/impl/add_correspondent_page.dart';
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/edit_label/view/impl/add_tag_page.dart';
import 'package:paperless_mobile/features/edit_label/view/impl/edit_correspondent_page.dart';
import 'package:paperless_mobile/features/edit_label/view/impl/edit_document_type_page.dart';
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/home/view/model/api_version.dart';
import 'package:paperless_mobile/features/labels/cubit/label_cubit.dart'; import 'package:paperless_mobile/features/labels/cubit/label_cubit.dart';
import 'package:paperless_mobile/features/labels/view/widgets/label_tab_view.dart'; import 'package:paperless_mobile/features/labels/view/widgets/label_tab_view.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
import 'package:provider/provider.dart'; import 'package:paperless_mobile/routes/typed/branches/labels_route.dart';
class LabelsPage extends StatefulWidget { class LabelsPage extends StatefulWidget {
const LabelsPage({Key? key}) : super(key: key); const LabelsPage({Key? key}) : super(key: key);
@@ -52,7 +42,7 @@ class _LabelsPageState extends State<LabelsPage>
@override @override
void initState() { void initState() {
super.initState(); super.initState();
final user = LocalUserAccount.current.paperlessUser; final user = context.read<LocalUserAccount>().paperlessUser;
_tabController = TabController( _tabController = TabController(
length: _calculateTabCount(user), vsync: this) length: _calculateTabCount(user), vsync: this)
..addListener(() => setState(() => _currentIndex = _tabController.index)); ..addListener(() => setState(() => _currentIndex = _tabController.index));
@@ -67,7 +57,7 @@ class _LabelsPageState extends State<LabelsPage>
final currentUserId = final currentUserId =
Hive.box<GlobalSettings>(HiveBoxes.globalSettings) Hive.box<GlobalSettings>(HiveBoxes.globalSettings)
.getValue()! .getValue()!
.currentLoggedInUser; .loggedInUserId;
final user = box.get(currentUserId)!.paperlessUser; final user = box.get(currentUserId)!.paperlessUser;
return BlocBuilder<ConnectivityCubit, ConnectivityState>( return BlocBuilder<ConnectivityCubit, ConnectivityState>(
@@ -77,10 +67,14 @@ class _LabelsPageState extends State<LabelsPage>
drawer: const AppDrawer(), drawer: const AppDrawer(),
floatingActionButton: FloatingActionButton( floatingActionButton: FloatingActionButton(
onPressed: [ onPressed: [
if (user.canViewCorrespondents) _openAddCorrespondentPage, if (user.canViewCorrespondents)
if (user.canViewDocumentTypes) _openAddDocumentTypePage, () => CreateLabelRoute<Correspondent>().push(context),
if (user.canViewTags) _openAddTagPage, if (user.canViewDocumentTypes)
if (user.canViewStoragePaths) _openAddStoragePathPage, () => CreateLabelRoute<DocumentType>().push(context),
if (user.canViewTags)
() => CreateLabelRoute<Tag>().push(context),
if (user.canViewStoragePaths)
() => CreateLabelRoute<StoragePath>().push(context),
][_currentIndex], ][_currentIndex],
child: const Icon(Icons.add), child: const Icon(Icons.add),
), ),
@@ -213,144 +207,13 @@ class _LabelsPageState extends State<LabelsPage>
controller: _tabController, controller: _tabController,
children: [ children: [
if (user.canViewCorrespondents) if (user.canViewCorrespondents)
Builder( _buildCorrespondentsView(state, user),
builder: (context) {
return CustomScrollView(
slivers: [
SliverOverlapInjector(
handle: searchBarHandle),
SliverOverlapInjector(
handle: tabBarHandle),
LabelTabView<Correspondent>(
labels: state.correspondents,
filterBuilder: (label) =>
DocumentFilter(
correspondent:
IdQueryParameter.fromId(
label.id!),
),
canEdit: user.canEditCorrespondents,
canAddNew:
user.canCreateCorrespondents,
onEdit: _openEditCorrespondentPage,
emptyStateActionButtonLabel: S
.of(context)!
.addNewCorrespondent,
emptyStateDescription: S
.of(context)!
.noCorrespondentsSetUp,
onAddNew: _openAddCorrespondentPage,
),
],
);
},
),
if (user.canViewDocumentTypes) if (user.canViewDocumentTypes)
Builder( _buildDocumentTypesView(state, user),
builder: (context) {
return CustomScrollView(
slivers: [
SliverOverlapInjector(
handle: searchBarHandle),
SliverOverlapInjector(
handle: tabBarHandle),
LabelTabView<DocumentType>(
labels: state.documentTypes,
filterBuilder: (label) =>
DocumentFilter(
documentType:
IdQueryParameter.fromId(
label.id!),
),
canEdit: user.canEditDocumentTypes,
canAddNew:
user.canCreateDocumentTypes,
onEdit: _openEditDocumentTypePage,
emptyStateActionButtonLabel: S
.of(context)!
.addNewDocumentType,
emptyStateDescription: S
.of(context)!
.noDocumentTypesSetUp,
onAddNew: _openAddDocumentTypePage,
),
],
);
},
),
if (user.canViewTags) if (user.canViewTags)
Builder( _buildTagsView(state, user),
builder: (context) {
return CustomScrollView(
slivers: [
SliverOverlapInjector(
handle: searchBarHandle),
SliverOverlapInjector(
handle: tabBarHandle),
LabelTabView<Tag>(
labels: state.tags,
filterBuilder: (label) =>
DocumentFilter(
tags: TagsQuery.ids(
include: [label.id!]),
),
canEdit: user.canEditTags,
canAddNew: user.canCreateTags,
onEdit: _openEditTagPage,
leadingBuilder: (t) => CircleAvatar(
backgroundColor: t.color,
child: t.isInboxTag
? Icon(
Icons.inbox,
color: t.textColor,
)
: null,
),
emptyStateActionButtonLabel:
S.of(context)!.addNewTag,
emptyStateDescription:
S.of(context)!.noTagsSetUp,
onAddNew: _openAddTagPage,
),
],
);
},
),
if (user.canViewStoragePaths) if (user.canViewStoragePaths)
Builder( _buildStoragePathView(state, user),
builder: (context) {
return CustomScrollView(
slivers: [
SliverOverlapInjector(
handle: searchBarHandle),
SliverOverlapInjector(
handle: tabBarHandle),
LabelTabView<StoragePath>(
labels: state.storagePaths,
onEdit: _openEditStoragePathPage,
filterBuilder: (label) =>
DocumentFilter(
storagePath:
IdQueryParameter.fromId(
label.id!),
),
canEdit: user.canEditStoragePaths,
canAddNew:
user.canCreateStoragePaths,
contentBuilder: (path) =>
Text(path.path),
emptyStateActionButtonLabel: S
.of(context)!
.addNewStoragePath,
emptyStateDescription: S
.of(context)!
.noStoragePathsSetUp,
onAddNew: _openAddStoragePathPage,
),
],
);
},
),
], ],
), ),
), ),
@@ -365,73 +228,121 @@ class _LabelsPageState extends State<LabelsPage>
}); });
} }
void _openEditCorrespondentPage(Correspondent correspondent) { Widget _buildCorrespondentsView(LabelState state, UserModel user) {
Navigator.push( return Builder(
context, builder: (context) {
_buildLabelPageRoute(EditCorrespondentPage(correspondent: correspondent)), return CustomScrollView(
); slivers: [
} SliverOverlapInjector(handle: searchBarHandle),
SliverOverlapInjector(handle: tabBarHandle),
void _openEditDocumentTypePage(DocumentType docType) { LabelTabView<Correspondent>(
Navigator.push( labels: state.correspondents,
context, filterBuilder: (label) => DocumentFilter(
_buildLabelPageRoute(EditDocumentTypePage(documentType: docType)), correspondent: IdQueryParameter.fromId(label.id!),
);
}
void _openEditTagPage(Tag tag) {
Navigator.push(
context,
_buildLabelPageRoute(EditTagPage(tag: tag)),
);
}
void _openEditStoragePathPage(StoragePath path) {
Navigator.push(
context,
_buildLabelPageRoute(EditStoragePathPage(
storagePath: path,
)),
);
}
void _openAddCorrespondentPage() {
Navigator.push(
context,
_buildLabelPageRoute(const AddCorrespondentPage()),
);
}
void _openAddDocumentTypePage() {
Navigator.push(
context,
_buildLabelPageRoute(const AddDocumentTypePage()),
);
}
void _openAddTagPage() {
Navigator.push(
context,
_buildLabelPageRoute(const AddTagPage()),
);
}
void _openAddStoragePathPage() {
Navigator.push(
context,
_buildLabelPageRoute(const AddStoragePathPage()),
);
}
MaterialPageRoute<dynamic> _buildLabelPageRoute(Widget page) {
return MaterialPageRoute(
builder: (_) => MultiProvider(
providers: [
Provider.value(value: context.read<LabelRepository>()),
Provider.value(value: context.read<ApiVersion>())
],
child: page,
), ),
canEdit: user.canEditCorrespondents,
canAddNew: user.canCreateCorrespondents,
onEdit: (correspondent) {
EditLabelRoute(correspondent).push(context);
},
emptyStateActionButtonLabel: S.of(context)!.addNewCorrespondent,
emptyStateDescription: S.of(context)!.noCorrespondentsSetUp,
onAddNew: () => CreateLabelRoute<Correspondent>().push(context),
),
],
);
},
);
}
Widget _buildDocumentTypesView(LabelState state, UserModel user) {
return Builder(
builder: (context) {
return CustomScrollView(
slivers: [
SliverOverlapInjector(handle: searchBarHandle),
SliverOverlapInjector(handle: tabBarHandle),
LabelTabView<DocumentType>(
labels: state.documentTypes,
filterBuilder: (label) => DocumentFilter(
documentType: IdQueryParameter.fromId(label.id!),
),
canEdit: user.canEditDocumentTypes,
canAddNew: user.canCreateDocumentTypes,
onEdit: (label) {
EditLabelRoute(label).push(context);
},
emptyStateActionButtonLabel: S.of(context)!.addNewDocumentType,
emptyStateDescription: S.of(context)!.noDocumentTypesSetUp,
onAddNew: () => CreateLabelRoute<DocumentType>().push(context),
),
],
);
},
);
}
Widget _buildTagsView(LabelState state, UserModel user) {
return Builder(
builder: (context) {
return CustomScrollView(
slivers: [
SliverOverlapInjector(handle: searchBarHandle),
SliverOverlapInjector(handle: tabBarHandle),
LabelTabView<Tag>(
labels: state.tags,
filterBuilder: (label) => DocumentFilter(
tags: TagsQuery.ids(include: [label.id!]),
),
canEdit: user.canEditTags,
canAddNew: user.canCreateTags,
onEdit: (label) {
EditLabelRoute(label).push(context);
},
leadingBuilder: (t) => CircleAvatar(
backgroundColor: t.color,
child: t.isInboxTag
? Icon(
Icons.inbox,
color: t.textColor,
)
: null,
),
emptyStateActionButtonLabel: S.of(context)!.addNewTag,
emptyStateDescription: S.of(context)!.noTagsSetUp,
onAddNew: () => CreateLabelRoute<Tag>().push(context),
),
],
);
},
);
}
Widget _buildStoragePathView(LabelState state, UserModel user) {
return Builder(
builder: (context) {
return CustomScrollView(
slivers: [
SliverOverlapInjector(handle: searchBarHandle),
SliverOverlapInjector(handle: tabBarHandle),
LabelTabView<StoragePath>(
labels: state.storagePaths,
onEdit: (label) {
EditLabelRoute(label).push(context);
},
filterBuilder: (label) => DocumentFilter(
storagePath: IdQueryParameter.fromId(label.id!),
),
canEdit: user.canEditStoragePaths,
canAddNew: user.canCreateStoragePaths,
contentBuilder: (path) => Text(path.path),
emptyStateActionButtonLabel: S.of(context)!.addNewStoragePath,
emptyStateDescription: S.of(context)!.noStoragePathsSetUp,
onAddNew: () => CreateLabelRoute<StoragePath>().push(context),
),
],
);
},
); );
} }
} }
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/database/tables/local_user_account.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/navigation/push_routes.dart';
@@ -36,7 +37,7 @@ class LabelItem<T extends Label> extends StatelessWidget {
Widget _buildReferencedDocumentsWidget(BuildContext context) { Widget _buildReferencedDocumentsWidget(BuildContext context) {
final canOpen = (label.documentCount ?? 0) > 0 && final canOpen = (label.documentCount ?? 0) > 0 &&
LocalUserAccount.current.paperlessUser.canViewDocuments; context.watch<LocalUserAccount>().paperlessUser.canViewDocuments;
return TextButton.icon( return TextButton.icon(
label: const Icon(Icons.link), label: const Icon(Icons.link),
icon: Text(formatMaxCount(label.documentCount)), icon: Text(formatMaxCount(label.documentCount)),
@@ -0,0 +1,51 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.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/cubit/documents_cubit.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
import 'package:sliver_tools/sliver_tools.dart';
class LandingPage extends StatefulWidget {
const LandingPage({super.key});
@override
State<LandingPage> createState() => _LandingPageState();
}
class _LandingPageState extends State<LandingPage> {
final _searchBarHandle = SliverOverlapAbsorberHandle();
@override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
drawer: const AppDrawer(),
body: NestedScrollView(
headerSliverBuilder: (context, innerBoxIsScrolled) => [
SliverOverlapAbsorber(
handle: _searchBarHandle,
sliver: SliverSearchBar(
floating: true,
titleText: S.of(context)!.documents,
),
),
],
body: CustomScrollView(
slivers: [
SliverPadding(
padding: const EdgeInsets.all(16),
sliver: SliverToBoxAdapter(
child: Text(
"Welcome!",
style: Theme.of(context).textTheme.titleLarge,
),
),
),
],
),
),
),
);
}
}
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart'; import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
import 'package:paperless_mobile/core/navigation/push_routes.dart'; import 'package:paperless_mobile/core/navigation/push_routes.dart';
import 'package:paperless_mobile/features/documents/view/widgets/adaptive_documents_view.dart'; import 'package:paperless_mobile/features/documents/view/widgets/adaptive_documents_view.dart';
@@ -7,6 +8,7 @@ import 'package:paperless_mobile/features/documents/view/widgets/selection/view_
import 'package:paperless_mobile/features/linked_documents/cubit/linked_documents_cubit.dart'; import 'package:paperless_mobile/features/linked_documents/cubit/linked_documents_cubit.dart';
import 'package:paperless_mobile/features/paged_document_view/view/document_paging_view_mixin.dart'; import 'package:paperless_mobile/features/paged_document_view/view/document_paging_view_mixin.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
import 'package:paperless_mobile/routes/typed/branches/documents_route.dart';
class LinkedDocumentsPage extends StatefulWidget { class LinkedDocumentsPage extends StatefulWidget {
const LinkedDocumentsPage({super.key}); const LinkedDocumentsPage({super.key});
@@ -51,11 +53,10 @@ class _LinkedDocumentsPageState extends State<LinkedDocumentsPage>
isLoading: state.isLoading, isLoading: state.isLoading,
hasLoaded: state.hasLoaded, hasLoaded: state.hasLoaded,
onTap: (document) { onTap: (document) {
pushDocumentDetailsRoute( DocumentDetailsRoute(
context, $extra: document,
document: document,
isLabelClickable: false, isLabelClickable: false,
); ).push(context);
}, },
), ),
], ],
@@ -55,12 +55,11 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
// Mark logged in user as currently active user. // Mark logged in user as currently active user.
final globalSettings = final globalSettings =
Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!; Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!;
globalSettings.currentLoggedInUser = localUserId; globalSettings.loggedInUserId = localUserId;
await globalSettings.save(); await globalSettings.save();
emit( emit(
AuthenticationState.authenticated( AuthenticationState.authenticated(
apiVersion: apiVersion,
localUserId: localUserId, localUserId: localUserId,
), ),
); );
@@ -75,7 +74,8 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
emit(const AuthenticationState.switchingAccounts()); emit(const AuthenticationState.switchingAccounts());
final globalSettings = final globalSettings =
Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!; Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!;
if (globalSettings.currentLoggedInUser == localUserId) { if (globalSettings.loggedInUserId == localUserId) {
emit(AuthenticationState.authenticated(localUserId: localUserId));
return; return;
} }
final userAccountBox = final userAccountBox =
@@ -112,7 +112,7 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
baseUrl: account.serverUrl, baseUrl: account.serverUrl,
); );
globalSettings.currentLoggedInUser = localUserId; globalSettings.loggedInUserId = localUserId;
await globalSettings.save(); await globalSettings.save();
final apiVersion = await _getApiVersion(_sessionManager.client); final apiVersion = await _getApiVersion(_sessionManager.client);
@@ -126,7 +126,6 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
emit(AuthenticationState.authenticated( emit(AuthenticationState.authenticated(
localUserId: localUserId, localUserId: localUserId,
apiVersion: apiVersion,
)); ));
}); });
} }
@@ -175,13 +174,14 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
); );
final globalSettings = final globalSettings =
Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!; Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!;
final localUserId = globalSettings.currentLoggedInUser; final localUserId = globalSettings.loggedInUserId;
if (localUserId == null) { if (localUserId == null) {
_debugPrintMessage( _debugPrintMessage(
"restoreSessionState", "restoreSessionState",
"There is nothing to restore.", "There is nothing to restore.",
); );
// If there is nothing to restore, we can quit here. // If there is nothing to restore, we can quit here.
emit(const AuthenticationState.unauthenticated());
return; return;
} }
final localUserAccountBox = final localUserAccountBox =
@@ -223,7 +223,7 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
final authentication = final authentication =
await withEncryptedBox<UserCredentials, UserCredentials>( await withEncryptedBox<UserCredentials, UserCredentials>(
HiveBoxes.localUserCredentials, (box) { HiveBoxes.localUserCredentials, (box) {
return box.get(globalSettings.currentLoggedInUser!); return box.get(globalSettings.loggedInUserId!);
}); });
if (authentication == null) { if (authentication == null) {
@@ -261,7 +261,6 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
); );
emit( emit(
AuthenticationState.authenticated( AuthenticationState.authenticated(
apiVersion: apiVersion,
localUserId: localUserId, localUserId: localUserId,
), ),
); );
@@ -279,7 +278,7 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
await _resetExternalState(); await _resetExternalState();
final globalSettings = final globalSettings =
Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!; Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!;
globalSettings.currentLoggedInUser = null; globalSettings.loggedInUserId = null;
await globalSettings.save(); await globalSettings.save();
emit(const AuthenticationState.unauthenticated()); emit(const AuthenticationState.unauthenticated());
@@ -389,6 +388,7 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
settings: LocalUserSettings(), settings: LocalUserSettings(),
serverUrl: serverUrl, serverUrl: serverUrl,
paperlessUser: serverUser, paperlessUser: serverUser,
apiVersion: apiVersion,
), ),
); );
_debugPrintMessage( _debugPrintMessage(
@@ -2,12 +2,18 @@ part of 'authentication_cubit.dart';
@freezed @freezed
class AuthenticationState with _$AuthenticationState { class AuthenticationState with _$AuthenticationState {
const AuthenticationState._();
const factory AuthenticationState.unauthenticated() = _Unauthenticated; const factory AuthenticationState.unauthenticated() = _Unauthenticated;
const factory AuthenticationState.requriresLocalAuthentication() = const factory AuthenticationState.requriresLocalAuthentication() =
_RequiresLocalAuthentication; _RequiresLocalAuthentication;
const factory AuthenticationState.authenticated({ const factory AuthenticationState.authenticated({
required String localUserId, required String localUserId,
required int apiVersion,
}) = _Authenticated; }) = _Authenticated;
const factory AuthenticationState.switchingAccounts() = _SwitchingAccounts; const factory AuthenticationState.switchingAccounts() = _SwitchingAccounts;
bool get isAuthenticated => maybeWhen(
authenticated: (_) => true,
orElse: () => false,
);
} }
@@ -0,0 +1,161 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_form_builder/flutter_form_builder.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/exception/server_message_exception.dart';
import 'package:paperless_mobile/features/login/cubit/authentication_cubit.dart';
import 'package:paperless_mobile/features/login/model/client_certificate.dart';
import 'package:paperless_mobile/features/login/model/client_certificate_form_model.dart';
import 'package:paperless_mobile/features/login/model/login_form_credentials.dart';
import 'package:paperless_mobile/features/login/view/widgets/form_fields/client_certificate_form_field.dart';
import 'package:paperless_mobile/features/login/view/widgets/form_fields/server_address_form_field.dart';
import 'package:paperless_mobile/features/login/view/widgets/form_fields/user_credentials_form_field.dart';
import 'package:paperless_mobile/features/login/view/widgets/login_pages/server_connection_page.dart';
import 'package:paperless_mobile/features/users/view/widgets/user_account_list_tile.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
import 'package:paperless_mobile/helpers/message_helpers.dart';
import 'widgets/login_pages/server_login_page.dart';
import 'widgets/never_scrollable_scroll_behavior.dart';
class AddAccountPage extends StatefulWidget {
final FutureOr<void> Function(
BuildContext context,
String username,
String password,
String serverUrl,
ClientCertificate? clientCertificate,
) onSubmit;
final String submitText;
final String titleString;
final bool showLocalAccounts;
const AddAccountPage({
Key? key,
required this.onSubmit,
required this.submitText,
required this.titleString,
this.showLocalAccounts = false,
}) : super(key: key);
@override
State<AddAccountPage> createState() => _AddAccountPageState();
}
class _AddAccountPageState extends State<AddAccountPage> {
final _formKey = GlobalKey<FormBuilderState>();
final PageController _pageController = PageController();
@override
Widget build(BuildContext context) {
final localAccounts =
Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount);
return Scaffold(
resizeToAvoidBottomInset: false,
body: FormBuilder(
key: _formKey,
child: PageView(
controller: _pageController,
scrollBehavior: NeverScrollableScrollBehavior(),
children: [
if (widget.showLocalAccounts && localAccounts.isNotEmpty)
Scaffold(
appBar: AppBar(
title: Text(S.of(context)!.logInToExistingAccount),
),
bottomNavigationBar: BottomAppBar(
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
FilledButton(
child: Text(S.of(context)!.goToLogin),
onPressed: () {
_pageController.nextPage(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
},
),
],
),
),
body: ListView.builder(
itemBuilder: (context, index) {
final account = localAccounts.values.elementAt(index);
return Card(
child: UserAccountListTile(
account: account,
onTap: () {
context
.read<AuthenticationCubit>()
.switchAccount(account.id);
},
),
);
},
itemCount: localAccounts.length,
),
),
ServerConnectionPage(
titleText: widget.titleString,
formBuilderKey: _formKey,
onContinue: () {
_pageController.nextPage(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
},
),
ServerLoginPage(
formBuilderKey: _formKey,
submitText: widget.submitText,
onSubmit: _login,
),
],
),
),
);
}
Future<void> _login() async {
FocusScope.of(context).unfocus();
if (_formKey.currentState?.saveAndValidate() ?? false) {
final form = _formKey.currentState!.value;
ClientCertificate? clientCert;
final clientCertFormModel =
form[ClientCertificateFormField.fkClientCertificate]
as ClientCertificateFormModel?;
if (clientCertFormModel != null) {
clientCert = ClientCertificate(
bytes: clientCertFormModel.bytes,
passphrase: clientCertFormModel.passphrase,
);
}
final credentials =
form[UserCredentialsFormField.fkCredentials] as LoginFormCredentials;
try {
await widget.onSubmit(
context,
credentials.username!,
credentials.password!,
form[ServerAddressFormField.fkServerAddress],
clientCert,
);
} on PaperlessApiException catch (error) {
showErrorMessage(context, error);
} on ServerMessageException catch (error) {
showLocalizedError(context, error.message);
} catch (error) {
showGenericError(context, error);
}
}
}
}
+56 -139
View File
@@ -1,161 +1,78 @@
import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:go_router/go_router.dart';
import 'package:hive_flutter/adapters.dart'; import 'package:hive_flutter/adapters.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/config/hive/hive_config.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/exception/server_message_exception.dart'; import 'package:paperless_mobile/features/app_intro/application_intro_slideshow.dart';
import 'package:paperless_mobile/features/login/cubit/authentication_cubit.dart'; import 'package:paperless_mobile/features/login/cubit/authentication_cubit.dart';
import 'package:paperless_mobile/features/login/model/client_certificate.dart'; import 'package:paperless_mobile/features/login/model/client_certificate.dart';
import 'package:paperless_mobile/features/login/model/client_certificate_form_model.dart';
import 'package:paperless_mobile/features/login/model/login_form_credentials.dart'; import 'package:paperless_mobile/features/login/model/login_form_credentials.dart';
import 'package:paperless_mobile/features/login/view/widgets/form_fields/client_certificate_form_field.dart'; import 'package:paperless_mobile/features/login/view/add_account_page.dart';
import 'package:paperless_mobile/features/login/view/widgets/form_fields/server_address_form_field.dart';
import 'package:paperless_mobile/features/login/view/widgets/form_fields/user_credentials_form_field.dart';
import 'package:paperless_mobile/features/login/view/widgets/login_pages/server_connection_page.dart';
import 'package:paperless_mobile/features/users/view/widgets/user_account_list_tile.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
import 'package:paperless_mobile/helpers/message_helpers.dart'; import 'package:paperless_mobile/helpers/message_helpers.dart';
import 'package:paperless_mobile/routes/typed/branches/documents_route.dart';
import 'widgets/login_pages/server_login_page.dart'; class LoginPage extends StatelessWidget {
import 'widgets/never_scrollable_scroll_behavior.dart'; const LoginPage({super.key});
class LoginPage extends StatefulWidget { @override
final FutureOr<void> Function( Widget build(BuildContext context) {
return AddAccountPage(
titleString: S.of(context)!.connectToPaperless,
submitText: S.of(context)!.signIn,
onSubmit: _onLogin,
showLocalAccounts: true,
);
}
void _onLogin(
BuildContext context, BuildContext context,
String username, String username,
String password, String password,
String serverUrl, String serverUrl,
ClientCertificate? clientCertificate, ClientCertificate? clientCertificate,
) onSubmit; ) async {
final String submitText;
final String titleString;
final bool showLocalAccounts;
const LoginPage({
Key? key,
required this.onSubmit,
required this.submitText,
required this.titleString,
this.showLocalAccounts = false,
}) : super(key: key);
@override
State<LoginPage> createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> {
final _formKey = GlobalKey<FormBuilderState>();
final PageController _pageController = PageController();
@override
Widget build(BuildContext context) {
final localAccounts =
Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount);
return Scaffold(
resizeToAvoidBottomInset: false,
body: FormBuilder(
key: _formKey,
child: PageView(
controller: _pageController,
scrollBehavior: NeverScrollableScrollBehavior(),
children: [
if (widget.showLocalAccounts && localAccounts.isNotEmpty)
Scaffold(
appBar: AppBar(
title: Text(S.of(context)!.logInToExistingAccount),
),
bottomNavigationBar: BottomAppBar(
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
FilledButton(
child: Text(S.of(context)!.goToLogin),
onPressed: () {
_pageController.nextPage(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
},
),
],
),
),
body: ListView.builder(
itemBuilder: (context, index) {
final account = localAccounts.values.elementAt(index);
return Card(
child: UserAccountListTile(
account: account,
onTap: () {
context
.read<AuthenticationCubit>()
.switchAccount(account.id);
},
),
);
},
itemCount: localAccounts.length,
),
),
ServerConnectionPage(
titleText: widget.titleString,
formBuilderKey: _formKey,
onContinue: () {
_pageController.nextPage(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
},
),
ServerLoginPage(
formBuilderKey: _formKey,
submitText: widget.submitText,
onSubmit: _login,
),
],
),
),
);
}
Future<void> _login() async {
FocusScope.of(context).unfocus();
if (_formKey.currentState?.saveAndValidate() ?? false) {
final form = _formKey.currentState!.value;
ClientCertificate? clientCert;
final clientCertFormModel =
form[ClientCertificateFormField.fkClientCertificate]
as ClientCertificateFormModel?;
if (clientCertFormModel != null) {
clientCert = ClientCertificate(
bytes: clientCertFormModel.bytes,
passphrase: clientCertFormModel.passphrase,
);
}
final credentials =
form[UserCredentialsFormField.fkCredentials] as LoginFormCredentials;
try { try {
await widget.onSubmit( await context.read<AuthenticationCubit>().login(
context, credentials: LoginFormCredentials(
credentials.username!, username: username,
credentials.password!, password: password,
form[ServerAddressFormField.fkServerAddress], ),
clientCert, serverUrl: serverUrl,
clientCertificate: clientCertificate,
); );
} on PaperlessApiException catch (error) { // Show onboarding after first login!
showErrorMessage(context, error); final globalSettings =
} on ServerMessageException catch (error) { Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!;
showLocalizedError(context, error.message); if (globalSettings.showOnboarding) {
} catch (error) { Navigator.push(
showGenericError(context, error); context,
} MaterialPageRoute(
builder: (context) => const ApplicationIntroSlideshow(),
fullscreenDialog: true,
),
).then((value) {
globalSettings.showOnboarding = false;
globalSettings.save();
});
}
// DocumentsRoute().go(context);
} on PaperlessApiException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);
} on PaperlessFormValidationException catch (exception, stackTrace) {
if (exception.hasUnspecificErrorMessage()) {
showLocalizedError(context, exception.unspecificErrorMessage()!);
} else {
showGenericError(
context,
exception.validationMessages.values.first,
stackTrace,
); //TODO: Check if we can show error message directly on field here.
}
} catch (unknownError, stackTrace) {
showGenericError(context, unknownError.toString(), stackTrace);
} }
} }
} }
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart'; import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
import 'package:paperless_mobile/core/navigation/push_routes.dart'; import 'package:paperless_mobile/core/navigation/push_routes.dart';
@@ -9,6 +10,7 @@ import 'package:paperless_mobile/features/documents/view/widgets/selection/confi
import 'package:paperless_mobile/features/documents/view/widgets/selection/view_type_selection_widget.dart'; import 'package:paperless_mobile/features/documents/view/widgets/selection/view_type_selection_widget.dart';
import 'package:paperless_mobile/features/paged_document_view/view/document_paging_view_mixin.dart'; import 'package:paperless_mobile/features/paged_document_view/view/document_paging_view_mixin.dart';
import 'package:paperless_mobile/features/saved_view_details/cubit/saved_view_details_cubit.dart'; import 'package:paperless_mobile/features/saved_view_details/cubit/saved_view_details_cubit.dart';
import 'package:paperless_mobile/routes/typed/branches/documents_route.dart';
class SavedViewDetailsPage extends StatefulWidget { class SavedViewDetailsPage extends StatefulWidget {
final Future<void> Function(SavedView savedView) onDelete; final Future<void> Function(SavedView savedView) onDelete;
@@ -28,7 +30,7 @@ class _SavedViewDetailsPageState extends State<SavedViewDetailsPage>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final cubit = context.read<SavedViewDetailsCubit>(); final cubit = context.watch<SavedViewDetailsCubit>();
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text(cubit.savedView.name), title: Text(cubit.savedView.name),
@@ -76,11 +78,10 @@ class _SavedViewDetailsPageState extends State<SavedViewDetailsPage>
isLoading: state.isLoading, isLoading: state.isLoading,
hasLoaded: state.hasLoaded, hasLoaded: state.hasLoaded,
onTap: (document) { onTap: (document) {
pushDocumentDetailsRoute( DocumentDetailsRoute(
context, $extra: document,
document: document,
isLabelClickable: false, isLabelClickable: false,
); ).push(context);
}, },
viewType: state.viewType, viewType: state.viewType,
), ),
@@ -6,7 +6,7 @@ import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
import 'package:paperless_mobile/features/home/view/model/api_version.dart'; import 'package:paperless_mobile/features/home/view/model/api_version.dart';
import 'package:paperless_mobile/features/login/cubit/authentication_cubit.dart'; import 'package:paperless_mobile/features/login/cubit/authentication_cubit.dart';
import 'package:paperless_mobile/features/login/model/login_form_credentials.dart'; import 'package:paperless_mobile/features/login/model/login_form_credentials.dart';
import 'package:paperless_mobile/features/login/view/login_page.dart'; import 'package:paperless_mobile/features/login/view/add_account_page.dart';
import 'package:paperless_mobile/features/settings/view/dialogs/switch_account_dialog.dart'; import 'package:paperless_mobile/features/settings/view/dialogs/switch_account_dialog.dart';
import 'package:paperless_mobile/features/settings/view/widgets/global_settings_builder.dart'; import 'package:paperless_mobile/features/settings/view/widgets/global_settings_builder.dart';
import 'package:paperless_mobile/features/users/view/widgets/user_account_list_tile.dart'; import 'package:paperless_mobile/features/users/view/widgets/user_account_list_tile.dart';
@@ -22,7 +22,7 @@ class ManageAccountsPage extends StatelessWidget {
builder: (context, globalSettings) { builder: (context, globalSettings) {
// This is one of the few places where the currentLoggedInUser can be null // This is one of the few places where the currentLoggedInUser can be null
// (exactly after loggin out as the current user to be precise). // (exactly after loggin out as the current user to be precise).
if (globalSettings.currentLoggedInUser == null) { if (globalSettings.loggedInUserId == null) {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
return ValueListenableBuilder( return ValueListenableBuilder(
@@ -32,8 +32,7 @@ class ManageAccountsPage extends StatelessWidget {
builder: (context, box, _) { builder: (context, box, _) {
final userIds = box.keys.toList().cast<String>(); final userIds = box.keys.toList().cast<String>();
final otherAccounts = userIds final otherAccounts = userIds
.whereNot( .whereNot((element) => element == globalSettings.loggedInUserId)
(element) => element == globalSettings.currentLoggedInUser)
.toList(); .toList();
return SimpleDialog( return SimpleDialog(
insetPadding: const EdgeInsets.all(24), insetPadding: const EdgeInsets.all(24),
@@ -54,7 +53,7 @@ class ManageAccountsPage extends StatelessWidget {
children: [ children: [
Card( Card(
child: UserAccountListTile( child: UserAccountListTile(
account: box.get(globalSettings.currentLoggedInUser!)!, account: box.get(globalSettings.loggedInUserId!)!,
trailing: PopupMenuButton( trailing: PopupMenuButton(
icon: const Icon(Icons.more_vert), icon: const Icon(Icons.more_vert),
itemBuilder: (context) => [ itemBuilder: (context) => [
@@ -71,8 +70,7 @@ class ManageAccountsPage extends StatelessWidget {
], ],
onSelected: (value) async { onSelected: (value) async {
if (value == 0) { if (value == 0) {
final currentUser = final currentUser = globalSettings.loggedInUserId!;
globalSettings.currentLoggedInUser!;
await context.read<AuthenticationCubit>().logout(); await context.read<AuthenticationCubit>().logout();
Navigator.of(context).pop(); Navigator.of(context).pop();
await context await context
@@ -117,7 +115,7 @@ class ManageAccountsPage extends StatelessWidget {
// Switch // Switch
_onSwitchAccount( _onSwitchAccount(
context, context,
globalSettings.currentLoggedInUser!, globalSettings.loggedInUserId!,
otherAccounts[index], otherAccounts[index],
); );
} else if (value == 1) { } else if (value == 1) {
@@ -135,10 +133,10 @@ class ManageAccountsPage extends StatelessWidget {
title: Text(S.of(context)!.addAccount), title: Text(S.of(context)!.addAccount),
leading: const Icon(Icons.person_add), leading: const Icon(Icons.person_add),
onTap: () { onTap: () {
_onAddAccount(context, globalSettings.currentLoggedInUser!); _onAddAccount(context, globalSettings.loggedInUserId!);
}, },
), ),
if (context.watch<ApiVersion>().hasMultiUserSupport) if (context.watch<LocalUserAccount>().hasMultiUserSupport)
ListTile( ListTile(
leading: const Icon(Icons.admin_panel_settings), leading: const Icon(Icons.admin_panel_settings),
title: Text(S.of(context)!.managePermissions), title: Text(S.of(context)!.managePermissions),
@@ -155,7 +153,7 @@ class ManageAccountsPage extends StatelessWidget {
final userId = await Navigator.push( final userId = await Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => LoginPage( builder: (context) => AddAccountPage(
titleString: S.of(context)!.addAccount, titleString: S.of(context)!.addAccount,
onSubmit: (context, username, password, serverUrl, onSubmit: (context, username, password, serverUrl,
clientCertificate) async { clientCertificate) async {
@@ -100,14 +100,4 @@ class SettingsPage extends StatelessWidget {
), ),
); );
} }
void _goto(Widget page, BuildContext context) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => page,
maintainState: true,
),
);
}
} }
@@ -23,7 +23,7 @@ class UserAccountBuilder extends StatelessWidget {
builder: (context, accountBox, _) { builder: (context, accountBox, _) {
final currentUser = Hive.box<GlobalSettings>(HiveBoxes.globalSettings) final currentUser = Hive.box<GlobalSettings>(HiveBoxes.globalSettings)
.getValue()! .getValue()!
.currentLoggedInUser; .loggedInUserId;
if (currentUser != null) { if (currentUser != null) {
final account = accountBox.get(currentUser); final account = accountBox.get(currentUser);
return builder(context, account); return builder(context, account);
@@ -2,13 +2,13 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart'; import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
import 'package:paperless_mobile/core/navigation/push_routes.dart';
import 'package:paperless_mobile/core/widgets/offline_widget.dart'; import 'package:paperless_mobile/core/widgets/offline_widget.dart';
import 'package:paperless_mobile/features/documents/view/widgets/adaptive_documents_view.dart'; import 'package:paperless_mobile/features/documents/view/widgets/adaptive_documents_view.dart';
import 'package:paperless_mobile/features/paged_document_view/view/document_paging_view_mixin.dart'; import 'package:paperless_mobile/features/paged_document_view/view/document_paging_view_mixin.dart';
import 'package:paperless_mobile/features/similar_documents/cubit/similar_documents_cubit.dart'; import 'package:paperless_mobile/features/similar_documents/cubit/similar_documents_cubit.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
import 'package:paperless_mobile/helpers/message_helpers.dart'; import 'package:paperless_mobile/helpers/message_helpers.dart';
import 'package:paperless_mobile/routes/typed/branches/documents_route.dart';
class SimilarDocumentsView extends StatefulWidget { class SimilarDocumentsView extends StatefulWidget {
final ScrollController pagingScrollController; final ScrollController pagingScrollController;
@@ -64,11 +64,10 @@ class _SimilarDocumentsViewState extends State<SimilarDocumentsView>
hasLoaded: state.hasLoaded, hasLoaded: state.hasLoaded,
enableHeroAnimation: false, enableHeroAnimation: false,
onTap: (document) { onTap: (document) {
pushDocumentDetailsRoute( DocumentDetailsRoute(
context, $extra: document,
document: document,
isLabelClickable: false, isLabelClickable: false,
); ).push(context);
}, },
); );
}, },
+246 -139
View File
@@ -9,6 +9,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_displaymode/flutter_displaymode.dart'; import 'package:flutter_displaymode/flutter_displaymode.dart';
import 'package:flutter_native_splash/flutter_native_splash.dart'; import 'package:flutter_native_splash/flutter_native_splash.dart';
import 'package:go_router/go_router.dart';
import 'package:hive_flutter/adapters.dart'; import 'package:hive_flutter/adapters.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart'; import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:intl/date_symbol_data_local.dart'; import 'package:intl/date_symbol_data_local.dart';
@@ -30,19 +31,23 @@ import 'package:paperless_mobile/core/interceptor/language_header.interceptor.da
import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart'; import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart';
import 'package:paperless_mobile/core/security/session_manager.dart'; import 'package:paperless_mobile/core/security/session_manager.dart';
import 'package:paperless_mobile/core/service/connectivity_status_service.dart'; import 'package:paperless_mobile/core/service/connectivity_status_service.dart';
import 'package:paperless_mobile/features/app_intro/application_intro_slideshow.dart';
import 'package:paperless_mobile/features/home/view/home_route.dart';
import 'package:paperless_mobile/features/home/view/widget/verify_identity_page.dart';
import 'package:paperless_mobile/features/login/cubit/authentication_cubit.dart'; import 'package:paperless_mobile/features/login/cubit/authentication_cubit.dart';
import 'package:paperless_mobile/features/login/model/client_certificate.dart';
import 'package:paperless_mobile/features/login/model/login_form_credentials.dart';
import 'package:paperless_mobile/features/login/services/authentication_service.dart'; import 'package:paperless_mobile/features/login/services/authentication_service.dart';
import 'package:paperless_mobile/features/login/view/login_page.dart';
import 'package:paperless_mobile/features/notifications/services/local_notification_service.dart'; import 'package:paperless_mobile/features/notifications/services/local_notification_service.dart';
import 'package:paperless_mobile/features/settings/view/pages/switching_accounts_page.dart';
import 'package:paperless_mobile/features/settings/view/widgets/global_settings_builder.dart'; import 'package:paperless_mobile/features/settings/view/widgets/global_settings_builder.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
import 'package:paperless_mobile/helpers/message_helpers.dart'; import 'package:paperless_mobile/routes/navigation_keys.dart';
import 'package:paperless_mobile/routes/typed/branches/documents_route.dart';
import 'package:paperless_mobile/routes/typed/branches/inbox_route.dart';
import 'package:paperless_mobile/routes/typed/branches/labels_route.dart';
import 'package:paperless_mobile/routes/typed/branches/landing_route.dart';
import 'package:paperless_mobile/routes/typed/branches/scanner_route.dart';
import 'package:paperless_mobile/routes/typed/shells/provider_shell_route.dart';
import 'package:paperless_mobile/routes/typed/shells/scaffold_shell_route.dart';
import 'package:paperless_mobile/routes/typed/top_level/login_route.dart';
import 'package:paperless_mobile/routes/typed/top_level/settings_route.dart';
import 'package:paperless_mobile/routes/typed/top_level/switching_accounts_route.dart';
import 'package:paperless_mobile/routes/typed/top_level/verify_identity_route.dart';
import 'package:paperless_mobile/theme.dart'; import 'package:paperless_mobile/theme.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@@ -138,7 +143,9 @@ void main() async {
}); });
final apiFactory = PaperlessApiFactoryImpl(sessionManager); final apiFactory = PaperlessApiFactoryImpl(sessionManager);
final authenticationCubit =
AuthenticationCubit(localAuthService, apiFactory, sessionManager);
await authenticationCubit.restoreSessionState();
runApp( runApp(
MultiProvider( MultiProvider(
providers: [ providers: [
@@ -154,13 +161,10 @@ void main() async {
child: MultiBlocProvider( child: MultiBlocProvider(
providers: [ providers: [
BlocProvider<ConnectivityCubit>.value(value: connectivityCubit), BlocProvider<ConnectivityCubit>.value(value: connectivityCubit),
BlocProvider( BlocProvider.value(value: authenticationCubit),
create: (context) => AuthenticationCubit(
localAuthService, apiFactory, sessionManager),
),
], ],
child: PaperlessMobileEntrypoint( child: GoRouterShell(
paperlessProviderFactory: apiFactory, apiFactory: apiFactory,
), ),
), ),
), ),
@@ -182,70 +186,69 @@ void main() async {
}); });
} }
class PaperlessMobileEntrypoint extends StatefulWidget { // class PaperlessMobileEntrypoint extends StatefulWidget {
final PaperlessApiFactory paperlessProviderFactory; // final PaperlessApiFactory paperlessProviderFactory;
const PaperlessMobileEntrypoint({ // const PaperlessMobileEntrypoint({
Key? key, // Key? key,
required this.paperlessProviderFactory, // required this.paperlessProviderFactory,
}) : super(key: key); // }) : super(key: key);
// @override
// State<PaperlessMobileEntrypoint> createState() =>
// _PaperlessMobileEntrypointState();
// }
// class _PaperlessMobileEntrypointState extends State<PaperlessMobileEntrypoint> {
// @override
// Widget build(BuildContext context) {
// return GlobalSettingsBuilder(
// builder: (context, settings) {
// return DynamicColorBuilder(
// builder: (lightDynamic, darkDynamic) {
// return MaterialApp(
// debugShowCheckedModeBanner: true,
// title: "Paperless Mobile",
// theme: buildTheme(
// brightness: Brightness.light,
// dynamicScheme: lightDynamic,
// preferredColorScheme: settings.preferredColorSchemeOption,
// ),
// darkTheme: buildTheme(
// brightness: Brightness.dark,
// dynamicScheme: darkDynamic,
// preferredColorScheme: settings.preferredColorSchemeOption,
// ),
// themeMode: settings.preferredThemeMode,
// supportedLocales: S.supportedLocales,
// locale: Locale.fromSubtags(
// languageCode: settings.preferredLocaleSubtag,
// ),
// localizationsDelegates: const [
// ...S.localizationsDelegates,
// ],
// home: AuthenticationWrapper(
// paperlessProviderFactory: widget.paperlessProviderFactory,
// ),
// );
// },
// );
// },
// );
// }
// }
class GoRouterShell extends StatefulWidget {
final PaperlessApiFactory apiFactory;
const GoRouterShell({
super.key,
required this.apiFactory,
});
@override @override
State<PaperlessMobileEntrypoint> createState() => State<GoRouterShell> createState() => _GoRouterShellState();
_PaperlessMobileEntrypointState();
} }
class _PaperlessMobileEntrypointState extends State<PaperlessMobileEntrypoint> { class _GoRouterShellState extends State<GoRouterShell> {
@override
Widget build(BuildContext context) {
return GlobalSettingsBuilder(
builder: (context, settings) {
return DynamicColorBuilder(
builder: (lightDynamic, darkDynamic) {
return MaterialApp(
debugShowCheckedModeBanner: true,
title: "Paperless Mobile",
theme: buildTheme(
brightness: Brightness.light,
dynamicScheme: lightDynamic,
preferredColorScheme: settings.preferredColorSchemeOption,
),
darkTheme: buildTheme(
brightness: Brightness.dark,
dynamicScheme: darkDynamic,
preferredColorScheme: settings.preferredColorSchemeOption,
),
themeMode: settings.preferredThemeMode,
supportedLocales: S.supportedLocales,
locale: Locale.fromSubtags(
languageCode: settings.preferredLocaleSubtag,
),
localizationsDelegates: const [
...S.localizationsDelegates,
],
home: AuthenticationWrapper(
paperlessProviderFactory: widget.paperlessProviderFactory,
),
);
},
);
},
);
}
}
class AuthenticationWrapper extends StatefulWidget {
final PaperlessApiFactory paperlessProviderFactory;
const AuthenticationWrapper({
Key? key,
required this.paperlessProviderFactory,
}) : super(key: key);
@override
State<AuthenticationWrapper> createState() => _AuthenticationWrapperState();
}
class _AuthenticationWrapperState extends State<AuthenticationWrapper> {
@override @override
void didChangeDependencies() { void didChangeDependencies() {
super.didChangeDependencies(); super.didChangeDependencies();
@@ -257,7 +260,6 @@ class _AuthenticationWrapperState extends State<AuthenticationWrapper> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
// Activate the highest supported refresh rate on the device // Activate the highest supported refresh rate on the device
if (Platform.isAndroid) { if (Platform.isAndroid) {
_setOptimalDisplayMode(); _setOptimalDisplayMode();
@@ -281,75 +283,180 @@ class _AuthenticationWrapperState extends State<AuthenticationWrapper> {
await FlutterDisplayMode.setPreferredMode(mostOptimalMode); await FlutterDisplayMode.setPreferredMode(mostOptimalMode);
} }
late final _router = GoRouter(
debugLogDiagnostics: true,
initialLocation: "/login",
routes: [
$loginRoute,
$verifyIdentityRoute,
$switchingAccountsRoute,
$settingsRoute,
ShellRoute(
navigatorKey: rootNavigatorKey,
builder: ProviderShellRoute(widget.apiFactory).build,
routes: [
// GoRoute(
// parentNavigatorKey: rootNavigatorKey,
// name: R.savedView,
// path: "/saved_view/:id",
// builder: (context, state) {
// return Placeholder(
// child: Text("Documents"),
// );
// },
// routes: [
// GoRoute(
// path: "create",
// name: R.createSavedView,
// builder: (context, state) {
// return Placeholder(
// child: Text("Documents"),
// );
// },
// ),
// ],
// ),
StatefulShellRoute.indexedStack(
builder: const ScaffoldShellRoute().builder,
branches: [
StatefulShellBranch(
navigatorKey: landingNavigatorKey,
routes: [$landingRoute],
),
StatefulShellBranch(
navigatorKey: documentsNavigatorKey,
routes: [$documentsRoute],
),
StatefulShellBranch(
navigatorKey: scannerNavigatorKey,
routes: [$scannerRoute],
),
StatefulShellBranch(
navigatorKey: labelsNavigatorKey,
routes: [$labelsRoute],
),
StatefulShellBranch(
navigatorKey: inboxNavigatorKey,
routes: [$inboxRoute],
),
],
),
],
),
],
);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocBuilder<AuthenticationCubit, AuthenticationState>( return GlobalSettingsBuilder(
builder: (context, authentication) { builder: (context, settings) {
return authentication.when( return DynamicColorBuilder(
unauthenticated: () => LoginPage( builder: (lightDynamic, darkDynamic) {
titleString: S.of(context)!.connectToPaperless, return BlocListener<AuthenticationCubit, AuthenticationState>(
submitText: S.of(context)!.signIn, listener: (context, state) {
onSubmit: _onLogin, state.when(
showLocalAccounts: true, unauthenticated: () => const LoginRoute().go(context),
requriresLocalAuthentication: () =>
const VerifyIdentityRoute().go(context),
authenticated: (localUserId) =>
const LandingRoute().go(context),
switchingAccounts: () =>
const SwitchingAccountsRoute().go(context),
);
},
child: MaterialApp.router(
routerConfig: _router,
debugShowCheckedModeBanner: true,
title: "Paperless Mobile",
theme: buildTheme(
brightness: Brightness.light,
dynamicScheme: lightDynamic,
preferredColorScheme: settings.preferredColorSchemeOption,
), ),
requriresLocalAuthentication: () => const VerifyIdentityPage(), darkTheme: buildTheme(
authenticated: (localUserId, apiVersion) => HomeRoute( brightness: Brightness.dark,
key: ValueKey(localUserId), dynamicScheme: darkDynamic,
paperlessApiVersion: apiVersion, preferredColorScheme: settings.preferredColorSchemeOption,
paperlessProviderFactory: widget.paperlessProviderFactory,
localUserId: localUserId,
), ),
switchingAccounts: () => const SwitchingAccountsPage(), themeMode: settings.preferredThemeMode,
supportedLocales: S.supportedLocales,
locale: Locale.fromSubtags(
languageCode: settings.preferredLocaleSubtag,
),
localizationsDelegates: S.localizationsDelegates,
),
);
},
); );
}, },
); );
} }
}
void _onLogin( // class AuthenticationWrapper extends StatefulWidget {
BuildContext context, // final PaperlessApiFactory paperlessProviderFactory;
String username,
String password, // const AuthenticationWrapper({
String serverUrl, // Key? key,
ClientCertificate? clientCertificate, // required this.paperlessProviderFactory,
) async { // }) : super(key: key);
try {
await context.read<AuthenticationCubit>().login( // @override
credentials: LoginFormCredentials( // State<AuthenticationWrapper> createState() => _AuthenticationWrapperState();
username: username, // }
password: password,
), // class _AuthenticationWrapperState extends State<AuthenticationWrapper> {
serverUrl: serverUrl, // @override
clientCertificate: clientCertificate, // void didChangeDependencies() {
); // super.didChangeDependencies();
// Show onboarding after first login! // context.read<AuthenticationCubit>().restoreSessionState().then((value) {
final globalSettings = // FlutterNativeSplash.remove();
Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!; // });
if (globalSettings.showOnboarding) { // }
Navigator.push(
context, // @override
MaterialPageRoute( // void initState() {
builder: (context) => const ApplicationIntroSlideshow(), // super.initState();
fullscreenDialog: true,
), // // Activate the highest supported refresh rate on the device
).then((value) { // if (Platform.isAndroid) {
globalSettings.showOnboarding = false; // _setOptimalDisplayMode();
globalSettings.save(); // }
}); // initializeDateFormatting();
} // }
} on PaperlessApiException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace); // Future<void> _setOptimalDisplayMode() async {
} on PaperlessFormValidationException catch (exception, stackTrace) { // final List<DisplayMode> supported = await FlutterDisplayMode.supported;
if (exception.hasUnspecificErrorMessage()) { // final DisplayMode active = await FlutterDisplayMode.active;
showLocalizedError(context, exception.unspecificErrorMessage()!);
} else { // final List<DisplayMode> sameResolution = supported
showGenericError( // .where((m) => m.width == active.width && m.height == active.height)
context, // .toList()
exception.validationMessages.values.first, // ..sort((a, b) => b.refreshRate.compareTo(a.refreshRate));
stackTrace,
); //TODO: Check if we can show error message directly on field here. // final DisplayMode mostOptimalMode =
} // sameResolution.isNotEmpty ? sameResolution.first : active;
} catch (unknownError, stackTrace) { // debugPrint('Setting refresh rate to ${mostOptimalMode.refreshRate}');
showGenericError(context, unknownError.toString(), stackTrace);
} // await FlutterDisplayMode.setPreferredMode(mostOptimalMode);
} // }
}
// @override
// Widget build(BuildContext context) {
// return BlocBuilder<AuthenticationCubit, AuthenticationState>(
// builder: (context, authentication) {
// return authentication.when(
// unauthenticated: () => const LoginPage(),
// requriresLocalAuthentication: () => const VerifyIdentityPage(),
// authenticated: (localUserId, apiVersion) => HomeShellWidget(
// key: ValueKey(localUserId),
// paperlessApiVersion: apiVersion,
// paperlessProviderFactory: widget.paperlessProviderFactory,
// localUserId: localUserId,
// ),
// switchingAccounts: () => const SwitchingAccountsPage(),
// );
// },
// );
// }
// }
-51
View File
@@ -1,51 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/features/document_details/cubit/document_details_cubit.dart';
import 'package:paperless_mobile/features/document_details/view/pages/document_details_page.dart';
class DocumentDetailsRoute extends StatelessWidget {
final DocumentModel document;
final bool isLabelClickable;
final String? titleAndContentQueryString;
const DocumentDetailsRoute({
super.key,
required this.document,
this.isLabelClickable = true,
this.titleAndContentQueryString,
});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => DocumentDetailsCubit(
context.read(),
context.read(),
context.read(),
context.read(),
initialDocument: document,
),
lazy: false,
child: DocumentDetailsPage(
isLabelClickable: isLabelClickable,
titleAndContentQueryString: titleAndContentQueryString,
),
);
}
}
class DocumentDetailsRouteArguments {
final DocumentModel document;
final bool isLabelClickable;
final bool allowEdit;
final String? titleAndContentQueryString;
DocumentDetailsRouteArguments({
required this.document,
this.isLabelClickable = true,
this.allowEdit = true,
this.titleAndContentQueryString,
});
}
+8
View File
@@ -0,0 +1,8 @@
import 'package:flutter/material.dart';
final rootNavigatorKey = GlobalKey<NavigatorState>();
final landingNavigatorKey = GlobalKey<NavigatorState>();
final documentsNavigatorKey = GlobalKey<NavigatorState>();
final scannerNavigatorKey = GlobalKey<NavigatorState>();
final labelsNavigatorKey = GlobalKey<NavigatorState>();
final inboxNavigatorKey = GlobalKey<NavigatorState>();
+20
View File
@@ -0,0 +1,20 @@
class R {
const R._();
static const landing = "landing";
static const login = "login";
static const documents = "documents";
static const verifyIdentity = "verifyIdentity";
static const switchingAccounts = "switchingAccounts";
static const savedView = "savedView";
static const createSavedView = "createSavedView";
static const documentDetails = "documentDetails";
static const editDocument = "editDocument";
static const labels = "labels";
static const createLabel = "createLabel";
static const editLabel = "editLabel";
static const scanner = "scanner";
static const uploadDocument = "upload";
static const inbox = "inbox";
static const documentPreview = "documentPreview";
static const settings = "settings";
}
@@ -0,0 +1,113 @@
import 'dart:ffi';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'package:flutter/widgets.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/features/document_details/cubit/document_details_cubit.dart';
import 'package:paperless_mobile/features/document_details/view/pages/document_details_page.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/pages/documents_page.dart';
import 'package:paperless_mobile/routes/navigation_keys.dart';
import 'package:paperless_mobile/routes/routes.dart';
part 'documents_route.g.dart';
class DocumentsBranch extends StatefulShellBranchData {
static final GlobalKey<NavigatorState> $navigatorKey = documentsNavigatorKey;
const DocumentsBranch();
}
@TypedGoRoute<DocumentsRoute>(
path: "/documents",
name: R.documents,
routes: [
TypedGoRoute<EditDocumentRoute>(
path: "edit",
name: R.editDocument,
),
TypedGoRoute<DocumentDetailsRoute>(
path: "details",
name: R.documentDetails,
),
TypedGoRoute<DocumentPreviewRoute>(
path: "preview",
name: R.documentPreview,
)
],
)
class DocumentsRoute extends GoRouteData {
@override
Widget build(BuildContext context, GoRouterState state) {
return const DocumentsPage();
}
}
class DocumentDetailsRoute extends GoRouteData {
static final GlobalKey<NavigatorState> $parentNavigatorKey = rootNavigatorKey;
final bool isLabelClickable;
final DocumentModel $extra;
final String? queryString;
const DocumentDetailsRoute({
required this.$extra,
this.isLabelClickable = true,
this.queryString,
});
@override
Widget build(BuildContext context, GoRouterState state) {
return BlocProvider(
create: (_) => DocumentDetailsCubit(
context.read(),
context.read(),
context.read(),
context.read(),
initialDocument: $extra,
),
lazy: false,
child: DocumentDetailsPage(
isLabelClickable: isLabelClickable,
titleAndContentQueryString: queryString,
),
);
}
}
class EditDocumentRoute extends GoRouteData {
static final GlobalKey<NavigatorState> $parentNavigatorKey = rootNavigatorKey;
final DocumentModel $extra;
const EditDocumentRoute(this.$extra);
@override
Widget build(BuildContext context, GoRouterState state) {
return BlocProvider(
create: (context) => DocumentEditCubit(
context.read(),
context.read(),
context.read(),
document: $extra,
)..loadFieldSuggestions(),
child: const DocumentEditPage(),
);
}
}
class DocumentPreviewRoute extends GoRouteData {
final DocumentModel $extra;
const DocumentPreviewRoute(this.$extra);
@override
Widget build(BuildContext context, GoRouterState state) {
return DocumentView(
documentBytes: context.read<PaperlessDocumentsApi>().download($extra),
);
}
}
@@ -0,0 +1,17 @@
import 'package:flutter/src/widgets/framework.dart';
import 'package:go_router/go_router.dart';
import 'package:paperless_mobile/features/inbox/view/pages/inbox_page.dart';
import 'package:paperless_mobile/routes/routes.dart';
part 'inbox_route.g.dart';
@TypedGoRoute<InboxRoute>(
path: "/inbox",
name: R.inbox,
)
class InboxRoute extends GoRouteData {
@override
Widget build(BuildContext context, GoRouterState state) {
return const InboxPage();
}
}
@@ -0,0 +1,84 @@
import 'package:flutter/widgets.dart';
import 'package:go_router/go_router.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/features/edit_label/view/impl/add_correspondent_page.dart';
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/edit_label/view/impl/add_tag_page.dart';
import 'package:paperless_mobile/features/edit_label/view/impl/edit_correspondent_page.dart';
import 'package:paperless_mobile/features/edit_label/view/impl/edit_document_type_page.dart';
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/view/pages/labels_page.dart';
import 'package:paperless_mobile/routes/navigation_keys.dart';
import 'package:paperless_mobile/routes/routes.dart';
part 'labels_route.g.dart';
class LabelsBranch extends StatefulShellBranchData {
static final GlobalKey<NavigatorState> $navigatorKey = labelsNavigatorKey;
const LabelsBranch();
}
@TypedGoRoute<LabelsRoute>(
path: "/labels",
name: R.labels,
routes: [
TypedGoRoute<EditLabelRoute>(
path: "edit",
name: R.editLabel,
),
TypedGoRoute<CreateLabelRoute>(
path: "create",
name: R.createLabel,
),
],
)
class LabelsRoute extends GoRouteData {
@override
Widget build(BuildContext context, GoRouterState state) {
return const LabelsPage();
}
}
class EditLabelRoute extends GoRouteData {
static final GlobalKey<NavigatorState> $parentNavigatorKey = rootNavigatorKey;
final Label $extra;
const EditLabelRoute(this.$extra);
@override
Widget build(BuildContext context, GoRouterState state) {
return switch ($extra) {
Correspondent c => EditCorrespondentPage(correspondent: c),
DocumentType d => EditDocumentTypePage(documentType: d),
Tag t => EditTagPage(tag: t),
StoragePath s => EditStoragePathPage(storagePath: s),
};
}
}
class CreateLabelRoute<T extends Label> extends GoRouteData {
static final GlobalKey<NavigatorState> $parentNavigatorKey = rootNavigatorKey;
final String? name;
CreateLabelRoute({
this.name,
});
@override
Widget build(BuildContext context, GoRouterState state) {
if (T is Correspondent) {
return AddCorrespondentPage(initialName: name);
} else if (T is DocumentType) {
return AddDocumentTypePage(initialName: name);
} else if (T is Tag) {
return AddTagPage(initialName: name);
} else if (T is StoragePath) {
return AddStoragePathPage(initialName: name);
}
throw ArgumentError();
}
}
@@ -0,0 +1,38 @@
import 'package:flutter/widgets.dart';
import 'package:go_router/go_router.dart';
import 'package:paperless_mobile/features/landing/view/landing_page.dart';
import 'package:paperless_mobile/routes/navigation_keys.dart';
import 'package:paperless_mobile/routes/routes.dart';
part 'landing_route.g.dart';
class LandingBranch extends StatefulShellBranchData {
static final GlobalKey<NavigatorState> $navigatorKey = landingNavigatorKey;
const LandingBranch();
}
@TypedGoRoute<LandingRoute>(
path: "/landing",
name: R.landing,
routes: [
TypedGoRoute<SavedViewRoute>(
path: "saved-view",
name: R.savedView,
),
],
)
class LandingRoute extends GoRouteData {
const LandingRoute();
@override
Widget build(BuildContext context, GoRouterState state) {
return const LandingPage();
}
}
class SavedViewRoute extends GoRouteData {
@override
Widget build(BuildContext context, GoRouterState state) {
return Placeholder();
}
}
@@ -0,0 +1,82 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.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/routes/navigation_keys.dart';
import 'package:paperless_mobile/routes/routes.dart';
part 'scanner_route.g.dart';
// @TypedStatefulShellBranch<ScannerBranch>(
// routes: [
// TypedGoRoute<ScannerRoute>(
// path: "/scanner",
// name: R.scanner,
// routes: [
// TypedGoRoute<DocumentUploadRoute>(
// path: "upload",
// name: R.uploadDocument,
// ),
// ],
// ),
// ],
// )
class ScannerBranch extends StatefulShellBranchData {
static final GlobalKey<NavigatorState> $navigatorKey = scannerNavigatorKey;
const ScannerBranch();
}
@TypedGoRoute<ScannerRoute>(
path: "/scanner",
name: R.scanner,
routes: [
TypedGoRoute<DocumentUploadRoute>(
path: "upload",
name: R.uploadDocument,
),
],
)
class ScannerRoute extends GoRouteData {
const ScannerRoute();
@override
Widget build(BuildContext context, GoRouterState state) {
return const ScannerPage();
}
}
class DocumentUploadRoute extends GoRouteData {
static final GlobalKey<NavigatorState> $parentNavigatorKey = rootNavigatorKey;
final Uint8List $extra;
final String? title;
final String? filename;
final String? fileExtension;
const DocumentUploadRoute({
required this.$extra,
this.title,
this.filename,
this.fileExtension,
});
@override
Widget build(BuildContext context, GoRouterState state) {
return BlocProvider(
create: (_) => DocumentUploadCubit(
context.read(),
context.read(),
context.read(),
),
child: DocumentUploadPreparationPage(
title: title,
fileExtension: fileExtension,
filename: filename,
fileBytes: $extra,
),
);
}
}
@@ -0,0 +1,72 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
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_account.dart';
import 'package:paperless_mobile/core/factory/paperless_api_factory.dart';
import 'package:paperless_mobile/features/home/view/home_shell_widget.dart';
import 'package:paperless_mobile/routes/navigation_keys.dart';
//part 'provider_shell_route.g.dart';
//TODO: Wait for https://github.com/flutter/flutter/issues/127371 to be merged
// @TypedShellRoute<ProviderShellRoute>(
// routes: [
// TypedStatefulShellRoute(
// branches: [
// TypedStatefulShellBranch<LandingBranch>(
// routes: [
// TypedGoRoute<LandingRoute>(
// path: "/landing",
// // name: R.landing,
// )
// ],
// ),
// TypedStatefulShellBranch<DocumentsBranch>(
// routes: [
// TypedGoRoute<DocumentsRoute>(
// path: "/documents",
// routes: [
// TypedGoRoute<DocumentDetailsRoute>(
// path: "details",
// // name: R.documentDetails,
// ),
// TypedGoRoute<DocumentEditRoute>(
// path: "edit",
// // name: R.editDocument,
// ),
// ],
// )
// ],
// ),
// ],
// ),
// ],
// )
class ProviderShellRoute extends ShellRouteData {
final PaperlessApiFactory apiFactory;
static final GlobalKey<NavigatorState> $navigatorKey = rootNavigatorKey;
const ProviderShellRoute(this.apiFactory);
Widget build(
BuildContext context,
GoRouterState state,
Widget navigator,
) {
final currentUserId = Hive.box<GlobalSettings>(HiveBoxes.globalSettings)
.getValue()!
.loggedInUserId!;
final authenticatedUser =
Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount).get(
currentUserId,
)!;
return HomeShellWidget(
localUserId: authenticatedUser.id,
paperlessApiVersion: authenticatedUser.apiVersion,
paperlessProviderFactory: apiFactory,
child: navigator,
);
}
}
@@ -0,0 +1,29 @@
import 'package:flutter/widgets.dart';
import 'package:go_router/go_router.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_account.dart';
import 'package:paperless_mobile/features/home/view/scaffold_with_navigation_bar.dart';
class ScaffoldShellRoute extends StatefulShellRouteData {
const ScaffoldShellRoute();
@override
Widget builder(
BuildContext context,
GoRouterState state,
StatefulNavigationShell navigationShell,
) {
final currentUserId = Hive.box<GlobalSettings>(HiveBoxes.globalSettings)
.getValue()!
.loggedInUserId!;
final authenticatedUser =
Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount).get(
currentUserId,
)!;
return ScaffoldWithNavigationBar(
authenticatedUser: authenticatedUser.paperlessUser,
navigationShell: navigationShell,
);
}
}
@@ -0,0 +1,30 @@
import 'dart:async';
import 'package:flutter/src/widgets/framework.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'package:paperless_mobile/features/login/cubit/authentication_cubit.dart';
import 'package:paperless_mobile/features/login/view/login_page.dart';
import 'package:paperless_mobile/routes/routes.dart';
part 'login_route.g.dart';
@TypedGoRoute<LoginRoute>(
path: "/login",
name: R.login,
)
class LoginRoute extends GoRouteData {
const LoginRoute();
@override
Widget build(BuildContext context, GoRouterState state) {
return const LoginPage();
}
@override
FutureOr<String?> redirect(BuildContext context, GoRouterState state) {
if (context.read<AuthenticationCubit>().state.isAuthenticated) {
return "/landing";
}
return null;
}
}
@@ -0,0 +1,17 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:paperless_mobile/features/settings/view/settings_page.dart';
import 'package:paperless_mobile/routes/routes.dart';
part 'settings_route.g.dart';
@TypedGoRoute<SettingsRoute>(
path: "/settings",
name: R.settings,
)
class SettingsRoute extends GoRouteData {
@override
Widget build(BuildContext context, GoRouterState state) {
return const SettingsPage();
}
}
@@ -0,0 +1,18 @@
import 'package:flutter/widgets.dart';
import 'package:go_router/go_router.dart';
import 'package:paperless_mobile/features/settings/view/pages/switching_accounts_page.dart';
import 'package:paperless_mobile/routes/routes.dart';
part 'switching_accounts_route.g.dart';
@TypedGoRoute<SwitchingAccountsRoute>(
path: '/switching-accounts',
name: R.switchingAccounts,
)
class SwitchingAccountsRoute extends GoRouteData {
const SwitchingAccountsRoute();
@override
Widget build(BuildContext context, GoRouterState state) {
return const SwitchingAccountsPage();
}
}
@@ -0,0 +1,19 @@
import 'package:go_router/go_router.dart';
import 'package:flutter/widgets.dart';
import 'package:paperless_mobile/features/home/view/widget/verify_identity_page.dart';
import 'package:paperless_mobile/routes/routes.dart';
part 'verify_identity_route.g.dart';
@TypedGoRoute<VerifyIdentityRoute>(
path: '/verify-identity',
name: R.verifyIdentity,
)
class VerifyIdentityRoute extends GoRouteData {
const VerifyIdentityRoute();
@override
Widget build(BuildContext context, GoRouterState state) {
return const VerifyIdentityPage();
}
}
@@ -1,6 +1,6 @@
import 'package:json_annotation/json_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
part 'paperless_server_exception.g.dart'; part 'paperless_server_message_exception.g.dart';
@JsonSerializable(createToJson: false) @JsonSerializable(createToJson: false)
class PaperlessServerMessageException implements Exception { class PaperlessServerMessageException implements Exception {
@@ -13,5 +13,5 @@ class PaperlessServerMessageException implements Exception {
} }
factory PaperlessServerMessageException.fromJson(Map<String, dynamic> json) => factory PaperlessServerMessageException.fromJson(Map<String, dynamic> json) =>
_$PaperlessServerExceptionFromJson(json); _$PaperlessServerMessageExceptionFromJson(json);
} }
@@ -1,74 +0,0 @@
import 'package:json_annotation/json_annotation.dart';
import 'package:paperless_api/src/converters/local_date_time_json_converter.dart';
import 'package:paperless_api/src/models/labels/label_model.dart';
import 'package:paperless_api/src/models/labels/matching_algorithm.dart';
part 'correspondent_model.g.dart';
@LocalDateTimeJsonConverter()
@JsonSerializable(includeIfNull: false, fieldRename: FieldRename.snake)
class Correspondent extends Label {
final DateTime? lastCorrespondence;
const Correspondent({
this.lastCorrespondence,
required super.name,
super.id,
super.slug,
super.match,
super.matchingAlgorithm,
super.isInsensitive,
super.documentCount,
super.owner,
super.userCanChange,
});
factory Correspondent.fromJson(Map<String, dynamic> json) =>
_$CorrespondentFromJson(json);
@override
Map<String, dynamic> toJson() => _$CorrespondentToJson(this);
@override
String toString() {
return name;
}
@override
Correspondent copyWith({
int? id,
String? name,
String? slug,
String? match,
MatchingAlgorithm? matchingAlgorithm,
bool? isInsensitive,
int? documentCount,
DateTime? lastCorrespondence,
}) {
return Correspondent(
id: id ?? this.id,
name: name ?? this.name,
documentCount: documentCount ?? documentCount,
isInsensitive: isInsensitive ?? isInsensitive,
match: match ?? this.match,
matchingAlgorithm: matchingAlgorithm ?? this.matchingAlgorithm,
slug: slug ?? this.slug,
lastCorrespondence: lastCorrespondence ?? this.lastCorrespondence,
);
}
@override
String get queryEndpoint => 'correspondents';
@override
List<Object?> get props => [
id,
name,
slug,
isInsensitive,
documentCount,
lastCorrespondence,
matchingAlgorithm,
match,
];
}
@@ -1,59 +0,0 @@
import 'package:json_annotation/json_annotation.dart';
import 'package:paperless_api/src/models/labels/label_model.dart';
import 'package:paperless_api/src/models/labels/matching_algorithm.dart';
part 'document_type_model.g.dart';
@JsonSerializable(includeIfNull: false, fieldRename: FieldRename.snake)
class DocumentType extends Label {
const DocumentType({
super.id,
required super.name,
super.slug,
super.match,
super.matchingAlgorithm,
super.isInsensitive,
super.documentCount,
super.owner,
super.userCanChange,
});
factory DocumentType.fromJson(Map<String, dynamic> json) => _$DocumentTypeFromJson(json);
@override
String get queryEndpoint => 'document_types';
@override
DocumentType copyWith({
int? id,
String? name,
String? match,
MatchingAlgorithm? matchingAlgorithm,
bool? isInsensitive,
int? documentCount,
String? slug,
}) {
return DocumentType(
id: id ?? this.id,
name: name ?? this.name,
match: match ?? this.match,
matchingAlgorithm: matchingAlgorithm ?? this.matchingAlgorithm,
isInsensitive: isInsensitive ?? this.isInsensitive,
documentCount: documentCount ?? this.documentCount,
slug: slug ?? this.slug,
);
}
@override
Map<String, dynamic> toJson() => _$DocumentTypeToJson(this);
@override
List<Object?> get props => [
id,
name,
slug,
isInsensitive,
documentCount,
matchingAlgorithm,
match,
];
}
@@ -1,7 +1,14 @@
import 'dart:ui';
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:paperless_api/src/converters/hex_color_json_converter.dart';
import 'package:paperless_api/src/converters/local_date_time_json_converter.dart';
import 'package:paperless_api/src/models/labels/matching_algorithm.dart'; import 'package:paperless_api/src/models/labels/matching_algorithm.dart';
abstract class Label extends Equatable implements Comparable { part 'label_model.g.dart';
sealed class Label extends Equatable implements Comparable {
static const idKey = "id"; static const idKey = "id";
static const nameKey = "name"; static const nameKey = "name";
static const slugKey = "slug"; static const slugKey = "slug";
@@ -56,3 +63,278 @@ abstract class Label extends Equatable implements Comparable {
Map<String, dynamic> toJson(); Map<String, dynamic> toJson();
} }
@LocalDateTimeJsonConverter()
@JsonSerializable(includeIfNull: false, fieldRename: FieldRename.snake)
class Correspondent extends Label {
final DateTime? lastCorrespondence;
const Correspondent({
this.lastCorrespondence,
required super.name,
super.id,
super.slug,
super.match,
super.matchingAlgorithm,
super.isInsensitive,
super.documentCount,
super.owner,
super.userCanChange,
});
factory Correspondent.fromJson(Map<String, dynamic> json) =>
_$CorrespondentFromJson(json);
@override
Map<String, dynamic> toJson() => _$CorrespondentToJson(this);
@override
String toString() {
return name;
}
@override
Correspondent copyWith({
int? id,
String? name,
String? slug,
String? match,
MatchingAlgorithm? matchingAlgorithm,
bool? isInsensitive,
int? documentCount,
DateTime? lastCorrespondence,
}) {
return Correspondent(
id: id ?? this.id,
name: name ?? this.name,
documentCount: documentCount ?? documentCount,
isInsensitive: isInsensitive ?? isInsensitive,
match: match ?? this.match,
matchingAlgorithm: matchingAlgorithm ?? this.matchingAlgorithm,
slug: slug ?? this.slug,
lastCorrespondence: lastCorrespondence ?? this.lastCorrespondence,
);
}
@override
String get queryEndpoint => 'correspondents';
@override
List<Object?> get props => [
id,
name,
slug,
isInsensitive,
documentCount,
lastCorrespondence,
matchingAlgorithm,
match,
];
}
@JsonSerializable(includeIfNull: false, fieldRename: FieldRename.snake)
class DocumentType extends Label {
const DocumentType({
super.id,
required super.name,
super.slug,
super.match,
super.matchingAlgorithm,
super.isInsensitive,
super.documentCount,
super.owner,
super.userCanChange,
});
factory DocumentType.fromJson(Map<String, dynamic> json) =>
_$DocumentTypeFromJson(json);
@override
String get queryEndpoint => 'document_types';
@override
DocumentType copyWith({
int? id,
String? name,
String? match,
MatchingAlgorithm? matchingAlgorithm,
bool? isInsensitive,
int? documentCount,
String? slug,
}) {
return DocumentType(
id: id ?? this.id,
name: name ?? this.name,
match: match ?? this.match,
matchingAlgorithm: matchingAlgorithm ?? this.matchingAlgorithm,
isInsensitive: isInsensitive ?? this.isInsensitive,
documentCount: documentCount ?? this.documentCount,
slug: slug ?? this.slug,
);
}
@override
Map<String, dynamic> toJson() => _$DocumentTypeToJson(this);
@override
List<Object?> get props => [
id,
name,
slug,
isInsensitive,
documentCount,
matchingAlgorithm,
match,
];
}
@JsonSerializable(includeIfNull: false, fieldRename: FieldRename.snake)
class StoragePath extends Label {
static const pathKey = 'path';
final String path;
const StoragePath({
super.id,
required super.name,
required this.path,
super.slug,
super.match,
super.matchingAlgorithm,
super.isInsensitive,
super.documentCount,
super.owner,
super.userCanChange,
});
factory StoragePath.fromJson(Map<String, dynamic> json) =>
_$StoragePathFromJson(json);
@override
String toString() {
return name;
}
@override
StoragePath copyWith({
int? id,
String? name,
String? slug,
String? match,
MatchingAlgorithm? matchingAlgorithm,
bool? isInsensitive,
int? documentCount,
String? path,
}) {
return StoragePath(
id: id ?? this.id,
name: name ?? this.name,
documentCount: documentCount ?? documentCount,
isInsensitive: isInsensitive ?? isInsensitive,
path: path ?? this.path,
match: match ?? this.match,
matchingAlgorithm: matchingAlgorithm ?? this.matchingAlgorithm,
slug: slug ?? this.slug,
);
}
@override
String get queryEndpoint => 'storage_paths';
@override
List<Object?> get props => [
id,
name,
slug,
isInsensitive,
documentCount,
path,
matchingAlgorithm,
match,
];
@override
Map<String, dynamic> toJson() => _$StoragePathToJson(this);
}
@HexColorJsonConverter()
@JsonSerializable(
fieldRename: FieldRename.snake,
explicitToJson: true,
)
class Tag extends Label {
static const colorKey = 'color';
static const isInboxTagKey = 'is_inbox_tag';
static const textColorKey = 'text_color';
static const legacyColourKey = 'colour';
final Color? textColor;
final Color? color;
final bool isInboxTag;
const Tag({
super.id,
required super.name,
super.documentCount,
super.isInsensitive,
super.match,
super.matchingAlgorithm = MatchingAlgorithm.defaultValue,
super.slug,
this.color,
this.textColor,
this.isInboxTag = false,
super.owner,
super.userCanChange,
});
@override
String toString() => name;
@override
Tag copyWith({
int? id,
String? name,
String? match,
MatchingAlgorithm? matchingAlgorithm,
bool? isInsensitive,
int? documentCount,
String? slug,
Color? color,
Color? textColor,
bool? isInboxTag,
}) {
return Tag(
id: id ?? this.id,
name: name ?? this.name,
match: match ?? this.match,
matchingAlgorithm: matchingAlgorithm ?? this.matchingAlgorithm,
isInsensitive: isInsensitive ?? this.isInsensitive,
documentCount: documentCount ?? this.documentCount,
slug: slug ?? this.slug,
color: color ?? this.color,
textColor: textColor ?? this.textColor,
isInboxTag: isInboxTag ?? this.isInboxTag,
);
}
@override
String get queryEndpoint => 'tags';
@override
List<Object?> get props => [
id,
name,
slug,
isInsensitive,
documentCount,
matchingAlgorithm,
color,
textColor,
isInboxTag,
match,
];
factory Tag.fromJson(Map<String, dynamic> json) => _$TagFromJson(json);
@override
Map<String, dynamic> toJson() => _$TagToJson(this);
}
@@ -1,71 +0,0 @@
import 'package:json_annotation/json_annotation.dart';
import 'package:paperless_api/src/models/labels/label_model.dart';
import 'package:paperless_api/src/models/labels/matching_algorithm.dart';
part 'storage_path_model.g.dart';
@JsonSerializable(includeIfNull: false, fieldRename: FieldRename.snake)
class StoragePath extends Label {
static const pathKey = 'path';
final String path;
const StoragePath({
super.id,
required super.name,
required this.path,
super.slug,
super.match,
super.matchingAlgorithm,
super.isInsensitive,
super.documentCount,
super.owner,
super.userCanChange,
});
factory StoragePath.fromJson(Map<String, dynamic> json) => _$StoragePathFromJson(json);
@override
String toString() {
return name;
}
@override
StoragePath copyWith({
int? id,
String? name,
String? slug,
String? match,
MatchingAlgorithm? matchingAlgorithm,
bool? isInsensitive,
int? documentCount,
String? path,
}) {
return StoragePath(
id: id ?? this.id,
name: name ?? this.name,
documentCount: documentCount ?? documentCount,
isInsensitive: isInsensitive ?? isInsensitive,
path: path ?? this.path,
match: match ?? this.match,
matchingAlgorithm: matchingAlgorithm ?? this.matchingAlgorithm,
slug: slug ?? this.slug,
);
}
@override
String get queryEndpoint => 'storage_paths';
@override
List<Object?> get props => [
id,
name,
slug,
isInsensitive,
documentCount,
path,
matchingAlgorithm,
match,
];
@override
Map<String, dynamic> toJson() => _$StoragePathToJson(this);
}
@@ -1,91 +0,0 @@
import 'dart:ui';
import 'package:json_annotation/json_annotation.dart';
import 'package:paperless_api/src/converters/hex_color_json_converter.dart';
import 'package:paperless_api/src/models/labels/label_model.dart';
import 'package:paperless_api/src/models/labels/matching_algorithm.dart';
part 'tag_model.g.dart';
@HexColorJsonConverter()
@JsonSerializable(
fieldRename: FieldRename.snake,
explicitToJson: true,
)
class Tag extends Label {
static const colorKey = 'color';
static const isInboxTagKey = 'is_inbox_tag';
static const textColorKey = 'text_color';
static const legacyColourKey = 'colour';
final Color? textColor;
final Color? color;
final bool isInboxTag;
const Tag({
super.id,
required super.name,
super.documentCount,
super.isInsensitive,
super.match,
super.matchingAlgorithm = MatchingAlgorithm.defaultValue,
super.slug,
this.color,
this.textColor,
this.isInboxTag = false,
super.owner,
super.userCanChange,
});
@override
String toString() => name;
@override
Tag copyWith({
int? id,
String? name,
String? match,
MatchingAlgorithm? matchingAlgorithm,
bool? isInsensitive,
int? documentCount,
String? slug,
Color? color,
Color? textColor,
bool? isInboxTag,
}) {
return Tag(
id: id ?? this.id,
name: name ?? this.name,
match: match ?? this.match,
matchingAlgorithm: matchingAlgorithm ?? this.matchingAlgorithm,
isInsensitive: isInsensitive ?? this.isInsensitive,
documentCount: documentCount ?? this.documentCount,
slug: slug ?? this.slug,
color: color ?? this.color,
textColor: textColor ?? this.textColor,
isInboxTag: isInboxTag ?? this.isInboxTag,
);
}
@override
String get queryEndpoint => 'tags';
@override
List<Object?> get props => [
id,
name,
slug,
isInsensitive,
documentCount,
matchingAlgorithm,
color,
textColor,
isInboxTag,
match,
];
factory Tag.fromJson(Map<String, dynamic> json) => _$TagFromJson(json);
@override
Map<String, dynamic> toJson() => _$TagToJson(this);
}
@@ -5,12 +5,8 @@ export 'document_model.dart';
export 'field_suggestions.dart'; export 'field_suggestions.dart';
export 'filter_rule_model.dart'; export 'filter_rule_model.dart';
export 'group_model.dart'; export 'group_model.dart';
export 'labels/correspondent_model.dart';
export 'labels/document_type_model.dart';
export 'labels/label_model.dart'; export 'labels/label_model.dart';
export 'labels/matching_algorithm.dart'; export 'labels/matching_algorithm.dart';
export 'labels/storage_path_model.dart';
export 'labels/tag_model.dart';
export 'paged_search_result.dart'; export 'paged_search_result.dart';
export 'paperless_api_exception.dart'; export 'paperless_api_exception.dart';
export 'paperless_server_information_model.dart'; export 'paperless_server_information_model.dart';
@@ -81,4 +81,10 @@ extension UserPermissionExtension on UserModel {
hasPermission(PermissionAction.add, PermissionTarget.storagePath); hasPermission(PermissionAction.add, PermissionTarget.storagePath);
bool get canCreateSavedViews => bool get canCreateSavedViews =>
hasPermission(PermissionAction.add, PermissionTarget.savedView); hasPermission(PermissionAction.add, PermissionTarget.savedView);
bool get canViewAnyLabel =>
canViewCorrespondents ||
canViewDocumentTypes ||
canViewTags ||
canViewStoragePaths;
} }
@@ -1,7 +1,5 @@
import 'package:paperless_api/src/models/labels/correspondent_model.dart';
import 'package:paperless_api/src/models/labels/document_type_model.dart'; import 'package:paperless_api/src/models/models.dart';
import 'package:paperless_api/src/models/labels/storage_path_model.dart';
import 'package:paperless_api/src/models/labels/tag_model.dart';
/// ///
/// Provides basic CRUD operations for labels, including: /// Provides basic CRUD operations for labels, including:
@@ -3,10 +3,7 @@ import 'dart:io';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:paperless_api/src/extensions/dio_exception_extension.dart'; import 'package:paperless_api/src/extensions/dio_exception_extension.dart';
import 'package:paperless_api/src/models/labels/correspondent_model.dart'; import 'package:paperless_api/src/models/models.dart';
import 'package:paperless_api/src/models/labels/document_type_model.dart';
import 'package:paperless_api/src/models/labels/storage_path_model.dart';
import 'package:paperless_api/src/models/labels/tag_model.dart';
import 'package:paperless_api/src/models/paperless_api_exception.dart'; import 'package:paperless_api/src/models/paperless_api_exception.dart';
import 'package:paperless_api/src/modules/labels_api/paperless_labels_api.dart'; import 'package:paperless_api/src/modules/labels_api/paperless_labels_api.dart';
import 'package:paperless_api/src/request_utils.dart'; import 'package:paperless_api/src/request_utils.dart';
+56 -40
View File
@@ -317,10 +317,10 @@ packages:
dependency: "direct dev" dependency: "direct dev"
description: description:
name: dart_code_metrics name: dart_code_metrics
sha256: "1dc1fa763b73ed52147bd91b015d81903edc3f227b77b1672fcddba43390ed18" sha256: "3dede3f7abc077a4181ec7445448a289a9ce08e2981e6a4d49a3fb5099d47e1f"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.7.5" version: "5.7.6"
dart_code_metrics_presets: dart_code_metrics_presets:
dependency: transitive dependency: transitive
description: description:
@@ -381,18 +381,18 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: dio name: dio
sha256: a9d76e72985d7087eb7c5e7903224ae52b337131518d127c554b9405936752b8 sha256: "3866d67f93523161b643187af65f5ac08bc991a5bcdaf41a2d587fe4ccb49993"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.2.1+1" version: "5.3.0"
dots_indicator: dots_indicator:
dependency: transitive dependency: transitive
description: description:
name: dots_indicator name: dots_indicator
sha256: "58b6a365744aa62aa1b70c4ea29e5106fbe064f5edaf7e9652e9b856edbfd9bb" sha256: f1599baa429936ba87f06ae5f2adc920a367b16d08f74db58c3d0f6e93bcdb5c
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.0" version: "2.1.2"
dynamic_color: dynamic_color:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -727,18 +727,18 @@ packages:
dependency: "direct dev" dependency: "direct dev"
description: description:
name: freezed name: freezed
sha256: a9520490532087cf38bf3f7de478ab6ebeb5f68bb1eb2641546d92719b224445 sha256: "2df89855fe181baae3b6d714dc3c4317acf4fccd495a6f36e5e00f24144c6c3b"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.5" version: "2.4.1"
freezed_annotation: freezed_annotation:
dependency: "direct main" dependency: "direct main"
description: description:
name: freezed_annotation name: freezed_annotation
sha256: aeac15850ef1b38ee368d4c53ba9a847e900bb2c53a4db3f6881cbb3cb684338 sha256: c3fd9336eb55a38cc1bbd79ab17573113a8deccd0ecbbf926cca3c62803b5c2d
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.2.0" version: "2.4.1"
frontend_server_client: frontend_server_client:
dependency: transitive dependency: transitive
description: description:
@@ -760,6 +760,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.2" version: "2.1.2"
go_router:
dependency: "direct main"
description:
name: go_router
sha256: b3cadd2cd59a4103fd5f6bc572ca75111264698784e927aa471921c3477d5475
url: "https://pub.dev"
source: hosted
version: "10.0.0"
go_router_builder:
dependency: "direct dev"
description:
name: go_router_builder
sha256: df2034629637d0c7c380aba5daa2f91be4733f2d632e7dff0b082d5ff3155068
url: "https://pub.dev"
source: hosted
version: "2.2.4"
graphs: graphs:
dependency: transitive dependency: transitive
description: description:
@@ -865,10 +881,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: introduction_screen name: introduction_screen
sha256: f39be426026785b8fea4ed93e226e7fc28ef49a4c78c3f86c958bae26dabef00 sha256: ef5a5479a8e06a84b9a7eff16c698b9b82f70cd1b6203b264bc3686f9bfb77e2
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.1.9" version: "3.1.11"
io: io:
dependency: transitive dependency: transitive
description: description:
@@ -1143,10 +1159,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: path_provider_foundation name: path_provider_foundation
sha256: "1995d88ec2948dac43edf8fe58eb434d35d22a2940ecee1a9fefcd62beee6eb3" sha256: "916731ccbdce44d545414dd9961f26ba5fbaa74bcbb55237d8e65a623a8c7297"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.2.3" version: "2.2.4"
path_provider_linux: path_provider_linux:
dependency: transitive dependency: transitive
description: description:
@@ -1183,34 +1199,34 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: permission_handler name: permission_handler
sha256: "415af30ba76a84faccfe1eb251fe1e4fdc790f876924c65ad7d6ed7a1404bcd6" sha256: "63e5216aae014a72fe9579ccd027323395ce7a98271d9defa9d57320d001af81"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "10.4.2" version: "10.4.3"
permission_handler_android: permission_handler_android:
dependency: transitive dependency: transitive
description: description:
name: permission_handler_android name: permission_handler_android
sha256: "3b61f3da3b1c83bc3fb6a2b431e8dab01d0e5b45f6a3d9c7609770ec88b2a89e" sha256: "2ffaf52a21f64ac9b35fe7369bb9533edbd4f698e5604db8645b1064ff4cf221"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "10.3.0" version: "10.3.3"
permission_handler_apple: permission_handler_apple:
dependency: transitive dependency: transitive
description: description:
name: permission_handler_apple name: permission_handler_apple
sha256: "7a187b671a39919462af2b5e813148365b71a615979165a119868d667fe90c03" sha256: "99e220bce3f8877c78e4ace901082fb29fa1b4ebde529ad0932d8d664b34f3f5"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "9.1.3" version: "9.1.4"
permission_handler_platform_interface: permission_handler_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: permission_handler_platform_interface name: permission_handler_platform_interface
sha256: "463a07cb7cc6c758a7a1c7da36ce666bb80a0b4b5e92df0fa36872e0ed456993" sha256: "7c6b1500385dd1d2ca61bb89e2488ca178e274a69144d26bbd65e33eae7c02a9"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.11.1" version: "3.11.3"
permission_handler_windows: permission_handler_windows:
dependency: transitive dependency: transitive
description: description:
@@ -1247,10 +1263,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: plugin_platform_interface name: plugin_platform_interface
sha256: "6a2128648c854906c53fa8e33986fc0247a1116122f9534dd20e3ab9e16a32bc" sha256: "43798d895c929056255600343db8f049921cbec94d31ec87f1dc5c16c01935dd"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.4" version: "2.1.5"
pointer_interceptor: pointer_interceptor:
dependency: transitive dependency: transitive
description: description:
@@ -1468,10 +1484,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: sliver_tools name: sliver_tools
sha256: ccdc502098a8bfa07b3ec582c282620031481300035584e1bb3aca296a505e8c sha256: eae28220badfb9d0559207badcbbc9ad5331aac829a88cb0964d330d2a4636a6
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.2.10" version: "0.2.12"
source_gen: source_gen:
dependency: transitive dependency: transitive
description: description:
@@ -1516,18 +1532,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: sqflite name: sqflite
sha256: b4d6710e1200e96845747e37338ea8a819a12b51689a3bcf31eff0003b37a0b9 sha256: "591f1602816e9c31377d5f008c2d9ef7b8aca8941c3f89cc5fd9d84da0c38a9a"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.2.8+4" version: "2.3.0"
sqflite_common: sqflite_common:
dependency: transitive dependency: transitive
description: description:
name: sqflite_common name: sqflite_common
sha256: "8f7603f3f8f126740bc55c4ca2d1027aab4b74a1267a3e31ce51fe40e3b65b8f" sha256: "1b92f368f44b0dee2425bb861cfa17b6f6cf3961f762ff6f941d20b33355660a"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.4.5+1" version: "2.5.0"
stack_trace: stack_trace:
dependency: transitive dependency: transitive
description: description:
@@ -1652,10 +1668,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: url_launcher_android name: url_launcher_android
sha256: "15f5acbf0dce90146a0f5a2c4a002b1814a6303c4c5c075aa2623b2d16156f03" sha256: "78cb6dea3e93148615109e58e42c35d1ffbf5ef66c44add673d0ab75f12ff3af"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.0.36" version: "6.0.37"
url_launcher_ios: url_launcher_ios:
dependency: transitive dependency: transitive
description: description:
@@ -1676,10 +1692,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: url_launcher_macos name: url_launcher_macos
sha256: "91ee3e75ea9dadf38036200c5d3743518f4a5eb77a8d13fda1ee5764373f185e" sha256: "1c4fdc0bfea61a70792ce97157e5cc17260f61abbe4f39354513f39ec6fd73b1"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.5" version: "3.0.6"
url_launcher_platform_interface: url_launcher_platform_interface:
dependency: transitive dependency: transitive
description: description:
@@ -1772,26 +1788,26 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: webview_flutter_android name: webview_flutter_android
sha256: "1c93e96f3069bacdc734fad6b7e1d3a480fd516a3ae5b8858becf7f07515a2f3" sha256: d936a09fbfd08cb78f7329e0bbacf6158fbdfe24ffc908b22444c07d295eb193
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.8.2" version: "3.9.2"
webview_flutter_platform_interface: webview_flutter_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: webview_flutter_platform_interface name: webview_flutter_platform_interface
sha256: "656e2aeaef318900fffd21468b6ddc7958c7092a642f0e7220bac328b70d4a81" sha256: "564ef378cafc1a0e29f1d76ce175ef517a0a6115875dff7b43fccbef2b0aeb30"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.1" version: "2.4.0"
webview_flutter_wkwebview: webview_flutter_wkwebview:
dependency: transitive dependency: transitive
description: description:
name: webview_flutter_wkwebview name: webview_flutter_wkwebview
sha256: a8d7e8b4be2a79e83b70235369971ec97d14df4cdbb40d305a8eeae67d8e6432 sha256: "5fa098f28b606f699e8ca52d9e4e11edbbfef65189f5f77ae92703ba5408fd25"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.6.2" version: "3.7.2"
win32: win32:
dependency: transitive dependency: transitive
description: description:
+2
View File
@@ -90,6 +90,7 @@ dependencies:
webview_flutter: ^4.2.1 webview_flutter: ^4.2.1
printing: ^5.11.0 printing: ^5.11.0
flutter_pdfview: ^1.3.1 flutter_pdfview: ^1.3.1
go_router: ^10.0.0
dependency_overrides: dependency_overrides:
intl: ^0.18.1 intl: ^0.18.1
@@ -113,6 +114,7 @@ dev_dependencies:
hive_generator: ^2.0.0 hive_generator: ^2.0.0
mock_server: mock_server:
path: packages/mock_server path: packages/mock_server
go_router_builder: ^2.2.4
# For information on the generic Dart part of this file, see the # For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec # following page: https://dart.dev/tools/pub/pubspec