fix: Improve receiving shares

This commit is contained in:
Anton Stubenbord
2023-10-03 17:49:38 +02:00
parent 37ed8bbb04
commit ad23df4f89
29 changed files with 529 additions and 348 deletions

View File

@@ -18,7 +18,6 @@ class HiveBoxes {
static const localUserCredentials = 'localUserCredentials'; static const localUserCredentials = 'localUserCredentials';
static const localUserAccount = 'localUserAccount'; static const localUserAccount = 'localUserAccount';
static const localUserAppState = 'localUserAppState'; static const localUserAppState = 'localUserAppState';
static const localUserSettings = 'localUserSettings';
static const hosts = 'hosts'; static const hosts = 'hosts';
} }

View File

@@ -8,7 +8,6 @@ import 'package:paperless_mobile/core/config/hive/hive_config.dart';
import 'package:paperless_mobile/core/database/tables/global_settings.dart'; import 'package:paperless_mobile/core/database/tables/global_settings.dart';
import 'package:paperless_mobile/core/database/tables/local_user_account.dart'; import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
import 'package:paperless_mobile/core/database/tables/local_user_app_state.dart'; import 'package:paperless_mobile/core/database/tables/local_user_app_state.dart';
import 'package:paperless_mobile/core/database/tables/local_user_settings.dart';
/// ///
/// Opens an encrypted box, calls [callback] with the now opened box, awaits /// Opens an encrypted box, calls [callback] with the now opened box, awaits
@@ -53,8 +52,6 @@ extension HiveBoxAccessors on HiveInterface {
box<LocalUserAccount>(HiveBoxes.localUserAccount); box<LocalUserAccount>(HiveBoxes.localUserAccount);
Box<LocalUserAppState> get localUserAppStateBox => Box<LocalUserAppState> get localUserAppStateBox =>
box<LocalUserAppState>(HiveBoxes.localUserAppState); box<LocalUserAppState>(HiveBoxes.localUserAppState);
Box<LocalUserSettings> get localUserSettingsBox =>
box<LocalUserSettings>(HiveBoxes.localUserSettings);
Box<GlobalSettings> get globalSettingsBox => Box<GlobalSettings> get globalSettingsBox =>
box<GlobalSettings>(HiveBoxes.globalSettings); box<GlobalSettings>(HiveBoxes.globalSettings);
} }

View File

@@ -108,7 +108,7 @@ class AppDrawer extends StatelessWidget {
final child = ListTile( final child = ListTile(
dense: true, dense: true,
leading: const Icon(Icons.drive_folder_upload_outlined), leading: const Icon(Icons.drive_folder_upload_outlined),
title: const Text("Upload Queue"), title: const Text("Pending Files"),
onTap: () { onTap: () {
UploadQueueRoute().push(context); UploadQueueRoute().push(context);
}, },

View File

@@ -6,6 +6,7 @@ import 'package:flutter/material.dart';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:open_filex/open_filex.dart'; import 'package:open_filex/open_filex.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart'; import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart'; import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/service/file_description.dart'; import 'package:paperless_mobile/core/service/file_description.dart';
@@ -120,6 +121,7 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
Future<void> downloadDocument({ Future<void> downloadDocument({
bool downloadOriginal = false, bool downloadOriginal = false,
required String locale, required String locale,
required String userId,
}) async { }) async {
if (state.metaData == null) { if (state.metaData == null) {
await loadMetaData(); await loadMetaData();
@@ -141,6 +143,7 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
filePath: filePath, filePath: filePath,
finished: true, finished: true,
locale: locale, locale: locale,
userId: userId,
); );
} }
@@ -150,6 +153,7 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
filePath: filePath, filePath: filePath,
finished: false, finished: false,
locale: locale, locale: locale,
userId: userId,
); );
await _api.downloadToFile( await _api.downloadToFile(
@@ -163,6 +167,7 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
filePath: filePath, filePath: filePath,
finished: true, finished: true,
locale: locale, locale: locale,
userId: userId,
); );
debugPrint("Downloaded file to $filePath"); debugPrint("Downloaded file to $filePath");
} }

View File

