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

View File

@@ -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();
}
}

View File

@@ -21,7 +21,7 @@ class GlobalSettings with HiveObjectMixin {
bool showOnboarding;
@HiveField(4)
String? currentLoggedInUser;
String? loggedInUserId;
@HiveField(5)
FileDownloadType defaultDownloadType;
@@ -37,7 +37,7 @@ class GlobalSettings with HiveObjectMixin {
this.preferredThemeMode = ThemeMode.system,
this.preferredColorSchemeOption = ColorSchemeOption.classic,
this.showOnboarding = true,
this.currentLoggedInUser,
this.loggedInUserId,
this.defaultDownloadType = FileDownloadType.alwaysAsk,
this.defaultShareType = FileDownloadType.alwaysAsk,
this.enforceSinglePagePdfUpload = false,

View File

@@ -20,16 +20,16 @@ class LocalUserAccount extends HiveObject {
@HiveField(7)
UserModel paperlessUser;
@HiveField(8, defaultValue: 2)
int apiVersion;
LocalUserAccount({
required this.id,
required this.serverUrl,
required this.settings,
required this.paperlessUser,
required this.apiVersion,
});
static LocalUserAccount get current =>
Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount).get(
Hive.box<GlobalSettings>(HiveBoxes.globalSettings)
.getValue()!
.currentLoggedInUser)!;
bool get hasMultiUserSupport => apiVersion >= 3;
}

View File

@@ -43,7 +43,7 @@ class LocalUserAppState extends HiveObject {
final currentLocalUserId =
Hive.box<GlobalSettings>(HiveBoxes.globalSettings)
.getValue()!
.currentLoggedInUser!;
.loggedInUserId!;
return Hive.box<LocalUserAppState>(HiveBoxes.localUserAppState)
.get(currentLocalUserId)!;
}

View File

@@ -4,11 +4,13 @@ import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:go_router/go_router.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/database/tables/local_user_app_state.dart';
import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
@@ -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/view/saved_view_details_page.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
import 'package:paperless_mobile/routes/document_details_route.dart';
import 'package:provider/provider.dart';
// These are convenience methods for nativating to views without having to pass providers around explicitly.
@@ -38,59 +39,18 @@ import 'package:provider/provider.dart';
Future<void> pushDocumentSearchPage(BuildContext context) {
final currentUser = Hive.box<GlobalSettings>(HiveBoxes.globalSettings)
.getValue()!
.currentLoggedInUser;
.loggedInUserId;
final userRepo = context.read<UserRepository>();
return Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => MultiProvider(
providers: [
Provider.value(value: context.read<LabelRepository>()),
Provider.value(value: context.read<PaperlessDocumentsApi>()),
Provider.value(value: context.read<DocumentChangedNotifier>()),
Provider.value(value: context.read<CacheManager>()),
Provider.value(value: userRepo),
],
builder: (context, _) {
return BlocProvider(
create: (context) => DocumentSearchCubit(
context.read(),
context.read(),
Hive.box<LocalUserAppState>(HiveBoxes.localUserAppState)
.get(currentUser)!,
),
child: const DocumentSearchPage(),
);
},
),
),
);
}
Future<void> pushDocumentDetailsRoute(
BuildContext context, {
required DocumentModel document,
bool isLabelClickable = true,
bool allowEdit = true,
String? titleAndContentQueryString,
}) {
return Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => MultiProvider(
providers: [
Provider.value(value: context.read<ApiVersion>()),
Provider.value(value: context.read<LabelRepository>()),
Provider.value(value: context.read<DocumentChangedNotifier>()),
Provider.value(value: context.read<PaperlessDocumentsApi>()),
Provider.value(value: context.read<LocalNotificationService>()),
Provider.value(value: context.read<CacheManager>()),
Provider.value(value: context.read<ConnectivityCubit>()),
if (context.read<ApiVersion>().hasMultiUserSupport)
Provider.value(value: context.read<UserRepository>()),
],
child: DocumentDetailsRoute(
document: document,
isLabelClickable: isLabelClickable,
builder: (_) => BlocProvider(
create: (context) => DocumentSearchCubit(
context.read(),
context.read(),
Hive.box<LocalUserAppState>(HiveBoxes.localUserAppState)
.get(currentUser)!,
),
child: const DocumentSearchPage(),
),
),
);
@@ -106,7 +66,7 @@ Future<void> pushSavedViewDetailsRoute(
builder: (_) => MultiProvider(
providers: [
Provider.value(value: apiVersion),
if (apiVersion.hasMultiUserSupport)
if (context.watch<LocalUserAccount>().hasMultiUserSupport)
Provider.value(value: context.read<UserRepository>()),
Provider.value(value: context.read<LabelRepository>()),
Provider.value(value: context.read<DocumentChangedNotifier>()),
@@ -147,8 +107,10 @@ Future<SavedView?> pushAddSavedViewRoute(BuildContext context,
);
}
Future<void> pushLinkedDocumentsView(BuildContext context,
{required DocumentFilter filter}) {
Future<void> pushLinkedDocumentsView(
BuildContext context, {
required DocumentFilter filter,
}) {
return Navigator.push(
context,
MaterialPageRoute(
@@ -161,7 +123,7 @@ Future<void> pushLinkedDocumentsView(BuildContext context,
Provider.value(value: context.read<LocalNotificationService>()),
Provider.value(value: context.read<CacheManager>()),
Provider.value(value: context.read<ConnectivityCubit>()),
if (context.read<ApiVersion>().hasMultiUserSupport)
if (context.watch<LocalUserAccount>().hasMultiUserSupport)
Provider.value(value: context.read<UserRepository>()),
],
builder: (context, _) => BlocProvider(

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/settings/view/settings_page.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:url_launcher/url_launcher_string.dart';
@@ -91,18 +92,7 @@ class AppDrawer extends StatelessWidget {
title: Text(
S.of(context)!.settings,
),
onTap: () => Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => MultiProvider(
providers: [
Provider.value(
value: context.read<PaperlessServerStatsApi>()),
Provider.value(value: context.read<ApiVersion>()),
],
child: const SettingsPage(),
),
),
),
onTap: () => SettingsRoute().push(context),
),
],
),

View File

@@ -45,7 +45,6 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
),
),
);
loadSuggestions();
loadMetaData();
}
@@ -54,13 +53,6 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
_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 {
final metaData = await _api.getMetaData(state.document);
if (!isClosed) {

View File

@@ -7,7 +7,6 @@ class DocumentDetailsState with _$DocumentDetailsState {
DocumentMetaData? metaData,
@Default(false) bool isFullContentLoaded,
String? fullContent,
FieldSuggestions? suggestions,
@Default({}) Map<int, Correspondent> correspondents,
@Default({}) Map<int, DocumentType> documentTypes,
@Default({}) Map<int, Tag> tags,

View File

@@ -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_permissions_widget.dart';
import 'package:paperless_mobile/features/document_details/view/widgets/document_share_button.dart';
import 'package:paperless_mobile/features/document_edit/cubit/document_edit_cubit.dart';
import 'package:paperless_mobile/features/document_edit/view/document_edit_page.dart';
import 'package:paperless_mobile/features/documents/view/pages/document_view.dart';
import 'package:paperless_mobile/features/documents/view/widgets/delete_document_confirmation_dialog.dart';
import 'package:paperless_mobile/features/documents/view/widgets/document_preview.dart';
@@ -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/generated/l10n/app_localizations.dart';
import 'package:paperless_mobile/helpers/message_helpers.dart';
import 'package:paperless_mobile/routes/typed/branches/documents_route.dart';
class DocumentDetailsPage extends StatefulWidget {
final bool isLabelClickable;
@@ -46,9 +45,9 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
@override
Widget build(BuildContext context) {
final apiVersion = context.watch<ApiVersion>();
final tabLength = 4 + (apiVersion.hasMultiUserSupport ? 1 : 0);
final hasMultiUserSupport =
context.watch<LocalUserAccount>().hasMultiUserSupport;
final tabLength = 4 + (hasMultiUserSupport ? 1 : 0);
return WillPopScope(
onWillPop: () async {
Navigator.of(context)
@@ -171,7 +170,7 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
),
),
),
if (apiVersion.hasMultiUserSupport)
if (hasMultiUserSupport)
Tab(
child: Text(
"Permissions",
@@ -259,7 +258,7 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
),
],
),
if (apiVersion.hasMultiUserSupport)
if (hasMultiUserSupport)
CustomScrollView(
controller: _pagingScrollController,
slivers: [
@@ -286,8 +285,10 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
}
Widget _buildEditButton() {
final currentUser = context.watch<LocalUserAccount>();
bool canEdit = context.watchInternetConnection &&
LocalUserAccount.current.paperlessUser.canEditDocuments;
currentUser.paperlessUser.canEditDocuments;
if (!canEdit) {
return const SizedBox.shrink();
}
@@ -302,7 +303,7 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
verticalOffset: 40,
child: FloatingActionButton(
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>(
builder: (context, connectivityState) {
final isConnected = connectivityState.isConnected;
final canDelete = isConnected &&
LocalUserAccount.current.paperlessUser.canDeleteDocuments;
final currentUser = context.watch<LocalUserAccount>();
final canDelete =
isConnected && currentUser.paperlessUser.canDeleteDocuments;
return Row(
mainAxisAlignment: MainAxisAlignment.start,
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 {
final status =
await context.read<DocumentDetailsCubit>().openDocumentInSystemViewer();

View File

@@ -47,7 +47,7 @@ class _ArchiveSerialNumberFieldState extends State<ArchiveSerialNumberField> {
@override
Widget build(BuildContext context) {
final userCanEditDocument =
LocalUserAccount.current.paperlessUser.canEditDocuments;
context.watch<LocalUserAccount>().paperlessUser.canEditDocuments;
return BlocListener<DocumentDetailsCubit, DocumentDetailsState>(
listenWhen: (previous, current) =>
previous.document.archiveSerialNumber !=

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:intl/intl.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
@@ -47,7 +48,10 @@ class DocumentOverviewWidget extends StatelessWidget {
label: S.of(context)!.createdAt,
).paddedOnly(bottom: itemSpacing),
if (document.documentType != null &&
LocalUserAccount.current.paperlessUser.canViewDocumentTypes)
context
.watch<LocalUserAccount>()
.paperlessUser
.canViewDocumentTypes)
DetailsItem(
label: S.of(context)!.documentType,
content: LabelText<DocumentType>(
@@ -56,7 +60,10 @@ class DocumentOverviewWidget extends StatelessWidget {
),
).paddedOnly(bottom: itemSpacing),
if (document.correspondent != null &&
LocalUserAccount.current.paperlessUser.canViewCorrespondents)
context
.watch<LocalUserAccount>()
.paperlessUser
.canViewCorrespondents)
DetailsItem(
label: S.of(context)!.correspondent,
content: LabelText<Correspondent>(
@@ -65,7 +72,10 @@ class DocumentOverviewWidget extends StatelessWidget {
),
).paddedOnly(bottom: itemSpacing),
if (document.storagePath != null &&
LocalUserAccount.current.paperlessUser.canViewStoragePaths)
context
.watch<LocalUserAccount>()
.paperlessUser
.canViewStoragePaths)
DetailsItem(
label: S.of(context)!.storagePath,
content: LabelText<StoragePath>(
@@ -73,7 +83,7 @@ class DocumentOverviewWidget extends StatelessWidget {
),
).paddedOnly(bottom: itemSpacing),
if (document.tags.isNotEmpty &&
LocalUserAccount.current.paperlessUser.canViewTags)
context.watch<LocalUserAccount>().paperlessUser.canViewTags)
DetailsItem(
label: S.of(context)!.tags,
content: Padding(

View File

@@ -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) {
emit(state.copyWith(document: document));
}

View File

@@ -4,6 +4,7 @@ part of 'document_edit_cubit.dart';
class DocumentEditState with _$DocumentEditState {
const factory DocumentEditState({
required DocumentModel document,
FieldSuggestions? suggestions,
@Default({}) Map<int, Correspondent> correspondents,
@Default({}) Map<int, DocumentType> documentTypes,
@Default({}) Map<int, StoragePath> storagePaths,

View File

@@ -22,10 +22,8 @@ import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
import 'package:paperless_mobile/helpers/message_helpers.dart';
class DocumentEditPage extends StatefulWidget {
final FieldSuggestions? suggestions;
const DocumentEditPage({
Key? key,
required this.suggestions,
}) : super(key: key);
@override
@@ -44,19 +42,12 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
final GlobalKey<FormBuilderState> _formKey = GlobalKey();
bool _isSubmitLoading = false;
late final FieldSuggestions? _filteredSuggestions;
@override
void initState() {
super.initState();
_filteredSuggestions = widget.suggestions
?.documentDifference(context.read<DocumentEditCubit>().state.document);
}
@override
Widget build(BuildContext context) {
return BlocBuilder<DocumentEditCubit, DocumentEditState>(
builder: (context, state) {
final filteredSuggestions = state.suggestions?.documentDifference(
context.read<DocumentEditCubit>().state.document);
return DefaultTabController(
length: 2,
child: Scaffold(
@@ -94,8 +85,10 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
ListView(
children: [
_buildTitleFormField(state.document.title).padded(),
_buildCreatedAtFormField(state.document.created)
.padded(),
_buildCreatedAtFormField(
state.document.created,
filteredSuggestions,
).padded(),
// Correspondent form field
Column(
children: [
@@ -123,15 +116,17 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
name: fkCorrespondent,
prefixIcon: const Icon(Icons.person_outlined),
allowSelectUnassigned: true,
canCreateNewLabel: LocalUserAccount.current
.paperlessUser.canCreateCorrespondents,
canCreateNewLabel: context
.watch<LocalUserAccount>()
.paperlessUser
.canCreateCorrespondents,
),
if (_filteredSuggestions
if (filteredSuggestions
?.hasSuggestedCorrespondents ??
false)
_buildSuggestionsSkeleton<int>(
suggestions:
_filteredSuggestions!.correspondents,
filteredSuggestions!.correspondents,
itemBuilder: (context, itemData) =>
ActionChip(
label: Text(
@@ -160,8 +155,10 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
initialName: currentInput,
),
),
canCreateNewLabel: LocalUserAccount.current
.paperlessUser.canCreateDocumentTypes,
canCreateNewLabel: context
.watch<LocalUserAccount>()
.paperlessUser
.canCreateDocumentTypes,
addLabelText: S.of(context)!.addDocumentType,
labelText: S.of(context)!.documentType,
initialValue:
@@ -175,12 +172,12 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
const Icon(Icons.description_outlined),
allowSelectUnassigned: true,
),
if (_filteredSuggestions
if (filteredSuggestions
?.hasSuggestedDocumentTypes ??
false)
_buildSuggestionsSkeleton<int>(
suggestions:
_filteredSuggestions!.documentTypes,
filteredSuggestions!.documentTypes,
itemBuilder: (context, itemData) =>
ActionChip(
label: Text(
@@ -204,10 +201,12 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
RepositoryProvider.value(
value: context.read<LabelRepository>(),
child: AddStoragePathPage(
initalName: initialValue),
initialName: initialValue),
),
canCreateNewLabel: LocalUserAccount.current
.paperlessUser.canCreateStoragePaths,
canCreateNewLabel: context
.watch<LocalUserAccount>()
.paperlessUser
.canCreateStoragePaths,
addLabelText: S.of(context)!.addStoragePath,
labelText: S.of(context)!.storagePath,
options: state.storagePaths,
@@ -232,14 +231,14 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
include: state.document.tags.toList(),
),
).padded(),
if (_filteredSuggestions?.tags
if (filteredSuggestions?.tags
.toSet()
.difference(state.document.tags.toSet())
.isNotEmpty ??
false)
_buildSuggestionsSkeleton<int>(
suggestions:
(_filteredSuggestions?.tags.toSet() ?? {}),
(filteredSuggestions?.tags.toSet() ?? {}),
itemBuilder: (context, itemData) {
final tag = state.tags[itemData]!;
return ActionChip(
@@ -343,7 +342,8 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
);
}
Widget _buildCreatedAtFormField(DateTime? initialCreatedAtDate) {
Widget _buildCreatedAtFormField(
DateTime? initialCreatedAtDate, FieldSuggestions? filteredSuggestions) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -358,9 +358,9 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
format: DateFormat.yMMMMd(),
initialEntryMode: DatePickerEntryMode.calendar,
),
if (_filteredSuggestions?.hasSuggestedDates ?? false)
if (filteredSuggestions?.hasSuggestedDates ?? false)
_buildSuggestionsSkeleton<DateTime>(
suggestions: _filteredSuggestions!.dates,
suggestions: filteredSuggestions!.dates,
itemBuilder: (context, itemData) => ActionChip(
label: Text(DateFormat.yMMMd().format(itemData)),
onPressed: () => _formKey.currentState?.fields[fkCreatedDate]

View File

@@ -91,7 +91,7 @@ class _DocumentSearchBarState extends State<DocumentSearchBar> {
Provider.value(value: context.read<PaperlessDocumentsApi>()),
Provider.value(value: context.read<CacheManager>()),
Provider.value(value: context.read<ApiVersion>()),
if (context.read<ApiVersion>().hasMultiUserSupport)
if (context.watch<LocalUserAccount>().hasMultiUserSupport)
Provider.value(value: context.read<UserRepository>()),
],
child: Provider(
@@ -99,7 +99,7 @@ class _DocumentSearchBarState extends State<DocumentSearchBar> {
context.read(),
context.read(),
Hive.box<LocalUserAppState>(HiveBoxes.localUserAppState)
.get(LocalUserAccount.current.id)!,
.get(context.watch<LocalUserAccount>().id)!,
),
builder: (_, __) => const DocumentSearchPage(),
),
@@ -112,19 +112,7 @@ class _DocumentSearchBarState extends State<DocumentSearchBar> {
IconButton _buildUserAvatar(BuildContext context) {
return IconButton(
padding: const EdgeInsets.all(6),
icon: GlobalSettingsBuilder(
builder: (context, settings) {
return ValueListenableBuilder(
valueListenable:
Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount)
.listenable(),
builder: (context, box, _) {
final account = box.get(settings.currentLoggedInUser!)!;
return UserAvatar(account: account);
},
);
},
),
icon: UserAvatar(account: context.watch<LocalUserAccount>()),
onPressed: () {
final apiVersion = context.read<ApiVersion>();
showDialog(

View File

@@ -4,6 +4,7 @@ import 'dart:math' as math;
import 'package:collection/collection.dart';
import 'package:flutter/material.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/extensions/flutter_extensions.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/selection/view_type_selection_widget.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
import 'package:paperless_mobile/routes/typed/branches/documents_route.dart';
class DocumentSearchPage extends StatefulWidget {
const DocumentSearchPage({super.key});
@@ -218,11 +220,8 @@ class _DocumentSearchPageState extends State<DocumentSearchPage> {
hasLoaded: state.hasLoaded,
enableHeroAnimation: false,
onTap: (document) {
pushDocumentDetailsRoute(
context,
document: document,
isLabelClickable: false,
);
DocumentDetailsRoute($extra: document, isLabelClickable: false)
.push(context);
},
)
],

View File

@@ -25,7 +25,7 @@ class SliverSearchBar extends StatelessWidget {
@override
Widget build(BuildContext context) {
if (LocalUserAccount.current.paperlessUser.canViewDocuments) {
if (context.watch<LocalUserAccount>().paperlessUser.canViewDocuments) {
return SliverAppBar(
toolbarHeight: kToolbarHeight,
flexibleSpace: Container(
@@ -49,7 +49,7 @@ class SliverSearchBar extends StatelessWidget {
Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount)
.listenable(),
builder: (context, box, _) {
final account = box.get(settings.currentLoggedInUser!)!;
final account = box.get(settings.loggedInUserId!)!;
return UserAvatar(account: account);
},
);

View File

@@ -198,8 +198,10 @@ class _DocumentUploadPreparationPageState
),
),
// Correspondent
if (LocalUserAccount
.current.paperlessUser.canViewCorrespondents)
if (context
.watch<LocalUserAccount>()
.paperlessUser
.canViewCorrespondents)
LabelFormField<Correspondent>(
showAnyAssignedOption: false,
showNotAssignedOption: false,
@@ -220,11 +222,16 @@ class _DocumentUploadPreparationPageState
options: state.correspondents,
prefixIcon: const Icon(Icons.person_outline),
allowSelectUnassigned: true,
canCreateNewLabel: LocalUserAccount
.current.paperlessUser.canCreateCorrespondents,
canCreateNewLabel: context
.watch<LocalUserAccount>()
.paperlessUser
.canCreateCorrespondents,
),
// Document type
if (LocalUserAccount.current.paperlessUser.canViewDocumentTypes)
if (context
.watch<LocalUserAccount>()
.paperlessUser
.canViewDocumentTypes)
LabelFormField<DocumentType>(
showAnyAssignedOption: false,
showNotAssignedOption: false,
@@ -245,10 +252,12 @@ class _DocumentUploadPreparationPageState
options: state.documentTypes,
prefixIcon: const Icon(Icons.description_outlined),
allowSelectUnassigned: true,
canCreateNewLabel: LocalUserAccount
.current.paperlessUser.canCreateDocumentTypes,
canCreateNewLabel: context
.watch<LocalUserAccount>()
.paperlessUser
.canCreateDocumentTypes,
),
if (LocalUserAccount.current.paperlessUser.canViewTags)
if (context.watch<LocalUserAccount>().paperlessUser.canViewTags)
TagsFormField(
name: DocumentModel.tagsKey,
allowCreation: true,
@@ -296,7 +305,7 @@ class _DocumentUploadPreparationPageState
),
userId: Hive.box<GlobalSettings>(HiveBoxes.globalSettings)
.getValue()!
.currentLoggedInUser!,
.loggedInUserId!,
title: title,
documentType: docType,
correspondent: correspondent,

View File

@@ -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/generated/l10n/app_localizations.dart';
import 'package:paperless_mobile/helpers/message_helpers.dart';
import 'package:paperless_mobile/routes/typed/branches/documents_route.dart';
class DocumentFilterIntent {
final DocumentFilter? filter;
@@ -55,7 +56,7 @@ class _DocumentsPageState extends State<DocumentsPage>
void initState() {
super.initState();
final showSavedViews =
LocalUserAccount.current.paperlessUser.canViewSavedViews;
context.read<LocalUserAccount>().paperlessUser.canViewSavedViews;
_tabController = TabController(
length: showSavedViews ? 2 : 1,
vsync: this,
@@ -116,7 +117,7 @@ class _DocumentsPageState extends State<DocumentsPage>
return SafeArea(
top: true,
child: Scaffold(
drawer: const AppDrawer(),
drawer: AppDrawer(),
floatingActionButton: BlocBuilder<DocumentsCubit, DocumentsState>(
builder: (context, state) {
final appliedFiltersCount = state.filter.appliedFiltersCount;
@@ -232,7 +233,9 @@ class _DocumentsPageState extends State<DocumentsPage>
controller: _tabController,
tabs: [
Tab(text: S.of(context)!.documents),
if (LocalUserAccount.current.paperlessUser
if (context
.watch<LocalUserAccount>()
.paperlessUser
.canViewSavedViews)
Tab(text: S.of(context)!.views),
],
@@ -276,8 +279,10 @@ class _DocumentsPageState extends State<DocumentsPage>
);
},
),
if (LocalUserAccount
.current.paperlessUser.canViewSavedViews)
if (context
.watch<LocalUserAccount>()
.paperlessUser
.canViewSavedViews)
Builder(
builder: (context) {
return _buildSavedViewsTab(
@@ -378,7 +383,9 @@ class _DocumentsPageState extends State<DocumentsPage>
final allowToggleFilter = state.selection.isEmpty;
return SliverAdaptiveDocumentsView(
viewType: state.viewType,
onTap: _openDetails,
onTap: (document) {
DocumentDetailsRoute($extra: document).push(context);
},
onSelected:
context.read<DocumentsCubit>().toggleDocumentSelection,
hasInternetConnection: connectivityState.isConnected,
@@ -488,13 +495,6 @@ class _DocumentsPageState extends State<DocumentsPage>
}
}
void _openDetails(DocumentModel document) {
pushDocumentDetailsRoute(
context,
document: document,
);
}
void _addTagToFilter(int tagId) {
final cubit = context.read<DocumentsCubit>();
try {

View File

@@ -2,7 +2,12 @@ import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_html/flutter_html.dart';
import 'package:hive_flutter/adapters.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/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/documents/view/widgets/document_preview.dart';
@@ -32,6 +37,12 @@ class DocumentDetailedItem extends DocumentItem {
@override
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 insets = MediaQuery.of(context).viewInsets;
final padding = MediaQuery.of(context).viewPadding;
@@ -104,48 +115,51 @@ class DocumentDetailedItem extends DocumentItem {
maxLines: 2,
overflow: TextOverflow.ellipsis,
).paddedLTRB(8, 0, 8, 4),
Row(
children: [
const Icon(
Icons.person_outline,
size: 16,
).paddedOnly(right: 4.0),
CorrespondentWidget(
onSelected: onCorrespondentSelected,
textStyle: Theme.of(context).textTheme.titleSmall?.apply(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
correspondent: context
.watch<LabelRepository>()
.state
.correspondents[document.correspondent],
),
],
).paddedLTRB(8, 0, 8, 4),
Row(
children: [
const Icon(
Icons.description_outlined,
size: 16,
).paddedOnly(right: 4.0),
DocumentTypeWidget(
onSelected: onDocumentTypeSelected,
textStyle: Theme.of(context).textTheme.titleSmall?.apply(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
documentType: context
.watch<LabelRepository>()
.state
.documentTypes[document.documentType],
),
],
).paddedLTRB(8, 0, 8, 4),
TagsWidget(
tags: document.tags
.map((e) => context.watch<LabelRepository>().state.tags[e]!)
.toList(),
onTagSelected: onTagSelected,
).padded(),
if (paperlessUser.canViewCorrespondents)
Row(
children: [
const Icon(
Icons.person_outline,
size: 16,
).paddedOnly(right: 4.0),
CorrespondentWidget(
onSelected: onCorrespondentSelected,
textStyle: Theme.of(context).textTheme.titleSmall?.apply(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
correspondent: context
.watch<LabelRepository>()
.state
.correspondents[document.correspondent],
),
],
).paddedLTRB(8, 0, 8, 4),
if (paperlessUser.canViewDocumentTypes)
Row(
children: [
const Icon(
Icons.description_outlined,
size: 16,
).paddedOnly(right: 4.0),
DocumentTypeWidget(
onSelected: onDocumentTypeSelected,
textStyle: Theme.of(context).textTheme.titleSmall?.apply(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
documentType: context
.watch<LabelRepository>()
.state
.documentTypes[document.documentType],
),
],
).paddedLTRB(8, 0, 8, 4),
if (paperlessUser.canViewTags)
TagsWidget(
tags: document.tags
.map((e) => context.watch<LabelRepository>().state.tags[e]!)
.toList(),
onTagSelected: onTagSelected,
).padded(),
if (highlights != null)
Html(
data: '<p>${highlights!}</p>',

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
@@ -160,8 +161,10 @@ class _DocumentFilterFormState extends State<DocumentFilterForm> {
initialValue: widget.initialFilter.documentType,
prefixIcon: const Icon(Icons.description_outlined),
allowSelectUnassigned: false,
canCreateNewLabel:
LocalUserAccount.current.paperlessUser.canCreateDocumentTypes,
canCreateNewLabel: context
.watch<LocalUserAccount>()
.paperlessUser
.canCreateDocumentTypes,
);
}
@@ -173,8 +176,10 @@ class _DocumentFilterFormState extends State<DocumentFilterForm> {
initialValue: widget.initialFilter.correspondent,
prefixIcon: const Icon(Icons.person_outline),
allowSelectUnassigned: false,
canCreateNewLabel:
LocalUserAccount.current.paperlessUser.canCreateCorrespondents,
canCreateNewLabel: context
.watch<LocalUserAccount>()
.paperlessUser
.canCreateCorrespondents,
);
}
@@ -187,7 +192,7 @@ class _DocumentFilterFormState extends State<DocumentFilterForm> {
prefixIcon: const Icon(Icons.folder_outlined),
allowSelectUnassigned: false,
canCreateNewLabel:
LocalUserAccount.current.paperlessUser.canCreateStoragePaths,
context.watch<LocalUserAccount>().paperlessUser.canCreateStoragePaths,
);
}

View File

@@ -7,8 +7,8 @@ import 'package:paperless_mobile/features/labels/storage_path/view/widgets/stora
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
class AddStoragePathPage extends StatelessWidget {
final String? initalName;
const AddStoragePathPage({Key? key, this.initalName}) : super(key: key);
final String? initialName;
const AddStoragePathPage({Key? key, this.initialName}) : super(key: key);
@override
Widget build(BuildContext context) {
@@ -19,7 +19,7 @@ class AddStoragePathPage extends StatelessWidget {
child: AddLabelPage<StoragePath>(
pageTitle: Text(S.of(context)!.addStoragePath),
fromJsonT: StoragePath.fromJson,
initialName: initalName,
initialName: initialName,
onSubmit: (context, label) =>
context.read<EditLabelCubit>().addStoragePath(label),
additionalFields: const [

View File

@@ -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';
class AddTagPage extends StatelessWidget {
final String? initialValue;
const AddTagPage({Key? key, this.initialValue}) : super(key: key);
final String? initialName;
const AddTagPage({Key? key, this.initialName}) : super(key: key);
@override
Widget build(BuildContext context) {
@@ -22,7 +22,7 @@ class AddTagPage extends StatelessWidget {
child: AddLabelPage<Tag>(
pageTitle: Text(S.of(context)!.addTag),
fromJsonT: Tag.fromJson,
initialName: initialValue,
initialName: initialName,
onSubmit: (context, label) =>
context.read<EditLabelCubit>().addTag(label),
additionalFields: [

View File

@@ -24,8 +24,10 @@ class EditCorrespondentPage extends StatelessWidget {
context.read<EditLabelCubit>().replaceCorrespondent(label),
onDelete: (context, label) =>
context.read<EditLabelCubit>().removeCorrespondent(label),
canDelete:
LocalUserAccount.current.paperlessUser.canDeleteCorrespondents,
canDelete: context
.watch<LocalUserAccount>()
.paperlessUser
.canDeleteCorrespondents,
);
}),
);

View File

@@ -22,8 +22,10 @@ class EditDocumentTypePage extends StatelessWidget {
context.read<EditLabelCubit>().replaceDocumentType(label),
onDelete: (context, label) =>
context.read<EditLabelCubit>().removeDocumentType(label),
canDelete:
LocalUserAccount.current.paperlessUser.canDeleteDocumentTypes,
canDelete: context
.watch<LocalUserAccount>()
.paperlessUser
.canDeleteDocumentTypes,
),
);
}

View File

@@ -23,7 +23,10 @@ class EditStoragePathPage extends StatelessWidget {
context.read<EditLabelCubit>().replaceStoragePath(label),
onDelete: (context, label) =>
context.read<EditLabelCubit>().removeStoragePath(label),
canDelete: LocalUserAccount.current.paperlessUser.canDeleteStoragePaths,
canDelete: context
.watch<LocalUserAccount>()
.paperlessUser
.canDeleteStoragePaths,
additionalFields: [
StoragePathAutofillFormBuilderField(
name: StoragePath.pathKey,

View File

@@ -26,7 +26,8 @@ class EditTagPage extends StatelessWidget {
context.read<EditLabelCubit>().replaceTag(label),
onDelete: (context, label) =>
context.read<EditLabelCubit>().removeTag(label),
canDelete: LocalUserAccount.current.paperlessUser.canDeleteTags,
canDelete:
context.watch<LocalUserAccount>().paperlessUser.canDeleteTags,
additionalFields: [
FormBuilderColorPickerField(
initialValue: tag.color,

View File

@@ -3,6 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
import 'package:paperless_mobile/core/translation/matching_algorithm_localization_mapper.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.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) {
List<MatchingAlgorithm> selectableMatchingAlgorithmValues =
getSelectableMatchingAlgorithmValues(
context.watch<ApiVersion>().hasMultiUserSupport,
context.watch<LocalUserAccount>().hasMultiUserSupport,
);
return Scaffold(
resizeToAvoidBottomInset: false,

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);
}
}
}

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),
);
},
);
},
);
},
);
}
}

View File

@@ -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,
);
},
);
},
);
},
);
},
);
}
}

View File

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

View File

@@ -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,
);
}
}

View File

@@ -61,6 +61,7 @@ class InboxCubit extends HydratedCubit<InboxState>
Future<void> initialize() async {
await refreshItemsInInboxCount(false);
await loadInbox();
super.initialize();
}
Future<void> refreshItemsInInboxCount([bool shouldLoadInbox = true]) async {

View File

@@ -39,7 +39,7 @@ class _InboxPageState extends State<InboxPage>
@override
Widget build(BuildContext context) {
final canEditDocument =
LocalUserAccount.current.paperlessUser.canEditDocuments;
context.watch<LocalUserAccount>().paperlessUser.canEditDocuments;
return Scaffold(
drawer: const AppDrawer(),
floatingActionButton: BlocBuilder<InboxCubit, InboxState>(

View File

@@ -1,6 +1,7 @@
import 'package:collection/collection.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/core/database/tables/local_user_account.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/view/widgets/label_text.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
import 'package:paperless_mobile/routes/typed/branches/documents_route.dart';
class InboxItemPlaceholder extends StatelessWidget {
const InboxItemPlaceholder({super.key});
@@ -150,11 +152,10 @@ class _InboxItemState extends State<InboxItem> {
return GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () {
pushDocumentDetailsRoute(
context,
document: widget.document,
DocumentDetailsRoute(
$extra: widget.document,
isLabelClickable: false,
);
).push(context);
},
child: SizedBox(
height: 200,
@@ -238,8 +239,9 @@ class _InboxItemState extends State<InboxItem> {
}
Widget _buildActions(BuildContext context) {
final canEdit = LocalUserAccount.current.paperlessUser.canEditDocuments;
final canDelete = LocalUserAccount.current.paperlessUser.canDeleteDocuments;
final currentUser = context.watch<LocalUserAccount>().paperlessUser;
final canEdit = currentUser.canEditDocuments;
final canDelete = currentUser.canDeleteDocuments;
final chipShape = RoundedRectangleBorder(
borderRadius: BorderRadius.circular(32),
);

View File

@@ -191,7 +191,7 @@ class _FullscreenTagsFormState extends State<FullscreenTagsForm> {
final createdTag = await Navigator.of(context).push<Tag?>(
MaterialPageRoute(
builder: (context) => AddTagPage(
initialValue: _textEditingController.text,
initialName: _textEditingController.text,
),
),
);

View File

@@ -1,6 +1,7 @@
import 'package:animations/animations.dart';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
@@ -73,7 +74,7 @@ class TagsFormField extends StatelessWidget {
initialValue: field.value,
allowOnlySelection: allowOnlySelection,
allowCreation: allowCreation &&
LocalUserAccount.current.paperlessUser.canCreateTags,
context.watch<LocalUserAccount>().paperlessUser.canCreateTags,
allowExclude: allowExclude,
),
onClosed: (data) {

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/local_user_account.dart';
import 'package:paperless_mobile/core/delegate/customizable_sliver_persistent_header_delegate.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/widgets/material/colored_tab_bar.dart';
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/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/view/widgets/label_tab_view.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 {
const LabelsPage({Key? key}) : super(key: key);
@@ -52,7 +42,7 @@ class _LabelsPageState extends State<LabelsPage>
@override
void initState() {
super.initState();
final user = LocalUserAccount.current.paperlessUser;
final user = context.read<LocalUserAccount>().paperlessUser;
_tabController = TabController(
length: _calculateTabCount(user), vsync: this)
..addListener(() => setState(() => _currentIndex = _tabController.index));
@@ -67,7 +57,7 @@ class _LabelsPageState extends State<LabelsPage>
final currentUserId =
Hive.box<GlobalSettings>(HiveBoxes.globalSettings)
.getValue()!
.currentLoggedInUser;
.loggedInUserId;
final user = box.get(currentUserId)!.paperlessUser;
return BlocBuilder<ConnectivityCubit, ConnectivityState>(
@@ -77,10 +67,14 @@ class _LabelsPageState extends State<LabelsPage>
drawer: const AppDrawer(),
floatingActionButton: FloatingActionButton(
onPressed: [
if (user.canViewCorrespondents) _openAddCorrespondentPage,
if (user.canViewDocumentTypes) _openAddDocumentTypePage,
if (user.canViewTags) _openAddTagPage,
if (user.canViewStoragePaths) _openAddStoragePathPage,
if (user.canViewCorrespondents)
() => CreateLabelRoute<Correspondent>().push(context),
if (user.canViewDocumentTypes)
() => CreateLabelRoute<DocumentType>().push(context),
if (user.canViewTags)
() => CreateLabelRoute<Tag>().push(context),
if (user.canViewStoragePaths)
() => CreateLabelRoute<StoragePath>().push(context),
][_currentIndex],
child: const Icon(Icons.add),
),
@@ -213,144 +207,13 @@ class _LabelsPageState extends State<LabelsPage>
controller: _tabController,
children: [
if (user.canViewCorrespondents)
Builder(
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,
),
],
);
},
),
_buildCorrespondentsView(state, user),
if (user.canViewDocumentTypes)
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: _openEditDocumentTypePage,
emptyStateActionButtonLabel: S
.of(context)!
.addNewDocumentType,
emptyStateDescription: S
.of(context)!
.noDocumentTypesSetUp,
onAddNew: _openAddDocumentTypePage,
),
],
);
},
),
_buildDocumentTypesView(state, user),
if (user.canViewTags)
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: _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,
),
],
);
},
),
_buildTagsView(state, user),
if (user.canViewStoragePaths)
Builder(
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,
),
],
);
},
),
_buildStoragePathView(state, user),
],
),
),
@@ -365,73 +228,121 @@ class _LabelsPageState extends State<LabelsPage>
});
}
void _openEditCorrespondentPage(Correspondent correspondent) {
Navigator.push(
context,
_buildLabelPageRoute(EditCorrespondentPage(correspondent: correspondent)),
Widget _buildCorrespondentsView(LabelState state, UserModel user) {
return Builder(
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: (correspondent) {
EditLabelRoute(correspondent).push(context);
},
emptyStateActionButtonLabel: S.of(context)!.addNewCorrespondent,
emptyStateDescription: S.of(context)!.noCorrespondentsSetUp,
onAddNew: () => CreateLabelRoute<Correspondent>().push(context),
),
],
);
},
);
}
void _openEditDocumentTypePage(DocumentType docType) {
Navigator.push(
context,
_buildLabelPageRoute(EditDocumentTypePage(documentType: docType)),
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),
),
],
);
},
);
}
void _openEditTagPage(Tag tag) {
Navigator.push(
context,
_buildLabelPageRoute(EditTagPage(tag: tag)),
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),
),
],
);
},
);
}
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,
),
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),
),
],
);
},
);
}
}

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
import 'package:paperless_mobile/core/navigation/push_routes.dart';
@@ -36,7 +37,7 @@ class LabelItem<T extends Label> extends StatelessWidget {
Widget _buildReferencedDocumentsWidget(BuildContext context) {
final canOpen = (label.documentCount ?? 0) > 0 &&
LocalUserAccount.current.paperlessUser.canViewDocuments;
context.watch<LocalUserAccount>().paperlessUser.canViewDocuments;
return TextButton.icon(
label: const Icon(Icons.link),
icon: Text(formatMaxCount(label.documentCount)),

View File

@@ -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,
),
),
),
],
),
),
),
);
}
}

