diff --git a/lib/core/database/tables/local_user_account.dart b/lib/core/database/tables/local_user_account.dart index 9c44504..256dd02 100644 --- a/lib/core/database/tables/local_user_account.dart +++ b/lib/core/database/tables/local_user_account.dart @@ -16,13 +16,13 @@ class LocalUserAccount extends HiveObject { @HiveField(4) final LocalUserSettings settings; - @HiveField(6) - final int paperlessUserId; + @HiveField(7) + final UserModel paperlessUser; LocalUserAccount({ required this.id, required this.serverUrl, required this.settings, - required this.paperlessUserId, + required this.paperlessUser, }); } diff --git a/lib/core/factory/paperless_api_factory.dart b/lib/core/factory/paperless_api_factory.dart index a273f7f..4295977 100644 --- a/lib/core/factory/paperless_api_factory.dart +++ b/lib/core/factory/paperless_api_factory.dart @@ -7,4 +7,6 @@ abstract class PaperlessApiFactory { PaperlessLabelsApi createLabelsApi(Dio dio, {required int apiVersion}); PaperlessServerStatsApi createServerStatsApi(Dio dio, {required int apiVersion}); PaperlessTasksApi createTasksApi(Dio dio, {required int apiVersion}); + PaperlessAuthenticationApi createAuthenticationApi(Dio dio); + PaperlessUserApi createUserApi(Dio dio, {required int apiVersion}); } diff --git a/lib/core/factory/paperless_api_factory_impl.dart b/lib/core/factory/paperless_api_factory_impl.dart index d4b0232..40b718f 100644 --- a/lib/core/factory/paperless_api_factory_impl.dart +++ b/lib/core/factory/paperless_api_factory_impl.dart @@ -32,4 +32,19 @@ class PaperlessApiFactoryImpl implements PaperlessApiFactory { PaperlessTasksApi createTasksApi(Dio dio, {required int apiVersion}) { return PaperlessTasksApiImpl(dio); } + + @override + PaperlessAuthenticationApi createAuthenticationApi(Dio dio) { + return PaperlessAuthenticationApiImpl(dio); + } + + @override + PaperlessUserApi createUserApi(Dio dio, {required int apiVersion}) { + if (apiVersion == 3) { + return PaperlessUserApiV3Impl(dio); + } else if (apiVersion == 1 || apiVersion == 2) { + return PaperlessUserApiV2Impl(dio); + } + throw Exception("API $apiVersion not supported."); + } } diff --git a/lib/core/repository/label_repository.dart b/lib/core/repository/label_repository.dart index b497bb2..b57b19f 100644 --- a/lib/core/repository/label_repository.dart +++ b/lib/core/repository/label_repository.dart @@ -4,28 +4,13 @@ import 'package:flutter/widgets.dart'; import 'package:hydrated_bloc/hydrated_bloc.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/repository/label_repository_state.dart'; +import 'package:paperless_mobile/core/repository/persistent_repository.dart'; -class LabelRepository extends HydratedCubit { +class LabelRepository extends PersistentRepository { final PaperlessLabelsApi _api; - final Map _subscribers = {}; LabelRepository(this._api) : super(const LabelRepositoryState()); - void addListener( - Object source, { - required void Function(LabelRepositoryState) onChanged, - }) { - onChanged(state); - _subscribers.putIfAbsent(source, () { - return stream.listen((event) => onChanged(event)); - }); - } - - void removeListener(Object source) async { - await _subscribers[source]?.cancel(); - _subscribers.remove(source); - } - Future initialize() { debugPrint("Initializing labels..."); return Future.wait([ @@ -195,14 +180,6 @@ class LabelRepository extends HydratedCubit { return updated; } - @override - Future close() { - _subscribers.forEach((key, subscription) { - subscription.cancel(); - }); - return super.close(); - } - @override Future clear() async { await super.clear(); diff --git a/lib/core/repository/persistent_repository.dart b/lib/core/repository/persistent_repository.dart new file mode 100644 index 0000000..db63c13 --- /dev/null +++ b/lib/core/repository/persistent_repository.dart @@ -0,0 +1,32 @@ +import 'dart:async'; + +import 'package:hydrated_bloc/hydrated_bloc.dart'; + +abstract class PersistentRepository extends HydratedCubit { + final Map _subscribers = {}; + + PersistentRepository(T initialState) : super(initialState); + + void addListener( + Object source, { + required void Function(T) onChanged, + }) { + onChanged(state); + _subscribers.putIfAbsent(source, () { + return stream.listen((event) => onChanged(event)); + }); + } + + void removeListener(Object source) async { + await _subscribers[source]?.cancel(); + _subscribers.remove(source); + } + + @override + Future close() { + _subscribers.forEach((key, subscription) { + subscription.cancel(); + }); + return super.close(); + } +} diff --git a/lib/core/repository/saved_view_repository.dart b/lib/core/repository/saved_view_repository.dart index 54ae9fe..d458358 100644 --- a/lib/core/repository/saved_view_repository.dart +++ b/lib/core/repository/saved_view_repository.dart @@ -1,28 +1,11 @@ import 'dart:async'; -import 'package:flutter/material.dart'; -import 'package:hydrated_bloc/hydrated_bloc.dart'; import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/repository/persistent_repository.dart'; import 'package:paperless_mobile/core/repository/saved_view_repository_state.dart'; -class SavedViewRepository extends HydratedCubit { +class SavedViewRepository extends PersistentRepository { final PaperlessSavedViewsApi _api; - final Map _subscribers = {}; - - void subscribe( - Object source, - void Function(Map) onChanged, - ) { - _subscribers.putIfAbsent(source, () { - onChanged(state.savedViews); - return stream.listen((event) => onChanged(event.savedViews)); - }); - } - - void unsubscribe(Object source) async { - await _subscribers[source]?.cancel(); - _subscribers.remove(source); - } SavedViewRepository(this._api) : super(const SavedViewRepositoryState()); @@ -63,14 +46,6 @@ class SavedViewRepository extends HydratedCubit { return found; } - @override - Future close() { - _subscribers.forEach((key, subscription) { - subscription.cancel(); - }); - return super.close(); - } - @override Future clear() async { await super.clear(); diff --git a/lib/core/repository/user_repository.dart b/lib/core/repository/user_repository.dart new file mode 100644 index 0000000..b1748c3 --- /dev/null +++ b/lib/core/repository/user_repository.dart @@ -0,0 +1,40 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/repository/persistent_repository.dart'; + +part 'user_repository_state.dart'; +part 'user_repository.freezed.dart'; +part 'user_repository.g.dart'; + +/// Repository for new users (API v3, server version 1.14.2+) +class UserRepository extends PersistentRepository { + final PaperlessUserApiV3 _userApiV3; + + UserRepository(this._userApiV3) : super(const UserRepositoryState()); + + Future initialize() async { + await findAll(); + } + + Future> findAll() async { + final users = await _userApiV3.findAll(); + emit(state.copyWith(users: {for (var e in users) e.id: e})); + return users; + } + + Future find(int id) async { + final user = await _userApiV3.find(id); + emit(state.copyWith(users: state.users..[id] = user)); + return user; + } + + @override + UserRepositoryState? fromJson(Map json) { + return UserRepositoryState.fromJson(json); + } + + @override + Map? toJson(UserRepositoryState state) { + return state.toJson(); + } +} diff --git a/lib/core/repository/user_repository.freezed.dart b/lib/core/repository/user_repository.freezed.dart new file mode 100644 index 0000000..e8b8b71 --- /dev/null +++ b/lib/core/repository/user_repository.freezed.dart @@ -0,0 +1,161 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'user_repository.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); + +UserRepositoryState _$UserRepositoryStateFromJson(Map json) { + return _UserRepositoryState.fromJson(json); +} + +/// @nodoc +mixin _$UserRepositoryState { + Map get users => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $UserRepositoryStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $UserRepositoryStateCopyWith<$Res> { + factory $UserRepositoryStateCopyWith( + UserRepositoryState value, $Res Function(UserRepositoryState) then) = + _$UserRepositoryStateCopyWithImpl<$Res, UserRepositoryState>; + @useResult + $Res call({Map users}); +} + +/// @nodoc +class _$UserRepositoryStateCopyWithImpl<$Res, $Val extends UserRepositoryState> + implements $UserRepositoryStateCopyWith<$Res> { + _$UserRepositoryStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? users = null, + }) { + return _then(_value.copyWith( + users: null == users + ? _value.users + : users // ignore: cast_nullable_to_non_nullable + as Map, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$_UserRepositoryStateCopyWith<$Res> + implements $UserRepositoryStateCopyWith<$Res> { + factory _$$_UserRepositoryStateCopyWith(_$_UserRepositoryState value, + $Res Function(_$_UserRepositoryState) then) = + __$$_UserRepositoryStateCopyWithImpl<$Res>; + @override + @useResult + $Res call({Map users}); +} + +/// @nodoc +class __$$_UserRepositoryStateCopyWithImpl<$Res> + extends _$UserRepositoryStateCopyWithImpl<$Res, _$_UserRepositoryState> + implements _$$_UserRepositoryStateCopyWith<$Res> { + __$$_UserRepositoryStateCopyWithImpl(_$_UserRepositoryState _value, + $Res Function(_$_UserRepositoryState) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? users = null, + }) { + return _then(_$_UserRepositoryState( + users: null == users + ? _value._users + : users // ignore: cast_nullable_to_non_nullable + as Map, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$_UserRepositoryState implements _UserRepositoryState { + const _$_UserRepositoryState({final Map users = const {}}) + : _users = users; + + factory _$_UserRepositoryState.fromJson(Map json) => + _$$_UserRepositoryStateFromJson(json); + + final Map _users; + @override + @JsonKey() + Map get users { + if (_users is EqualUnmodifiableMapView) return _users; + // ignore: implicit_dynamic_type + return EqualUnmodifiableMapView(_users); + } + + @override + String toString() { + return 'UserRepositoryState(users: $users)'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$_UserRepositoryState && + const DeepCollectionEquality().equals(other._users, _users)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => + Object.hash(runtimeType, const DeepCollectionEquality().hash(_users)); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$_UserRepositoryStateCopyWith<_$_UserRepositoryState> get copyWith => + __$$_UserRepositoryStateCopyWithImpl<_$_UserRepositoryState>( + this, _$identity); + + @override + Map toJson() { + return _$$_UserRepositoryStateToJson( + this, + ); + } +} + +abstract class _UserRepositoryState implements UserRepositoryState { + const factory _UserRepositoryState({final Map users}) = + _$_UserRepositoryState; + + factory _UserRepositoryState.fromJson(Map json) = + _$_UserRepositoryState.fromJson; + + @override + Map get users; + @override + @JsonKey(ignore: true) + _$$_UserRepositoryStateCopyWith<_$_UserRepositoryState> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/core/repository/user_repository_state.dart b/lib/core/repository/user_repository_state.dart new file mode 100644 index 0000000..3cd32ac --- /dev/null +++ b/lib/core/repository/user_repository_state.dart @@ -0,0 +1,11 @@ +part of 'user_repository.dart'; + +@freezed +class UserRepositoryState with _$UserRepositoryState { + const factory UserRepositoryState({ + @Default({}) Map users, + }) = _UserRepositoryState; + + factory UserRepositoryState.fromJson(Map json) => + _$UserRepositoryStateFromJson(json); +} diff --git a/lib/features/document_details/view/pages/document_details_page.dart b/lib/features/document_details/view/pages/document_details_page.dart index 465e2c1..0a90b4e 100644 --- a/lib/features/document_details/view/pages/document_details_page.dart +++ b/lib/features/document_details/view/pages/document_details_page.dart @@ -39,20 +39,13 @@ class DocumentDetailsPage extends StatefulWidget { } class _DocumentDetailsPageState extends State { - late Future _metaData; static const double _itemSpacing = 24; final _pagingScrollController = ScrollController(); - @override - void initState() { - super.initState(); - _loadMetaData(); - } - void _loadMetaData() { - _metaData = context - .read() - .getMetaData(context.read().state.document); + @override + void didChangeDependencies() { + super.didChangeDependencies(); } @override @@ -67,8 +60,7 @@ class _DocumentDetailsPageState extends State { child: BlocListener( listenWhen: (previous, current) => !previous.isConnected && current.isConnected, listener: (context, state) { - _loadMetaData(); - setState(() {}); + context.read().loadMetaData(); }, child: Scaffold( extendBodyBehindAppBar: false, @@ -98,7 +90,7 @@ class _DocumentDetailsPageState extends State { ), ), ), - Positioned.fill( + Positioned.fill( top: 0, child: Container( height: 100, @@ -285,7 +277,6 @@ class _DocumentDetailsPageState extends State { DocumentDownloadButton( document: state.document, enabled: isConnected, - metaData: _metaData, ), IconButton( tooltip: S.of(context)!.previewTooltip, diff --git a/lib/features/document_details/view/widgets/document_download_button.dart b/lib/features/document_details/view/widgets/document_download_button.dart index 217e129..de696de 100644 --- a/lib/features/document_details/view/widgets/document_download_button.dart +++ b/lib/features/document_details/view/widgets/document_download_button.dart @@ -20,12 +20,10 @@ import 'package:permission_handler/permission_handler.dart'; class DocumentDownloadButton extends StatefulWidget { final DocumentModel? document; final bool enabled; - final Future metaData; const DocumentDownloadButton({ super.key, required this.document, this.enabled = true, - required this.metaData, }); @override diff --git a/lib/features/document_scan/view/scanner_page.dart b/lib/features/document_scan/view/scanner_page.dart index 1cc3d96..ae60b2e 100644 --- a/lib/features/document_scan/view/scanner_page.dart +++ b/lib/features/document_scan/view/scanner_page.dart @@ -38,12 +38,9 @@ class ScannerPage extends StatefulWidget { State createState() => _ScannerPageState(); } -class _ScannerPageState extends State - with SingleTickerProviderStateMixin { - final SliverOverlapAbsorberHandle searchBarHandle = - SliverOverlapAbsorberHandle(); - final SliverOverlapAbsorberHandle actionsHandle = - SliverOverlapAbsorberHandle(); +class _ScannerPageState extends State with SingleTickerProviderStateMixin { + final SliverOverlapAbsorberHandle searchBarHandle = SliverOverlapAbsorberHandle(); + final SliverOverlapAbsorberHandle actionsHandle = SliverOverlapAbsorberHandle(); @override Widget build(BuildContext context) { @@ -180,8 +177,7 @@ class _ScannerPageState extends State final success = await EdgeDetection.detectEdge(file.path); if (!success) { if (kDebugMode) { - dev.log( - '[ScannerPage] Scan either not successful or canceled by user.'); + dev.log('[ScannerPage] Scan either not successful or canceled by user.'); } return; } @@ -198,7 +194,7 @@ class _ScannerPageState extends State final uploadResult = await Navigator.of(context).push( MaterialPageRoute( builder: (_) => BlocProvider( - create: (context) => DocumentUploadCubit( + create: (_) => DocumentUploadCubit( context.read(), context.read(), ), @@ -212,9 +208,7 @@ class _ScannerPageState extends State if ((uploadResult?.success ?? false) && uploadResult?.taskId != null) { // For paperless version older than 1.11.3, task id will always be null! context.read().reset(); - context - .read() - .listenToTaskChanges(uploadResult!.taskId!); + context.read().listenToTaskChanges(uploadResult!.taskId!); } } diff --git a/lib/features/document_search/view/document_search_page.dart b/lib/features/document_search/view/document_search_page.dart index 3bebab7..f28a701 100644 --- a/lib/features/document_search/view/document_search_page.dart +++ b/lib/features/document_search/view/document_search_page.dart @@ -226,13 +226,10 @@ class _DocumentSearchPageState extends State { hasLoaded: state.hasLoaded, enableHeroAnimation: false, onTap: (document) { - Navigator.pushNamed( + pushDocumentDetailsRoute( context, - DocumentDetailsRoute.routeName, - arguments: DocumentDetailsRouteArguments( - document: document, - isLabelClickable: false, - ), + document: document, + isLabelClickable: false, ); }, correspondents: state.correspondents, diff --git a/lib/features/document_search/view/sliver_search_bar.dart b/lib/features/document_search/view/sliver_search_bar.dart index 4cb1bf8..0f36ff7 100644 --- a/lib/features/document_search/view/sliver_search_bar.dart +++ b/lib/features/document_search/view/sliver_search_bar.dart @@ -47,7 +47,10 @@ class SliverSearchBar extends StatelessWidget { Hive.box(HiveBoxes.localUserAccount).listenable(), builder: (context, box, _) { final account = box.get(settings.currentLoggedInUser!)!; - return UserAvatar(userId: settings.currentLoggedInUser!, account: account); + return UserAvatar( + userId: settings.currentLoggedInUser!, + account: account, + ); }, ); }, diff --git a/lib/features/document_upload/view/document_upload_preparation_page.dart b/lib/features/document_upload/view/document_upload_preparation_page.dart index e1dd0f2..ea72e5c 100644 --- a/lib/features/document_upload/view/document_upload_preparation_page.dart +++ b/lib/features/document_upload/view/document_upload_preparation_page.dart @@ -32,6 +32,7 @@ class DocumentUploadPreparationPage extends StatefulWidget { final String? filename; final String? fileExtension; + const DocumentUploadPreparationPage({ Key? key, required this.fileBytes, diff --git a/lib/features/documents/view/pages/documents_page.dart b/lib/features/documents/view/pages/documents_page.dart index 22b7363..c76f86b 100644 --- a/lib/features/documents/view/pages/documents_page.dart +++ b/lib/features/documents/view/pages/documents_page.dart @@ -5,6 +5,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart'; import 'package:paperless_mobile/core/delegate/customizable_sliver_persistent_header_delegate.dart'; +import 'package:paperless_mobile/core/repository/label_repository.dart'; import 'package:paperless_mobile/core/widgets/material/colored_tab_bar.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/features/app_drawer/view/app_drawer.dart'; @@ -443,12 +444,9 @@ class _DocumentsPageState extends State with SingleTickerProvider } void _openDetails(DocumentModel document) { - Navigator.pushNamed( + pushDocumentDetailsRoute( context, - DocumentDetailsRoute.routeName, - arguments: DocumentDetailsRouteArguments( - document: document, - ), + document: document, ); } diff --git a/lib/features/documents/view/widgets/document_preview.dart b/lib/features/documents/view/widgets/document_preview.dart index b505ea8..74da008 100644 --- a/lib/features/documents/view/widgets/document_preview.dart +++ b/lib/features/documents/view/widgets/document_preview.dart @@ -2,7 +2,6 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:paperless_api/paperless_api.dart'; -import 'package:paperless_mobile/features/documents/view/pages/document_view.dart'; import 'package:provider/provider.dart'; import 'package:shimmer/shimmer.dart'; diff --git a/lib/features/home/view/home_route.dart b/lib/features/home/view/home_route.dart index eaa97e7..eb08550 100644 --- a/lib/features/home/view/home_route.dart +++ b/lib/features/home/view/home_route.dart @@ -23,8 +23,13 @@ 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({ @@ -44,6 +49,7 @@ class HomeRoute extends StatelessWidget { Provider( create: (context) => CacheManager( Config( + // Isolated cache per user. localUserId, fileService: DioFileService(context.read().client), ), @@ -121,11 +127,15 @@ class HomeRoute extends StatelessWidget { )..initialize(), ), ProxyProvider( - update: (context, value, previous) => ServerInformationCubit(value), + update: (context, value, previous) => + ServerInformationCubit(value)..updateInformation(), ), ProxyProvider( update: (context, value, previous) => LabelCubit(value), ), + ProxyProvider( + update: (context, value, previous) => TaskStatusCubit(value), + ), ], child: const HomePage(), ); diff --git a/lib/features/inbox/view/widgets/inbox_item.dart b/lib/features/inbox/view/widgets/inbox_item.dart index 6c12d56..235f9e1 100644 --- a/lib/features/inbox/view/widgets/inbox_item.dart +++ b/lib/features/inbox/view/widgets/inbox_item.dart @@ -37,14 +37,11 @@ class _InboxItemState extends State { builder: (context, state) { return GestureDetector( behavior: HitTestBehavior.translucent, - onTap: () async { - Navigator.pushNamed( + onTap: () { + pushDocumentDetailsRoute( context, - DocumentDetailsRoute.routeName, - arguments: DocumentDetailsRouteArguments( - document: widget.document, - isLabelClickable: false, - ), + document: widget.document, + isLabelClickable: false, ); }, child: SizedBox( diff --git a/lib/features/linked_documents/view/linked_documents_page.dart b/lib/features/linked_documents/view/linked_documents_page.dart index 2171f7c..e8a688b 100644 --- a/lib/features/linked_documents/view/linked_documents_page.dart +++ b/lib/features/linked_documents/view/linked_documents_page.dart @@ -52,13 +52,10 @@ class _LinkedDocumentsPageState extends State isLoading: state.isLoading, hasLoaded: state.hasLoaded, onTap: (document) { - Navigator.pushNamed( + pushDocumentDetailsRoute( context, - DocumentDetailsRoute.routeName, - arguments: DocumentDetailsRouteArguments( - document: document, - isLabelClickable: false, - ), + document: document, + isLabelClickable: false, ); }, correspondents: state.correspondents, diff --git a/lib/features/login/cubit/authentication_cubit.dart b/lib/features/login/cubit/authentication_cubit.dart index 47e9f84..042e093 100644 --- a/lib/features/login/cubit/authentication_cubit.dart +++ b/lib/features/login/cubit/authentication_cubit.dart @@ -1,7 +1,7 @@ import 'dart:convert'; import 'dart:typed_data'; -import 'package:equatable/equatable.dart'; +import 'package:dio/dio.dart'; import 'package:flutter/widgets.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:hive_flutter/adapters.dart'; @@ -9,6 +9,7 @@ import 'package:hydrated_bloc/hydrated_bloc.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/config/hive/hive_config.dart'; import 'package:paperless_mobile/core/database/tables/local_user_app_state.dart'; +import 'package:paperless_mobile/core/factory/paperless_api_factory.dart'; import 'package:paperless_mobile/core/security/session_manager.dart'; import 'package:paperless_mobile/features/login/model/client_certificate.dart'; import 'package:paperless_mobile/features/login/model/login_form_credentials.dart'; @@ -23,16 +24,14 @@ part 'authentication_state.dart'; part 'authentication_cubit.freezed.dart'; class AuthenticationCubit extends Cubit { - final PaperlessUserApi _userApi; final LocalAuthenticationService _localAuthService; - final PaperlessAuthenticationApi _authApi; + final PaperlessApiFactory _apiFactory; final SessionManager _sessionManager; AuthenticationCubit( this._localAuthService, - this._authApi, + this._apiFactory, this._sessionManager, - this._userApi, ) : super(const AuthenticationState.unauthenticated()); Future login({ @@ -43,7 +42,7 @@ class AuthenticationCubit extends Cubit { assert(credentials.username != null && credentials.password != null); final localUserId = "${credentials.username}@$serverUrl"; - final serverUser = await _addUser( + await _addUser( localUserId, serverUrl, credentials, @@ -51,13 +50,13 @@ class AuthenticationCubit extends Cubit { _sessionManager, ); - final response = await _sessionManager.client.get("/api/"); - final apiVersion = response.headers["x-api-version"] as int; + final apiVersion = await _getApiVersion(_sessionManager.client); // Mark logged in user as currently active user. final globalSettings = Hive.box(HiveBoxes.globalSettings).getValue()!; globalSettings.currentLoggedInUser = localUserId; await globalSettings.save(); + emit( AuthenticationState.authenticated( apiVersion: apiVersion, @@ -110,8 +109,8 @@ class AuthenticationCubit extends Cubit { globalSettings.currentLoggedInUser = localUserId; await globalSettings.save(); - final response = await _sessionManager.client.get("/api/"); - final apiVersion = response.headers["x-api-version"] as int; + final apiVersion = await _getApiVersion(_sessionManager.client); + emit(AuthenticationState.authenticated( localUserId: localUserId, apiVersion: apiVersion, @@ -188,8 +187,7 @@ class AuthenticationCubit extends Cubit { authToken: authentication.token, baseUrl: userAccount.serverUrl, ); - final response = await _sessionManager.client.get("/api/"); - final apiVersion = response.headers["x-api-version"] as int; + final apiVersion = await _getApiVersion(_sessionManager.client); emit( AuthenticationState.authenticated( apiVersion: apiVersion, @@ -247,26 +245,34 @@ class AuthenticationCubit extends Cubit { baseUrl: serverUrl, clientCertificate: clientCert, ); - final authApi = PaperlessAuthenticationApiImpl(sessionManager.client); + + final authApi = _apiFactory.createAuthenticationApi(sessionManager.client); final token = await authApi.login( username: credentials.username!, password: credentials.password!, ); + sessionManager.updateSettings( baseUrl: serverUrl, clientCertificate: clientCert, authToken: token, ); - + final userAccountBox = Hive.box(HiveBoxes.localUserAccount); final userStateBox = Hive.box(HiveBoxes.localUserAppState); if (userAccountBox.containsKey(localUserId)) { throw Exception("User with id $localUserId already exists!"); } + final apiVersion = await _getApiVersion(sessionManager.client); - final serverUserId = await _userApi.findCurrentUserId(); + final serverUser = await _apiFactory + .createUserApi( + sessionManager.client, + apiVersion: apiVersion, + ) + .findCurrentUser(); // Create user account await userAccountBox.put( @@ -275,7 +281,7 @@ class AuthenticationCubit extends Cubit { id: localUserId, settings: LocalUserSettings(), serverUrl: serverUrl, - paperlessUserId: 1, + paperlessUser: serverUser, ), ); @@ -295,6 +301,11 @@ class AuthenticationCubit extends Cubit { ), ); userCredentialsBox.close(); - return serverUserId; + return serverUser.id; + } + + Future _getApiVersion(Dio dio) async { + final response = await dio.get("/api/"); + return int.parse(response.headers.value('x-api-version') ?? "3"); } } diff --git a/lib/features/saved_view/cubit/saved_view_cubit.dart b/lib/features/saved_view/cubit/saved_view_cubit.dart index 8b56964..76912e8 100644 --- a/lib/features/saved_view/cubit/saved_view_cubit.dart +++ b/lib/features/saved_view/cubit/saved_view_cubit.dart @@ -1,6 +1,5 @@ import 'dart:async'; -import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:paperless_api/paperless_api.dart'; @@ -37,18 +36,20 @@ class SavedViewCubit extends Cubit { }, ); - _savedViewRepository.subscribe(this, (views) { - emit( - state.maybeWhen( - loaded: - (savedViews, correspondents, documentTypes, tags, storagePaths) => - (state as _SavedViewLoadedState).copyWith( - savedViews: views, + _savedViewRepository.addListener( + this, + onChanged: (views) { + emit( + state.maybeWhen( + loaded: (savedViews, correspondents, documentTypes, tags, storagePaths) => + (state as _SavedViewLoadedState).copyWith( + savedViews: views.savedViews, + ), + orElse: () => state, ), - orElse: () => state, - ), - ); - }); + ); + }, + ); } Future add(SavedView view) async { @@ -77,7 +78,7 @@ class SavedViewCubit extends Cubit { @override Future close() { - _savedViewRepository.unsubscribe(this); + _savedViewRepository.removeListener(this); _labelRepository.removeListener(this); return super.close(); } diff --git a/lib/features/saved_view_details/view/saved_view_details_page.dart b/lib/features/saved_view_details/view/saved_view_details_page.dart index 8887803..7bc08d9 100644 --- a/lib/features/saved_view_details/view/saved_view_details_page.dart +++ b/lib/features/saved_view_details/view/saved_view_details_page.dart @@ -76,12 +76,13 @@ class _SavedViewDetailsPageState extends State isLoading: state.isLoading, hasLoaded: state.hasLoaded, onTap: (document) { - Navigator.pushNamed( + Navigator.push( context, - DocumentDetailsRoute.routeName, - arguments: DocumentDetailsRouteArguments( - document: document, - isLabelClickable: false, + MaterialPageRoute( + builder: (context) => DocumentDetailsRoute( + document: document, + isLabelClickable: false, + ), ), ); }, diff --git a/lib/features/settings/view/manage_accounts_page.dart b/lib/features/settings/view/manage_accounts_page.dart index 9165ed0..55b8a95 100644 --- a/lib/features/settings/view/manage_accounts_page.dart +++ b/lib/features/settings/view/manage_accounts_page.dart @@ -86,11 +86,11 @@ class ManageAccountsPage extends StatelessWidget { final child = SizedBox( width: double.maxFinite, child: ListTile( - title: Text(account.username), + title: Text(account.paperlessUser.username), subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (account.fullName != null) Text(account.fullName!), + if (account.paperlessUser.fullName != null) Text(account.paperlessUser.fullName!), Text( account.serverUrl.replaceFirst(RegExp(r'https://?'), ''), style: TextStyle( @@ -99,7 +99,7 @@ class ManageAccountsPage extends StatelessWidget { ), ], ), - isThreeLine: account.fullName != null, + isThreeLine: account.paperlessUser.fullName != null, leading: UserAvatar( account: account, userId: userId, diff --git a/lib/features/settings/view/settings_page.dart b/lib/features/settings/view/settings_page.dart index 5ba5f4a..d3614fd 100644 --- a/lib/features/settings/view/settings_page.dart +++ b/lib/features/settings/view/settings_page.dart @@ -22,7 +22,7 @@ class SettingsPage extends StatelessWidget { final host = user!.serverUrl.replaceFirst(RegExp(r"https?://"), ""); return ListTile( title: Text( - S.of(context)!.loggedInAs(user.username) + "@$host", + S.of(context)!.loggedInAs(user.paperlessUser.username) + "@$host", style: Theme.of(context).textTheme.labelSmall, textAlign: TextAlign.center, ), diff --git a/lib/features/settings/view/widgets/user_avatar.dart b/lib/features/settings/view/widgets/user_avatar.dart index 8c11053..1e44bef 100644 --- a/lib/features/settings/view/widgets/user_avatar.dart +++ b/lib/features/settings/view/widgets/user_avatar.dart @@ -1,5 +1,8 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/database/tables/local_user_account.dart'; +import 'package:paperless_mobile/core/repository/user_repository.dart'; class UserAvatar extends StatelessWidget { final String userId; @@ -16,7 +19,7 @@ class UserAvatar extends StatelessWidget { final backgroundColor = Colors.primaries[userId.hashCode % Colors.primaries.length]; final foregroundColor = backgroundColor.computeLuminance() > 0.5 ? Colors.black : Colors.white; return CircleAvatar( - child: Text((account.fullName ?? account.username) + child: Text((account.paperlessUser.fullName ?? account.paperlessUser.username) .split(" ") .take(2) .map((e) => e.substring(0, 1)) diff --git a/lib/features/similar_documents/view/similar_documents_view.dart b/lib/features/similar_documents/view/similar_documents_view.dart index 6f96453..3b9352f 100644 --- a/lib/features/similar_documents/view/similar_documents_view.dart +++ b/lib/features/similar_documents/view/similar_documents_view.dart @@ -36,10 +36,8 @@ class _SimilarDocumentsViewState extends State @override Widget build(BuildContext context) { return BlocConsumer( - listenWhen: (previous, current) => - !previous.isConnected && current.isConnected, - listener: (context, state) => - context.read().initialize(), + listenWhen: (previous, current) => !previous.isConnected && current.isConnected, + listener: (context, state) => context.read().initialize(), builder: (context, connectivity) { return BlocBuilder( builder: (context, state) { @@ -48,9 +46,7 @@ class _SimilarDocumentsViewState extends State child: OfflineWidget(), ); } - if (state.hasLoaded && - !state.isLoading && - state.documents.isEmpty) { + if (state.hasLoaded && !state.isLoading && state.documents.isEmpty) { return SliverToBoxAdapter( child: Center( child: Text(S.of(context)!.noItemsFound), @@ -65,13 +61,10 @@ class _SimilarDocumentsViewState extends State hasLoaded: state.hasLoaded, enableHeroAnimation: false, onTap: (document) { - Navigator.pushNamed( + pushDocumentDetailsRoute( context, - DocumentDetailsRoute.routeName, - arguments: DocumentDetailsRouteArguments( - document: document, - isLabelClickable: false, - ), + document: document, + isLabelClickable: false, ); }, correspondents: state.correspondents, diff --git a/lib/main.dart b/lib/main.dart index cd7e1e5..792b4b1 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -5,7 +5,6 @@ import 'package:device_info_plus/device_info_plus.dart'; import 'package:dynamic_color/dynamic_color.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_cache_manager/flutter_cache_manager.dart' as cm; import 'package:flutter_displaymode/flutter_displaymode.dart'; import 'package:flutter_native_splash/flutter_native_splash.dart'; import 'package:hive_flutter/adapters.dart'; @@ -26,11 +25,8 @@ import 'package:paperless_mobile/core/factory/paperless_api_factory_impl.dart'; import 'package:paperless_mobile/core/interceptor/dio_http_error_interceptor.dart'; import 'package:paperless_mobile/core/interceptor/language_header.interceptor.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/security/session_manager.dart'; import 'package:paperless_mobile/core/service/connectivity_status_service.dart'; -import 'package:paperless_mobile/core/service/dio_file_service.dart'; import 'package:paperless_mobile/core/type/types.dart'; import 'package:paperless_mobile/features/app_intro/application_intro_slideshow.dart'; import 'package:paperless_mobile/features/home/view/home_route.dart'; @@ -125,6 +121,8 @@ void main() async { languageHeaderInterceptor.preferredLocaleSubtag = globalSettings.preferredLocaleSubtag; }); + final apiFactory = PaperlessApiFactoryImpl(sessionManager); + runApp( MultiProvider( providers: [ @@ -139,9 +137,17 @@ void main() async { child: MultiBlocProvider( providers: [ BlocProvider.value(value: connectivityCubit), + BlocProvider( + create: (context) => AuthenticationCubit( + localAuthService, + apiFactory, + sessionManager, + ), + child: Container(), + ) ], child: PaperlessMobileEntrypoint( - paperlessProviderFactory: PaperlessApiFactoryImpl(sessionManager), + paperlessProviderFactory: apiFactory, ), ), ), @@ -187,9 +193,6 @@ class _PaperlessMobileEntrypointState extends State { localizationsDelegates: const [ ...S.localizationsDelegates, ], - routes: { - DocumentDetailsRoute.routeName: (context) => const DocumentDetailsRoute(), - }, home: AuthenticationWrapper( paperlessProviderFactory: widget.paperlessProviderFactory, ), @@ -217,7 +220,10 @@ class _AuthenticationWrapperState extends State { @override void didChangeDependencies() { super.didChangeDependencies(); - FlutterNativeSplash.remove(); + context + .read() + .restoreSessionState() + .then((value) => FlutterNativeSplash.remove()); } @override diff --git a/lib/routes/document_details_route.dart b/lib/routes/document_details_route.dart index 41be2de..c2e1afa 100644 --- a/lib/routes/document_details_route.dart +++ b/lib/routes/document_details_route.dart @@ -1,34 +1,44 @@ +import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart'; import 'package:paperless_mobile/core/repository/label_repository.dart'; 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/notifications/services/local_notification_service.dart'; +import 'package:provider/provider.dart'; class DocumentDetailsRoute extends StatelessWidget { - static const String routeName = "/documentDetails"; - const DocumentDetailsRoute({super.key}); + final DocumentModel document; + final bool isLabelClickable; + final bool allowEdit; + final String? titleAndContentQueryString; + + const DocumentDetailsRoute({ + super.key, + required this.document, + this.isLabelClickable = true, + this.allowEdit = true, + this.titleAndContentQueryString, + }); @override Widget build(BuildContext context) { - final args = ModalRoute.of(context)!.settings.arguments - as DocumentDetailsRouteArguments; - return BlocProvider( - create: (context) => DocumentDetailsCubit( + create: (_) => DocumentDetailsCubit( context.read(), context.read(), context.read(), context.read(), - initialDocument: args.document, + initialDocument: document, ), - child: RepositoryProvider.value( - value: context.read(), - child: DocumentDetailsPage( - allowEdit: args.allowEdit, - isLabelClickable: args.isLabelClickable, - titleAndContentQueryString: args.titleAndContentQueryString, - ), + lazy: false, + child: DocumentDetailsPage( + allowEdit: allowEdit, + isLabelClickable: isLabelClickable, + titleAndContentQueryString: titleAndContentQueryString, ), ); } @@ -47,3 +57,33 @@ class DocumentDetailsRouteArguments { this.titleAndContentQueryString, }); } + +Future pushDocumentDetailsRoute( + BuildContext context, { + required DocumentModel document, + bool isLabelClickable = true, + bool allowEdit = true, + String? titleAndContentQueryString, +}) { + final LabelRepository labelRepository = context.read(); + final DocumentChangedNotifier changeNotifier = context.read(); + final PaperlessDocumentsApi documentsApi = context.read(); + final LocalNotificationService notificationservice = context.read(); + final CacheManager cacheManager = context.read(); + return Navigator.of(context).push(MaterialPageRoute( + builder: (_) => MultiProvider( + providers: [ + Provider.value(value: documentsApi), + Provider.value(value: labelRepository), + Provider.value(value: changeNotifier), + Provider.value(value: notificationservice), + Provider.value(value: cacheManager), + ], + child: DocumentDetailsRoute( + document: document, + allowEdit: allowEdit, + isLabelClickable: isLabelClickable, + ), + ), + )); +} diff --git a/packages/paperless_api/lib/src/models/user_model.dart b/packages/paperless_api/lib/src/models/user_model.dart index b02a377..53d9038 100644 --- a/packages/paperless_api/lib/src/models/user_model.dart +++ b/packages/paperless_api/lib/src/models/user_model.dart @@ -51,7 +51,7 @@ class UserModel with _$UserModel { if (value.firstName == null) { return value.lastName; } - return value.firstName! + (value.lastName ?? ''); + return "${value.firstName!} ${value.lastName ?? ''}"; }, ); diff --git a/packages/paperless_api/lib/src/modules/server_stats_api/paperless_server_stats_api_impl.dart b/packages/paperless_api/lib/src/modules/server_stats_api/paperless_server_stats_api_impl.dart index 9a2f8c0..92e033b 100644 --- a/packages/paperless_api/lib/src/modules/server_stats_api/paperless_server_stats_api_impl.dart +++ b/packages/paperless_api/lib/src/modules/server_stats_api/paperless_server_stats_api_impl.dart @@ -18,7 +18,12 @@ class PaperlessServerStatsApiImpl implements PaperlessServerStatsApi { @override Future getServerInformation() async { - final response = await client.get("/api/"); + final response = await client.get("/api/remote_version/"); + if (response.statusCode == 200) { + final version = response.data["version"] as String; + final updateAvailable = response.data["update_available"] as bool; + } + throw PaperlessServerException.unknown(); final version = response.headers[PaperlessServerInformationModel.versionHeader]?.first ?? 'unknown'; final apiVersion = int.tryParse( diff --git a/packages/paperless_api/lib/src/modules/user_api/paperless_user_api.dart b/packages/paperless_api/lib/src/modules/user_api/paperless_user_api.dart index d6cbae2..59c3171 100644 --- a/packages/paperless_api/lib/src/modules/user_api/paperless_user_api.dart +++ b/packages/paperless_api/lib/src/modules/user_api/paperless_user_api.dart @@ -2,5 +2,5 @@ import 'package:paperless_api/paperless_api.dart'; abstract class PaperlessUserApi { Future findCurrentUserId(); - Future find(int id); + Future findCurrentUser(); } diff --git a/packages/paperless_api/lib/src/modules/user_api/paperless_user_api_v2_impl.dart b/packages/paperless_api/lib/src/modules/user_api/paperless_user_api_v2_impl.dart index 2e63ba5..007a1c9 100644 --- a/packages/paperless_api/lib/src/modules/user_api/paperless_user_api_v2_impl.dart +++ b/packages/paperless_api/lib/src/modules/user_api/paperless_user_api_v2_impl.dart @@ -1,6 +1,5 @@ import 'package:dio/dio.dart'; import 'package:paperless_api/paperless_api.dart'; -import 'package:paperless_api/src/modules/user_api/paperless_user_api.dart'; class PaperlessUserApiV2Impl implements PaperlessUserApi { final Dio client; @@ -11,13 +10,13 @@ class PaperlessUserApiV2Impl implements PaperlessUserApi { Future findCurrentUserId() async { final response = await client.get("/api/ui_settings/"); if (response.statusCode == 200) { - return response.data['user']['id']; + return response.data['user_id']; } throw const PaperlessServerException.unknown(); } @override - Future find(int id) async { + Future findCurrentUser() async { final response = await client.get("/api/ui_settings/"); if (response.statusCode == 200) { return UserModelV2.fromJson(response.data); diff --git a/packages/paperless_api/lib/src/modules/user_api/paperless_user_api_v3.dart b/packages/paperless_api/lib/src/modules/user_api/paperless_user_api_v3.dart index 3f8447f..4d9dbc5 100644 --- a/packages/paperless_api/lib/src/modules/user_api/paperless_user_api_v3.dart +++ b/packages/paperless_api/lib/src/modules/user_api/paperless_user_api_v3.dart @@ -1,7 +1,9 @@ import 'package:paperless_api/src/models/user_model.dart'; abstract class PaperlessUserApiV3 { - Future> findWhere({ + Future find(int id); + Future> findAll(); + Future> findWhere({ String startsWith, String endsWith, String contains, diff --git a/packages/paperless_api/lib/src/modules/user_api/paperless_user_api_v3_impl.dart b/packages/paperless_api/lib/src/modules/user_api/paperless_user_api_v3_impl.dart index a3ce6ff..8e217c9 100644 --- a/packages/paperless_api/lib/src/modules/user_api/paperless_user_api_v3_impl.dart +++ b/packages/paperless_api/lib/src/modules/user_api/paperless_user_api_v3_impl.dart @@ -1,7 +1,5 @@ import 'package:dio/dio.dart'; import 'package:paperless_api/paperless_api.dart'; -import 'package:paperless_api/src/modules/user_api/paperless_user_api.dart'; -import 'package:paperless_api/src/modules/user_api/paperless_user_api_v3.dart'; class PaperlessUserApiV3Impl implements PaperlessUserApi, PaperlessUserApiV3 { final Dio dio; @@ -9,7 +7,7 @@ class PaperlessUserApiV3Impl implements PaperlessUserApi, PaperlessUserApiV3 { PaperlessUserApiV3Impl(this.dio); @override - Future find(int id) async { + Future find(int id) async { final response = await dio.get("/api/users/$id/"); if (response.statusCode == 200) { return UserModelV3.fromJson(response.data); @@ -18,7 +16,7 @@ class PaperlessUserApiV3Impl implements PaperlessUserApi, PaperlessUserApiV3 { } @override - Future> findWhere({ + Future> findWhere({ String startsWith = '', String endsWith = '', String contains = '', @@ -31,9 +29,9 @@ class PaperlessUserApiV3Impl implements PaperlessUserApi, PaperlessUserApiV3 { "username__iexact": username, }); if (response.statusCode == 200) { - return PagedSearchResult.fromJson( + return PagedSearchResult.fromJson( response.data, - UserModelV3.fromJson as UserModel Function(Object?), + UserModelV3.fromJson as UserModelV3 Function(Object?), ).results; } throw const PaperlessServerException.unknown(); @@ -43,8 +41,26 @@ class PaperlessUserApiV3Impl implements PaperlessUserApi, PaperlessUserApiV3 { Future findCurrentUserId() async { final response = await dio.get("/api/ui_settings/"); if (response.statusCode == 200) { - return response.data['user_id']; + return response.data['user']['id']; } throw const PaperlessServerException.unknown(); } + + @override + Future> findAll() async { + final response = await dio.get("/api/users/"); + if (response.statusCode == 200) { + return PagedSearchResult.fromJson( + response.data, + UserModelV3.fromJson as UserModelV3 Function(Object?), + ).results; + } + throw const PaperlessServerException.unknown(); + } + + @override + Future findCurrentUser() async { + final id = await findCurrentUserId(); + return find(id); + } }