@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/config/hive/hive_config.dart'; import 'package:paperless_mobile/core/config/hive/hive_config.dart';
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/document_details/cubit/document_details_cubit.dart'; import 'package:paperless_mobile/features/document_details/cubit/document_details_cubit.dart';
import 'package:paperless_mobile/features/document_details/view/dialogs/select_file_type_dialog.dart'; import 'package:paperless_mobile/features/document_details/view/dialogs/select_file_type_dialog.dart';
@@ -90,9 +91,11 @@ class _DocumentDownloadButtonState extends State<DocumentDownloadButton> {
} }
setState(() => _isDownloadPending = true); setState(() => _isDownloadPending = true);
final userId = context.read<LocalUserAccount>().id;
await context.read<DocumentDetailsCubit>().downloadDocument( await context.read<DocumentDetailsCubit>().downloadDocument(
downloadOriginal: original, downloadOriginal: original,
locale: globalSettings.preferredLocaleSubtag, locale: globalSettings.preferredLocaleSubtag,
userId: userId,
); );
// showSnackBar(context, S.of(context)!.documentSuccessfullyDownloaded); // showSnackBar(context, S.of(context)!.documentSuccessfullyDownloaded);
} on PaperlessApiException catch (error, stackTrace) { } on PaperlessApiException catch (error, stackTrace) {

View File

@@ -305,13 +305,13 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
var mergedDocument = document.copyWith( var mergedDocument = document.copyWith(
title: values[fkTitle], title: values[fkTitle],
created: values[fkCreatedDate], created: values[fkCreatedDate],
documentType: () => (values[fkDocumentType] as IdQueryParameter) documentType: () => (values[fkDocumentType] as IdQueryParameter?)
.whenOrNull(fromId: (id) => id), ?.whenOrNull(fromId: (id) => id),
correspondent: () => (values[fkCorrespondent] as IdQueryParameter) correspondent: () => (values[fkCorrespondent] as IdQueryParameter?)
.whenOrNull(fromId: (id) => id), ?.whenOrNull(fromId: (id) => id),
storagePath: () => (values[fkStoragePath] as IdQueryParameter) storagePath: () => (values[fkStoragePath] as IdQueryParameter?)
.whenOrNull(fromId: (id) => id), ?.whenOrNull(fromId: (id) => id),
tags: (values[fkTags] as IdsTagsQuery).include, tags: (values[fkTags] as IdsTagsQuery?)?.include,
content: values[fkContent], content: values[fkContent],
); );
setState(() { setState(() {

View File

@@ -22,7 +22,7 @@ import 'package:paperless_mobile/features/document_scan/view/widgets/scanned_ima
import 'package:paperless_mobile/features/document_search/view/sliver_search_bar.dart'; import 'package:paperless_mobile/features/document_search/view/sliver_search_bar.dart';
import 'package:paperless_mobile/features/document_upload/view/document_upload_preparation_page.dart'; import 'package:paperless_mobile/features/document_upload/view/document_upload_preparation_page.dart';
import 'package:paperless_mobile/features/documents/view/pages/document_view.dart'; import 'package:paperless_mobile/features/documents/view/pages/document_view.dart';
import 'package:paperless_mobile/features/tasks/cubit/task_status_cubit.dart'; import 'package:paperless_mobile/features/tasks/model/pending_tasks_notifier.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
import 'package:paperless_mobile/helpers/connectivity_aware_action_wrapper.dart'; import 'package:paperless_mobile/helpers/connectivity_aware_action_wrapper.dart';
import 'package:paperless_mobile/helpers/message_helpers.dart'; import 'package:paperless_mobile/helpers/message_helpers.dart';

View File

@@ -60,7 +60,6 @@ class _DocumentUploadPreparationPageState
static final fileNameDateFormat = DateFormat("yyyy_MM_ddTHH_mm_ss"); static final fileNameDateFormat = DateFormat("yyyy_MM_ddTHH_mm_ss");
final GlobalKey<FormBuilderState> _formKey = GlobalKey(); final GlobalKey<FormBuilderState> _formKey = GlobalKey();
Color? _titleColor;
Map<String, String> _errors = {}; Map<String, String> _errors = {};
bool _isUploadLoading = false; bool _isUploadLoading = false;
late bool _syncTitleAndFilename; late bool _syncTitleAndFilename;
@@ -71,10 +70,6 @@ class _DocumentUploadPreparationPageState
void initState() { void initState() {
super.initState(); super.initState();
_syncTitleAndFilename = widget.filename == null && widget.title == null; _syncTitleAndFilename = widget.filename == null && widget.title == null;
_computeAverageColor().then((value) {
_titleColor =
value.computeLuminance() > 0.5 ? Colors.black : Colors.white;
});
initializeDateFormatting(); initializeDateFormatting();
} }
@@ -102,9 +97,7 @@ class _DocumentUploadPreparationPageState
handle: handle:
NestedScrollView.sliverOverlapAbsorberHandleFor(context), NestedScrollView.sliverOverlapAbsorberHandleFor(context),
sliver: SliverAppBar( sliver: SliverAppBar(
leading: BackButton( leading: BackButton(),
color: _titleColor,
),
pinned: true, pinned: true,
expandedHeight: 150, expandedHeight: 150,
flexibleSpace: FlexibleSpaceBar( flexibleSpace: FlexibleSpaceBar(
@@ -112,20 +105,17 @@ class _DocumentUploadPreparationPageState
future: widget.fileBytes, future: widget.fileBytes,
builder: (context, snapshot) { builder: (context, snapshot) {
if (!snapshot.hasData) { if (!snapshot.hasData) {
return const SizedBox.shrink(); return SizedBox.shrink();
} }
return FileThumbnail( return FileThumbnail(
bytes: snapshot.data!, bytes: snapshot.data!,
fit: BoxFit.fitWidth, fit: BoxFit.fitWidth,
width: MediaQuery.sizeOf(context).width,
); );
}, },
), ),
title: Text( title: Text(S.of(context)!.prepareDocument),
S.of(context)!.prepareDocument, collapseMode: CollapseMode.pin,
style: TextStyle(
color: _titleColor,
),
),
), ),
bottom: _isUploadLoading bottom: _isUploadLoading
? PreferredSize( ? PreferredSize(
@@ -416,32 +406,32 @@ class _DocumentUploadPreparationPageState
return source.replaceAll(RegExp(r"[\W_]"), "_").toLowerCase(); return source.replaceAll(RegExp(r"[\W_]"), "_").toLowerCase();
} }
Future<Color> _computeAverageColor() async { // Future<Color> _computeAverageColor() async {
final bitmap = img.decodeImage(await widget.fileBytes); // final bitmap = img.decodeImage(await widget.fileBytes);
if (bitmap == null) { // if (bitmap == null) {
return Colors.black; // return Colors.black;
} // }
int redBucket = 0; // int redBucket = 0;
int greenBucket = 0; // int greenBucket = 0;
int blueBucket = 0; // int blueBucket = 0;
int pixelCount = 0; // int pixelCount = 0;
for (int y = 0; y < bitmap.height; y++) { // for (int y = 0; y < bitmap.height; y++) {
for (int x = 0; x < bitmap.width; x++) { // for (int x = 0; x < bitmap.width; x++) {
final c = bitmap.getPixel(x, y); // final c = bitmap.getPixel(x, y);
pixelCount++; // pixelCount++;
redBucket += c.r.toInt(); // redBucket += c.r.toInt();
greenBucket += c.g.toInt(); // greenBucket += c.g.toInt();
blueBucket += c.b.toInt(); // blueBucket += c.b.toInt();
} // }
} // }
return Color.fromRGBO( // return Color.fromRGBO(
redBucket ~/ pixelCount, // redBucket ~/ pixelCount,
greenBucket ~/ pixelCount, // greenBucket ~/ pixelCount,
blueBucket ~/ pixelCount, // blueBucket ~/ pixelCount,
1, // 1,
); // );
} // }
} }

View File

@@ -21,7 +21,7 @@ import 'package:paperless_mobile/features/documents/view/widgets/selection/view_
import 'package:paperless_mobile/features/documents/view/widgets/sort_documents_button.dart'; import 'package:paperless_mobile/features/documents/view/widgets/sort_documents_button.dart';
import 'package:paperless_mobile/features/labels/cubit/label_cubit.dart'; import 'package:paperless_mobile/features/labels/cubit/label_cubit.dart';
import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart'; import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart';
import 'package:paperless_mobile/features/tasks/cubit/task_status_cubit.dart'; import 'package:paperless_mobile/features/tasks/model/pending_tasks_notifier.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
import 'package:paperless_mobile/helpers/message_helpers.dart'; import 'package:paperless_mobile/helpers/message_helpers.dart';
import 'package:paperless_mobile/routes/typed/branches/documents_route.dart'; import 'package:paperless_mobile/routes/typed/branches/documents_route.dart';
@@ -59,7 +59,7 @@ class _DocumentsPageState extends State<DocumentsPage> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
context.read<PendingTasksNotifier>().addListener(_onTasksChanged); // context.read<PendingTasksNotifier>().addListener(_onTasksChanged);
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
_nestedScrollViewKey.currentState!.innerController _nestedScrollViewKey.currentState!.innerController
.addListener(_scrollExtentChangedListener); .addListener(_scrollExtentChangedListener);
@@ -126,8 +126,7 @@ class _DocumentsPageState extends State<DocumentsPage> {
void dispose() { void dispose() {
_nestedScrollViewKey.currentState?.innerController _nestedScrollViewKey.currentState?.innerController
.removeListener(_scrollExtentChangedListener); .removeListener(_scrollExtentChangedListener);
context.read<PendingTasksNotifier>().removeListener(_onTasksChanged); // context.read<PendingTasksNotifier>().removeListener(_onTasksChanged);
super.dispose(); super.dispose();
} }

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:hive_flutter/adapters.dart'; import 'package:hive_flutter/adapters.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
@@ -16,9 +17,14 @@ 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/home/view/model/api_version.dart';
import 'package:paperless_mobile/features/inbox/cubit/inbox_cubit.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/labels/cubit/label_cubit.dart';
import 'package:paperless_mobile/features/login/cubit/authentication_cubit.dart';
import 'package:paperless_mobile/features/saved_view/cubit/saved_view_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/settings/view/widgets/global_settings_builder.dart';
import 'package:paperless_mobile/features/tasks/cubit/task_status_cubit.dart'; import 'package:paperless_mobile/features/tasks/model/pending_tasks_notifier.dart';
import 'package:paperless_mobile/routes/typed/branches/landing_route.dart';
import 'package:paperless_mobile/routes/typed/top_level/login_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:provider/provider.dart'; import 'package:provider/provider.dart';
class HomeShellWidget extends StatelessWidget { class HomeShellWidget extends StatelessWidget {

View File

@@ -20,6 +20,7 @@ import 'package:paperless_mobile/core/service/connectivity_status_service.dart';
import 'package:paperless_mobile/core/service/file_service.dart'; import 'package:paperless_mobile/core/service/file_service.dart';
import 'package:paperless_mobile/features/login/model/client_certificate.dart'; import 'package:paperless_mobile/features/login/model/client_certificate.dart';
import 'package:paperless_mobile/features/login/model/login_form_credentials.dart'; import 'package:paperless_mobile/features/login/model/login_form_credentials.dart';
import 'package:paperless_mobile/features/login/model/reachability_status.dart';
import 'package:paperless_mobile/features/login/services/authentication_service.dart'; import 'package:paperless_mobile/features/login/services/authentication_service.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
@@ -44,34 +45,35 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
ClientCertificate? clientCertificate, ClientCertificate? clientCertificate,
}) async { }) async {
assert(credentials.username != null && credentials.password != null); assert(credentials.username != null && credentials.password != null);
emit(const CheckingLoginState());
final localUserId = "${credentials.username}@$serverUrl"; final localUserId = "${credentials.username}@$serverUrl";
_debugPrintMessage( _debugPrintMessage(
"login", "login",
"Trying to login $localUserId...", "Trying to login $localUserId...",
); );
await _addUser( try {
localUserId, await _addUser(
serverUrl, localUserId,
credentials, serverUrl,
clientCertificate, credentials,
_sessionManager, clientCertificate,
); _sessionManager,
);
// Mark logged in user as currently active user. // Mark logged in user as currently active user.
final globalSettings = final globalSettings =
Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!; Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!;
globalSettings.loggedInUserId = localUserId; globalSettings.loggedInUserId = localUserId;
await globalSettings.save(); await globalSettings.save();
emit( emit(AuthenticatedState(localUserId: localUserId));
AuthenticatedState( _debugPrintMessage(
localUserId: localUserId, "login",
), "User successfully logged in.",
); );
_debugPrintMessage( } catch (error) {
"login", emit(const UnauthenticatedState());
"User successfully logged in.", }
);
} }
/// Switches to another account if it exists. /// Switches to another account if it exists.
@@ -156,10 +158,8 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
} }
Future<void> removeAccount(String userId) async { Future<void> removeAccount(String userId) async {
final userAccountBox = final userAccountBox = Hive.localUserAccountBox;
Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount); final userAppStateBox = Hive.localUserAppStateBox;
final userAppStateBox =
Hive.box<LocalUserAppState>(HiveBoxes.localUserAppState);
await FileService.clearUserData(userId: userId); await FileService.clearUserData(userId: userId);
await userAccountBox.delete(userId); await userAccountBox.delete(userId);
await userAppStateBox.delete(userId); await userAppStateBox.delete(userId);
@@ -263,9 +263,13 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
"restoreSessionState", "restoreSessionState",
"Current session state successfully updated.", "Current session state successfully updated.",
); );
final hasInternetConnection = final isPaperlessServerReachable =
await _connectivityService.isConnectedToInternet(); await _connectivityService.isPaperlessServerReachable(
if (hasInternetConnection) { localUserAccount.serverUrl,
authentication.clientCertificate,
) ==
ReachabilityStatus.reachable;
if (isPaperlessServerReachable) {
_debugPrintMessage( _debugPrintMessage(
"restoreSessionMState", "restoreSessionMState",
"Updating server user...", "Updating server user...",
@@ -283,7 +287,7 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
} else { } else {
_debugPrintMessage( _debugPrintMessage(
"restoreSessionMState", "restoreSessionMState",
"Skipping update of server user (no internet connection).", "Skipping update of server user (server could not be reached).",
); );
} }
@@ -295,14 +299,18 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
); );
} }
Future<void> logout() async { Future<void> logout([bool removeAccount = false]) async {
emit(const LogginOutState());
_debugPrintMessage( _debugPrintMessage(
"logout", "logout",
"Trying to log out current user...", "Trying to log out current user...",
); );
await _resetExternalState(); await _resetExternalState();
final globalSettings = final globalSettings = Hive.globalSettingsBox.getValue()!;
Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!; final userId = globalSettings.loggedInUserId!;
if (removeAccount) {
this.removeAccount(userId);
}
globalSettings.loggedInUserId = null; globalSettings.loggedInUserId = null;
await globalSettings.save(); await globalSettings.save();
@@ -459,19 +467,32 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
return serverUser.id; return serverUser.id;
} }
Future<int> _getApiVersion(Dio dio) async { Future<int> _getApiVersion(
Dio dio, {
Duration? timeout,
int defaultValue = 2,
}) async {
_debugPrintMessage( _debugPrintMessage(
"_getApiVersion", "_getApiVersion",
"Trying to fetch API version...", "Trying to fetch API version...",
); );
final response = await dio.get("/api/"); try {
final apiVersion = final response = await dio.get(
int.parse(response.headers.value('x-api-version') ?? "3"); "/api/",
_debugPrintMessage( options: Options(
"_getApiVersion", sendTimeout: timeout,
"API version ($apiVersion) successfully retrieved.", ),
); );
return apiVersion; final apiVersion =
int.parse(response.headers.value('x-api-version') ?? "3");
_debugPrintMessage(
"_getApiVersion",
"API version ($apiVersion) successfully retrieved.",
);
return apiVersion;
} on DioException catch (e) {
return defaultValue;
}
} }
/// Fetches possibly updated (permissions, name, updated server version and thus new user model, ...) remote user data. /// Fetches possibly updated (permissions, name, updated server version and thus new user model, ...) remote user data.