View File

@@ -1,5 +1,6 @@
import 'package:flutter/material.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/navigation/push_routes.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/paged_document_view/view/document_paging_view_mixin.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
import 'package:paperless_mobile/routes/typed/branches/documents_route.dart';
class LinkedDocumentsPage extends StatefulWidget {
const LinkedDocumentsPage({super.key});
@@ -51,11 +53,10 @@ class _LinkedDocumentsPageState extends State<LinkedDocumentsPage>
isLoading: state.isLoading,
hasLoaded: state.hasLoaded,
onTap: (document) {
pushDocumentDetailsRoute(
context,
document: document,
DocumentDetailsRoute(
$extra: document,
isLabelClickable: false,
);
).push(context);
},
),
],

View File

@@ -55,12 +55,11 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
// Mark logged in user as currently active user.
final globalSettings =
Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!;
globalSettings.currentLoggedInUser = localUserId;
globalSettings.loggedInUserId = localUserId;
await globalSettings.save();
emit(
AuthenticationState.authenticated(
apiVersion: apiVersion,
localUserId: localUserId,
),
);
@@ -75,7 +74,8 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
emit(const AuthenticationState.switchingAccounts());
final globalSettings =
Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!;
if (globalSettings.currentLoggedInUser == localUserId) {
if (globalSettings.loggedInUserId == localUserId) {
emit(AuthenticationState.authenticated(localUserId: localUserId));
return;
}
final userAccountBox =
@@ -112,7 +112,7 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
baseUrl: account.serverUrl,
);
globalSettings.currentLoggedInUser = localUserId;
globalSettings.loggedInUserId = localUserId;
await globalSettings.save();
final apiVersion = await _getApiVersion(_sessionManager.client);
@@ -126,7 +126,6 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
emit(AuthenticationState.authenticated(
localUserId: localUserId,
apiVersion: apiVersion,
));
});
}
@@ -175,13 +174,14 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
);
final globalSettings =
Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!;
final localUserId = globalSettings.currentLoggedInUser;
final localUserId = globalSettings.loggedInUserId;
if (localUserId == null) {
_debugPrintMessage(
"restoreSessionState",
"There is nothing to restore.",
);
// If there is nothing to restore, we can quit here.
emit(const AuthenticationState.unauthenticated());
return;
}
final localUserAccountBox =
@@ -223,7 +223,7 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
final authentication =
await withEncryptedBox<UserCredentials, UserCredentials>(
HiveBoxes.localUserCredentials, (box) {
return box.get(globalSettings.currentLoggedInUser!);
return box.get(globalSettings.loggedInUserId!);
});
if (authentication == null) {
@@ -261,7 +261,6 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
);
emit(
AuthenticationState.authenticated(
apiVersion: apiVersion,
localUserId: localUserId,
),
);
@@ -279,7 +278,7 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
await _resetExternalState();
final globalSettings =
Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!;
globalSettings.currentLoggedInUser = null;
globalSettings.loggedInUserId = null;
await globalSettings.save();
emit(const AuthenticationState.unauthenticated());
@@ -389,6 +388,7 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
settings: LocalUserSettings(),
serverUrl: serverUrl,
paperlessUser: serverUser,
apiVersion: apiVersion,
),
);
_debugPrintMessage(

View File

@@ -2,12 +2,18 @@ part of 'authentication_cubit.dart';
@freezed
class AuthenticationState with _$AuthenticationState {
const AuthenticationState._();
const factory AuthenticationState.unauthenticated() = _Unauthenticated;
const factory AuthenticationState.requriresLocalAuthentication() =
_RequiresLocalAuthentication;
const factory AuthenticationState.authenticated({
required String localUserId,
required int apiVersion,
}) = _Authenticated;
const factory AuthenticationState.switchingAccounts() = _SwitchingAccounts;
bool get isAuthenticated => maybeWhen(
authenticated: (_) => true,
orElse: () => false,
);
}

View File

@@ -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);
}
}
}
}

