feat: More resturcturings, adapt code to previous changes

This commit is contained in:
Anton Stubenbord
2023-05-01 23:05:54 +02:00
parent 88085b5662
commit d5c68e023c
35 changed files with 475 additions and 200 deletions

View File

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

View File

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

View File

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

View File

@@ -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<LabelRepositoryState> {
class LabelRepository extends PersistentRepository<LabelRepositoryState> {
final PaperlessLabelsApi _api;
final Map<Object, StreamSubscription> _subscribers = {};
LabelRepository(this._api) : super(const LabelRepositoryState());
void addListener(
Object source, {
required void Function(LabelRepositoryState) onChanged,
}) {
onChanged(state);
_subscribers.putIfAbsent(source, () {
return stream.listen((event) => onChanged(event));
});
}
void removeListener(Object source) async {
await _subscribers[source]?.cancel();
_subscribers.remove(source);
}
Future<void> initialize() {
debugPrint("Initializing labels...");
return Future.wait([
@@ -195,14 +180,6 @@ class LabelRepository extends HydratedCubit<LabelRepositoryState> {
return updated;
}
@override
Future<void> close() {
_subscribers.forEach((key, subscription) {
subscription.cancel();
});
return super.close();
}
@override
Future<void> clear() async {
await super.clear();

View File

@@ -0,0 +1,32 @@
import 'dart:async';
import 'package:hydrated_bloc/hydrated_bloc.dart';
abstract class PersistentRepository<T> extends HydratedCubit<T> {
final Map<Object, StreamSubscription> _subscribers = {};
PersistentRepository(T initialState) : super(initialState);
void addListener(
Object source, {
required void Function(T) onChanged,
}) {
onChanged(state);
_subscribers.putIfAbsent(source, () {
return stream.listen((event) => onChanged(event));
});
}
void removeListener(Object source) async {
await _subscribers[source]?.cancel();
_subscribers.remove(source);
}
@override
Future<void> close() {
_subscribers.forEach((key, subscription) {
subscription.cancel();
});
return super.close();
}
}

View File

@@ -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<SavedViewRepositoryState> {
class SavedViewRepository extends PersistentRepository<SavedViewRepositoryState> {
final PaperlessSavedViewsApi _api;
final Map<Object, StreamSubscription> _subscribers = {};
void subscribe(
Object source,
void Function(Map<int, SavedView>) onChanged,
) {
_subscribers.putIfAbsent(source, () {
onChanged(state.savedViews);
return stream.listen((event) => onChanged(event.savedViews));
});
}
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<SavedViewRepositoryState> {
return found;
}
@override
Future<void> close() {
_subscribers.forEach((key, subscription) {
subscription.cancel();
});
return super.close();
}
@override
Future<void> clear() async {
await super.clear();

View File

@@ -0,0 +1,40 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/persistent_repository.dart';
part 'user_repository_state.dart';
part 'user_repository.freezed.dart';
part 'user_repository.g.dart';
/// Repository for new users (API v3, server version 1.14.2+)
class UserRepository extends PersistentRepository<UserRepositoryState> {
final PaperlessUserApiV3 _userApiV3;
UserRepository(this._userApiV3) : super(const UserRepositoryState());
Future<void> initialize() async {
await findAll();
}
Future<Iterable<UserModel>> findAll() async {
final users = await _userApiV3.findAll();
emit(state.copyWith(users: {for (var e in users) e.id: e}));
return users;
}
Future<UserModel?> find(int id) async {
final user = await _userApiV3.find(id);
emit(state.copyWith(users: state.users..[id] = user));
return user;
}
@override
UserRepositoryState? fromJson(Map<String, dynamic> json) {
return UserRepositoryState.fromJson(json);
}
@override
Map<String, dynamic>? toJson(UserRepositoryState state) {
return state.toJson();
}
}

View File

@@ -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>(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<String, dynamic> json) {
return _UserRepositoryState.fromJson(json);
}
/// @nodoc
mixin _$UserRepositoryState {
Map<int, UserModel> get users => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$UserRepositoryStateCopyWith<UserRepositoryState> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $UserRepositoryStateCopyWith<$Res> {
factory $UserRepositoryStateCopyWith(
UserRepositoryState value, $Res Function(UserRepositoryState) then) =
_$UserRepositoryStateCopyWithImpl<$Res, UserRepositoryState>;
@useResult
$Res call({Map<int, UserModel> 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<int, UserModel>,
) 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<int, UserModel> 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<int, UserModel>,
));
}
}
/// @nodoc
@JsonSerializable()
class _$_UserRepositoryState implements _UserRepositoryState {
const _$_UserRepositoryState({final Map<int, UserModel> users = const {}})
: _users = users;
factory _$_UserRepositoryState.fromJson(Map<String, dynamic> json) =>
_$$_UserRepositoryStateFromJson(json);
final Map<int, UserModel> _users;
@override
@JsonKey()
Map<int, UserModel> 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<String, dynamic> toJson() {
return _$$_UserRepositoryStateToJson(
this,
);
}
}
abstract class _UserRepositoryState implements UserRepositoryState {
const factory _UserRepositoryState({final Map<int, UserModel> users}) =
_$_UserRepositoryState;
factory _UserRepositoryState.fromJson(Map<String, dynamic> json) =
_$_UserRepositoryState.fromJson;
@override
Map<int, UserModel> get users;
@override
@JsonKey(ignore: true)
_$$_UserRepositoryStateCopyWith<_$_UserRepositoryState> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@@ -0,0 +1,11 @@
part of 'user_repository.dart';
@freezed
class UserRepositoryState with _$UserRepositoryState {
const factory UserRepositoryState({
@Default({}) Map<int, UserModel> users,
}) = _UserRepositoryState;
factory UserRepositoryState.fromJson(Map<String, dynamic> json) =>
_$UserRepositoryStateFromJson(json);
}

View File

@@ -39,20 +39,13 @@ class DocumentDetailsPage extends StatefulWidget {
}
class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
late Future<DocumentMetaData> _metaData;
static const double _itemSpacing = 24;
final _pagingScrollController = ScrollController();
@override
void initState() {
super.initState();
_loadMetaData();
}
void _loadMetaData() {
_metaData = context
.read<PaperlessDocumentsApi>()
.getMetaData(context.read<DocumentDetailsCubit>().state.document);
@override
void didChangeDependencies() {
super.didChangeDependencies();
}
@override
@@ -67,8 +60,7 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
child: BlocListener<ConnectivityCubit, ConnectivityState>(
listenWhen: (previous, current) => !previous.isConnected && current.isConnected,
listener: (context, state) {
_loadMetaData();
setState(() {});
context.read<DocumentDetailsCubit>().loadMetaData();
},
child: Scaffold(
extendBodyBehindAppBar: false,
@@ -98,7 +90,7 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
),
),
),
Positioned.fill(
Positioned.fill(
top: 0,
child: Container(
height: 100,
@@ -285,7 +277,6 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
DocumentDownloadButton(
document: state.document,
enabled: isConnected,
metaData: _metaData,
),
IconButton(
tooltip: S.of(context)!.previewTooltip,

View File

@@ -20,12 +20,10 @@ import 'package:permission_handler/permission_handler.dart';
class DocumentDownloadButton extends StatefulWidget {
final DocumentModel? document;
final bool enabled;
final Future<DocumentMetaData> metaData;
const DocumentDownloadButton({
super.key,
required this.document,
this.enabled = true,
required this.metaData,
});
@override

View File

@@ -38,12 +38,9 @@ class ScannerPage extends StatefulWidget {
State<ScannerPage> createState() => _ScannerPageState();
}
class _ScannerPageState extends State<ScannerPage>
with SingleTickerProviderStateMixin {
final SliverOverlapAbsorberHandle searchBarHandle =
SliverOverlapAbsorberHandle();
final SliverOverlapAbsorberHandle actionsHandle =
SliverOverlapAbsorberHandle();
class _ScannerPageState extends State<ScannerPage> with SingleTickerProviderStateMixin {
final SliverOverlapAbsorberHandle searchBarHandle = SliverOverlapAbsorberHandle();
final SliverOverlapAbsorberHandle actionsHandle = SliverOverlapAbsorberHandle();
@override
Widget build(BuildContext context) {
@@ -180,8 +177,7 @@ class _ScannerPageState extends State<ScannerPage>
final success = await EdgeDetection.detectEdge(file.path);
if (!success) {
if (kDebugMode) {
dev.log(
'[ScannerPage] Scan either not successful or canceled by user.');
dev.log('[ScannerPage] Scan either not successful or canceled by user.');
}
return;
}
@@ -198,7 +194,7 @@ class _ScannerPageState extends State<ScannerPage>
final uploadResult = await Navigator.of(context).push<DocumentUploadResult>(
MaterialPageRoute(
builder: (_) => BlocProvider(
create: (context) => DocumentUploadCubit(
create: (_) => DocumentUploadCubit(
context.read(),
context.read(),
),
@@ -212,9 +208,7 @@ class _ScannerPageState extends State<ScannerPage>
if ((uploadResult?.success ?? false) && uploadResult?.taskId != null) {
// For paperless version older than 1.11.3, task id will always be null!
context.read<DocumentScannerCubit>().reset();
context
.read<TaskStatusCubit>()
.listenToTaskChanges(uploadResult!.taskId!);
context.read<TaskStatusCubit>().listenToTaskChanges(uploadResult!.taskId!);
}
}

View File

@@ -226,13 +226,10 @@ class _DocumentSearchPageState extends State<DocumentSearchPage> {
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,

View File

@@ -47,7 +47,10 @@ class SliverSearchBar extends StatelessWidget {
Hive.box<LocalUserAccount>(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,
);
},
);
},

View File

@@ -32,6 +32,7 @@ class DocumentUploadPreparationPage extends StatefulWidget {
final String? filename;
final String? fileExtension;
const DocumentUploadPreparationPage({
Key? key,
required this.fileBytes,

View File

@@ -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<DocumentsPage> with SingleTickerProvider
}
void _openDetails(DocumentModel document) {
Navigator.pushNamed(
pushDocumentDetailsRoute(
context,
DocumentDetailsRoute.routeName,
arguments: DocumentDetailsRouteArguments(
document: document,
),
document: document,
);
}

View File

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

View File

@@ -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<CacheManager>(
create: (context) => CacheManager(
Config(
// Isolated cache per user.
localUserId,
fileService: DioFileService(context.read<SessionManager>().client),
),
@@ -121,11 +127,15 @@ class HomeRoute extends StatelessWidget {
)..initialize(),
),
ProxyProvider<PaperlessServerStatsApi, ServerInformationCubit>(
update: (context, value, previous) => ServerInformationCubit(value),
update: (context, value, previous) =>
ServerInformationCubit(value)..updateInformation(),
),
ProxyProvider<LabelRepository, LabelCubit>(
update: (context, value, previous) => LabelCubit(value),
),
ProxyProvider<PaperlessTasksApi, TaskStatusCubit>(
update: (context, value, previous) => TaskStatusCubit(value),
),
],
child: const HomePage(),
);

View File

@@ -37,14 +37,11 @@ class _InboxItemState extends State<InboxItem> {
builder: (context, state) {
return GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () async {
Navigator.pushNamed(
onTap: () {
pushDocumentDetailsRoute(
context,
DocumentDetailsRoute.routeName,
arguments: DocumentDetailsRouteArguments(
document: widget.document,
isLabelClickable: false,
),
document: widget.document,
isLabelClickable: false,
);
},
child: SizedBox(

View File

@@ -52,13 +52,10 @@ class _LinkedDocumentsPageState extends State<LinkedDocumentsPage>
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,

View File

@@ -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<AuthenticationState> {
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<void> login({
@@ -43,7 +42,7 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
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<AuthenticationState> {
_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<GlobalSettings>(HiveBoxes.globalSettings).getValue()!;
globalSettings.currentLoggedInUser = localUserId;
await globalSettings.save();
emit(
AuthenticationState.authenticated(
apiVersion: apiVersion,
@@ -110,8 +109,8 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
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<AuthenticationState> {
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<AuthenticationState> {
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<LocalUserAccount>(HiveBoxes.localUserAccount);
final userStateBox = Hive.box<LocalUserAppState>(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<AuthenticationState> {
id: localUserId,
settings: LocalUserSettings(),
serverUrl: serverUrl,
paperlessUserId: 1,
paperlessUser: serverUser,
),
);
@@ -295,6 +301,11 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
),
);
userCredentialsBox.close();
return serverUserId;
return serverUser.id;
}
Future<int> _getApiVersion(Dio dio) async {
final response = await dio.get("/api/");
return int.parse(response.headers.value('x-api-version') ?? "3");
}
}

View File

@@ -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<SavedViewState> {
},
);
_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<SavedView> add(SavedView view) async {
@@ -77,7 +78,7 @@ class SavedViewCubit extends Cubit<SavedViewState> {
@override
Future<void> close() {
_savedViewRepository.unsubscribe(this);
_savedViewRepository.removeListener(this);
_labelRepository.removeListener(this);
return super.close();
}

View File

@@ -76,12 +76,13 @@ class _SavedViewDetailsPageState extends State<SavedViewDetailsPage>
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,
),
),
);
},

View File

@@ -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,

View File

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

View File

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

View File

@@ -36,10 +36,8 @@ class _SimilarDocumentsViewState extends State<SimilarDocumentsView>
@override
Widget build(BuildContext context) {
return BlocConsumer<ConnectivityCubit, ConnectivityState>(
listenWhen: (previous, current) =>
!previous.isConnected && current.isConnected,
listener: (context, state) =>
context.read<SimilarDocumentsCubit>().initialize(),
listenWhen: (previous, current) => !previous.isConnected && current.isConnected,
listener: (context, state) => context.read<SimilarDocumentsCubit>().initialize(),
builder: (context, connectivity) {
return BlocBuilder<SimilarDocumentsCubit, SimilarDocumentsState>(
builder: (context, state) {
@@ -48,9 +46,7 @@ class _SimilarDocumentsViewState extends State<SimilarDocumentsView>
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<SimilarDocumentsView>
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,

View File

@@ -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<ConnectivityCubit>.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<PaperlessMobileEntrypoint> {
localizationsDelegates: const [
...S.localizationsDelegates,
],
routes: {
DocumentDetailsRoute.routeName: (context) => const DocumentDetailsRoute(),
},
home: AuthenticationWrapper(
paperlessProviderFactory: widget.paperlessProviderFactory,
),
@@ -217,7 +220,10 @@ class _AuthenticationWrapperState extends State<AuthenticationWrapper> {
@override
void didChangeDependencies() {
super.didChangeDependencies();
FlutterNativeSplash.remove();
context
.read<AuthenticationCubit>()
.restoreSessionState()
.then((value) => FlutterNativeSplash.remove());
}
@override

View File

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

View File

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

View File

@@ -18,7 +18,12 @@ class PaperlessServerStatsApiImpl implements PaperlessServerStatsApi {
@override
Future<PaperlessServerInformationModel> 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(

View File

@@ -2,5 +2,5 @@ import 'package:paperless_api/paperless_api.dart';
abstract class PaperlessUserApi {
Future<int> findCurrentUserId();
Future<UserModel> find(int id);
Future<UserModel> findCurrentUser();
}

View File

@@ -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<int> 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<UserModel> find(int id) async {
Future<UserModel> findCurrentUser() async {
final response = await client.get("/api/ui_settings/");
if (response.statusCode == 200) {
return UserModelV2.fromJson(response.data);

View File

@@ -1,7 +1,9 @@
import 'package:paperless_api/src/models/user_model.dart';
abstract class PaperlessUserApiV3 {
Future<Iterable<UserModel>> findWhere({
Future<UserModelV3> find(int id);
Future<Iterable<UserModelV3>> findAll();
Future<Iterable<UserModelV3>> findWhere({
String startsWith,
String endsWith,
String contains,

View File

@@ -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<UserModel> find(int id) async {
Future<UserModelV3> 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<Iterable<UserModel>> findWhere({
Future<Iterable<UserModelV3>> 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<UserModel>.fromJson(
return PagedSearchResult<UserModelV3>.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<int> 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<Iterable<UserModelV3>> findAll() async {
final response = await dio.get("/api/users/");
if (response.statusCode == 200) {
return PagedSearchResult<UserModelV3>.fromJson(
response.data,
UserModelV3.fromJson as UserModelV3 Function(Object?),
).results;
}
throw const PaperlessServerException.unknown();
}
@override
Future<UserModel> findCurrentUser() async {
final id = await findCurrentUserId();
return find(id);
}
}