View File

@@ -15,6 +15,14 @@ class RequiresLocalAuthenticationState extends AuthenticationState {
const RequiresLocalAuthenticationState(); const RequiresLocalAuthenticationState();
} }
class CheckingLoginState extends AuthenticationState {
const CheckingLoginState();
}
class LogginOutState extends AuthenticationState {
const LogginOutState();
}
class AuthenticatedState extends AuthenticationState { class AuthenticatedState extends AuthenticationState {
final String localUserId; final String localUserId;

View File

@@ -57,72 +57,76 @@ class _AddAccountPageState extends State<AddAccountPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final localAccounts = return ValueListenableBuilder(
Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount); valueListenable:
return Scaffold( Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount).listenable(),
resizeToAvoidBottomInset: false, builder: (context, localAccounts, child) {
body: FormBuilder( return Scaffold(
key: _formKey, resizeToAvoidBottomInset: false,
child: PageView( body: FormBuilder(
controller: _pageController, key: _formKey,
scrollBehavior: NeverScrollableScrollBehavior(), child: PageView(
children: [ controller: _pageController,
if (widget.showLocalAccounts && localAccounts.isNotEmpty) scrollBehavior: NeverScrollableScrollBehavior(),
Scaffold( children: [
appBar: AppBar( if (widget.showLocalAccounts && localAccounts.isNotEmpty)
title: Text(S.of(context)!.logInToExistingAccount), Scaffold(
), appBar: AppBar(
bottomNavigationBar: BottomAppBar( title: Text(S.of(context)!.logInToExistingAccount),
child: Row( ),
mainAxisAlignment: MainAxisAlignment.end, bottomNavigationBar: BottomAppBar(
children: [ child: Row(
FilledButton( mainAxisAlignment: MainAxisAlignment.end,
child: Text(S.of(context)!.goToLogin), children: [
onPressed: () { FilledButton(
_pageController.nextPage( child: Text(S.of(context)!.goToLogin),
duration: const Duration(milliseconds: 300), onPressed: () {
curve: Curves.easeInOut, _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(
body: ListView.builder( titleText: widget.titleString,
itemBuilder: (context, index) { formBuilderKey: _formKey,
final account = localAccounts.values.elementAt(index); onContinue: () {
return Card( _pageController.nextPage(
child: UserAccountListTile( duration: const Duration(milliseconds: 300),
account: account, curve: Curves.easeInOut,
onTap: () {
context
.read<AuthenticationCubit>()
.switchAccount(account.id);
},
),
); );
}, },
itemCount: localAccounts.length,
), ),
), ServerLoginPage(
ServerConnectionPage( formBuilderKey: _formKey,
titleText: widget.titleString, submitText: widget.submitText,
formBuilderKey: _formKey, onSubmit: _login,
onContinue: () { ),
_pageController.nextPage( ],
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
},
), ),
ServerLoginPage( ),
formBuilderKey: _formKey, );
submitText: widget.submitText, },
onSubmit: _login,
),
],
),
),
); );
} }

View File

@@ -5,6 +5,7 @@ import 'package:hive_flutter/adapters.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/config/hive/hive_config.dart'; import 'package:paperless_mobile/core/config/hive/hive_config.dart';
import 'package:paperless_mobile/core/database/tables/global_settings.dart'; import 'package:paperless_mobile/core/database/tables/global_settings.dart';
import 'package:paperless_mobile/core/model/info_message_exception.dart';
import 'package:paperless_mobile/features/app_intro/application_intro_slideshow.dart'; import 'package:paperless_mobile/features/app_intro/application_intro_slideshow.dart';
import 'package:paperless_mobile/features/login/cubit/authentication_cubit.dart'; import 'package:paperless_mobile/features/login/cubit/authentication_cubit.dart';
import 'package:paperless_mobile/features/login/model/client_certificate.dart'; import 'package:paperless_mobile/features/login/model/client_certificate.dart';
@@ -71,6 +72,8 @@ class LoginPage extends StatelessWidget {
stackTrace, stackTrace,
); //TODO: Check if we can show error message directly on field here. ); //TODO: Check if we can show error message directly on field here.
} }
} on InfoMessageException catch (error) {
showInfoMessage(context, error);
} catch (unknownError, stackTrace) { } catch (unknownError, stackTrace) {
showGenericError(context, unknownError.toString(), stackTrace); showGenericError(context, unknownError.toString(), stackTrace);
} }

View File

@@ -18,6 +18,8 @@ class LocalNotificationService {
LocalNotificationService(); LocalNotificationService();
final Map<String, List<int>> _pendingNotifications = {};
Future<void> initialize() async { Future<void> initialize() async {
const AndroidInitializationSettings initializationSettingsAndroid = const AndroidInitializationSettings initializationSettingsAndroid =
AndroidInitializationSettings('paperless_logo_green'); AndroidInitializationSettings('paperless_logo_green');
@@ -51,6 +53,7 @@ class LocalNotificationService {
required String filePath, required String filePath,
required bool finished, required bool finished,
required String locale, required String locale,
required String userId,
}) async { }) async {
final tr = await S.delegate.load(Locale(locale)); final tr = await S.delegate.load(Locale(locale));
@@ -88,6 +91,15 @@ class LocalNotificationService {
).toJson(), ).toJson(),
), ),
); //TODO: INTL ); //TODO: INTL
_addNotification(userId, id);
}
void _addNotification(String userId, int notificationId) {
_pendingNotifications.update(
userId,
(notifications) => [...notifications, notificationId],
ifAbsent: () => [notificationId],
);
} }
Future<void> notifyFileSaved({ Future<void> notifyFileSaved({
@@ -119,24 +131,20 @@ class LocalNotificationService {
), ),
iOS: DarwinNotificationDetails( iOS: DarwinNotificationDetails(
attachments: [ attachments: [
DarwinNotificationAttachment( DarwinNotificationAttachment(filePath),
filePath,
),
], ],
), ),
), ),
payload: jsonEncode( payload: jsonEncode(
OpenDownloadedDocumentPayload( OpenDownloadedDocumentPayload(filePath: filePath).toJson(),
filePath: filePath,
).toJson(),
), ),
); );
} }
//TODO: INTL //TODO: INTL
Future<void> notifyTaskChanged(Task task) { Future<void> notifyTaskChanged(Task task, {required String userId}) async {
log("[LocalNotificationService] notifyTaskChanged: ${task.toString()}"); log("[LocalNotificationService] notifyTaskChanged: ${task.toString()}");
int id = task.id; int id = task.id + 1000;
final status = task.status; final status = task.status;
late String title; late String title;
late String? body; late String? body;
@@ -171,7 +179,7 @@ class LocalNotificationService {
default: default:
break; break;
} }
return _plugin.show( await _plugin.show(
id, id,
title, title,
body, body,
@@ -204,6 +212,13 @@ class LocalNotificationService {
), ),
payload: jsonEncode(payload), payload: jsonEncode(payload),
); );
_addNotification(userId, id);
}
Future<void> cancelUserNotifications(String userId) async {
await Future.wait([
for (var id in _pendingNotifications[userId] ?? []) _plugin.cancel(id),
]);
} }
void onDidReceiveLocalNotification( void onDidReceiveLocalNotification(
@@ -272,6 +287,7 @@ class LocalNotificationService {
} }
} }
@protected
void onDidReceiveBackgroundNotificationResponse(NotificationResponse response) { void onDidReceiveBackgroundNotificationResponse(NotificationResponse response) {
//TODO: When periodic background inbox check is implemented, notification tap is handled here //TODO: When periodic background inbox check is implemented, notification tap is handled here
debugPrint(response.toString()); debugPrint(response.toString());

View File

@@ -70,12 +70,9 @@ class ManageAccountsPage extends StatelessWidget {
], ],
onSelected: (value) async { onSelected: (value) async {
if (value == 0) { if (value == 0) {
final currentUser = globalSettings.loggedInUserId!;
await context.read<AuthenticationCubit>().logout();
Navigator.of(context).pop();
await context await context
.read<AuthenticationCubit>() .read<AuthenticationCubit>()
.removeAccount(currentUser); .logout(true);
} }
}, },
), ),