View File

@@ -1,161 +1,78 @@
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:go_router/go_router.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/core/database/tables/global_settings.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/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/features/login/view/add_account_page.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.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';
import 'widgets/never_scrollable_scroll_behavior.dart';
class LoginPage extends StatelessWidget {
const LoginPage({super.key});
class LoginPage extends StatefulWidget {
final FutureOr<void> Function(
@override
Widget build(BuildContext context) {
return AddAccountPage(
titleString: S.of(context)!.connectToPaperless,
submitText: S.of(context)!.signIn,
onSubmit: _onLogin,
showLocalAccounts: true,
);
}
void _onLogin(
BuildContext context,
String username,
String password,
String serverUrl,
ClientCertificate? clientCertificate,
) onSubmit;
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,
);
},
) async {
try {
await context.read<AuthenticationCubit>().login(
credentials: LoginFormCredentials(
username: username,
password: password,
),
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(
serverUrl: serverUrl,
clientCertificate: clientCertificate,
);
// Show onboarding after first login!
final globalSettings =
Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!;
if (globalSettings.showOnboarding) {
Navigator.push(
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);
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);
}
}
}

View File

@@ -1,5 +1,6 @@
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/core/bloc/connectivity_cubit.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/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/routes/typed/branches/documents_route.dart';
class SavedViewDetailsPage extends StatefulWidget {
final Future<void> Function(SavedView savedView) onDelete;
@@ -28,7 +30,7 @@ class _SavedViewDetailsPageState extends State<SavedViewDetailsPage>
@override
Widget build(BuildContext context) {
final cubit = context.read<SavedViewDetailsCubit>();
final cubit = context.watch<SavedViewDetailsCubit>();
return Scaffold(
appBar: AppBar(
title: Text(cubit.savedView.name),
@@ -76,11 +78,10 @@ class _SavedViewDetailsPageState extends State<SavedViewDetailsPage>
isLoading: state.isLoading,
hasLoaded: state.hasLoaded,
onTap: (document) {
pushDocumentDetailsRoute(
context,
document: document,
DocumentDetailsRoute(
$extra: document,
isLabelClickable: false,
);
).push(context);
},
viewType: state.viewType,
),

View File

@@ -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/login/cubit/authentication_cubit.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/widgets/global_settings_builder.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) {
// This is one of the few places where the currentLoggedInUser can be null
// (exactly after loggin out as the current user to be precise).
if (globalSettings.currentLoggedInUser == null) {
if (globalSettings.loggedInUserId == null) {
return const SizedBox.shrink();
}
return ValueListenableBuilder(
@@ -32,8 +32,7 @@ class ManageAccountsPage extends StatelessWidget {
builder: (context, box, _) {
final userIds = box.keys.toList().cast<String>();
final otherAccounts = userIds
.whereNot(
(element) => element == globalSettings.currentLoggedInUser)
.whereNot((element) => element == globalSettings.loggedInUserId)
.toList();
return SimpleDialog(
insetPadding: const EdgeInsets.all(24),
@@ -54,7 +53,7 @@ class ManageAccountsPage extends StatelessWidget {
children: [
Card(
child: UserAccountListTile(
account: box.get(globalSettings.currentLoggedInUser!)!,
account: box.get(globalSettings.loggedInUserId!)!,
trailing: PopupMenuButton(
icon: const Icon(Icons.more_vert),
itemBuilder: (context) => [
@@ -71,8 +70,7 @@ class ManageAccountsPage extends StatelessWidget {
],
onSelected: (value) async {
if (value == 0) {
final currentUser =
globalSettings.currentLoggedInUser!;
final currentUser = globalSettings.loggedInUserId!;
await context.read<AuthenticationCubit>().logout();
Navigator.of(context).pop();
await context
@@ -117,7 +115,7 @@ class ManageAccountsPage extends StatelessWidget {
// Switch
_onSwitchAccount(
context,
globalSettings.currentLoggedInUser!,
globalSettings.loggedInUserId!,
otherAccounts[index],
);
} else if (value == 1) {
@@ -135,10 +133,10 @@ class ManageAccountsPage extends StatelessWidget {
title: Text(S.of(context)!.addAccount),
leading: const Icon(Icons.person_add),
onTap: () {
_onAddAccount(context, globalSettings.currentLoggedInUser!);
_onAddAccount(context, globalSettings.loggedInUserId!);
},
),
if (context.watch<ApiVersion>().hasMultiUserSupport)
if (context.watch<LocalUserAccount>().hasMultiUserSupport)
ListTile(
leading: const Icon(Icons.admin_panel_settings),
title: Text(S.of(context)!.managePermissions),
@@ -155,7 +153,7 @@ class ManageAccountsPage extends StatelessWidget {
final userId = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => LoginPage(
builder: (context) => AddAccountPage(
titleString: S.of(context)!.addAccount,
onSubmit: (context, username, password, serverUrl,
clientCertificate) async {

View File

@@ -100,14 +100,4 @@ class SettingsPage extends StatelessWidget {
),
);
}
void _goto(Widget page, BuildContext context) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => page,
maintainState: true,
),
);
}
}

View File

@@ -23,7 +23,7 @@ class UserAccountBuilder extends StatelessWidget {
builder: (context, accountBox, _) {
final currentUser = Hive.box<GlobalSettings>(HiveBoxes.globalSettings)
.getValue()!
.currentLoggedInUser;
.loggedInUserId;
if (currentUser != null) {
final account = accountBox.get(currentUser);
return builder(context, account);

View File

@@ -2,13 +2,13 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
import 'package:paperless_mobile/core/navigation/push_routes.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/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/generated/l10n/app_localizations.dart';
import 'package:paperless_mobile/helpers/message_helpers.dart';
import 'package:paperless_mobile/routes/typed/branches/documents_route.dart';
class SimilarDocumentsView extends StatefulWidget {
final ScrollController pagingScrollController;
@@ -64,11 +64,10 @@ class _SimilarDocumentsViewState extends State<SimilarDocumentsView>
hasLoaded: state.hasLoaded,
enableHeroAnimation: false,
onTap: (document) {
pushDocumentDetailsRoute(
context,
document: document,
DocumentDetailsRoute(
$extra: document,
isLabelClickable: false,
);
).push(context);
},
);
},

View File

@@ -9,6 +9,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_displaymode/flutter_displaymode.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:hydrated_bloc/hydrated_bloc.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/security/session_manager.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/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/view/login_page.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/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:path_provider/path_provider.dart';
import 'package:provider/provider.dart';
@@ -138,7 +143,9 @@ void main() async {
});
final apiFactory = PaperlessApiFactoryImpl(sessionManager);
final authenticationCubit =
AuthenticationCubit(localAuthService, apiFactory, sessionManager);
await authenticationCubit.restoreSessionState();
runApp(
MultiProvider(
providers: [
@@ -154,13 +161,10 @@ void main() async {
child: MultiBlocProvider(
providers: [
BlocProvider<ConnectivityCubit>.value(value: connectivityCubit),
BlocProvider(
create: (context) => AuthenticationCubit(
localAuthService, apiFactory, sessionManager),
),
BlocProvider.value(value: authenticationCubit),
],
child: PaperlessMobileEntrypoint(
paperlessProviderFactory: apiFactory,
child: GoRouterShell(
apiFactory: apiFactory,
),
),
),
@@ -182,70 +186,69 @@ void main() async {
});
}
class PaperlessMobileEntrypoint extends StatefulWidget {
final PaperlessApiFactory paperlessProviderFactory;
const PaperlessMobileEntrypoint({
Key? key,
required this.paperlessProviderFactory,
}) : super(key: key);
// class PaperlessMobileEntrypoint extends StatefulWidget {
// final PaperlessApiFactory paperlessProviderFactory;
// const PaperlessMobileEntrypoint({
// Key? key,
// required this.paperlessProviderFactory,
// }) : 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
State<PaperlessMobileEntrypoint> createState() =>
_PaperlessMobileEntrypointState();
State<GoRouterShell> createState() => _GoRouterShellState();
}
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 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> {
class _GoRouterShellState extends State<GoRouterShell> {
@override
void didChangeDependencies() {
super.didChangeDependencies();
@@ -257,7 +260,6 @@ class _AuthenticationWrapperState extends State<AuthenticationWrapper> {
@override
void initState() {
super.initState();
// Activate the highest supported refresh rate on the device
if (Platform.isAndroid) {
_setOptimalDisplayMode();
@@ -281,75 +283,180 @@ class _AuthenticationWrapperState extends State<AuthenticationWrapper> {
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
Widget build(BuildContext context) {
return BlocBuilder<AuthenticationCubit, AuthenticationState>(
builder: (context, authentication) {
return authentication.when(
unauthenticated: () => LoginPage(
titleString: S.of(context)!.connectToPaperless,
submitText: S.of(context)!.signIn,
onSubmit: _onLogin,
showLocalAccounts: true,
),
requriresLocalAuthentication: () => const VerifyIdentityPage(),
authenticated: (localUserId, apiVersion) => HomeRoute(
key: ValueKey(localUserId),
paperlessApiVersion: apiVersion,
paperlessProviderFactory: widget.paperlessProviderFactory,
localUserId: localUserId,
),
switchingAccounts: () => const SwitchingAccountsPage(),
return GlobalSettingsBuilder(
builder: (context, settings) {
return DynamicColorBuilder(
builder: (lightDynamic, darkDynamic) {
return BlocListener<AuthenticationCubit, AuthenticationState>(
listener: (context, state) {
state.when(
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,
),
darkTheme: buildTheme(
brightness: Brightness.dark,
dynamicScheme: darkDynamic,
preferredColorScheme: settings.preferredColorSchemeOption,
),
themeMode: settings.preferredThemeMode,
supportedLocales: S.supportedLocales,
locale: Locale.fromSubtags(
languageCode: settings.preferredLocaleSubtag,
),
localizationsDelegates: S.localizationsDelegates,
),
);
},
);
},
);
}
void _onLogin(
BuildContext context,
String username,
String password,
String serverUrl,
ClientCertificate? clientCertificate,
) async {
try {
await context.read<AuthenticationCubit>().login(
credentials: LoginFormCredentials(
username: username,
password: password,
),
serverUrl: serverUrl,
clientCertificate: clientCertificate,
);
// Show onboarding after first login!
final globalSettings =
Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!;
if (globalSettings.showOnboarding) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const ApplicationIntroSlideshow(),
fullscreenDialog: true,
),
).then((value) {
globalSettings.showOnboarding = false;
globalSettings.save();
});
}
} 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);
}
}
}
// 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
// void didChangeDependencies() {
// super.didChangeDependencies();
// context.read<AuthenticationCubit>().restoreSessionState().then((value) {
// FlutterNativeSplash.remove();
// });
// }
// @override
// void initState() {
// super.initState();
// // Activate the highest supported refresh rate on the device
// if (Platform.isAndroid) {
// _setOptimalDisplayMode();
// }
// initializeDateFormatting();
// }
// Future<void> _setOptimalDisplayMode() async {
// final List<DisplayMode> supported = await FlutterDisplayMode.supported;
// final DisplayMode active = await FlutterDisplayMode.active;
// final List<DisplayMode> sameResolution = supported
// .where((m) => m.width == active.width && m.height == active.height)
// .toList()
// ..sort((a, b) => b.refreshRate.compareTo(a.refreshRate));
// final DisplayMode mostOptimalMode =
// sameResolution.isNotEmpty ? sameResolution.first : active;
// debugPrint('Setting refresh rate to ${mostOptimalMode.refreshRate}');
// 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(),
// );
// },
// );
// }
// }

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,
});
}

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
lib/routes/routes.dart Normal file
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";
}

View File

@@ -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),
);
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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,
),
);
}
}

View File

@@ -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,
);
}
}

View File

@@ -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,
);
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}