View File

@@ -1,20 +1,24 @@
import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:bloc/bloc.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:paperless_mobile/core/service/file_service.dart'; import 'package:paperless_mobile/core/service/file_service.dart';
import 'package:path/path.dart' as p; import 'package:path/path.dart' as p;
import 'package:provider/provider.dart';
part 'receive_share_state.dart'; part 'receive_share_state.dart';
class ConsumptionChangeNotifier extends ChangeNotifier { class ConsumptionChangeNotifier extends ChangeNotifier {
List<File> pendingFiles = []; List<File> pendingFiles = [];
ConsumptionChangeNotifier(); final Completer _restored = Completer();
Future<void> get isInitialized => _restored.future;
Future<void> loadFromConsumptionDirectory({required String userId}) async { Future<void> loadFromConsumptionDirectory({required String userId}) async {
pendingFiles = await _getCurrentFiles(userId); pendingFiles = await _getCurrentFiles(userId);
if (!_restored.isCompleted) {
_restored.complete();
}
notifyListeners(); notifyListeners();
} }

View File

@@ -1,8 +1,4 @@
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:intl/intl.dart';
import 'package:paperless_mobile/core/database/tables/local_user_account.dart'; import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/sharing/cubit/receive_share_cubit.dart'; import 'package:paperless_mobile/features/sharing/cubit/receive_share_cubit.dart';
@@ -20,13 +16,13 @@ class ConsumptionQueueView extends StatelessWidget {
final currentUser = context.watch<LocalUserAccount>(); final currentUser = context.watch<LocalUserAccount>();
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text("Upload Queue"), //TODO: INTL title: Text("Pending Files"), //TODO: INTL
), ),
body: Consumer<ConsumptionChangeNotifier>( body: Consumer<ConsumptionChangeNotifier>(
builder: (context, value, child) { builder: (context, value, child) {
if (value.pendingFiles.isEmpty) { if (value.pendingFiles.isEmpty) {
return Center( return Center(
child: Text("No pending files."), child: Text("There are no pending files."), //TODO: INTL
); );
} }
return ListView.builder( return ListView.builder(
@@ -34,7 +30,37 @@ class ConsumptionQueueView extends StatelessWidget {
final file = value.pendingFiles.elementAt(index); final file = value.pendingFiles.elementAt(index);
final filename = p.basename(file.path); final filename = p.basename(file.path);
return ListTile( return ListTile(
title: Text(filename), title: Text(
filename,
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
subtitle: Row(
children: [
ActionChip(
label: Text(S.of(context)!.upload),
avatar: const Icon(Icons.file_upload_outlined),
onPressed: () {
consumeLocalFile(
context,
file: file,
userId: currentUser.id,
);
},
),
const SizedBox(width: 8),
ActionChip(
label: Text(S.of(context)!.discard),
avatar: const Icon(Icons.delete),
onPressed: () {
context.read<ConsumptionChangeNotifier>().discardFile(
file,
userId: currentUser.id,
);
},
),
],
),
leading: Padding( leading: Padding(
padding: const EdgeInsets.all(4), padding: const EdgeInsets.all(4),
child: ClipRRect( child: ClipRRect(
@@ -46,60 +72,7 @@ class ConsumptionQueueView extends StatelessWidget {
), ),
), ),
), ),
trailing: IconButton(
icon: Icon(Icons.delete),
onPressed: () {
context
.read<ConsumptionChangeNotifier>()
.discardFile(file, userId: currentUser.id);
},
),
); );
return Row(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: Column(
children: [
Text(filename, maxLines: 1),
SizedBox(
height: 56,
child: ListView(
scrollDirection: Axis.horizontal,
children: [
ActionChip(
label: Text(S.of(context)!.upload),
avatar: Icon(Icons.file_upload_outlined),
onPressed: () {
consumeLocalFile(
context,
file: file,
userId: currentUser.id,
);
},
),
SizedBox(width: 8),
ActionChip(
label: Text(S.of(context)!.discard),
avatar: Icon(Icons.delete),
onPressed: () {
context
.read<ConsumptionChangeNotifier>()
.discardFile(
file,
userId: currentUser.id,
);
},
),
],
),
),
],
).padded(),
),
],
).padded();
}, },
itemCount: value.pendingFiles.length, itemCount: value.pendingFiles.length,
); );

View File

@@ -6,7 +6,7 @@ import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_cancel_button.
import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_confirm_button.dart'; import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_confirm_button.dart';
import 'package:paperless_mobile/core/widgets/future_or_builder.dart'; import 'package:paperless_mobile/core/widgets/future_or_builder.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
import 'package:transparent_image/transparent_image.dart'; import 'package:paperless_mobile/features/sharing/view/widgets/file_thumbnail.dart';
class DiscardSharedFileDialog extends StatelessWidget { class DiscardSharedFileDialog extends StatelessWidget {
final FutureOr<Uint8List> bytes; final FutureOr<Uint8List> bytes;
@@ -24,13 +24,13 @@ class DiscardSharedFileDialog extends StatelessWidget {
if (!snapshot.hasData) { if (!snapshot.hasData) {
return const CircularProgressIndicator(); return const CircularProgressIndicator();
} }
return LimitedBox( return ClipRRect(
maxHeight: 200, borderRadius: BorderRadius.circular(12),
maxWidth: 200, child: FileThumbnail(
child: FadeInImage( bytes: snapshot.data!,
fit: BoxFit.contain, width: 150,
placeholder: MemoryImage(kTransparentImage), height: 100,
image: MemoryImage(snapshot.data!), fit: BoxFit.cover,
), ),
); );
}, },

View File

@@ -0,0 +1,29 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_cancel_button.dart';
import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_confirm_button.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
class PendingFilesInfoDialog extends StatelessWidget {
final List<File> pendingFiles;
const PendingFilesInfoDialog({super.key, required this.pendingFiles});
@override
Widget build(BuildContext context) {
final fileCount = pendingFiles.length;
return AlertDialog(
title: Text("Pending Files"),
content: Text(
"$fileCount files are waiting to be uploaded. Do you want to upload them now?",
),
actions: [
DialogCancelButton(),
DialogConfirmButton(
label: S.of(context)!.upload,
),
],
);
}
}

View File

@@ -28,13 +28,15 @@ class FileThumbnail extends StatefulWidget {
class _FileThumbnailState extends State<FileThumbnail> { class _FileThumbnailState extends State<FileThumbnail> {
late String? mimeType; late String? mimeType;
late final Future<Uint8List?> _fileBytes;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
mimeType = widget.file != null mimeType = widget.file != null
? mime.lookupMimeType(widget.file!.path) ? mime.lookupMimeType(widget.file!.path)
: mime.lookupMimeType('', headerBytes: widget.bytes); : mime.lookupMimeType('', headerBytes: widget.bytes);
_fileBytes = widget.file?.readAsBytes().then(_convertPdfToPng) ??
_convertPdfToPng(widget.bytes!);
} }
@override @override
@@ -45,8 +47,7 @@ class _FileThumbnailState extends State<FileThumbnail> {
height: widget.height, height: widget.height,
child: Center( child: Center(
child: FutureBuilder<Uint8List?>( child: FutureBuilder<Uint8List?>(
future: widget.file?.readAsBytes().then(_convertPdfToPng) ?? future: _fileBytes,
_convertPdfToPng(widget.bytes!),
builder: (context, snapshot) { builder: (context, snapshot) {
if (!snapshot.hasData) { if (!snapshot.hasData) {
return const SizedBox.shrink(); return const SizedBox.shrink();

View File

@@ -10,15 +10,16 @@ import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/config/hive/hive_config.dart'; import 'package:paperless_mobile/core/config/hive/hive_config.dart';
import 'package:paperless_mobile/core/config/hive/hive_extensions.dart'; import 'package:paperless_mobile/core/config/hive/hive_extensions.dart';
import 'package:paperless_mobile/core/database/tables/local_user_account.dart'; import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
import 'package:paperless_mobile/core/service/connectivity_status_service.dart';
import 'package:paperless_mobile/features/document_upload/view/document_upload_preparation_page.dart'; import 'package:paperless_mobile/features/document_upload/view/document_upload_preparation_page.dart';
import 'package:paperless_mobile/features/notifications/services/local_notification_service.dart'; import 'package:paperless_mobile/features/notifications/services/local_notification_service.dart';
import 'package:paperless_mobile/features/sharing/cubit/receive_share_cubit.dart'; import 'package:paperless_mobile/features/sharing/cubit/receive_share_cubit.dart';
import 'package:paperless_mobile/features/sharing/view/dialog/discard_shared_file_dialog.dart'; import 'package:paperless_mobile/features/sharing/view/dialog/discard_shared_file_dialog.dart';
import 'package:paperless_mobile/features/tasks/cubit/task_status_cubit.dart'; import 'package:paperless_mobile/features/sharing/view/dialog/pending_files_info_dialog.dart';
import 'package:paperless_mobile/features/tasks/model/pending_tasks_notifier.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
import 'package:paperless_mobile/helpers/message_helpers.dart'; import 'package:paperless_mobile/helpers/message_helpers.dart';
import 'package:paperless_mobile/routes/typed/branches/scanner_route.dart'; import 'package:paperless_mobile/routes/typed/branches/scanner_route.dart';
import 'package:paperless_mobile/routes/typed/branches/upload_queue_route.dart';
import 'package:path/path.dart' as p; import 'package:path/path.dart' as p;
import 'package:receive_sharing_intent/receive_sharing_intent.dart'; import 'package:receive_sharing_intent/receive_sharing_intent.dart';
@@ -39,57 +40,40 @@ class _UploadQueueShellState extends State<UploadQueueShell> {
ReceiveSharingIntent.getInitialMedia().then(_onReceiveSharedFiles); ReceiveSharingIntent.getInitialMedia().then(_onReceiveSharedFiles);
_subscription = _subscription =
ReceiveSharingIntent.getMediaStream().listen(_onReceiveSharedFiles); ReceiveSharingIntent.getMediaStream().listen(_onReceiveSharedFiles);
context.read<PendingTasksNotifier>().addListener(_onTasksChanged);
// WidgetsBinding.instance.addPostFrameCallback((timeStamp) { // WidgetsBinding.instance.addPostFrameCallback((_) async {
// context.read<ReceiveShareCubit>().loadFromConsumptionDirectory( // final notifier = context.read<ConsumptionChangeNotifier>();
// userId: context.read<LocalUserAccount>().id, // await notifier.isInitialized;
// ); // final pendingFiles = notifier.pendingFiles;
// final state = context.read<ReceiveShareCubit>().state; // if (pendingFiles.isEmpty) {
// print("Current state is " + state.toString()); // return;
// final files = state.files; // }
// if (files.isNotEmpty) {
// showSnackBar( // final shouldProcess = await showDialog<bool>(
// context: context,
// builder: (context) =>
// PendingFilesInfoDialog(pendingFiles: pendingFiles),
// ) ??
// false;
// if (shouldProcess) {
// final userId = context.read<LocalUserAccount>().id;
// await consumeLocalFiles(
// context, // context,
// "You have ${files.length} shared files waiting to be uploaded.", // files: pendingFiles,
// action: SnackBarActionConfig( // userId: userId,
// label: "Show me",
// onPressed: () {
// UploadQueueRoute().push(context);
// },
// ),
// ); // );
// // showDialog(
// // context: context,
// // builder: (context) => AlertDialog(
// // title: Text("Pending files"),
// // content: Text(
// // "You have ${files.length} files waiting to be uploaded.",
// // ),
// // actions: [
// // TextButton(
// // child: Text(S.of(context)!.gotIt),
// // onPressed: () {
// // Navigator.pop(context);
// // UploadQueueRoute().push(context);
// // },
// // ),
// // ],
// // ),
// // );
// } // }
// }); // });
} }
@override
void didChangeDependencies() {
super.didChangeDependencies();
context.read<PendingTasksNotifier>().addListener(_onTasksChanged);
}
void _onTasksChanged() { void _onTasksChanged() {
final taskNotifier = context.read<PendingTasksNotifier>(); final taskNotifier = context.read<PendingTasksNotifier>();
final userId = context.read<LocalUserAccount>().id;
for (var task in taskNotifier.value.values) { for (var task in taskNotifier.value.values) {
context.read<LocalNotificationService>().notifyTaskChanged(task); context
.read<LocalNotificationService>()
.notifyTaskChanged(task, userId: userId);
} }
} }
@@ -103,23 +87,18 @@ class _UploadQueueShellState extends State<UploadQueueShell> {
files: files, files: files,
userId: userId, userId: userId,
); );
final localFiles = notifier.pendingFiles; consumeLocalFiles(
for (int i = 0; i < localFiles.length; i++) { context,
final file = localFiles[i]; files: files,
await consumeLocalFile( userId: userId,
context, exitAppAfterConsumed: true,
file: file, );
userId: userId,
exitAppAfterConsumed: i == localFiles.length - 1,
);
}
} }
} }
@override @override
void dispose() { void dispose() {
_subscription?.cancel(); _subscription?.cancel();
context.read<PendingTasksNotifier>().removeListener(_onTasksChanged);
super.dispose(); super.dispose();
} }
@@ -135,28 +114,43 @@ Future<void> consumeLocalFile(
required String userId, required String userId,
bool exitAppAfterConsumed = false, bool exitAppAfterConsumed = false,
}) async { }) async {
final filename = p.basename(file.path);
final hasInternetConnection =
await context.read<ConnectivityStatusService>().isConnectedToInternet();
if (!hasInternetConnection) {
showSnackBar(
context,
"Could not consume $filename", //TODO: INTL
details: S.of(context)!.youreOffline,
);
return;
}
final consumptionNotifier = context.read<ConsumptionChangeNotifier>(); final consumptionNotifier = context.read<ConsumptionChangeNotifier>();
final taskNotifier = context.read<PendingTasksNotifier>(); final taskNotifier = context.read<PendingTasksNotifier>();
final ioFile = File(file.path);
// if (!await ioFile.exists()) {
// Fluttertoast.showToast(
// msg: S.of(context)!.couldNotAccessReceivedFile,
// toastLength: Toast.LENGTH_LONG,
// );
// }
final bytes = ioFile.readAsBytes(); final bytes = file.readAsBytes();
final shouldDirectlyUpload = final shouldDirectlyUpload =
Hive.globalSettingsBox.getValue()!.skipDocumentPreprarationOnUpload; Hive.globalSettingsBox.getValue()!.skipDocumentPreprarationOnUpload;
if (shouldDirectlyUpload) { if (shouldDirectlyUpload) {
final taskId = await context.read<PaperlessDocumentsApi>().create( try {
await bytes, final taskId = await context.read<PaperlessDocumentsApi>().create(
filename: p.basename(file.path), await bytes,
title: p.basenameWithoutExtension(file.path), filename: filename,
); title: p.basenameWithoutExtension(file.path),
consumptionNotifier.discardFile(file, userId: userId); );
if (taskId != null) { consumptionNotifier.discardFile(file, userId: userId);
taskNotifier.listenToTaskChanges(taskId); if (taskId != null) {
taskNotifier.listenToTaskChanges(taskId);
}
} catch (error) {
await Fluttertoast.showToast(
msg: S.of(context)!.couldNotUploadDocument,
);
return;
} finally {
if (exitAppAfterConsumed) {
SystemNavigator.pop();
}
} }
} else { } else {
final result = await DocumentUploadRoute( final result = await DocumentUploadRoute(
@@ -193,3 +187,20 @@ Future<void> consumeLocalFile(
} }
} }
} }
Future<void> consumeLocalFiles(
BuildContext context, {
required List<File> files,
required String userId,
bool exitAppAfterConsumed = false,
}) async {
for (int i = 0; i < files.length; i++) {
final file = files[i];
await consumeLocalFile(
context,
file: file,
userId: userId,
exitAppAfterConsumed: exitAppAfterConsumed && (i == files.length - 1),
);
}
}

View File

@@ -1,26 +0,0 @@
import 'package:flutter/material.dart';
import 'package:paperless_api/paperless_api.dart';
class PendingTasksNotifier extends ValueNotifier<Map<String, Task>> {
final PaperlessTasksApi _api;
PendingTasksNotifier(this._api) : super({});
void listenToTaskChanges(String taskId) {
_api.listenForTaskChanges(taskId).forEach((task) {
value = {...value, taskId: task};
notifyListeners();
}).whenComplete(
() {
value = value..remove(taskId);
notifyListeners();
},
);
}
Future<void> acknowledgeTasks(Iterable<String> taskIds) async {
final tasks = value.values.where((task) => taskIds.contains(task.taskId));
await Future.wait([for (var task in tasks) _api.acknowledgeTask(task)]);
value = value..removeWhere((key, value) => taskIds.contains(key));
notifyListeners();
}
}

View File

@@ -0,0 +1,68 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:paperless_api/paperless_api.dart';
class PendingTasksNotifier extends ValueNotifier<Map<String, Task>> {
final PaperlessTasksApi _api;
final Map<String, StreamSubscription> _subscriptions = {};
PendingTasksNotifier(this._api) : super({});
@override
void dispose() {
stopListeningToTaskChanges();
super.dispose();
}
void listenToTaskChanges(String taskId) {
final sub = _api.listenForTaskChanges(taskId).listen(
(task) {
if (value.containsKey(taskId)) {
final oldTask = value[taskId]!;
if (oldTask.status != task.status) {
// Only notify of changes if task status has changed...
value = {...value, taskId: task};
notifyListeners();
}
} else {
value = {...value, taskId: task};
notifyListeners();
}
},
);
sub
..onDone(() {
sub.cancel();
value = value..remove(taskId);
notifyListeners();
})
..onError((_) {
sub.cancel();
value = value..remove(taskId);
notifyListeners();
});
_subscriptions.putIfAbsent(taskId, () => sub);
}
void stopListeningToTaskChanges([String? taskId]) {
if (taskId != null) {
_subscriptions[taskId]?.cancel();
_subscriptions.remove(taskId);
} else {
_subscriptions.forEach((key, value) {
value.cancel();
_subscriptions.remove(key);
});
}
}
Future<void> acknowledgeTasks(Iterable<String> taskIds) async {
final tasks = value.values.where((task) => taskIds.contains(task.taskId));
await Future.wait([for (var task in tasks) _api.acknowledgeTask(task)]);
value = value..removeWhere((key, value) => taskIds.contains(key));
notifyListeners();
}
}

View File

@@ -28,6 +28,7 @@ import 'package:paperless_mobile/core/exception/server_message_exception.dart';
import 'package:paperless_mobile/core/factory/paperless_api_factory.dart'; import 'package:paperless_mobile/core/factory/paperless_api_factory.dart';
import 'package:paperless_mobile/core/factory/paperless_api_factory_impl.dart'; import 'package:paperless_mobile/core/factory/paperless_api_factory_impl.dart';
import 'package:paperless_mobile/core/interceptor/language_header.interceptor.dart'; import 'package:paperless_mobile/core/interceptor/language_header.interceptor.dart';
import 'package:paperless_mobile/core/model/info_message_exception.dart';
import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart'; import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart';
import 'package:paperless_mobile/core/security/session_manager.dart'; import 'package:paperless_mobile/core/security/session_manager.dart';
import 'package:paperless_mobile/core/service/connectivity_status_service.dart'; import 'package:paperless_mobile/core/service/connectivity_status_service.dart';
@@ -37,7 +38,9 @@ import 'package:paperless_mobile/features/notifications/services/local_notificat
import 'package:paperless_mobile/features/settings/view/widgets/global_settings_builder.dart'; import 'package:paperless_mobile/features/settings/view/widgets/global_settings_builder.dart';
import 'package:paperless_mobile/features/sharing/model/share_intent_queue.dart'; import 'package:paperless_mobile/features/sharing/model/share_intent_queue.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
import 'package:paperless_mobile/helpers/message_helpers.dart';
import 'package:paperless_mobile/routes/navigation_keys.dart'; import 'package:paperless_mobile/routes/navigation_keys.dart';
import 'package:paperless_mobile/routes/routes.dart';
import 'package:paperless_mobile/routes/typed/branches/documents_route.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/inbox_route.dart';
import 'package:paperless_mobile/routes/typed/branches/labels_route.dart'; import 'package:paperless_mobile/routes/typed/branches/labels_route.dart';
@@ -47,6 +50,8 @@ import 'package:paperless_mobile/routes/typed/branches/scanner_route.dart';
import 'package:paperless_mobile/routes/typed/branches/upload_queue_route.dart'; import 'package:paperless_mobile/routes/typed/branches/upload_queue_route.dart';
import 'package:paperless_mobile/routes/typed/shells/provider_shell_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/shells/scaffold_shell_route.dart';
import 'package:paperless_mobile/routes/typed/top_level/checking_login_route.dart';
import 'package:paperless_mobile/routes/typed/top_level/logging_out_route.dart';
import 'package:paperless_mobile/routes/typed/top_level/login_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/settings_route.dart';
import 'package:paperless_mobile/routes/typed/top_level/switching_accounts_route.dart'; import 'package:paperless_mobile/routes/typed/top_level/switching_accounts_route.dart';
@@ -234,6 +239,8 @@ class _GoRouterShellState extends State<GoRouterShell> {
$loginRoute, $loginRoute,
$verifyIdentityRoute, $verifyIdentityRoute,
$switchingAccountsRoute, $switchingAccountsRoute,
$logginOutRoute,
$checkingLoginRoute,
ShellRoute( ShellRoute(
navigatorKey: rootNavigatorKey, navigatorKey: rootNavigatorKey,
builder: ProviderShellRoute(widget.apiFactory).build, builder: ProviderShellRoute(widget.apiFactory).build,
@@ -280,19 +287,33 @@ class _GoRouterShellState extends State<GoRouterShell> {
listener: (context, state) { listener: (context, state) {
switch (state) { switch (state) {
case UnauthenticatedState(): case UnauthenticatedState():
const LoginRoute().go(context); _router.goNamed(R.login);
break; break;
case RequiresLocalAuthenticationState(): case RequiresLocalAuthenticationState():
const VerifyIdentityRoute().go(context); _router.goNamed(R.verifyIdentity);
break; break;
case SwitchingAccountsState(): case SwitchingAccountsState():
const SwitchingAccountsRoute().go(context); final userId = context.read<LocalUserAccount>().id;
context
.read<LocalNotificationService>()
.cancelUserNotifications(userId);
_router.goNamed(R.switchingAccounts);
break; break;
case AuthenticatedState(): case AuthenticatedState():
const LandingRoute().go(context); _router.goNamed(R.landing);
break;
case CheckingLoginState():
_router.goNamed(R.checkingLogin);
break;
case LogginOutState():
final userId = context.read<LocalUserAccount>().id;
context
.read<LocalNotificationService>()
.cancelUserNotifications(userId);
_router.goNamed(R.loggingOut);
break; break;
case AuthenticationErrorState(): case AuthenticationErrorState():
const LoginRoute().go(context); _router.goNamed(R.login);
break; break;
} }
}, },

View File

@@ -21,4 +21,6 @@ class R {
static const linkedDocuments = "linkedDocuments"; static const linkedDocuments = "linkedDocuments";
static const bulkEditDocuments = "bulkEditDocuments"; static const bulkEditDocuments = "bulkEditDocuments";
static const uploadQueue = "uploadQueue"; static const uploadQueue = "uploadQueue";
static const checkingLogin = "checkingLogin";
static const loggingOut = "loggingOut";
} }

View File

@@ -61,11 +61,15 @@ class ProviderShellRoute extends ShellRouteData {
) { ) {
final currentUserId = Hive.box<GlobalSettings>(HiveBoxes.globalSettings) final currentUserId = Hive.box<GlobalSettings>(HiveBoxes.globalSettings)
.getValue()! .getValue()!
.loggedInUserId!; .loggedInUserId;
if (currentUserId == null) {
return const SizedBox.shrink();
}
final authenticatedUser = final authenticatedUser =
Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount).get( Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount).get(
currentUserId, currentUserId,
)!; )!;
return HomeShellWidget( return HomeShellWidget(
localUserId: authenticatedUser.id, localUserId: authenticatedUser.id,
paperlessApiVersion: authenticatedUser.apiVersion, paperlessApiVersion: authenticatedUser.apiVersion,

View File

@@ -0,0 +1,23 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:paperless_mobile/routes/routes.dart';
part 'checking_login_route.g.dart';
@TypedGoRoute<CheckingLoginRoute>(
path: "/checking-login",
name: R.checkingLogin,
)
class CheckingLoginRoute extends GoRouteData {
const CheckingLoginRoute();
@override
Widget build(BuildContext context, GoRouterState state) {
return Scaffold(
body: Center(
child: Text("Logging in..."),
),
);
}
}

View File

@@ -0,0 +1,23 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:paperless_mobile/routes/routes.dart';
part 'logging_out_route.g.dart';
@TypedGoRoute<LogginOutRoute>(
path: "/logging-out",
name: R.loggingOut,
)
class LogginOutRoute extends GoRouteData {
const LogginOutRoute();
@override
Widget build(BuildContext context, GoRouterState state) {
return Scaffold(
body: Center(
child: Text("Logging out..."),
),
);
}
}