mirror of
https://github.com/Xevion/paperless-mobile.git
synced 2025-12-10 06:07:57 -06:00
Merge pull request #346 from astubenbord/feature/notes
Feature: Notes and bugfixes
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:requestLegacyExternalStorage="true"
|
||||
>
|
||||
>
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
@@ -135,6 +135,7 @@
|
||||
android:mimeType="application/vnd.openxmlformats-officedocument.presentationml.presentation" />
|
||||
</intent-filter>
|
||||
|
||||
|
||||
<!-- .xls -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
@@ -162,11 +163,22 @@
|
||||
</intent-filter>
|
||||
<!-- END Snippet from https://github.com/qcasey/paperless_share -->
|
||||
|
||||
|
||||
</activity>
|
||||
<!-- Don't delete the meta-data below. This is used by the Flutter tool to generate
|
||||
GeneratedPluginRegistrant.java -->
|
||||
<meta-data android:name="flutterEmbedding" android:value="2" />
|
||||
</application>
|
||||
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="https" />
|
||||
</intent>
|
||||
</queries>
|
||||
|
||||
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
- Neues Feature: Notizen
|
||||
- Neue Sprache: Italienisch
|
||||
- Mehere Fehlerbehebungen
|
||||
@@ -0,0 +1,3 @@
|
||||
- New feature: Notes
|
||||
- New language: Italian
|
||||
- Multiple bugfixes
|
||||
8
build.yaml
Normal file
8
build.yaml
Normal file
@@ -0,0 +1,8 @@
|
||||
targets:
|
||||
$default:
|
||||
builders:
|
||||
mockito|mockBuilder:
|
||||
generate_for:
|
||||
- lib/**.dart
|
||||
- test/**.dart
|
||||
- integration_test/**.dart
|
||||
@@ -1,224 +1,130 @@
|
||||
// import 'package:flutter/material.dart';
|
||||
// import 'package:flutter_test/flutter_test.dart';
|
||||
// import 'package:mockito/mockito.dart';
|
||||
// import 'package:paperless_api/paperless_api.dart';
|
||||
// import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
|
||||
// import 'package:paperless_mobile/core/service/connectivity_status.service.dart';
|
||||
// import 'package:paperless_mobile/core/store/local_vault.dart';
|
||||
// import 'package:paperless_mobile/di_test_mocks.mocks.dart';
|
||||
// import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart';
|
||||
// import 'package:paperless_mobile/features/settings/model/application_settings_state.dart';
|
||||
// import 'package:paperless_mobile/features/settings/model/view_type.dart';
|
||||
import 'dart:io';
|
||||
|
||||
// import 'src/framework.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
import 'package:mockito/mockito.dart';
|
||||
import 'package:paperless_mobile/core/database/hive/hive_config.dart';
|
||||
import 'package:paperless_mobile/core/database/hive/hive_extensions.dart';
|
||||
import 'package:paperless_mobile/core/database/hive/hive_initialization.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/global_settings.dart';
|
||||
import 'package:paperless_mobile/core/security/session_manager.dart';
|
||||
import 'package:paperless_mobile/core/service/connectivity_status_service.dart';
|
||||
import 'package:paperless_mobile/features/login/cubit/authentication_cubit.dart';
|
||||
import 'package:paperless_mobile/features/login/services/authentication_service.dart';
|
||||
import 'package:paperless_mobile/features/notifications/services/local_notification_service.dart';
|
||||
import 'package:paperless_mobile/keys.dart';
|
||||
import 'package:paperless_mobile/main.dart'
|
||||
show initializeDefaultParameters, AppEntrypoint;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
// void main() async {
|
||||
// final t = await initializeTestingFramework(languageCode: 'de');
|
||||
import 'src/mocks/mock_paperless_api.dart';
|
||||
|
||||
// const testServerUrl = 'https://example.com';
|
||||
// const testUsername = 'user';
|
||||
// const testPassword = 'pass';
|
||||
class MockConnectivityStatusService extends Mock
|
||||
implements ConnectivityStatusService {}
|
||||
|
||||
// final serverAddressField = find.byKey(const ValueKey('login-server-address'));
|
||||
// final usernameField = find.byKey(const ValueKey('login-username'));
|
||||
// final passwordField = find.byKey(const ValueKey('login-password'));
|
||||
// final loginBtn = find.byKey(const ValueKey('login-login-button'));
|
||||
class MockLocalAuthService extends Mock implements LocalAuthenticationService {}
|
||||
|
||||
// testWidgets('Test successful login flow', (WidgetTester tester) async {
|
||||
// await initAndLaunchTestApp(tester, () async {
|
||||
// // Initialize dat for mocked classes
|
||||
// when((getIt<ConnectivityStatusService>()).connectivityChanges())
|
||||
// .thenAnswer((i) => Stream.value(true));
|
||||
// when((getIt<LocalVault>() as MockLocalVault)
|
||||
// .loadAuthenticationInformation())
|
||||
// .thenAnswer((realInvocation) async => null);
|
||||
// when((getIt<LocalVault>() as MockLocalVault).loadApplicationSettings())
|
||||
// .thenAnswer((realInvocation) async => ApplicationSettingsState(
|
||||
// preferredLocaleSubtag: 'en',
|
||||
// preferredThemeMode: ThemeMode.light,
|
||||
// isLocalAuthenticationEnabled: false,
|
||||
// preferredViewType: ViewType.list,
|
||||
// showInboxOnStartup: false,
|
||||
// ));
|
||||
// when(getIt<PaperlessAuthenticationApi>().login(
|
||||
// username: testUsername,
|
||||
// password: testPassword,
|
||||
// )).thenAnswer((i) => Future.value("eyTestToken"));
|
||||
class MockSessionManager extends Mock implements SessionManager {}
|
||||
|
||||
// await getIt<ConnectivityCubit>().initialize();
|
||||
// await getIt<ApplicationSettingsCubit>().initialize();
|
||||
// });
|
||||
class MockLocalNotificationService extends Mock
|
||||
implements LocalNotificationService {}
|
||||
|
||||
// // Mocked classes
|
||||
void main() async {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
const locale = Locale("en", "US");
|
||||
const testServerUrl = 'https://example.com';
|
||||
const testUsername = 'user';
|
||||
const testPassword = 'pass';
|
||||
|
||||
// await t.binding.waitUntilFirstFrameRasterized;
|
||||
// await tester.pumpAndSettle();
|
||||
final hiveDirectory = await getTemporaryDirectory();
|
||||
|
||||
// await tester.enterText(serverAddressField, testServerUrl);
|
||||
// await tester.pumpAndSettle();
|
||||
late ConnectivityStatusService connectivityStatusService;
|
||||
late MockPaperlessApiFactory paperlessApiFactory;
|
||||
late AuthenticationCubit authenticationCubit;
|
||||
late LocalNotificationService localNotificationService;
|
||||
late SessionManager sessionManager;
|
||||
final localAuthService = MockLocalAuthService();
|
||||
|
||||
// await tester.enterText(usernameField, testUsername);
|
||||
// await tester.pumpAndSettle();
|
||||
setUp(() async {
|
||||
connectivityStatusService = MockConnectivityStatusService();
|
||||
paperlessApiFactory = MockPaperlessApiFactory();
|
||||
sessionManager = MockSessionManager();
|
||||
localNotificationService = MockLocalNotificationService();
|
||||
|
||||
// await tester.enterText(passwordField, testPassword);
|
||||
authenticationCubit = AuthenticationCubit(
|
||||
localAuthService,
|
||||
paperlessApiFactory,
|
||||
sessionManager,
|
||||
connectivityStatusService,
|
||||
localNotificationService,
|
||||
);
|
||||
await initHive(
|
||||
hiveDirectory,
|
||||
locale.toString(),
|
||||
);
|
||||
});
|
||||
testWidgets(
|
||||
'A user shall be successfully logged in when providing correct credentials.',
|
||||
(tester) async {
|
||||
// Reset data to initial state with given [locale].
|
||||
await Hive.globalSettingsBox.setValue(
|
||||
GlobalSettings(
|
||||
preferredLocaleSubtag: locale.toString(),
|
||||
loggedInUserId: null,
|
||||
),
|
||||
);
|
||||
when(paperlessApiFactory.authenticationApi.login(
|
||||
username: testUsername,
|
||||
password: testPassword,
|
||||
)).thenAnswer((_) async => "token");
|
||||
|
||||
// FocusManager.instance.primaryFocus?.unfocus();
|
||||
// await tester.pumpAndSettle();
|
||||
await initializeDefaultParameters();
|
||||
|
||||
// await tester.tap(loginBtn);
|
||||
await tester.pumpWidget(
|
||||
AppEntrypoint(
|
||||
apiFactory: paperlessApiFactory,
|
||||
authenticationCubit: authenticationCubit,
|
||||
connectivityStatusService: connectivityStatusService,
|
||||
localNotificationService: localNotificationService,
|
||||
localAuthService: localAuthService,
|
||||
sessionManager: sessionManager,
|
||||
),
|
||||
);
|
||||
await tester.binding.waitUntilFirstFrameRasterized;
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// verify(getIt<PaperlessAuthenticationApi>().login(
|
||||
// username: testUsername,
|
||||
// password: testPassword,
|
||||
// )).called(1);
|
||||
// });
|
||||
await tester.enterText(
|
||||
find.byKey(TestKeys.login.serverAddressFormField),
|
||||
testServerUrl,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// testWidgets('Test login validation missing password',
|
||||
// (WidgetTester tester) async {
|
||||
// await initAndLaunchTestApp(tester, () async {
|
||||
// when((getIt<ConnectivityStatusService>() as MockConnectivityStatusService)
|
||||
// .connectivityChanges())
|
||||
// .thenAnswer((i) => Stream.value(true));
|
||||
// when((getIt<LocalVault>() as MockLocalVault)
|
||||
// .loadAuthenticationInformation())
|
||||
// .thenAnswer((realInvocation) async => null);
|
||||
await tester.press(find.byKey(TestKeys.login.continueButton));
|
||||
|
||||
// when((getIt<LocalVault>() as MockLocalVault).loadApplicationSettings())
|
||||
// .thenAnswer((realInvocation) async => ApplicationSettingsState(
|
||||
// preferredLocaleSubtag: 'en',
|
||||
// preferredThemeMode: ThemeMode.light,
|
||||
// isLocalAuthenticationEnabled: false,
|
||||
// preferredViewType: ViewType.list,
|
||||
// showInboxOnStartup: false,
|
||||
// ));
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
find.byKey(TestKeys.login.usernameFormField),
|
||||
findsOneWidget,
|
||||
);
|
||||
|
||||
// await getIt<ConnectivityCubit>().initialize();
|
||||
// await getIt<ApplicationSettingsCubit>().initialize();
|
||||
// });
|
||||
// // Mocked classes
|
||||
await tester.enterText(
|
||||
find.byKey(TestKeys.login.usernameFormField),
|
||||
testUsername,
|
||||
);
|
||||
await tester.enterText(
|
||||
find.byKey(TestKeys.login.passwordFormField),
|
||||
testUsername,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// // Initialize dat for mocked classes
|
||||
await tester.press(find.byKey(TestKeys.login.loginButton));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// await t.binding.waitUntilFirstFrameRasterized;
|
||||
// await tester.pumpAndSettle();
|
||||
|
||||
// await tester.enterText(serverAddressField, testServerUrl);
|
||||
// await tester.pumpAndSettle();
|
||||
|
||||
// await tester.enterText(usernameField, testUsername);
|
||||
// await tester.pumpAndSettle();
|
||||
|
||||
// FocusManager.instance.primaryFocus?.unfocus();
|
||||
// await tester.pumpAndSettle();
|
||||
|
||||
// await tester.tap(loginBtn);
|
||||
// await tester.pumpAndSettle();
|
||||
|
||||
// verifyNever(
|
||||
// (getIt<PaperlessAuthenticationApi>() as MockPaperlessAuthenticationApi)
|
||||
// .login(
|
||||
// username: testUsername,
|
||||
// password: testPassword,
|
||||
// ));
|
||||
// expect(
|
||||
// find.textContaining(t.translations.passwordMustNotBeEmpty),
|
||||
// findsOneWidget,
|
||||
// );
|
||||
// });
|
||||
|
||||
// testWidgets('Test login validation missing username',
|
||||
// (WidgetTester tester) async {
|
||||
// await initAndLaunchTestApp(tester, () async {
|
||||
// when((getIt<ConnectivityStatusService>() as MockConnectivityStatusService)
|
||||
// .connectivityChanges())
|
||||
// .thenAnswer((i) => Stream.value(true));
|
||||
// when((getIt<LocalVault>() as MockLocalVault)
|
||||
// .loadAuthenticationInformation())
|
||||
// .thenAnswer((realInvocation) async => null);
|
||||
// when((getIt<LocalVault>() as MockLocalVault).loadApplicationSettings())
|
||||
// .thenAnswer((realInvocation) async => ApplicationSettingsState(
|
||||
// preferredLocaleSubtag: 'en',
|
||||
// preferredThemeMode: ThemeMode.light,
|
||||
// isLocalAuthenticationEnabled: false,
|
||||
// preferredViewType: ViewType.list,
|
||||
// showInboxOnStartup: false,
|
||||
// ));
|
||||
// await getIt<ConnectivityCubit>().initialize();
|
||||
// await getIt<ApplicationSettingsCubit>().initialize();
|
||||
// });
|
||||
|
||||
// await t.binding.waitUntilFirstFrameRasterized;
|
||||
// await tester.pumpAndSettle();
|
||||
|
||||
// await tester.enterText(serverAddressField, testServerUrl);
|
||||
// await tester.pumpAndSettle();
|
||||
|
||||
// await tester.enterText(passwordField, testPassword);
|
||||
// await tester.pumpAndSettle();
|
||||
|
||||
// FocusManager.instance.primaryFocus?.unfocus();
|
||||
// await tester.pumpAndSettle();
|
||||
|
||||
// await tester.tap(loginBtn);
|
||||
// await tester.pumpAndSettle();
|
||||
|
||||
// verifyNever(
|
||||
// (getIt<PaperlessAuthenticationApi>() as MockPaperlessAuthenticationApi)
|
||||
// .login(
|
||||
// username: testUsername,
|
||||
// password: testPassword,
|
||||
// ));
|
||||
// expect(
|
||||
// find.textContaining(t.translations.usernameMustNotBeEmpty),
|
||||
// findsOneWidget,
|
||||
// );
|
||||
// });
|
||||
|
||||
// testWidgets('Test login validation missing server address',
|
||||
// (WidgetTester tester) async {
|
||||
// initAndLaunchTestApp(tester, () async {
|
||||
// when((getIt<ConnectivityStatusService>()).connectivityChanges())
|
||||
// .thenAnswer((i) => Stream.value(true));
|
||||
|
||||
// when((getIt<LocalVault>()).loadAuthenticationInformation())
|
||||
// .thenAnswer((realInvocation) async => null);
|
||||
|
||||
// when((getIt<LocalVault>()).loadApplicationSettings())
|
||||
// .thenAnswer((realInvocation) async => ApplicationSettingsState(
|
||||
// preferredLocaleSubtag: 'en',
|
||||
// preferredThemeMode: ThemeMode.light,
|
||||
// isLocalAuthenticationEnabled: false,
|
||||
// preferredViewType: ViewType.list,
|
||||
// showInboxOnStartup: false,
|
||||
// ));
|
||||
|
||||
// await getIt<ConnectivityCubit>().initialize();
|
||||
// await getIt<ApplicationSettingsCubit>().initialize();
|
||||
// });
|
||||
|
||||
// await t.binding.waitUntilFirstFrameRasterized;
|
||||
// await tester.pumpAndSettle();
|
||||
|
||||
// await tester.enterText(usernameField, testUsername);
|
||||
// await tester.pumpAndSettle();
|
||||
|
||||
// await tester.enterText(passwordField, testPassword);
|
||||
// await tester.pumpAndSettle();
|
||||
|
||||
// FocusManager.instance.primaryFocus?.unfocus();
|
||||
// await tester.pumpAndSettle();
|
||||
|
||||
// await tester.tap(loginBtn);
|
||||
// await tester.pumpAndSettle();
|
||||
|
||||
// verifyNever(getIt<PaperlessAuthenticationApi>().login(
|
||||
// username: testUsername,
|
||||
// password: testPassword,
|
||||
// ));
|
||||
// expect(
|
||||
// find.textContaining(
|
||||
// t.translations.loginPageServerUrlValidatorMessageText),
|
||||
// findsOneWidget,
|
||||
// );
|
||||
// });
|
||||
// }
|
||||
expect(
|
||||
find.byKey(TestKeys.login.loggingInScreen),
|
||||
findsOneWidget,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
import 'package:paperless_mobile/core/factory/paperless_api_factory.dart';
|
||||
import 'package:paperless_mobile/core/security/session_manager.dart';
|
||||
import 'package:paperless_mobile/core/service/connectivity_status_service.dart';
|
||||
import 'package:paperless_mobile/features/login/cubit/authentication_cubit.dart';
|
||||
import 'package:paperless_mobile/features/login/services/authentication_service.dart';
|
||||
import 'package:paperless_mobile/features/notifications/services/local_notification_service.dart';
|
||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||
import 'package:paperless_mobile/main.dart'
|
||||
show initializeDefaultParameters, AppEntrypoint;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
Future<TestingFrameworkVariables> initializeTestingFramework(
|
||||
{String languageCode = 'en'}) async {
|
||||
@@ -26,11 +35,3 @@ class TestingFrameworkVariables {
|
||||
required this.translations,
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> initAndLaunchTestApp(
|
||||
WidgetTester tester,
|
||||
Future<void> Function() initializationCallback,
|
||||
) async {
|
||||
await initializationCallback();
|
||||
//runApp(const PaperlessMobileEntrypoint(authenticationCubit: ),));
|
||||
}
|
||||
|
||||
65
integration_test/src/mocks/mock_paperless_api.dart
Normal file
65
integration_test/src/mocks/mock_paperless_api.dart
Normal file
@@ -0,0 +1,65 @@
|
||||
import 'package:dio/src/dio.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/core/factory/paperless_api_factory.dart';
|
||||
import 'package:mockito/annotations.dart';
|
||||
|
||||
@GenerateNiceMocks([
|
||||
MockSpec<PaperlessAuthenticationApi>(),
|
||||
MockSpec<PaperlessDocumentsApi>(),
|
||||
MockSpec<PaperlessLabelsApi>(),
|
||||
MockSpec<PaperlessUserApi>(),
|
||||
MockSpec<PaperlessServerStatsApi>(),
|
||||
MockSpec<PaperlessSavedViewsApi>(),
|
||||
MockSpec<PaperlessTasksApi>(),
|
||||
])
|
||||
import 'mock_paperless_api.mocks.dart';
|
||||
|
||||
class MockPaperlessApiFactory implements PaperlessApiFactory {
|
||||
final PaperlessAuthenticationApi authenticationApi =
|
||||
MockPaperlessAuthenticationApi();
|
||||
final PaperlessDocumentsApi documentApi = MockPaperlessDocumentsApi();
|
||||
final PaperlessLabelsApi labelsApi = MockPaperlessLabelsApi();
|
||||
final PaperlessUserApi userApi = MockPaperlessUserApi();
|
||||
final PaperlessSavedViewsApi savedViewsApi = MockPaperlessSavedViewsApi();
|
||||
final PaperlessServerStatsApi serverStatsApi = MockPaperlessServerStatsApi();
|
||||
final PaperlessTasksApi tasksApi = MockPaperlessTasksApi();
|
||||
|
||||
@override
|
||||
PaperlessAuthenticationApi createAuthenticationApi(Dio dio) {
|
||||
return authenticationApi;
|
||||
}
|
||||
|
||||
@override
|
||||
PaperlessDocumentsApi createDocumentsApi(Dio dio, {required int apiVersion}) {
|
||||
return documentApi;
|
||||
}
|
||||
|
||||
@override
|
||||
PaperlessLabelsApi createLabelsApi(Dio dio, {required int apiVersion}) {
|
||||
return labelsApi;
|
||||
}
|
||||
|
||||
@override
|
||||
PaperlessSavedViewsApi createSavedViewsApi(
|
||||
Dio dio, {
|
||||
required int apiVersion,
|
||||
}) {
|
||||
return savedViewsApi;
|
||||
}
|
||||
|
||||
@override
|
||||
PaperlessServerStatsApi createServerStatsApi(Dio dio,
|
||||
{required int apiVersion}) {
|
||||
return serverStatsApi;
|
||||
}
|
||||
|
||||
@override
|
||||
PaperlessTasksApi createTasksApi(Dio dio, {required int apiVersion}) {
|
||||
return tasksApi;
|
||||
}
|
||||
|
||||
@override
|
||||
PaperlessUserApi createUserApi(Dio dio, {required int apiVersion}) {
|
||||
return userApi;
|
||||
}
|
||||
}
|
||||
@@ -5,3 +5,5 @@ import 'package:package_info_plus/package_info_plus.dart';
|
||||
late final PackageInfo packageInfo;
|
||||
late final AndroidDeviceInfo? androidInfo;
|
||||
late final IosDeviceInfo? iosInfo;
|
||||
|
||||
const latestSupportedApiVersion = 3;
|
||||
|
||||
25
lib/core/bloc/base_state.dart
Normal file
25
lib/core/bloc/base_state.dart
Normal file
@@ -0,0 +1,25 @@
|
||||
import 'package:paperless_mobile/core/bloc/loading_status.dart';
|
||||
|
||||
class BaseState<T> {
|
||||
final Object? error;
|
||||
final T? value;
|
||||
final LoadingStatus status;
|
||||
|
||||
BaseState({
|
||||
required this.error,
|
||||
required this.value,
|
||||
required this.status,
|
||||
});
|
||||
|
||||
BaseState<T> copyWith({
|
||||
Object? error,
|
||||
T? value,
|
||||
LoadingStatus? status,
|
||||
}) {
|
||||
return BaseState(
|
||||
error: error ?? this.error,
|
||||
value: value ?? this.value,
|
||||
status: status ?? this.status,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -19,12 +19,14 @@ class HiveBoxes {
|
||||
static const localUserAccount = 'localUserAccount';
|
||||
static const localUserAppState = 'localUserAppState';
|
||||
static const hosts = 'hosts';
|
||||
static const hintStateBox = 'hintStateBox';
|
||||
|
||||
static List<String> get all => [
|
||||
globalSettings,
|
||||
localUserCredentials,
|
||||
localUserAccount,
|
||||
localUserAppState,
|
||||
hintStateBox,
|
||||
hosts,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -54,4 +54,5 @@ extension HiveBoxAccessors on HiveInterface {
|
||||
box<LocalUserAppState>(HiveBoxes.localUserAppState);
|
||||
Box<GlobalSettings> get globalSettingsBox =>
|
||||
box<GlobalSettings>(HiveBoxes.globalSettings);
|
||||
Box<bool> get hintStateBox => box<bool>(HiveBoxes.hintStateBox);
|
||||
}
|
||||
|
||||
25
lib/core/database/hive/hive_initialization.dart
Normal file
25
lib/core/database/hive/hive_initialization.dart
Normal file
@@ -0,0 +1,25 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:hive_flutter/adapters.dart';
|
||||
import 'package:paperless_mobile/core/database/hive/hive_config.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/global_settings.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/local_user_app_state.dart';
|
||||
|
||||
Future<void> initHive(Directory directory, String defaultLocale) async {
|
||||
Hive.init(directory.path);
|
||||
registerHiveAdapters();
|
||||
await Hive.openBox<LocalUserAccount>(HiveBoxes.localUserAccount);
|
||||
await Hive.openBox<LocalUserAppState>(HiveBoxes.localUserAppState);
|
||||
await Hive.openBox<bool>(HiveBoxes.hintStateBox);
|
||||
await Hive.openBox<String>(HiveBoxes.hosts);
|
||||
final globalSettingsBox =
|
||||
await Hive.openBox<GlobalSettings>(HiveBoxes.globalSettings);
|
||||
|
||||
if (!globalSettingsBox.hasValue) {
|
||||
await globalSettingsBox.setValue(
|
||||
GlobalSettings(preferredLocaleSubtag: defaultLocale),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -11,9 +11,18 @@ extension WidgetPadding on Widget {
|
||||
Widget paddedSymmetrically({
|
||||
double horizontal = 0.0,
|
||||
double vertical = 0.0,
|
||||
bool sliver = false,
|
||||
}) {
|
||||
final insets =
|
||||
EdgeInsets.symmetric(horizontal: horizontal, vertical: vertical);
|
||||
if (sliver) {
|
||||
return SliverPadding(
|
||||
padding: insets,
|
||||
sliver: this,
|
||||
);
|
||||
}
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: horizontal, vertical: vertical),
|
||||
padding: insets,
|
||||
child: this,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import 'package:dio/dio.dart';
|
||||
|
||||
class LanguageHeaderInterceptor extends Interceptor {
|
||||
String preferredLocaleSubtag;
|
||||
LanguageHeaderInterceptor(this.preferredLocaleSubtag);
|
||||
final String Function() preferredLocaleSubtagBuilder;
|
||||
LanguageHeaderInterceptor(this.preferredLocaleSubtagBuilder);
|
||||
|
||||
@override
|
||||
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
|
||||
late String languages;
|
||||
if (preferredLocaleSubtag == "en") {
|
||||
if (preferredLocaleSubtagBuilder() == "en") {
|
||||
languages = "en";
|
||||
} else {
|
||||
languages = "$preferredLocaleSubtag,en;q=0.7,en-US;q=0.6";
|
||||
languages = "${preferredLocaleSubtagBuilder()},en;q=0.7,en-US;q=0.6";
|
||||
}
|
||||
options.headers.addAll({"Accept-Language": languages});
|
||||
handler.next(options);
|
||||
|
||||
@@ -9,4 +9,9 @@ class InfoMessageException implements Exception {
|
||||
this.message,
|
||||
this.stackTrace,
|
||||
});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'InfoMessageException(code: $code, message: $message, stackTrace: $stackTrace)';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,93 +1,14 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:dio/io.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:paperless_mobile/core/interceptor/dio_http_error_interceptor.dart';
|
||||
import 'package:paperless_mobile/core/interceptor/dio_offline_interceptor.dart';
|
||||
import 'package:paperless_mobile/core/interceptor/dio_unauthorized_interceptor.dart';
|
||||
import 'package:paperless_mobile/core/interceptor/retry_on_connection_change_interceptor.dart';
|
||||
import 'package:paperless_mobile/features/login/model/client_certificate.dart';
|
||||
import 'package:pretty_dio_logger/pretty_dio_logger.dart';
|
||||
|
||||
/// Manages the security context, authentication and base request URL for
|
||||
/// an underlying [Dio] client which is injected into all services
|
||||
/// requiring authenticated access to the Paperless REST API.
|
||||
class SessionManager extends ValueNotifier<Dio> {
|
||||
Dio get client => value;
|
||||
|
||||
SessionManager([List<Interceptor> interceptors = const []])
|
||||
: super(_initDio(interceptors));
|
||||
|
||||
static Dio _initDio(List<Interceptor> interceptors) {
|
||||
//en- and decoded by utf8 by default
|
||||
final Dio dio = Dio(
|
||||
BaseOptions(
|
||||
contentType: Headers.jsonContentType,
|
||||
followRedirects: true,
|
||||
maxRedirects: 10,
|
||||
),
|
||||
);
|
||||
dio.options
|
||||
..receiveTimeout = const Duration(seconds: 30)
|
||||
..sendTimeout = const Duration(seconds: 60)
|
||||
..responseType = ResponseType.json;
|
||||
(dio.httpClientAdapter as IOHttpClientAdapter).createHttpClient =
|
||||
() => HttpClient()..badCertificateCallback = (cert, host, port) => true;
|
||||
dio.interceptors.addAll([
|
||||
...interceptors,
|
||||
DioUnauthorizedInterceptor(),
|
||||
DioHttpErrorInterceptor(),
|
||||
DioOfflineInterceptor(),
|
||||
RetryOnConnectionChangeInterceptor(dio: dio)
|
||||
]);
|
||||
return dio;
|
||||
}
|
||||
abstract interface class SessionManager implements ChangeNotifier {
|
||||
Dio get client;
|
||||
|
||||
void updateSettings({
|
||||
String? baseUrl,
|
||||
String? authToken,
|
||||
ClientCertificate? clientCertificate,
|
||||
}) {
|
||||
if (clientCertificate != null) {
|
||||
final context = SecurityContext()
|
||||
..usePrivateKeyBytes(
|
||||
clientCertificate.bytes,
|
||||
password: clientCertificate.passphrase,
|
||||
)
|
||||
..useCertificateChainBytes(
|
||||
clientCertificate.bytes,
|
||||
password: clientCertificate.passphrase,
|
||||
)
|
||||
..setTrustedCertificatesBytes(
|
||||
clientCertificate.bytes,
|
||||
password: clientCertificate.passphrase,
|
||||
);
|
||||
final adapter = IOHttpClientAdapter()
|
||||
..createHttpClient = () => HttpClient(context: context)
|
||||
..badCertificateCallback =
|
||||
(X509Certificate cert, String host, int port) => true;
|
||||
|
||||
client.httpClientAdapter = adapter;
|
||||
}
|
||||
|
||||
if (baseUrl != null) {
|
||||
client.options.baseUrl = baseUrl;
|
||||
}
|
||||
|
||||
if (authToken != null) {
|
||||
client.options.headers.addAll({
|
||||
HttpHeaders.authorizationHeader: 'Token $authToken',
|
||||
});
|
||||
}
|
||||
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void resetSettings() {
|
||||
client.httpClientAdapter = IOHttpClientAdapter();
|
||||
client.options.baseUrl = '';
|
||||
client.options.headers.remove(HttpHeaders.authorizationHeader);
|
||||
notifyListeners();
|
||||
}
|
||||
});
|
||||
void resetSettings();
|
||||
}
|
||||
|
||||
96
lib/core/security/session_manager_impl.dart
Normal file
96
lib/core/security/session_manager_impl.dart
Normal file
@@ -0,0 +1,96 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:dio/io.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/core/interceptor/dio_offline_interceptor.dart';
|
||||
import 'package:paperless_mobile/core/interceptor/dio_unauthorized_interceptor.dart';
|
||||
import 'package:paperless_mobile/core/interceptor/retry_on_connection_change_interceptor.dart';
|
||||
import 'package:paperless_mobile/core/security/session_manager.dart';
|
||||
import 'package:paperless_mobile/features/login/model/client_certificate.dart';
|
||||
|
||||
/// Manages the security context, authentication and base request URL for
|
||||
/// an underlying [Dio] client which is injected into all services
|
||||
/// requiring authenticated access to the Paperless REST API.
|
||||
class SessionManagerImpl extends ValueNotifier<Dio> implements SessionManager {
|
||||
@override
|
||||
Dio get client => value;
|
||||
|
||||
SessionManagerImpl([List<Interceptor> interceptors = const []])
|
||||
: super(_initDio(interceptors));
|
||||
|
||||
static Dio _initDio(List<Interceptor> interceptors) {
|
||||
//en- and decoded by utf8 by default
|
||||
final Dio dio = Dio(
|
||||
BaseOptions(
|
||||
contentType: Headers.jsonContentType,
|
||||
followRedirects: true,
|
||||
maxRedirects: 10,
|
||||
),
|
||||
);
|
||||
dio.options
|
||||
..receiveTimeout = const Duration(seconds: 30)
|
||||
..sendTimeout = const Duration(seconds: 60)
|
||||
..responseType = ResponseType.json;
|
||||
(dio.httpClientAdapter as IOHttpClientAdapter).createHttpClient =
|
||||
() => HttpClient()..badCertificateCallback = (cert, host, port) => true;
|
||||
dio.interceptors.addAll([
|
||||
...interceptors,
|
||||
DioUnauthorizedInterceptor(),
|
||||
DioHttpErrorInterceptor(),
|
||||
DioOfflineInterceptor(),
|
||||
RetryOnConnectionChangeInterceptor(dio: dio)
|
||||
]);
|
||||
return dio;
|
||||
}
|
||||
|
||||
@override
|
||||
void updateSettings({
|
||||
String? baseUrl,
|
||||
String? authToken,
|
||||
ClientCertificate? clientCertificate,
|
||||
}) {
|
||||
if (clientCertificate != null) {
|
||||
final context = SecurityContext()
|
||||
..usePrivateKeyBytes(
|
||||
clientCertificate.bytes,
|
||||
password: clientCertificate.passphrase,
|
||||
)
|
||||
..useCertificateChainBytes(
|
||||
clientCertificate.bytes,
|
||||
password: clientCertificate.passphrase,
|
||||
)
|
||||
..setTrustedCertificatesBytes(
|
||||
clientCertificate.bytes,
|
||||
password: clientCertificate.passphrase,
|
||||
);
|
||||
final adapter = IOHttpClientAdapter()
|
||||
..createHttpClient = () => HttpClient(context: context)
|
||||
..badCertificateCallback =
|
||||
(X509Certificate cert, String host, int port) => true;
|
||||
|
||||
client.httpClientAdapter = adapter;
|
||||
}
|
||||
|
||||
if (baseUrl != null) {
|
||||
client.options.baseUrl = baseUrl;
|
||||
}
|
||||
|
||||
if (authToken != null) {
|
||||
client.options.headers.addAll({
|
||||
HttpHeaders.authorizationHeader: 'Token $authToken',
|
||||
});
|
||||
}
|
||||
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@override
|
||||
void resetSettings() {
|
||||
client.httpClientAdapter = IOHttpClientAdapter();
|
||||
client.options.baseUrl = '';
|
||||
client.options.headers.remove(HttpHeaders.authorizationHeader);
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import 'package:dio/dio.dart';
|
||||
import 'package:paperless_mobile/core/global/os_error_codes.dart';
|
||||
import 'package:paperless_mobile/core/interceptor/server_reachability_error_interceptor.dart';
|
||||
import 'package:paperless_mobile/core/security/session_manager.dart';
|
||||
import 'package:paperless_mobile/core/security/session_manager_impl.dart';
|
||||
import 'package:paperless_mobile/features/login/model/client_certificate.dart';
|
||||
import 'package:paperless_mobile/features/login/model/reachability_status.dart';
|
||||
import 'package:rxdart/subjects.dart';
|
||||
@@ -79,7 +80,7 @@ class ConnectivityStatusServiceImpl implements ConnectivityStatusService {
|
||||
}
|
||||
try {
|
||||
SessionManager manager =
|
||||
SessionManager([ServerReachabilityErrorInterceptor()])
|
||||
SessionManagerImpl([ServerReachabilityErrorInterceptor()])
|
||||
..updateSettings(clientCertificate: clientCertificate)
|
||||
..client.options.connectTimeout = const Duration(seconds: 5)
|
||||
..client.options.receiveTimeout = const Duration(seconds: 5);
|
||||
|
||||
@@ -82,5 +82,7 @@ String translateError(BuildContext context, ErrorCode code) {
|
||||
'Could not load custom field.', //TODO: INTL
|
||||
ErrorCode.customFieldDeleteFailed =>
|
||||
'Could not delete custom field, please try again.', //TODO: INTL
|
||||
ErrorCode.deleteNoteFailed => 'Could not delete note, please try again.',
|
||||
ErrorCode.addNoteFailed => 'Could not create note, please try again.',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3,9 +3,11 @@
|
||||
import 'dart:collection';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:paperless_mobile/core/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/features/landing/view/widgets/mime_types_pie_chart.dart';
|
||||
@@ -83,7 +85,6 @@ class _FormBuilderLocalizedDatePickerState
|
||||
|
||||
final _textFieldControls =
|
||||
LinkedList<_NeighbourAwareDateInputSegmentControls>();
|
||||
String? _error;
|
||||
bool _temporarilyDisableListeners = false;
|
||||
@override
|
||||
void initState() {
|
||||
@@ -184,10 +185,7 @@ class _FormBuilderLocalizedDatePickerState
|
||||
// Imitate the functionality of the validator function in "normal" form fields.
|
||||
// The error is shown on the outer decorator as if this was a regular text input.
|
||||
// Errors are cleared after the next user interaction.
|
||||
final error = _validateDate(value);
|
||||
setState(() {
|
||||
_error = error;
|
||||
});
|
||||
// final error = _validateDate(value);
|
||||
},
|
||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||
initialValue: widget.initialValue != null
|
||||
@@ -201,7 +199,7 @@ class _FormBuilderLocalizedDatePickerState
|
||||
child: InputDecorator(
|
||||
textAlignVertical: TextAlignVertical.bottom,
|
||||
decoration: InputDecoration(
|
||||
errorText: _error,
|
||||
errorText: field.errorText,
|
||||
labelText: widget.labelText,
|
||||
suffixIcon: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@@ -271,16 +269,10 @@ class _FormBuilderLocalizedDatePickerState
|
||||
if (d.day != date.day && d.month != date.month && d.year != date.year) {
|
||||
return "Invalid date.";
|
||||
}
|
||||
if (d.isBefore(widget.firstDate)) {
|
||||
final formattedDateHint =
|
||||
DateFormat.yMd(widget.locale.toString()).format(widget.firstDate);
|
||||
return "Date must be after $formattedDateHint.";
|
||||
}
|
||||
if (d.isAfter(widget.lastDate)) {
|
||||
final formattedDateHint =
|
||||
DateFormat.yMd(widget.locale.toString()).format(widget.lastDate);
|
||||
return "Date must be before $formattedDateHint.";
|
||||
if (d.isBefore(widget.firstDate) || d.isAfter(widget.lastDate)) {
|
||||
return S.of(context)!.dateOutOfRange(widget.firstDate, widget.lastDate);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -332,6 +324,7 @@ class _FormBuilderLocalizedDatePickerState
|
||||
_DateInputSegment.year => fieldValue.copyWith(year: number),
|
||||
};
|
||||
field.setValue(newValue);
|
||||
field.validate();
|
||||
}
|
||||
},
|
||||
inputFormatters: [
|
||||
|
||||
@@ -61,7 +61,7 @@ class HintCard extends StatelessWidget {
|
||||
const Padding(padding: EdgeInsets.only(bottom: 24)),
|
||||
],
|
||||
).padded(),
|
||||
).padded(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
24
lib/core/widgets/hint_state_builder.dart
Normal file
24
lib/core/widgets/hint_state_builder.dart
Normal file
@@ -0,0 +1,24 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hive_flutter/adapters.dart';
|
||||
import 'package:paperless_mobile/core/database/hive/hive_extensions.dart';
|
||||
|
||||
class HintStateBuilder extends StatelessWidget {
|
||||
final String? listenKey;
|
||||
final Widget Function(BuildContext context, Box<bool> box) builder;
|
||||
const HintStateBuilder({
|
||||
super.key,
|
||||
required this.builder,
|
||||
this.listenKey,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ValueListenableBuilder<Box<bool>>(
|
||||
valueListenable: Hive.hintStateBox
|
||||
.listenable(keys: listenKey != null ? [listenKey] : null),
|
||||
builder: (context, box, child) {
|
||||
return builder(context, box);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -63,6 +63,7 @@ class ChangelogDialog extends StatelessWidget {
|
||||
}
|
||||
|
||||
const _versionNumbers = {
|
||||
"4043": "3.2.0",
|
||||
"4033": "3.1.8",
|
||||
"4023": "3.1.7",
|
||||
"4013": "3.1.6",
|
||||
|
||||
@@ -87,6 +87,47 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> updateNote(NoteModel note) async {
|
||||
assert(state.status == LoadingStatus.loaded);
|
||||
final document = state.document!;
|
||||
final updatedNotes = document.notes.map((e) => e.id == note.id ? note : e);
|
||||
try {
|
||||
final updatedDocument = await _api.update(
|
||||
state.document!.copyWith(
|
||||
notes: updatedNotes,
|
||||
),
|
||||
);
|
||||
_notifier.notifyUpdated(updatedDocument);
|
||||
} on PaperlessApiException catch (e) {
|
||||
addError(
|
||||
TransientPaperlessApiError(
|
||||
code: e.code,
|
||||
details: e.details,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> deleteNote(NoteModel note) async {
|
||||
assert(state.status == LoadingStatus.loaded,
|
||||
"Document data has to be loaded before calling this method.");
|
||||
assert(note.id != null, "Note id cannot be null.");
|
||||
try {
|
||||
final updatedDocument = await _api.deleteNote(
|
||||
state.document!,
|
||||
note.id!,
|
||||
);
|
||||
_notifier.notifyUpdated(updatedDocument);
|
||||
} on PaperlessApiException catch (e) {
|
||||
addError(
|
||||
TransientPaperlessApiError(
|
||||
code: e.code,
|
||||
details: e.details,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> assignAsn(
|
||||
DocumentModel document, {
|
||||
int? asn,
|
||||
@@ -270,4 +311,17 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
|
||||
_notifier.removeListener(this);
|
||||
await super.close();
|
||||
}
|
||||
|
||||
Future<void> addNote(String text) async {
|
||||
assert(state.status == LoadingStatus.loaded);
|
||||
try {
|
||||
final updatedDocument = await _api.addNote(
|
||||
document: state.document!,
|
||||
text: text,
|
||||
);
|
||||
_notifier.notifyUpdated(updatedDocument);
|
||||
} on PaperlessApiException catch (err) {
|
||||
addError(TransientPaperlessApiError(code: err.code));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import 'package:paperless_mobile/features/document_details/cubit/document_detail
|
||||
import 'package:paperless_mobile/features/document_details/view/widgets/document_content_widget.dart';
|
||||
import 'package:paperless_mobile/features/document_details/view/widgets/document_download_button.dart';
|
||||
import 'package:paperless_mobile/features/document_details/view/widgets/document_meta_data_widget.dart';
|
||||
import 'package:paperless_mobile/features/document_details/view/widgets/document_notes_widget.dart';
|
||||
import 'package:paperless_mobile/features/document_details/view/widgets/document_overview_widget.dart';
|
||||
import 'package:paperless_mobile/features/document_details/view/widgets/document_permissions_widget.dart';
|
||||
import 'package:paperless_mobile/features/document_details/view/widgets/document_share_button.dart';
|
||||
@@ -67,7 +68,7 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
||||
debugPrint(disableAnimations.toString());
|
||||
final hasMultiUserSupport =
|
||||
context.watch<LocalUserAccount>().hasMultiUserSupport;
|
||||
final tabLength = 4 + (hasMultiUserSupport ? 1 : 0);
|
||||
final tabLength = 5 + (hasMultiUserSupport ? 1 : 0);
|
||||
return AnnotatedRegion(
|
||||
value: buildOverlayStyle(
|
||||
Theme.of(context),
|
||||
@@ -160,6 +161,7 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
||||
bottom: ColoredTabBar(
|
||||
tabBar: TabBar(
|
||||
isScrollable: true,
|
||||
tabAlignment: TabAlignment.start,
|
||||
tabs: [
|
||||
Tab(
|
||||
child: Text(
|
||||
@@ -201,10 +203,34 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
||||
),
|
||||
),
|
||||
),
|
||||
Tab(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
S.of(context)!.notes(0),
|
||||
style: TextStyle(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onPrimaryContainer,
|
||||
),
|
||||
),
|
||||
if ((state.document?.notes.length ?? 0) >
|
||||
0)
|
||||
Card(
|
||||
child: Text(state
|
||||
.document!.notes.length
|
||||
.toString())
|
||||
.paddedSymmetrically(
|
||||
horizontal: 8, vertical: 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (hasMultiUserSupport)
|
||||
Tab(
|
||||
child: Text(
|
||||
"Permissions",
|
||||
S.of(context)!.permissions,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
@@ -229,67 +255,103 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
||||
context.read(),
|
||||
documentId: widget.id,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 16,
|
||||
horizontal: 16,
|
||||
),
|
||||
child: TabBarView(
|
||||
children: [
|
||||
CustomScrollView(
|
||||
slivers: [
|
||||
SliverOverlapInjector(
|
||||
handle: NestedScrollView
|
||||
.sliverOverlapAbsorberHandleFor(context),
|
||||
),
|
||||
switch (state.status) {
|
||||
LoadingStatus.loaded =>
|
||||
DocumentOverviewWidget(
|
||||
document: state.document!,
|
||||
itemSpacing: _itemSpacing,
|
||||
queryString:
|
||||
widget.titleAndContentQueryString,
|
||||
),
|
||||
LoadingStatus.error => _buildErrorState(),
|
||||
_ => _buildLoadingState(),
|
||||
},
|
||||
],
|
||||
),
|
||||
CustomScrollView(
|
||||
slivers: [
|
||||
SliverOverlapInjector(
|
||||
handle: NestedScrollView
|
||||
.sliverOverlapAbsorberHandleFor(context),
|
||||
),
|
||||
switch (state.status) {
|
||||
LoadingStatus.loaded => DocumentContentWidget(
|
||||
document: state.document!,
|
||||
queryString:
|
||||
widget.titleAndContentQueryString,
|
||||
),
|
||||
LoadingStatus.error => _buildErrorState(),
|
||||
_ => _buildLoadingState(),
|
||||
}
|
||||
],
|
||||
),
|
||||
CustomScrollView(
|
||||
slivers: [
|
||||
SliverOverlapInjector(
|
||||
handle: NestedScrollView
|
||||
.sliverOverlapAbsorberHandleFor(context),
|
||||
),
|
||||
switch (state.status) {
|
||||
LoadingStatus.loaded =>
|
||||
DocumentMetaDataWidget(
|
||||
document: state.document!,
|
||||
itemSpacing: _itemSpacing,
|
||||
metaData: state.metaData!,
|
||||
),
|
||||
LoadingStatus.error => _buildErrorState(),
|
||||
_ => _buildLoadingState(),
|
||||
},
|
||||
],
|
||||
),
|
||||
child: TabBarView(
|
||||
children: [
|
||||
CustomScrollView(
|
||||
slivers: [
|
||||
SliverOverlapInjector(
|
||||
handle: NestedScrollView
|
||||
.sliverOverlapAbsorberHandleFor(context),
|
||||
),
|
||||
switch (state.status) {
|
||||
LoadingStatus.loaded => DocumentOverviewWidget(
|
||||
document: state.document!,
|
||||
itemSpacing: _itemSpacing,
|
||||
queryString:
|
||||
widget.titleAndContentQueryString,
|
||||
).paddedSymmetrically(
|
||||
vertical: 16,
|
||||
sliver: true,
|
||||
),
|
||||
LoadingStatus.error => _buildErrorState(),
|
||||
_ => _buildLoadingState(),
|
||||
},
|
||||
],
|
||||
),
|
||||
CustomScrollView(
|
||||
slivers: [
|
||||
SliverOverlapInjector(
|
||||
handle: NestedScrollView
|
||||
.sliverOverlapAbsorberHandleFor(context),
|
||||
),
|
||||
switch (state.status) {
|
||||
LoadingStatus.loaded => DocumentContentWidget(
|
||||
document: state.document!,
|
||||
queryString:
|
||||
widget.titleAndContentQueryString,
|
||||
).paddedSymmetrically(
|
||||
vertical: 16,
|
||||
sliver: true,
|
||||
),
|
||||
LoadingStatus.error => _buildErrorState(),
|
||||
_ => _buildLoadingState(),
|
||||
}
|
||||
],
|
||||
),
|
||||
CustomScrollView(
|
||||
slivers: [
|
||||
SliverOverlapInjector(
|
||||
handle: NestedScrollView
|
||||
.sliverOverlapAbsorberHandleFor(context),
|
||||
),
|
||||
switch (state.status) {
|
||||
LoadingStatus.loaded => DocumentMetaDataWidget(
|
||||
document: state.document!,
|
||||
itemSpacing: _itemSpacing,
|
||||
metaData: state.metaData!,
|
||||
).paddedSymmetrically(
|
||||
vertical: 16,
|
||||
sliver: true,
|
||||
),
|
||||
LoadingStatus.error => _buildErrorState(),
|
||||
_ => _buildLoadingState(),
|
||||
},
|
||||
],
|
||||
),
|
||||
CustomScrollView(
|
||||
controller: _pagingScrollController,
|
||||
slivers: [
|
||||
SliverOverlapInjector(
|
||||
handle: NestedScrollView
|
||||
.sliverOverlapAbsorberHandleFor(context),
|
||||
),
|
||||
SimilarDocumentsView(
|
||||
pagingScrollController: _pagingScrollController,
|
||||
).paddedSymmetrically(
|
||||
vertical: 16,
|
||||
sliver: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
CustomScrollView(
|
||||
slivers: [
|
||||
SliverOverlapInjector(
|
||||
handle: NestedScrollView
|
||||
.sliverOverlapAbsorberHandleFor(context),
|
||||
),
|
||||
switch (state.status) {
|
||||
LoadingStatus.loaded => DocumentNotesWidget(
|
||||
document: state.document!,
|
||||
).paddedSymmetrically(
|
||||
vertical: 16,
|
||||
sliver: true,
|
||||
),
|
||||
LoadingStatus.error => _buildErrorState(),
|
||||
_ => _buildLoadingState(),
|
||||
},
|
||||
],
|
||||
),
|
||||
if (hasMultiUserSupport)
|
||||
CustomScrollView(
|
||||
controller: _pagingScrollController,
|
||||
slivers: [
|
||||
@@ -297,33 +359,27 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
||||
handle: NestedScrollView
|
||||
.sliverOverlapAbsorberHandleFor(context),
|
||||
),
|
||||
SimilarDocumentsView(
|
||||
pagingScrollController:
|
||||
_pagingScrollController,
|
||||
),
|
||||
switch (state.status) {
|
||||
LoadingStatus.loaded =>
|
||||
DocumentPermissionsWidget(
|
||||
document: state.document!,
|
||||
).paddedSymmetrically(
|
||||
vertical: 16,
|
||||
sliver: true,
|
||||
),
|
||||
LoadingStatus.error => _buildErrorState(),
|
||||
_ => _buildLoadingState(),
|
||||
}
|
||||
],
|
||||
),
|
||||
if (hasMultiUserSupport)
|
||||
CustomScrollView(
|
||||
controller: _pagingScrollController,
|
||||
slivers: [
|
||||
SliverOverlapInjector(
|
||||
handle: NestedScrollView
|
||||
.sliverOverlapAbsorberHandleFor(
|
||||
context),
|
||||
),
|
||||
switch (state.status) {
|
||||
LoadingStatus.loaded =>
|
||||
DocumentPermissionsWidget(
|
||||
document: state.document!,
|
||||
),
|
||||
LoadingStatus.error => _buildErrorState(),
|
||||
_ => _buildLoadingState(),
|
||||
}
|
||||
],
|
||||
]
|
||||
.map(
|
||||
(child) => Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16),
|
||||
child: child,
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -25,54 +25,51 @@ class DocumentMetaDataWidget extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final currentUser = context.watch<LocalUserAccount>().paperlessUser;
|
||||
|
||||
return SliverList(
|
||||
delegate: SliverChildListDelegate(
|
||||
[
|
||||
if (currentUser.canEditDocuments)
|
||||
ArchiveSerialNumberField(
|
||||
document: document,
|
||||
).paddedOnly(bottom: itemSpacing),
|
||||
DetailsItem.text(
|
||||
DateFormat.yMMMMd(Localizations.localeOf(context).toString())
|
||||
.format(document.modified),
|
||||
context: context,
|
||||
label: S.of(context)!.modifiedAt,
|
||||
return SliverList.list(
|
||||
children: [
|
||||
if (currentUser.canEditDocuments)
|
||||
ArchiveSerialNumberField(
|
||||
document: document,
|
||||
).paddedOnly(bottom: itemSpacing),
|
||||
DetailsItem.text(
|
||||
DateFormat.yMMMMd(Localizations.localeOf(context).toString())
|
||||
.format(document.modified),
|
||||
context: context,
|
||||
label: S.of(context)!.modifiedAt,
|
||||
).paddedOnly(bottom: itemSpacing),
|
||||
DetailsItem.text(
|
||||
DateFormat.yMMMMd(Localizations.localeOf(context).toString())
|
||||
.format(document.added),
|
||||
context: context,
|
||||
label: S.of(context)!.addedAt,
|
||||
).paddedOnly(bottom: itemSpacing),
|
||||
DetailsItem.text(
|
||||
metaData.mediaFilename,
|
||||
context: context,
|
||||
label: S.of(context)!.mediaFilename,
|
||||
).paddedOnly(bottom: itemSpacing),
|
||||
if (document.originalFileName != null)
|
||||
DetailsItem.text(
|
||||
DateFormat.yMMMMd(Localizations.localeOf(context).toString())
|
||||
.format(document.added),
|
||||
context: context,
|
||||
label: S.of(context)!.addedAt,
|
||||
).paddedOnly(bottom: itemSpacing),
|
||||
DetailsItem.text(
|
||||
metaData.mediaFilename,
|
||||
context: context,
|
||||
label: S.of(context)!.mediaFilename,
|
||||
).paddedOnly(bottom: itemSpacing),
|
||||
if (document.originalFileName != null)
|
||||
DetailsItem.text(
|
||||
document.originalFileName!,
|
||||
context: context,
|
||||
label: S.of(context)!.originalMD5Checksum,
|
||||
).paddedOnly(bottom: itemSpacing),
|
||||
DetailsItem.text(
|
||||
metaData.originalChecksum,
|
||||
document.originalFileName!,
|
||||
context: context,
|
||||
label: S.of(context)!.originalMD5Checksum,
|
||||
).paddedOnly(bottom: itemSpacing),
|
||||
DetailsItem.text(
|
||||
formatBytes(metaData.originalSize, 2),
|
||||
context: context,
|
||||
label: S.of(context)!.originalFileSize,
|
||||
).paddedOnly(bottom: itemSpacing),
|
||||
DetailsItem.text(
|
||||
metaData.originalMimeType,
|
||||
context: context,
|
||||
label: S.of(context)!.originalMIMEType,
|
||||
).paddedOnly(bottom: itemSpacing),
|
||||
|
||||
],
|
||||
),
|
||||
DetailsItem.text(
|
||||
metaData.originalChecksum,
|
||||
context: context,
|
||||
label: S.of(context)!.originalMD5Checksum,
|
||||
).paddedOnly(bottom: itemSpacing),
|
||||
DetailsItem.text(
|
||||
formatBytes(metaData.originalSize, 2),
|
||||
context: context,
|
||||
label: S.of(context)!.originalFileSize,
|
||||
).paddedOnly(bottom: itemSpacing),
|
||||
DetailsItem.text(
|
||||
metaData.originalMimeType,
|
||||
context: context,
|
||||
label: S.of(context)!.originalMIMEType,
|
||||
).paddedOnly(bottom: itemSpacing),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_html/flutter_html.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/core/database/hive/hive_config.dart';
|
||||
import 'package:paperless_mobile/core/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/core/widgets/hint_card.dart';
|
||||
import 'package:paperless_mobile/core/widgets/hint_state_builder.dart';
|
||||
import 'package:paperless_mobile/features/document_details/cubit/document_details_cubit.dart';
|
||||
import 'package:paperless_mobile/features/settings/view/widgets/global_settings_builder.dart';
|
||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||
import 'package:paperless_mobile/helpers/message_helpers.dart';
|
||||
import 'package:markdown/markdown.dart' show markdownToHtml;
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
class DocumentNotesWidget extends StatefulWidget {
|
||||
final DocumentModel document;
|
||||
const DocumentNotesWidget({super.key, required this.document});
|
||||
|
||||
@override
|
||||
State<DocumentNotesWidget> createState() => _DocumentNotesWidgetState();
|
||||
}
|
||||
|
||||
class _DocumentNotesWidgetState extends State<DocumentNotesWidget> {
|
||||
final _noteContentController = TextEditingController();
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
bool _isNoteSubmitting = false;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
const hintKey = "hideMarkdownSyntaxHint";
|
||||
return SliverMainAxisGroup(
|
||||
slivers: [
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.only(bottom: 16),
|
||||
sliver: SliverToBoxAdapter(
|
||||
child: HintStateBuilder(
|
||||
listenKey: hintKey,
|
||||
builder: (context, box) {
|
||||
return HintCard(
|
||||
hintText: S.of(context)!.notesMarkdownSyntaxSupportHint,
|
||||
show: !box.get(hintKey, defaultValue: false)!,
|
||||
onHintAcknowledged: () {
|
||||
box.put(hintKey, true);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: _noteContentController,
|
||||
maxLines: null,
|
||||
validator: (value) {
|
||||
if (value?.trim().isEmpty ?? true) {
|
||||
return S.of(context)!.thisFieldIsRequired;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
textInputAction: TextInputAction.newline,
|
||||
decoration: InputDecoration(
|
||||
labelText: S.of(context)!.newNote,
|
||||
suffixIcon: IconButton(
|
||||
icon: const Icon(Icons.clear),
|
||||
onPressed: () {
|
||||
_noteContentController.clear();
|
||||
},
|
||||
),
|
||||
),
|
||||
).paddedOnly(bottom: 8),
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: ElevatedButton.icon(
|
||||
icon: _isNoteSubmitting
|
||||
? const SizedBox.square(
|
||||
dimension: 20,
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 3,
|
||||
),
|
||||
),
|
||||
)
|
||||
: const Icon(Icons.note_add_outlined),
|
||||
label: Text(S.of(context)!.addNote),
|
||||
onPressed: () async {
|
||||
_formKey.currentState?.save();
|
||||
FocusScope.of(context).unfocus();
|
||||
|
||||
if (_formKey.currentState?.validate() ?? false) {
|
||||
setState(() {
|
||||
_isNoteSubmitting = true;
|
||||
});
|
||||
try {
|
||||
await context
|
||||
.read<DocumentDetailsCubit>()
|
||||
.addNote(_noteContentController.text.trim());
|
||||
_noteContentController.clear();
|
||||
} catch (error) {
|
||||
showGenericError(context, error);
|
||||
} finally {
|
||||
setState(() {
|
||||
_isNoteSubmitting = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SliverToBoxAdapter(
|
||||
child: SizedBox(height: 16),
|
||||
),
|
||||
SliverList.separated(
|
||||
separatorBuilder: (context, index) => const SizedBox(height: 16),
|
||||
itemBuilder: (context, index) {
|
||||
final note = widget.document.notes.elementAt(index);
|
||||
return Card(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Html(
|
||||
data: markdownToHtml(note.note!),
|
||||
onLinkTap: (url, attributes, element) async {
|
||||
if (url?.isEmpty ?? true) {
|
||||
return;
|
||||
}
|
||||
if (await canLaunchUrlString(url!)) {
|
||||
launchUrlString(url);
|
||||
}
|
||||
},
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
if (note.created != null)
|
||||
Text(
|
||||
DateFormat.yMMMd(
|
||||
Localizations.localeOf(context).toString())
|
||||
.addPattern('\u2014')
|
||||
.add_jm()
|
||||
.format(note.created!),
|
||||
style:
|
||||
Theme.of(context).textTheme.labelMedium?.copyWith(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurface
|
||||
.withOpacity(.5),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
tooltip: S.of(context)!.delete,
|
||||
icon: const Icon(Icons.delete),
|
||||
onPressed: () {
|
||||
context.read<DocumentDetailsCubit>().deleteNote(note);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
).padded(16),
|
||||
);
|
||||
},
|
||||
itemCount: widget.document.notes.length,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -424,7 +424,7 @@ class _DocumentEditPageState extends State<DocumentEditPage>
|
||||
initialValue: initialCreatedAtDate,
|
||||
labelText: S.of(context)!.createdAt,
|
||||
firstDate: DateTime(1970, 1, 1),
|
||||
lastDate: DateTime.now(),
|
||||
lastDate: DateTime(2100, 1, 1),
|
||||
locale: Localizations.localeOf(context),
|
||||
prefixIcon: Icon(Icons.calendar_today),
|
||||
),
|
||||
|
||||
@@ -54,7 +54,9 @@ class DocumentScannerCubit extends Cubit<DocumentScannerState> {
|
||||
|
||||
Future<void> removeScan(File file) async {
|
||||
try {
|
||||
await file.delete();
|
||||
if (await file.exists()) {
|
||||
await file.delete();
|
||||
}
|
||||
} catch (error, stackTrace) {
|
||||
throw InfoMessageException(
|
||||
code: ErrorCode.scanRemoveFailed,
|
||||
|
||||
@@ -14,6 +14,7 @@ import 'package:paperless_mobile/core/bloc/loading_status.dart';
|
||||
import 'package:paperless_mobile/core/database/hive/hive_config.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/global_settings.dart';
|
||||
import 'package:paperless_mobile/core/global/constants.dart';
|
||||
import 'package:paperless_mobile/core/model/info_message_exception.dart';
|
||||
import 'package:paperless_mobile/core/service/file_service.dart';
|
||||
import 'package:paperless_mobile/features/app_drawer/view/app_drawer.dart';
|
||||
import 'package:paperless_mobile/features/document_scan/cubit/document_scanner_cubit.dart';
|
||||
@@ -326,6 +327,8 @@ class _ScannerPageState extends State<ScannerPage>
|
||||
.removeScan(scans[index]);
|
||||
} on PaperlessApiException catch (error, stackTrace) {
|
||||
showErrorMessage(context, error, stackTrace);
|
||||
} on InfoMessageException catch (error, stackTrace) {
|
||||
showInfoMessage(context, error, stackTrace);
|
||||
}
|
||||
},
|
||||
index: index,
|
||||
|
||||
@@ -222,7 +222,7 @@ class _DocumentUploadPreparationPageState
|
||||
FormBuilderLocalizedDatePicker(
|
||||
name: DocumentModel.createdKey,
|
||||
firstDate: DateTime(1970, 1, 1),
|
||||
lastDate: DateTime.now(),
|
||||
lastDate: DateTime(2100, 1, 1),
|
||||
locale: Localizations.localeOf(context),
|
||||
labelText: S.of(context)!.createdAt + " *",
|
||||
allowUnset: true,
|
||||
|
||||
@@ -21,6 +21,7 @@ import 'package:paperless_mobile/features/documents/view/widgets/selection/docum
|
||||
import 'package:paperless_mobile/features/documents/view/widgets/selection/view_type_selection_widget.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/logging/data/logger.dart';
|
||||
import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart';
|
||||
import 'package:paperless_mobile/features/tasks/model/pending_tasks_notifier.dart';
|
||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||
@@ -308,8 +309,18 @@ class _DocumentsPageState extends State<DocumentsPage> {
|
||||
// Listen for scroll notifications to load new data.
|
||||
// Scroll controller does not work here due to nestedscrollview limitations.
|
||||
final offset = notification.metrics.pixels;
|
||||
if (offset > 128 && _savedViewsExpansionController.isExpanded) {
|
||||
_savedViewsExpansionController.collapse();
|
||||
try {
|
||||
if (offset > 128 && _savedViewsExpansionController.isExpanded) {
|
||||
_savedViewsExpansionController.collapse();
|
||||
}
|
||||
// Workaround for https://github.com/astubenbord/paperless-mobile/issues/341 probably caused by https://github.com/flutter/flutter/issues/138153
|
||||
} on TypeError catch (error) {
|
||||
logger.fw(
|
||||
"An exception was thrown, but this message can probably be ignored. See issue #341 for more details.",
|
||||
error: error,
|
||||
className: runtimeType.toString(),
|
||||
methodName: "_buildDocumentsTab",
|
||||
);
|
||||
}
|
||||
|
||||
final max = notification.metrics.maxScrollExtent;
|
||||
|
||||
@@ -4,6 +4,8 @@ import 'package:flutter/widgets.dart';
|
||||
import 'package:hive_flutter/adapters.dart';
|
||||
import 'package:hydrated_bloc/hydrated_bloc.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/constants.dart';
|
||||
import 'package:paperless_mobile/core/bloc/transient_error.dart';
|
||||
import 'package:paperless_mobile/core/database/hive/hive_config.dart';
|
||||
import 'package:paperless_mobile/core/database/hive/hive_extensions.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/global_settings.dart';
|
||||
@@ -13,6 +15,7 @@ import 'package:paperless_mobile/core/database/tables/local_user_settings.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/user_credentials.dart';
|
||||
import 'package:paperless_mobile/core/factory/paperless_api_factory.dart';
|
||||
import 'package:paperless_mobile/core/interceptor/language_header.interceptor.dart';
|
||||
import 'package:paperless_mobile/core/security/session_manager_impl.dart';
|
||||
import 'package:paperless_mobile/features/logging/data/logger.dart';
|
||||
import 'package:paperless_mobile/features/logging/utils/redaction_utils.dart';
|
||||
import 'package:paperless_mobile/core/model/info_message_exception.dart';
|
||||
@@ -83,7 +86,7 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
||||
AuthenticatingStage.persistingLocalUserData));
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
} on PaperlessApiException catch (exception, stackTrace) {
|
||||
emit(
|
||||
AuthenticationErrorState(
|
||||
serverUrl: serverUrl,
|
||||
@@ -207,8 +210,8 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
||||
methodName: 'switchAccount',
|
||||
);
|
||||
|
||||
final sessionManager = SessionManager([
|
||||
LanguageHeaderInterceptor(locale),
|
||||
final SessionManager sessionManager = SessionManagerImpl([
|
||||
LanguageHeaderInterceptor(() => locale),
|
||||
]);
|
||||
await _addUser(
|
||||
localUserId,
|
||||
@@ -462,14 +465,12 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
||||
|
||||
final authApi = _apiFactory.createAuthenticationApi(sessionManager.client);
|
||||
|
||||
await onPerformLogin?.call();
|
||||
logger.fd(
|
||||
"Fetching bearer token from the server...",
|
||||
className: runtimeType.toString(),
|
||||
methodName: '_addUser',
|
||||
);
|
||||
|
||||
await onPerformLogin?.call();
|
||||
|
||||
final token = await authApi.login(
|
||||
username: credentials.username!,
|
||||
password: credentials.password!,
|
||||
@@ -486,7 +487,6 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
||||
clientCertificate: clientCert,
|
||||
authToken: token,
|
||||
);
|
||||
|
||||
final userAccountBox =
|
||||
Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount);
|
||||
final userStateBox =
|
||||
@@ -586,12 +586,14 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
||||
clientCertificate: clientCert,
|
||||
),
|
||||
);
|
||||
|
||||
logger.fd(
|
||||
"User credentials successfully saved.",
|
||||
className: runtimeType.toString(),
|
||||
methodName: '_addUser',
|
||||
);
|
||||
});
|
||||
|
||||
final hostsBox = Hive.box<String>(HiveBoxes.hosts);
|
||||
if (!hostsBox.values.contains(serverUrl)) {
|
||||
await hostsBox.add(serverUrl);
|
||||
@@ -618,12 +620,19 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
||||
try {
|
||||
final response = await dio.get(
|
||||
"/api/",
|
||||
options: Options(
|
||||
sendTimeout: timeout,
|
||||
),
|
||||
options: Options(sendTimeout: timeout),
|
||||
);
|
||||
final apiVersion =
|
||||
int apiVersion =
|
||||
int.parse(response.headers.value('x-api-version') ?? "3");
|
||||
if (apiVersion > latestSupportedApiVersion) {
|
||||
logger.fw(
|
||||
"The server is running a newer API version ($apiVersion) than the app supports (v$latestSupportedApiVersion), falling back to latest supported version (v$latestSupportedApiVersion). "
|
||||
"Warning: This might lead to unexpected behavior!",
|
||||
className: runtimeType.toString(),
|
||||
methodName: '_getApiVersion',
|
||||
);
|
||||
apiVersion = latestSupportedApiVersion;
|
||||
}
|
||||
logger.fd(
|
||||
"Successfully retrieved API version ($apiVersion).",
|
||||
className: runtimeType.toString(),
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
@@ -17,7 +16,6 @@ import 'package:paperless_mobile/features/login/model/client_certificate_form_mo
|
||||
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/view/widgets/form_fields/client_certificate_form_field.dart';
|
||||
import 'package:paperless_mobile/features/login/view/widgets/form_fields/login_settings_page.dart';
|
||||
import 'package:paperless_mobile/features/login/view/widgets/form_fields/server_address_form_field.dart';
|
||||
import 'package:paperless_mobile/features/login/view/widgets/form_fields/user_credentials_form_field.dart';
|
||||
import 'package:paperless_mobile/generated/assets.gen.dart';
|
||||
@@ -44,6 +42,7 @@ class AddAccountPage extends StatefulWidget {
|
||||
final bool showLocalAccounts;
|
||||
|
||||
final Widget? bottomLeftButton;
|
||||
|
||||
const AddAccountPage({
|
||||
Key? key,
|
||||
required this.onSubmit,
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'package:hive_flutter/adapters.dart';
|
||||
import 'package:paperless_mobile/core/database/hive/hive_config.dart';
|
||||
|
||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||
import 'package:paperless_mobile/keys.dart';
|
||||
|
||||
class ServerAddressFormField extends StatefulWidget {
|
||||
static const String fkServerAddress = "serverAddress";
|
||||
@@ -59,7 +60,7 @@ class _ServerAddressFormFieldState extends State<ServerAddressFormField>
|
||||
maxWidth: MediaQuery.sizeOf(context).width - 40,
|
||||
);
|
||||
},
|
||||
key: const ValueKey('login-server-address'),
|
||||
key: TestKeys.login.serverAddressFormField,
|
||||
optionsBuilder: (textEditingValue) {
|
||||
return Hive.box<String>(HiveBoxes.hosts)
|
||||
.values
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/features/settings/view/widgets/app_logs_tile.dart';
|
||||
@@ -15,6 +17,7 @@ import 'package:paperless_mobile/features/settings/view/widgets/theme_mode_setti
|
||||
import 'package:paperless_mobile/features/settings/view/widgets/user_settings_builder.dart';
|
||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
class SettingsPage extends StatelessWidget {
|
||||
const SettingsPage({super.key});
|
||||
@@ -80,15 +83,49 @@ class SettingsPage extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
final serverData = snapshot.data!;
|
||||
return Text(
|
||||
S.of(context)!.paperlessServerVersion +
|
||||
' ' +
|
||||
serverData.version.toString() +
|
||||
' (API v${serverData.apiVersion})',
|
||||
style: Theme.of(context).textTheme.labelSmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
S.of(context)!.paperlessServerVersion +
|
||||
' ' +
|
||||
serverData.version.toString() +
|
||||
' (API v${serverData.apiVersion})',
|
||||
style: Theme.of(context).textTheme.labelSmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
if (serverData.isUpdateAvailable) ...[
|
||||
SizedBox(height: 8),
|
||||
Text.rich(
|
||||
TextSpan(
|
||||
style: Theme.of(context).textTheme.labelSmall!,
|
||||
text: '${S.of(context)!.newerVersionAvailable} ',
|
||||
children: [
|
||||
TextSpan(
|
||||
text: serverData.latestVersion,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.labelSmall!
|
||||
.copyWith(
|
||||
decoration: TextDecoration.underline,
|
||||
color: CupertinoColors.link,
|
||||
decorationColor: CupertinoColors.link,
|
||||
),
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () {
|
||||
launchUrlString(
|
||||
"https://github.com/paperless-ngx/paperless-ngx/releases/tag/${serverData.latestVersion}",
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
]
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
@@ -23,6 +23,7 @@ class _LanguageSelectionSettingState extends State<LanguageSelectionSetting> {
|
||||
'pl': LanguageOption('Polska', true),
|
||||
'ca': LanguageOption('Català', true),
|
||||
'ru': LanguageOption('Русский', true),
|
||||
'it': LanguageOption('Italiano', true),
|
||||
};
|
||||
|
||||
@override
|
||||
|
||||
19
lib/keys.dart
Normal file
19
lib/keys.dart
Normal file
@@ -0,0 +1,19 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class TestKeys {
|
||||
TestKeys._();
|
||||
|
||||
static final login = _LoginTestKeys();
|
||||
}
|
||||
|
||||
class _LoginTestKeys {
|
||||
final serverAddressFormField = const Key('login-server-address');
|
||||
final continueButton = const Key('login-continue-button');
|
||||
final usernameFormField = const Key('login-username');
|
||||
final passwordFormField = const Key('login-password');
|
||||
final loginButton = const Key('login-login-button');
|
||||
final clientCertificateFormField = const Key('login-client-certificate');
|
||||
final clientCertificatePassphraseFormField =
|
||||
const Key('login-client-certificate-passphrase');
|
||||
final loggingInScreen = const Key('login-logging-in-screen');
|
||||
}
|
||||
@@ -1024,5 +1024,25 @@
|
||||
"@testingConnection": {
|
||||
"description": "Text shown while the app tries to establish a connection to the specified host."
|
||||
},
|
||||
"version": "Versió {versionCode}"
|
||||
"version": "Versió {versionCode}",
|
||||
"notes": "{count, plural, zero{Notes} one{Nota} other{Notes}}",
|
||||
"addNote": "Afegir Nota",
|
||||
"newerVersionAvailable": "Newer version available:",
|
||||
"dateOutOfRange": "Date must be between {firstDate} and {lastDate}.",
|
||||
"@dateOutOfRange": {
|
||||
"description": "Error message shown when the user tries to select a date outside of the allowed range.",
|
||||
"placeholders": {
|
||||
"firstDate": {
|
||||
"type": "DateTime",
|
||||
"format": "yMd"
|
||||
},
|
||||
"lastDate": {
|
||||
"type": "DateTime",
|
||||
"format": "yMd"
|
||||
}
|
||||
}
|
||||
},
|
||||
"permissions": "Permissions",
|
||||
"newNote": "New note",
|
||||
"notesMarkdownSyntaxSupportHint": "Paperless Mobile can render notes using basic markdown syntax. Try it out!"
|
||||
}
|
||||
@@ -1024,5 +1024,25 @@
|
||||
"@testingConnection": {
|
||||
"description": "Text shown while the app tries to establish a connection to the specified host."
|
||||
},
|
||||
"version": "Version {versionCode}"
|
||||
"version": "Version {versionCode}",
|
||||
"notes": "{count, plural, zero{Notes} one{Note} other{Notes}}",
|
||||
"addNote": "Add note",
|
||||
"newerVersionAvailable": "Newer version available:",
|
||||
"dateOutOfRange": "Date must be between {firstDate} and {lastDate}.",
|
||||
"@dateOutOfRange": {
|
||||
"description": "Error message shown when the user tries to select a date outside of the allowed range.",
|
||||
"placeholders": {
|
||||
"firstDate": {
|
||||
"type": "DateTime",
|
||||
"format": "yMd"
|
||||
},
|
||||
"lastDate": {
|
||||
"type": "DateTime",
|
||||
"format": "yMd"
|
||||
}
|
||||
}
|
||||
},
|
||||
"permissions": "Permissions",
|
||||
"newNote": "New note",
|
||||
"notesMarkdownSyntaxSupportHint": "Paperless Mobile can render notes using basic markdown syntax. Try it out!"
|
||||
}
|
||||
@@ -1024,5 +1024,25 @@
|
||||
"@testingConnection": {
|
||||
"description": "Text shown while the app tries to establish a connection to the specified host."
|
||||
},
|
||||
"version": "Version {versionCode}"
|
||||
"version": "Version {versionCode}",
|
||||
"notes": "{count, plural, zero{Notizen} one{Notiz} other{Notizen}}",
|
||||
"addNote": "Notiz hinzufügen",
|
||||
"newerVersionAvailable": "Neuere Version verfügbar:",
|
||||
"dateOutOfRange": "Das Datum muss zwischen {firstDate} und {lastDate} liegen.",
|
||||
"@dateOutOfRange": {
|
||||
"description": "Error message shown when the user tries to select a date outside of the allowed range.",
|
||||
"placeholders": {
|
||||
"firstDate": {
|
||||
"type": "DateTime",
|
||||
"format": "yMd"
|
||||
},
|
||||
"lastDate": {
|
||||
"type": "DateTime",
|
||||
"format": "yMd"
|
||||
}
|
||||
}
|
||||
},
|
||||
"permissions": "Berechtigungen",
|
||||
"newNote": "Neue Notiz",
|
||||
"notesMarkdownSyntaxSupportHint": "Paperless Mobile unterstützt Markdown-Syntax zur Darstellung und Formatierung von Notizen. Probiere es aus!"
|
||||
}
|
||||
@@ -1024,5 +1024,25 @@
|
||||
"@testingConnection": {
|
||||
"description": "Text shown while the app tries to establish a connection to the specified host."
|
||||
},
|
||||
"version": "Version {versionCode}"
|
||||
"version": "Version {versionCode}",
|
||||
"notes": "{count, plural, zero{Notes} one{Note} other{Notes}}",
|
||||
"addNote": "Add note",
|
||||
"newerVersionAvailable": "Newer version available:",
|
||||
"dateOutOfRange": "Date must be between {firstDate} and {lastDate}.",
|
||||
"@dateOutOfRange": {
|
||||
"description": "Error message shown when the user tries to select a date outside of the allowed range.",
|
||||
"placeholders": {
|
||||
"firstDate": {
|
||||
"type": "DateTime",
|
||||
"format": "yMd"
|
||||
},
|
||||
"lastDate": {
|
||||
"type": "DateTime",
|
||||
"format": "yMd"
|
||||
}
|
||||
}
|
||||
},
|
||||
"permissions": "Permissions",
|
||||
"newNote": "New note",
|
||||
"notesMarkdownSyntaxSupportHint": "Paperless Mobile can render notes using basic markdown syntax. Try it out!"
|
||||
}
|
||||
@@ -1020,9 +1020,29 @@
|
||||
},
|
||||
"misc": "Otros",
|
||||
"loggingOut": "Cerrando sesión...",
|
||||
"testingConnection": "Testing connection...",
|
||||
"testingConnection": "Probando conexión...",
|
||||
"@testingConnection": {
|
||||
"description": "Text shown while the app tries to establish a connection to the specified host."
|
||||
},
|
||||
"version": "Version {versionCode}"
|
||||
"version": "Version {versionCode}",
|
||||
"notes": "{count, plural, zero{Notas} one{Nota} other{Notas}}",
|
||||
"addNote": "Añadir nota",
|
||||
"newerVersionAvailable": "Newer version available:",
|
||||
"dateOutOfRange": "Date must be between {firstDate} and {lastDate}.",
|
||||
"@dateOutOfRange": {
|
||||
"description": "Error message shown when the user tries to select a date outside of the allowed range.",
|
||||
"placeholders": {
|
||||
"firstDate": {
|
||||
"type": "DateTime",
|
||||
"format": "yMd"
|
||||
},
|
||||
"lastDate": {
|
||||
"type": "DateTime",
|
||||
"format": "yMd"
|
||||
}
|
||||
}
|
||||
},
|
||||
"permissions": "Permissions",
|
||||
"newNote": "New note",
|
||||
"notesMarkdownSyntaxSupportHint": "Paperless Mobile can render notes using basic markdown syntax. Try it out!"
|
||||
}
|
||||
@@ -703,7 +703,7 @@
|
||||
"@confirmAction": {
|
||||
"description": "Typically used as a title to confirm a previously selected action"
|
||||
},
|
||||
"areYouSureYouWantToContinue": "Etes-vous sûr(e) de vouloir continuer?",
|
||||
"areYouSureYouWantToContinue": "Êtes-vous sûr(e) de vouloir continuer ?",
|
||||
"bulkEditTagsAddMessage": "{count, plural, one{Cette opération va ajouter les balises {tags} au document sélectionné} other{Cette opération va ajouter les balises {tags} à {count} documents sélectionnés!}}",
|
||||
"@bulkEditTagsAddMessage": {
|
||||
"description": "Message of the confirmation dialog when bulk adding tags."
|
||||
@@ -717,7 +717,7 @@
|
||||
"description": "Message of the confirmation dialog when both adding and removing tags."
|
||||
},
|
||||
"bulkEditCorrespondentAssignMessage": "{count, plural, one{Cette opération assignera le correspondant {correspondent} au document sélectionné} other{Cette opération va assigner le correspondant {correspondent} à {count} documents sélectionnés!}}",
|
||||
"bulkEditDocumentTypeAssignMessage": "{count, plural, one{Cette opération assignera le type de document {docType} au document sélectionné.} other{Cette opération va assigner le documentType {docType} à {count} documents sélectionnés.}}",
|
||||
"bulkEditDocumentTypeAssignMessage": "{count, plural, one{Cette opération assignera le type de document {docType} au document sélectionné.} other{Cette opération va assigner le type de document {docType} à {count} documents sélectionnés.}}",
|
||||
"bulkEditStoragePathAssignMessage": "{count, plural, one{Cette opération assignera le chemin de stockage {path} au document sélectionné.} other{Cette opération va assigner le chemin de stockage {path} à {count} documents sélectionnés.}}",
|
||||
"bulkEditCorrespondentRemoveMessage": "{count, plural, one{Cette opération va supprimer le correspondant du document sélectionné.} other{Cette opération va supprimer le correspondant de {count} documents sélectionnés.}}",
|
||||
"bulkEditDocumentTypeRemoveMessage": "{count, plural, one{Cette opération va supprimer le type de document du document sélectionné.} other{Cette opération va supprimer le type de document de {count} documents sélectionnés.}}",
|
||||
@@ -772,7 +772,7 @@
|
||||
"@defaultDownloadFileType": {
|
||||
"description": "Label indicating the default filetype to download (one of archived, original and always ask)"
|
||||
},
|
||||
"defaultShareFileType": "Type de fichier par défaut de partage",
|
||||
"defaultShareFileType": "Type de fichier par défaut pour le partage",
|
||||
"@defaultShareFileType": {
|
||||
"description": "Label indicating the default filetype to share (one of archived, original and always ask)"
|
||||
},
|
||||
@@ -861,7 +861,7 @@
|
||||
"@loginRequiredPermissionsHint": {
|
||||
"description": "Hint shown on the login page informing the user of the required permissions to use the app."
|
||||
},
|
||||
"missingPermissions": "You do not have the necessary permissions to perform this action.",
|
||||
"missingPermissions": "Vous n'avez pas les permissions nécessaires pour faire cette action.",
|
||||
"@missingPermissions": {
|
||||
"description": "Message shown in a snackbar when a user without the reequired permissions performs an action."
|
||||
},
|
||||
@@ -873,156 +873,176 @@
|
||||
"@donate": {
|
||||
"description": "Label of the in-app donate button"
|
||||
},
|
||||
"donationDialogContent": "Thank you for considering to support this app! Due to both Google's and Apple's Payment Policies, no links leading to donations may be displayed in-app. Not even linking to the project's repository page appears to be allowed in this context. Therefore, maybe have a look at the 'Donations' section in the project's README. Your support is much appreciated and keeps the development of this app alive. Thanks!",
|
||||
"donationDialogContent": "Merci d'avoir envisagé de soutenir cette application ! En raison des politiques de paiement de Google et d'Apple, aucun lien menant aux dons ne peut être affiché dans l'application. Même un lien vers la page du dépôt du projet ne semble pas autorisé dans ce contexte. Par conséquent, jetez peut-être un coup d'oeil à la section « Donations » dans le README du projet. Votre soutien est très apprécié et maintient en vie le développement de cette application. Merci !",
|
||||
"@donationDialogContent": {
|
||||
"description": "Text displayed in the donation dialog"
|
||||
},
|
||||
"noDocumentsFound": "No documents found.",
|
||||
"noDocumentsFound": "Aucun document trouvé.",
|
||||
"@noDocumentsFound": {
|
||||
"description": "Message shown when no documents were found."
|
||||
},
|
||||
"couldNotDeleteCorrespondent": "Could not delete correspondent, please try again.",
|
||||
"couldNotDeleteCorrespondent": "Impossible de supprimer le correspondant, veuillez réessayer.",
|
||||
"@couldNotDeleteCorrespondent": {
|
||||
"description": "Message shown in snackbar when a correspondent could not be deleted."
|
||||
},
|
||||
"couldNotDeleteDocumentType": "Could not delete document type, please try again.",
|
||||
"couldNotDeleteDocumentType": "Impossible de supprimer ce type de document, veuillez réessayer.",
|
||||
"@couldNotDeleteDocumentType": {
|
||||
"description": "Message shown when a document type could not be deleted"
|
||||
},
|
||||
"couldNotDeleteTag": "Could not delete tag, please try again.",
|
||||
"couldNotDeleteTag": "Impossible de supprimer l'étiquette, veuillez réessayer.",
|
||||
"@couldNotDeleteTag": {
|
||||
"description": "Message shown when a tag could not be deleted"
|
||||
},
|
||||
"couldNotDeleteStoragePath": "Could not delete storage path, please try again.",
|
||||
"couldNotDeleteStoragePath": "Impossible de supprimer le chemin de stockage, veuillez réessayer.",
|
||||
"@couldNotDeleteStoragePath": {
|
||||
"description": "Message shown when a storage path could not be deleted"
|
||||
},
|
||||
"couldNotUpdateCorrespondent": "Could not update correspondent, please try again.",
|
||||
"couldNotUpdateCorrespondent": "Impossible de mettre à jour le correspondant, veuillez réessayer.",
|
||||
"@couldNotUpdateCorrespondent": {
|
||||
"description": "Message shown when a correspondent could not be updated"
|
||||
},
|
||||
"couldNotUpdateDocumentType": "Could not update document type, please try again.",
|
||||
"couldNotUpdateDocumentType": "Impossible de mettre à jour le type de document, veuillez réessayer.",
|
||||
"@couldNotUpdateDocumentType": {
|
||||
"description": "Message shown when a document type could not be updated"
|
||||
},
|
||||
"couldNotUpdateTag": "Could not update tag, please try again.",
|
||||
"couldNotUpdateTag": "Impossible de mettre à jour l'étiquette, veuillez réessayer.",
|
||||
"@couldNotUpdateTag": {
|
||||
"description": "Message shown when a tag could not be updated"
|
||||
},
|
||||
"couldNotLoadServerInformation": "Could not load server information.",
|
||||
"couldNotLoadServerInformation": "Impossible de charger les informations du serveur.",
|
||||
"@couldNotLoadServerInformation": {
|
||||
"description": "Message shown when the server information could not be loaded"
|
||||
},
|
||||
"couldNotLoadStatistics": "Could not load server statistics.",
|
||||
"couldNotLoadStatistics": "Impossible de charger les statistiques du serveur.",
|
||||
"@couldNotLoadStatistics": {
|
||||
"description": "Message shown when the server statistics could not be loaded"
|
||||
},
|
||||
"couldNotLoadUISettings": "Could not load UI settings.",
|
||||
"couldNotLoadUISettings": "Impossible de charger les paramètres de l'interface.",
|
||||
"@couldNotLoadUISettings": {
|
||||
"description": "Message shown when the UI settings could not be loaded"
|
||||
},
|
||||
"couldNotLoadTasks": "Could not load tasks.",
|
||||
"couldNotLoadTasks": "Impossible de charger les tâches.",
|
||||
"@couldNotLoadTasks": {
|
||||
"description": "Message shown when the tasks (e.g. document consumed) could not be loaded"
|
||||
},
|
||||
"userNotFound": "User could not be found.",
|
||||
"userNotFound": "L'utilisateur ne peut pas être trouvé.",
|
||||
"@userNotFound": {
|
||||
"description": "Message shown when the specified user (e.g. by id) could not be found"
|
||||
},
|
||||
"couldNotUpdateSavedView": "Could not update saved view, please try again.",
|
||||
"couldNotUpdateSavedView": "Impossible de mettre à jour la vue, veuillez réessayer.",
|
||||
"@couldNotUpdateSavedView": {
|
||||
"description": "Message shown when a saved view could not be updated"
|
||||
},
|
||||
"couldNotUpdateStoragePath": "Could not update storage path, please try again.",
|
||||
"savedViewSuccessfullyUpdated": "Saved view successfully updated.",
|
||||
"couldNotUpdateStoragePath": "Impossible de mettre à jour le chemin de stockage, veuillez réessayer.",
|
||||
"savedViewSuccessfullyUpdated": "Vue enregistrée mise à jour avec succès.",
|
||||
"@savedViewSuccessfullyUpdated": {
|
||||
"description": "Message shown when a saved view was successfully updated."
|
||||
},
|
||||
"discardChanges": "Discard changes?",
|
||||
"discardChanges": "Annuler les modifications ?",
|
||||
"@discardChanges": {
|
||||
"description": "Title of the alert dialog shown when a user tries to close a view with unsaved changes."
|
||||
},
|
||||
"savedViewChangedDialogContent": "The filter conditions of the active view have changed. By resetting the filter, these changes will be lost. Do you still wish to continue?",
|
||||
"savedViewChangedDialogContent": "Les conditions de filtre de la vue active ont changé. En réinitialisant le filtre, ces modifications seront perdues. Voulez-vous continuer ?",
|
||||
"@savedViewChangedDialogContent": {
|
||||
"description": "Content of the alert dialog shown when all of the following applies:\r\n* User has saved view selected\r\n* User has performed changes to the current document filter\r\n* User now tries to reset this filter without having saved the changes to the view."
|
||||
},
|
||||
"createFromCurrentFilter": "Create from current filter",
|
||||
"createFromCurrentFilter": "Créer à partir du filtre actuel",
|
||||
"@createFromCurrentFilter": {
|
||||
"description": "Tooltip of the \"New saved view\" button"
|
||||
},
|
||||
"home": "Home",
|
||||
"home": "Accueil",
|
||||
"@home": {
|
||||
"description": "Label of the \"Home\" route"
|
||||
},
|
||||
"welcomeUser": "Welcome, {name}!",
|
||||
"welcomeUser": "Bienvenue {name} !",
|
||||
"@welcomeUser": {
|
||||
"description": "Top message shown on the home page"
|
||||
},
|
||||
"statistics": "Statistics",
|
||||
"documentsInInbox": "Documents in inbox",
|
||||
"totalDocuments": "Total documents",
|
||||
"totalCharacters": "Total characters",
|
||||
"showAll": "Show all",
|
||||
"statistics": "Statistiques",
|
||||
"documentsInInbox": "Documents dans la boîte de réception",
|
||||
"totalDocuments": "Nombre total de documents",
|
||||
"totalCharacters": "Nombre total de caractères",
|
||||
"showAll": "Tout afficher",
|
||||
"@showAll": {
|
||||
"description": "Button label shown on a saved view preview to open this view in the documents page"
|
||||
},
|
||||
"userAlreadyExists": "This user already exists.",
|
||||
"userAlreadyExists": "Cet utilisateur existe déjà.",
|
||||
"@userAlreadyExists": {
|
||||
"description": "Error message shown when the user tries to add an already existing account."
|
||||
},
|
||||
"youDidNotSaveAnyViewsYet": "You did not save any views yet, create one and it will be shown here.",
|
||||
"youDidNotSaveAnyViewsYet": "Vous n'avez pas encore enregistré de vues, créez en une et elle sera affichée ici.",
|
||||
"@youDidNotSaveAnyViewsYet": {
|
||||
"description": "Message shown when there are no saved views yet."
|
||||
},
|
||||
"tryAgain": "Try again",
|
||||
"discardFile": "Discard file?",
|
||||
"discard": "Discard",
|
||||
"backToLogin": "Back to login",
|
||||
"skipEditingReceivedFiles": "Skip editing received files",
|
||||
"uploadWithoutPromptingUploadForm": "Always upload without prompting the upload form when sharing files with the app.",
|
||||
"authenticatingDots": "Authenticating...",
|
||||
"tryAgain": "Veuillez réessayer",
|
||||
"discardFile": "Abandonner le fichier ?",
|
||||
"discard": "Abandonner",
|
||||
"backToLogin": "Retour à la page de connexion",
|
||||
"skipEditingReceivedFiles": "Passer l'édition des fichiers reçus",
|
||||
"uploadWithoutPromptingUploadForm": "Toujours mettre en ligne sans montrer le formulaire de mise en ligne lors du partage de fichiers avec l'application.",
|
||||
"authenticatingDots": "Authentification en cours...",
|
||||
"@authenticatingDots": {
|
||||
"description": "Message shown when the app is authenticating the user"
|
||||
},
|
||||
"persistingUserInformation": "Persisting user information...",
|
||||
"fetchingUserInformation": "Fetching user information...",
|
||||
"persistingUserInformation": "Sauvegarde des informations utilisateur...",
|
||||
"fetchingUserInformation": "Récupération des informations utilisateur...",
|
||||
"@fetchingUserInformation": {
|
||||
"description": "Message shown when the app loads user data from the server"
|
||||
},
|
||||
"restoringSession": "Restoring session...",
|
||||
"restoringSession": "Restauration de la session...",
|
||||
"@restoringSession": {
|
||||
"description": "Message shown when the user opens the app and the previous user is tried to be authenticated and logged in"
|
||||
},
|
||||
"documentsAssigned": "{count, plural, zero{No documents} one{1 document} other{{count} documents}}",
|
||||
"documentsAssigned": "{count, plural, zero{Pas de document} one{1 document} other{{count} documents}}",
|
||||
"@documentsAssigned": {
|
||||
"description": "Text shown with a correspondent, document type etc. to indicate the number of documents this filter will maximally yield."
|
||||
},
|
||||
"discardChangesWarning": "You have unsaved changes. By continuing, all changes will be lost. Do you want to discard these changes?",
|
||||
"discardChangesWarning": "Vous avez des modifications non enregistrées. En continuant, toutes les modifications seront perdues. Voulez-vous abandonner ces modifications ?",
|
||||
"@discardChangesWarning": {
|
||||
"description": "Warning message shown when the user tries to close a route without saving the changes."
|
||||
},
|
||||
"changelog": "Changelog",
|
||||
"noLogsFoundOn": "No logs found on {date}.",
|
||||
"logfileBottomReached": "You have reached the bottom of this logfile.",
|
||||
"appLogs": "App logs {date}",
|
||||
"saveLogsToFile": "Save logs to file",
|
||||
"copyToClipboard": "Copy to clipboard",
|
||||
"couldNotLoadLogfileFrom": "Could not load logfile from {date}.",
|
||||
"loadingLogsFrom": "Loading logs from {date}...",
|
||||
"clearLogs": "Clear logs from {date}",
|
||||
"showPdf": "Show PDF",
|
||||
"changelog": "Notes de version",
|
||||
"noLogsFoundOn": "Aucun journal trouvé sur {date}.",
|
||||
"logfileBottomReached": "Vous avez atteint le bas de ce fichier journal.",
|
||||
"appLogs": "Journaux d'application {date}",
|
||||
"saveLogsToFile": "Enregistrer le fichier journal",
|
||||
"copyToClipboard": "Copier dans le presse-papier",
|
||||
"couldNotLoadLogfileFrom": "Impossible de charger le fichier journal depuis {date}.",
|
||||
"loadingLogsFrom": "Chargement des journaux depuis {date}...",
|
||||
"clearLogs": "Effacer les journaux de {date}",
|
||||
"showPdf": "Afficher le PDF",
|
||||
"@showPdf": {
|
||||
"description": "Tooltip shown on the \"show pdf\" button on the document edit page"
|
||||
},
|
||||
"hidePdf": "Hide PDF",
|
||||
"hidePdf": "Masquer le PDF",
|
||||
"@hidePdf": {
|
||||
"description": "Tooltip shown on the \"show pdf\" icon button on the document edit page"
|
||||
},
|
||||
"misc": "Sonstige",
|
||||
"loggingOut": "Logging out...",
|
||||
"testingConnection": "Testing connection...",
|
||||
"loggingOut": "Déconnexion...",
|
||||
"testingConnection": "Vérifier la connexion...",
|
||||
"@testingConnection": {
|
||||
"description": "Text shown while the app tries to establish a connection to the specified host."
|
||||
},
|
||||
"version": "Version {versionCode}"
|
||||
"version": "Version {versionCode}",
|
||||
"notes": "{count, plural, zero{Notes} one{Note} other{Notes}}",
|
||||
"addNote": "Ajouter une note",
|
||||
"newerVersionAvailable": "Newer version available:",
|
||||
"dateOutOfRange": "Date must be between {firstDate} and {lastDate}.",
|
||||
"@dateOutOfRange": {
|
||||
"description": "Error message shown when the user tries to select a date outside of the allowed range.",
|
||||
"placeholders": {
|
||||
"firstDate": {
|
||||
"type": "DateTime",
|
||||
"format": "yMd"
|
||||
},
|
||||
"lastDate": {
|
||||
"type": "DateTime",
|
||||
"format": "yMd"
|
||||
}
|
||||
}
|
||||
},
|
||||
"permissions": "Permissions",
|
||||
"newNote": "New note",
|
||||
"notesMarkdownSyntaxSupportHint": "Paperless Mobile can render notes using basic markdown syntax. Try it out!"
|
||||
}
|
||||
1048
lib/l10n/intl_it.arb
Normal file
1048
lib/l10n/intl_it.arb
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1024,5 +1024,25 @@
|
||||
"@testingConnection": {
|
||||
"description": "Text shown while the app tries to establish a connection to the specified host."
|
||||
},
|
||||
"version": "Version {versionCode}"
|
||||
"version": "Version {versionCode}",
|
||||
"notes": "{count, plural, zero{Notes} one{Note} other{Notes}}",
|
||||
"addNote": "Add note",
|
||||
"newerVersionAvailable": "Newer version available:",
|
||||
"dateOutOfRange": "Date must be between {firstDate} and {lastDate}.",
|
||||
"@dateOutOfRange": {
|
||||
"description": "Error message shown when the user tries to select a date outside of the allowed range.",
|
||||
"placeholders": {
|
||||
"firstDate": {
|
||||
"type": "DateTime",
|
||||
"format": "yMd"
|
||||
},
|
||||
"lastDate": {
|
||||
"type": "DateTime",
|
||||
"format": "yMd"
|
||||
}
|
||||
}
|
||||
},
|
||||
"permissions": "Permissions",
|
||||
"newNote": "New note",
|
||||
"notesMarkdownSyntaxSupportHint": "Paperless Mobile can render notes using basic markdown syntax. Try it out!"
|
||||
}
|
||||
@@ -1024,5 +1024,25 @@
|
||||
"@testingConnection": {
|
||||
"description": "Text shown while the app tries to establish a connection to the specified host."
|
||||
},
|
||||
"version": "Version {versionCode}"
|
||||
"version": "Version {versionCode}",
|
||||
"notes": "{count, plural, zero{Notes} one{Note} other{Notes}}",
|
||||
"addNote": "Add note",
|
||||
"newerVersionAvailable": "Newer version available:",
|
||||
"dateOutOfRange": "Date must be between {firstDate} and {lastDate}.",
|
||||
"@dateOutOfRange": {
|
||||
"description": "Error message shown when the user tries to select a date outside of the allowed range.",
|
||||
"placeholders": {
|
||||
"firstDate": {
|
||||
"type": "DateTime",
|
||||
"format": "yMd"
|
||||
},
|
||||
"lastDate": {
|
||||
"type": "DateTime",
|
||||
"format": "yMd"
|
||||
}
|
||||
}
|
||||
},
|
||||
"permissions": "Permissions",
|
||||
"newNote": "New note",
|
||||
"notesMarkdownSyntaxSupportHint": "Paperless Mobile can render notes using basic markdown syntax. Try it out!"
|
||||
}
|
||||
1048
lib/l10n/intl_ro.arb
Normal file
1048
lib/l10n/intl_ro.arb
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1024,5 +1024,25 @@
|
||||
"@testingConnection": {
|
||||
"description": "Text shown while the app tries to establish a connection to the specified host."
|
||||
},
|
||||
"version": "Version {versionCode}"
|
||||
"version": "Version {versionCode}",
|
||||
"notes": "{count, plural, zero{Notes} one{Note} other{Notes}}",
|
||||
"addNote": "Add note",
|
||||
"newerVersionAvailable": "Newer version available:",
|
||||
"dateOutOfRange": "Date must be between {firstDate} and {lastDate}.",
|
||||
"@dateOutOfRange": {
|
||||
"description": "Error message shown when the user tries to select a date outside of the allowed range.",
|
||||
"placeholders": {
|
||||
"firstDate": {
|
||||
"type": "DateTime",
|
||||
"format": "yMd"
|
||||
},
|
||||
"lastDate": {
|
||||
"type": "DateTime",
|
||||
"format": "yMd"
|
||||
}
|
||||
}
|
||||
},
|
||||
"permissions": "Permissions",
|
||||
"newNote": "New note",
|
||||
"notesMarkdownSyntaxSupportHint": "Paperless Mobile can render notes using basic markdown syntax. Try it out!"
|
||||
}
|
||||
@@ -1024,5 +1024,25 @@
|
||||
"@testingConnection": {
|
||||
"description": "Text shown while the app tries to establish a connection to the specified host."
|
||||
},
|
||||
"version": "Version {versionCode}"
|
||||
"version": "Version {versionCode}",
|
||||
"notes": "{count, plural, zero{Notes} one{Note} other{Notes}}",
|
||||
"addNote": "Add note",
|
||||
"newerVersionAvailable": "Newer version available:",
|
||||
"dateOutOfRange": "Date must be between {firstDate} and {lastDate}.",
|
||||
"@dateOutOfRange": {
|
||||
"description": "Error message shown when the user tries to select a date outside of the allowed range.",
|
||||
"placeholders": {
|
||||
"firstDate": {
|
||||
"type": "DateTime",
|
||||
"format": "yMd"
|
||||
},
|
||||
"lastDate": {
|
||||
"type": "DateTime",
|
||||
"format": "yMd"
|
||||
}
|
||||
}
|
||||
},
|
||||
"permissions": "Permissions",
|
||||
"newNote": "New note",
|
||||
"notesMarkdownSyntaxSupportHint": "Paperless Mobile can render notes using basic markdown syntax. Try it out!"
|
||||
}
|
||||
172
lib/main.dart
172
lib/main.dart
@@ -24,6 +24,8 @@ import 'package:paperless_mobile/constants.dart';
|
||||
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
|
||||
import 'package:paperless_mobile/core/bloc/my_bloc_observer.dart';
|
||||
import 'package:paperless_mobile/core/database/hive/hive_config.dart';
|
||||
import 'package:paperless_mobile/core/database/hive/hive_extensions.dart';
|
||||
import 'package:paperless_mobile/core/database/hive/hive_initialization.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/global_settings.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/local_user_app_state.dart';
|
||||
@@ -31,7 +33,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_impl.dart';
|
||||
import 'package:paperless_mobile/core/interceptor/language_header.interceptor.dart';
|
||||
import 'package:paperless_mobile/core/notifier/go_router_refresh_stream.dart';
|
||||
import 'package:paperless_mobile/core/security/session_manager_impl.dart';
|
||||
import 'package:paperless_mobile/features/logging/data/formatted_printer.dart';
|
||||
import 'package:paperless_mobile/features/logging/data/logger.dart';
|
||||
import 'package:paperless_mobile/features/logging/data/mirrored_file_output.dart';
|
||||
@@ -105,64 +107,36 @@ Future<void> performMigrations() async {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _initHive() async {
|
||||
await Hive.initFlutter();
|
||||
await performMigrations();
|
||||
registerHiveAdapters();
|
||||
await Hive.openBox<LocalUserAccount>(HiveBoxes.localUserAccount);
|
||||
await Hive.openBox<LocalUserAppState>(HiveBoxes.localUserAppState);
|
||||
await Hive.openBox<String>(HiveBoxes.hosts);
|
||||
final globalSettingsBox =
|
||||
await Hive.openBox<GlobalSettings>(HiveBoxes.globalSettings);
|
||||
Future<void> initializeDefaultParameters() async {
|
||||
Bloc.observer = MyBlocObserver();
|
||||
await FileService.instance.initialize();
|
||||
logger = l.Logger(
|
||||
output: MirroredFileOutput(),
|
||||
printer: FormattedPrinter(),
|
||||
level: l.Level.trace,
|
||||
filter: l.ProductionFilter(),
|
||||
);
|
||||
|
||||
if (!globalSettingsBox.hasValue) {
|
||||
await globalSettingsBox.setValue(
|
||||
GlobalSettings(preferredLocaleSubtag: defaultPreferredLocale.toString()),
|
||||
);
|
||||
packageInfo = await PackageInfo.fromPlatform();
|
||||
|
||||
if (Platform.isAndroid) {
|
||||
androidInfo = await DeviceInfoPlugin().androidInfo;
|
||||
}
|
||||
if (Platform.isIOS) {
|
||||
iosInfo = await DeviceInfoPlugin().iosInfo;
|
||||
}
|
||||
|
||||
await findSystemLocale();
|
||||
}
|
||||
|
||||
void main() async {
|
||||
runZonedGuarded(() async {
|
||||
Bloc.observer = MyBlocObserver();
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
await FileService.instance.initialize();
|
||||
|
||||
logger = l.Logger(
|
||||
output: MirroredFileOutput(),
|
||||
printer: FormattedPrinter(),
|
||||
level: l.Level.trace,
|
||||
filter: l.ProductionFilter(),
|
||||
);
|
||||
Paint.enableDithering = true;
|
||||
|
||||
// if (kDebugMode) {
|
||||
// // URL: http://localhost:3131
|
||||
// // Login: admin:test
|
||||
// await LocalMockApiServer(
|
||||
// // RandomDelayGenerator(
|
||||
// // const Duration(milliseconds: 100),
|
||||
// // const Duration(milliseconds: 800),
|
||||
// // ),
|
||||
// )
|
||||
// .start();
|
||||
// }
|
||||
|
||||
packageInfo = await PackageInfo.fromPlatform();
|
||||
|
||||
if (Platform.isAndroid) {
|
||||
androidInfo = await DeviceInfoPlugin().androidInfo;
|
||||
}
|
||||
if (Platform.isIOS) {
|
||||
iosInfo = await DeviceInfoPlugin().iosInfo;
|
||||
}
|
||||
await _initHive();
|
||||
final widgetsBinding = WidgetsFlutterBinding.ensureInitialized();
|
||||
final globalSettingsBox =
|
||||
Hive.box<GlobalSettings>(HiveBoxes.globalSettings);
|
||||
final globalSettings = globalSettingsBox.getValue()!;
|
||||
|
||||
await findSystemLocale();
|
||||
final hiveDirectory = await getApplicationDocumentsDirectory();
|
||||
final defaultLocale = defaultPreferredLocale.languageCode;
|
||||
await initializeDefaultParameters();
|
||||
await initHive(hiveDirectory, defaultLocale);
|
||||
await performMigrations();
|
||||
|
||||
final connectivityStatusService = ConnectivityStatusServiceImpl(
|
||||
Connectivity(),
|
||||
@@ -178,10 +152,10 @@ void main() async {
|
||||
FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding);
|
||||
|
||||
final languageHeaderInterceptor = LanguageHeaderInterceptor(
|
||||
globalSettings.preferredLocaleSubtag,
|
||||
() => Hive.globalSettingsBox.getValue()!.preferredLocaleSubtag,
|
||||
);
|
||||
// Manages security context, required for self signed client certificates
|
||||
final sessionManager = SessionManager([
|
||||
final SessionManager sessionManager = SessionManagerImpl([
|
||||
PrettyDioLogger(
|
||||
compact: true,
|
||||
responseBody: false,
|
||||
@@ -194,21 +168,9 @@ void main() async {
|
||||
languageHeaderInterceptor,
|
||||
]);
|
||||
|
||||
// Initialize Blocs/Cubits
|
||||
final connectivityCubit = ConnectivityCubit(connectivityStatusService);
|
||||
|
||||
// Load application settings and stored authentication data
|
||||
await connectivityCubit.initialize();
|
||||
|
||||
final localNotificationService = LocalNotificationService();
|
||||
await localNotificationService.initialize();
|
||||
|
||||
//Update language header in interceptor on language change.
|
||||
globalSettingsBox.listenable().addListener(() {
|
||||
languageHeaderInterceptor.preferredLocaleSubtag =
|
||||
globalSettings.preferredLocaleSubtag;
|
||||
});
|
||||
|
||||
final apiFactory = PaperlessApiFactoryImpl(sessionManager);
|
||||
final authenticationCubit = AuthenticationCubit(
|
||||
localAuthService,
|
||||
@@ -218,33 +180,19 @@ void main() async {
|
||||
localNotificationService,
|
||||
);
|
||||
runApp(
|
||||
MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider.value(value: sessionManager),
|
||||
Provider<LocalAuthenticationService>.value(value: localAuthService),
|
||||
Provider<ConnectivityStatusService>.value(
|
||||
value: connectivityStatusService),
|
||||
Provider<LocalNotificationService>.value(
|
||||
value: localNotificationService),
|
||||
Provider.value(value: DocumentChangedNotifier()),
|
||||
],
|
||||
child: MultiProvider(
|
||||
providers: [
|
||||
Provider<ConnectivityCubit>.value(value: connectivityCubit),
|
||||
Provider.value(value: authenticationCubit),
|
||||
],
|
||||
child: GoRouterShell(
|
||||
apiFactory: apiFactory,
|
||||
),
|
||||
),
|
||||
AppEntrypoint(
|
||||
sessionManager: sessionManager,
|
||||
apiFactory: apiFactory,
|
||||
authenticationCubit: authenticationCubit,
|
||||
connectivityStatusService: connectivityStatusService,
|
||||
localNotificationService: localNotificationService,
|
||||
localAuthService: localAuthService,
|
||||
),
|
||||
);
|
||||
}, (error, stackTrace) {
|
||||
if (error is StateError &&
|
||||
error.message.contains("Cannot emit new states")) {
|
||||
{
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Catches all unexpected/uncaught errors and prints them to the console.
|
||||
final message = switch (error) {
|
||||
@@ -261,9 +209,52 @@ void main() async {
|
||||
});
|
||||
}
|
||||
|
||||
class AppEntrypoint extends StatelessWidget {
|
||||
final PaperlessApiFactory apiFactory;
|
||||
final AuthenticationCubit authenticationCubit;
|
||||
final ConnectivityStatusService connectivityStatusService;
|
||||
final LocalNotificationService localNotificationService;
|
||||
final LocalAuthenticationService localAuthService;
|
||||
final SessionManager sessionManager;
|
||||
|
||||
const AppEntrypoint({
|
||||
super.key,
|
||||
required this.apiFactory,
|
||||
required this.authenticationCubit,
|
||||
required this.connectivityStatusService,
|
||||
required this.localNotificationService,
|
||||
required this.localAuthService,
|
||||
required this.sessionManager,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MultiProvider(
|
||||
providers: [
|
||||
Provider.value(value: DocumentChangedNotifier()),
|
||||
Provider.value(value: authenticationCubit),
|
||||
Provider.value(
|
||||
value: ConnectivityCubit(connectivityStatusService)..initialize(),
|
||||
),
|
||||
ChangeNotifierProvider.value(value: sessionManager),
|
||||
Provider.value(value: connectivityStatusService),
|
||||
Provider.value(value: localNotificationService),
|
||||
Provider.value(value: localAuthService),
|
||||
],
|
||||
child: GoRouterShell(
|
||||
apiFactory: apiFactory,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class GoRouterShell extends StatefulWidget {
|
||||
final PaperlessApiFactory apiFactory;
|
||||
const GoRouterShell({super.key, required this.apiFactory});
|
||||
|
||||
const GoRouterShell({
|
||||
super.key,
|
||||
required this.apiFactory,
|
||||
});
|
||||
|
||||
@override
|
||||
State<GoRouterShell> createState() => _GoRouterShellState();
|
||||
@@ -396,7 +387,7 @@ class _GoRouterShellState extends State<GoRouterShell> {
|
||||
dynamicScheme: darkDynamic,
|
||||
preferredColorScheme: settings.preferredColorSchemeOption,
|
||||
),
|
||||
themeMode: settings.preferredThemeMode,
|
||||
themeMode: settings.preferredThemeMode,
|
||||
supportedLocales: const [
|
||||
Locale('en'),
|
||||
Locale('de'),
|
||||
@@ -408,6 +399,7 @@ class _GoRouterShellState extends State<GoRouterShell> {
|
||||
Locale('pl'),
|
||||
Locale('ru'),
|
||||
Locale('tr'),
|
||||
Locale('it'),
|
||||
],
|
||||
localeResolutionCallback: (locale, supportedLocales) {
|
||||
if (locale == null) {
|
||||
|
||||
@@ -26,4 +26,5 @@ class R {
|
||||
static const loggingOut = "loggingOut";
|
||||
static const restoringSession = "restoringSession";
|
||||
static const addAccount = 'addAccount';
|
||||
static const addNote = 'addNote';
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import 'package:paperless_mobile/features/login/view/login_to_existing_account_p
|
||||
import 'package:paperless_mobile/features/login/view/verify_identity_page.dart';
|
||||
import 'package:paperless_mobile/features/login/view/widgets/login_transition_page.dart';
|
||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||
import 'package:paperless_mobile/keys.dart';
|
||||
import 'package:paperless_mobile/routing/navigation_keys.dart';
|
||||
import 'package:paperless_mobile/routing/routes.dart';
|
||||
part 'login_route.g.dart';
|
||||
@@ -108,6 +109,7 @@ class AuthenticatingRoute extends GoRouteData {
|
||||
};
|
||||
return NoTransitionPage(
|
||||
child: LoginTransitionPage(
|
||||
key: TestKeys.login.loggingInScreen,
|
||||
text: text,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -71,6 +71,7 @@ part 'authenticated_route.g.dart';
|
||||
TypedGoRoute<DocumentDetailsRoute>(
|
||||
path: "details/:id",
|
||||
name: R.documentDetails,
|
||||
routes: [],
|
||||
),
|
||||
TypedGoRoute<EditDocumentRoute>(
|
||||
path: "edit",
|
||||
|
||||
@@ -4,3 +4,4 @@ export 'src/models/models.dart';
|
||||
export 'src/modules/modules.dart';
|
||||
export 'src/converters/converters.dart';
|
||||
export 'config/hive/hive_type_ids.dart';
|
||||
export 'src/interceptor/dio_http_error_interceptor.dart';
|
||||
|
||||
@@ -125,8 +125,8 @@ class DocumentFilter extends Equatable {
|
||||
return queryParams;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() => toQueryParameters().toString();
|
||||
// @override
|
||||
// String toString() => toQueryParameters().toString();
|
||||
|
||||
DocumentFilter copyWith({
|
||||
int? pageSize,
|
||||
@@ -249,9 +249,4 @@ class DocumentFilter extends Equatable {
|
||||
moreLike,
|
||||
selectedView,
|
||||
];
|
||||
|
||||
// factory DocumentFilter.fromJson(Map<String, dynamic> json) =>
|
||||
// _$DocumentFilterFromJson(json);
|
||||
|
||||
// Map<String, dynamic> toJson() => _$DocumentFilterToJson(this);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// ignore_for_file: non_constant_identifier_names
|
||||
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_api/src/converters/local_date_time_json_converter.dart';
|
||||
import 'package:paperless_api/src/models/custom_field_model.dart';
|
||||
import 'package:paperless_api/src/models/note_model.dart';
|
||||
import 'package:paperless_api/src/models/search_hit.dart';
|
||||
|
||||
part 'document_model.g.dart';
|
||||
@@ -48,8 +48,9 @@ class DocumentModel extends Equatable {
|
||||
|
||||
final int? owner;
|
||||
final bool? userCanChange;
|
||||
final Iterable<NoteModel> notes;
|
||||
|
||||
// Only present if full_perms=true
|
||||
/// Only present if full_perms=true
|
||||
final Permissions? permissions;
|
||||
final Iterable<CustomFieldModel> customFields;
|
||||
|
||||
@@ -72,6 +73,7 @@ class DocumentModel extends Equatable {
|
||||
this.userCanChange,
|
||||
this.permissions,
|
||||
this.customFields = const [],
|
||||
this.notes = const [],
|
||||
});
|
||||
|
||||
factory DocumentModel.fromJson(Map<String, dynamic> json) =>
|
||||
@@ -94,6 +96,9 @@ class DocumentModel extends Equatable {
|
||||
String? archivedFileName,
|
||||
int? Function()? owner,
|
||||
bool? userCanChange,
|
||||
Iterable<NoteModel>? notes,
|
||||
Permissions? permissions,
|
||||
Iterable<CustomFieldModel>? customFields,
|
||||
}) {
|
||||
return DocumentModel(
|
||||
id: id,
|
||||
@@ -114,6 +119,9 @@ class DocumentModel extends Equatable {
|
||||
archivedFileName: archivedFileName ?? this.archivedFileName,
|
||||
owner: owner != null ? owner() : this.owner,
|
||||
userCanChange: userCanChange ?? this.userCanChange,
|
||||
customFields: customFields ?? this.customFields,
|
||||
notes: notes ?? this.notes,
|
||||
permissions: permissions ?? this.permissions,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -134,5 +142,8 @@ class DocumentModel extends Equatable {
|
||||
archivedFileName,
|
||||
owner,
|
||||
userCanChange,
|
||||
customFields,
|
||||
notes,
|
||||
permissions,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -82,7 +82,6 @@ class FilterRule with EquatableMixin {
|
||||
assert(filter.tags is IdsTagsQuery);
|
||||
return filter.copyWith(
|
||||
tags: switch (filter.tags) {
|
||||
// TODO: Handle this case.
|
||||
IdsTagsQuery(include: var i, exclude: var e) => IdsTagsQuery(
|
||||
include: [...i, int.parse(value!)],
|
||||
exclude: e,
|
||||
|
||||
@@ -28,3 +28,4 @@ export 'task/task.dart';
|
||||
export 'task/task_status.dart';
|
||||
export 'user_model.dart';
|
||||
export 'exception/exceptions.dart';
|
||||
export 'note_model.dart' show NoteModel;
|
||||
|
||||
29
packages/paperless_api/lib/src/models/note_model.dart
Normal file
29
packages/paperless_api/lib/src/models/note_model.dart
Normal file
@@ -0,0 +1,29 @@
|
||||
// ignore_for_file: invalid_annotation_target
|
||||
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
part 'note_model.freezed.dart';
|
||||
part 'note_model.g.dart';
|
||||
|
||||
@freezed
|
||||
class NoteModel with _$NoteModel {
|
||||
const factory NoteModel({
|
||||
required int? id,
|
||||
required String? note,
|
||||
required DateTime? created,
|
||||
required int? document,
|
||||
@JsonKey(fromJson: parseNoteUserFromJson) required int? user,
|
||||
}) = _NoteModel;
|
||||
|
||||
factory NoteModel.fromJson(Map<String, dynamic> json) =>
|
||||
_$NoteModelFromJson(json);
|
||||
}
|
||||
|
||||
int? parseNoteUserFromJson(dynamic json) {
|
||||
if (json == null) return null;
|
||||
if (json is Map) {
|
||||
return json['id'];
|
||||
} else if (json is int) {
|
||||
return json;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -11,7 +11,16 @@ class PaperlessApiException implements Exception {
|
||||
this.httpStatusCode,
|
||||
});
|
||||
|
||||
const PaperlessApiException.unknown() : this(ErrorCode.unknown);
|
||||
const PaperlessApiException.unknown({
|
||||
String? details,
|
||||
StackTrace? stackTrace,
|
||||
int? httpStatusCode,
|
||||
}) : this(
|
||||
ErrorCode.unknown,
|
||||
details: details,
|
||||
stackTrace: stackTrace,
|
||||
httpStatusCode: httpStatusCode,
|
||||
);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
@@ -71,5 +80,7 @@ enum ErrorCode {
|
||||
updateSavedViewError,
|
||||
customFieldCreateFailed,
|
||||
customFieldLoadFailed,
|
||||
customFieldDeleteFailed;
|
||||
customFieldDeleteFailed,
|
||||
deleteNoteFailed,
|
||||
addNoteFailed;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ class PaperlessServerInformationModel {
|
||||
static const String versionHeader = 'x-version';
|
||||
static const String apiVersionHeader = 'x-api-version';
|
||||
final String version;
|
||||
final String latestVersion;
|
||||
final int apiVersion;
|
||||
final bool isUpdateAvailable;
|
||||
|
||||
@@ -11,9 +12,11 @@ class PaperlessServerInformationModel {
|
||||
required this.version,
|
||||
required this.apiVersion,
|
||||
required this.isUpdateAvailable,
|
||||
required this.latestVersion,
|
||||
});
|
||||
|
||||
int compareToOtherVersion(String other) {
|
||||
return getExtendedVersionNumber(version).compareTo(getExtendedVersionNumber(other));
|
||||
return getExtendedVersionNumber(version)
|
||||
.compareTo(getExtendedVersionNumber(other));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import 'date_range_unit.dart';
|
||||
|
||||
part 'date_range_query.g.dart';
|
||||
|
||||
sealed class DateRangeQuery {
|
||||
sealed class DateRangeQuery with EquatableMixin {
|
||||
const DateRangeQuery();
|
||||
|
||||
Map<String, String> toQueryParameter(DateRangeQueryField field);
|
||||
@@ -28,10 +28,13 @@ class UnsetDateRangeQuery extends DateRangeQuery {
|
||||
|
||||
@override
|
||||
bool matches(DateTime dt) => true;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [];
|
||||
}
|
||||
|
||||
@HiveType(typeId: PaperlessApiHiveTypeIds.relativeDateRangeQuery)
|
||||
class RelativeDateRangeQuery extends DateRangeQuery with EquatableMixin {
|
||||
class RelativeDateRangeQuery extends DateRangeQuery {
|
||||
@HiveField(0)
|
||||
final int offset;
|
||||
@HiveField(1)
|
||||
@@ -84,7 +87,7 @@ class RelativeDateRangeQuery extends DateRangeQuery with EquatableMixin {
|
||||
|
||||
@JsonSerializable()
|
||||
@HiveType(typeId: PaperlessApiHiveTypeIds.absoluteDateRangeQuery)
|
||||
class AbsoluteDateRangeQuery extends DateRangeQuery with EquatableMixin {
|
||||
class AbsoluteDateRangeQuery extends DateRangeQuery {
|
||||
@LocalDateTimeJsonConverter()
|
||||
@HiveField(0)
|
||||
final DateTime? after;
|
||||
|
||||
@@ -4,7 +4,7 @@ import 'package:paperless_api/config/hive/hive_type_ids.dart';
|
||||
|
||||
part 'id_query_parameter.g.dart';
|
||||
|
||||
sealed class IdQueryParameter {
|
||||
sealed class IdQueryParameter with EquatableMixin {
|
||||
const IdQueryParameter();
|
||||
Map<String, String> toQueryParameter(String field);
|
||||
bool matches(int? id);
|
||||
@@ -23,6 +23,9 @@ class UnsetIdQueryParameter extends IdQueryParameter {
|
||||
|
||||
@override
|
||||
bool matches(int? id) => true;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [];
|
||||
}
|
||||
|
||||
// @HiveType(typeId: PaperlessApiHiveTypeIds.notAssignedIdQueryParameter)
|
||||
@@ -36,6 +39,8 @@ class NotAssignedIdQueryParameter extends IdQueryParameter {
|
||||
|
||||
@override
|
||||
bool matches(int? id) => id == null;
|
||||
@override
|
||||
List<Object?> get props => [];
|
||||
}
|
||||
|
||||
// @HiveType(typeId: PaperlessApiHiveTypeIds.anyAssignedIdQueryParameter)
|
||||
@@ -48,6 +53,8 @@ class AnyAssignedIdQueryParameter extends IdQueryParameter {
|
||||
|
||||
@override
|
||||
bool matches(int? id) => id != null;
|
||||
@override
|
||||
List<Object?> get props => [];
|
||||
}
|
||||
|
||||
@HiveType(typeId: PaperlessApiHiveTypeIds.setIdQueryParameter)
|
||||
|
||||
@@ -4,7 +4,7 @@ import 'package:paperless_api/config/hive/hive_type_ids.dart';
|
||||
|
||||
part 'tags_query.g.dart';
|
||||
|
||||
sealed class TagsQuery {
|
||||
sealed class TagsQuery with EquatableMixin {
|
||||
const TagsQuery();
|
||||
Map<String, String> toQueryParameter();
|
||||
bool matches(Iterable<int> ids);
|
||||
@@ -20,10 +20,13 @@ class NotAssignedTagsQuery extends TagsQuery {
|
||||
|
||||
@override
|
||||
bool matches(Iterable<int> ids) => ids.isEmpty;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [];
|
||||
}
|
||||
|
||||
@HiveType(typeId: PaperlessApiHiveTypeIds.anyAssignedTagsQuery)
|
||||
class AnyAssignedTagsQuery extends TagsQuery with EquatableMixin {
|
||||
class AnyAssignedTagsQuery extends TagsQuery {
|
||||
@HiveField(0)
|
||||
final List<int> tagIds;
|
||||
const AnyAssignedTagsQuery({
|
||||
@@ -54,7 +57,7 @@ class AnyAssignedTagsQuery extends TagsQuery with EquatableMixin {
|
||||
}
|
||||
|
||||
@HiveType(typeId: PaperlessApiHiveTypeIds.idsTagsQuery)
|
||||
class IdsTagsQuery extends TagsQuery with EquatableMixin {
|
||||
class IdsTagsQuery extends TagsQuery {
|
||||
@HiveField(0)
|
||||
final List<int> include;
|
||||
@HiveField(1)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:paperless_api/config/hive/hive_type_ids.dart';
|
||||
@@ -91,6 +92,11 @@ class TextQuery {
|
||||
return other.queryText == queryText && other.queryType == queryType;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return "TextQuery($queryText, $queryType)";
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(queryText, queryType);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
import 'package:paperless_api/src/models/exception/exceptions.dart';
|
||||
|
||||
abstract class PaperlessAuthenticationApi {
|
||||
///
|
||||
/// @throws [PaperlessUnauthorizedException]
|
||||
///
|
||||
Future<String> login({
|
||||
required String username,
|
||||
required String password,
|
||||
|
||||
@@ -37,6 +37,11 @@ class PaperlessAuthenticationApiImpl implements PaperlessAuthenticationApi {
|
||||
// return AuthenticationTemporaryRedirect(redirectUrl!);
|
||||
} on DioException catch (exception) {
|
||||
throw exception.unravel();
|
||||
} catch (error, stackTrace) {
|
||||
throw PaperlessApiException.unknown(
|
||||
details: error.toString(),
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ abstract class PaperlessDocumentsApi {
|
||||
Future<DocumentModel> find(int id);
|
||||
Future<int> delete(DocumentModel doc);
|
||||
Future<DocumentMetaData> getMetaData(int id);
|
||||
Future<DocumentModel> deleteNote(DocumentModel document, int noteId);
|
||||
Future<Iterable<int>> bulkAction(BulkAction action);
|
||||
Future<Uint8List> getPreview(int docId);
|
||||
String getThumbnailUrl(int docId);
|
||||
@@ -35,4 +36,7 @@ abstract class PaperlessDocumentsApi {
|
||||
Future<FieldSuggestions> findSuggestions(DocumentModel document);
|
||||
|
||||
Future<List<String>> autocomplete(String query, [int limit = 10]);
|
||||
|
||||
Future<DocumentModel> addNote(
|
||||
{required DocumentModel document, required String text});
|
||||
}
|
||||
|
||||
@@ -323,4 +323,45 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<DocumentModel> deleteNote(DocumentModel document, int noteId) async {
|
||||
try {
|
||||
final response = await client.delete(
|
||||
"/api/documents/${document.id}/notes/?id=$noteId",
|
||||
options: Options(validateStatus: (status) => status == 200),
|
||||
);
|
||||
final notes =
|
||||
(response.data as List).map((e) => NoteModel.fromJson(e)).toList();
|
||||
|
||||
return document.copyWith(notes: notes);
|
||||
} on DioException catch (exception) {
|
||||
throw exception.unravel(
|
||||
orElse: const PaperlessApiException(ErrorCode.deleteNoteFailed),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<DocumentModel> addNote({
|
||||
required DocumentModel document,
|
||||
required String text,
|
||||
}) async {
|
||||
try {
|
||||
final response = await client.post(
|
||||
"/api/documents/${document.id}/notes/",
|
||||
options: Options(validateStatus: (status) => status == 200),
|
||||
data: {'note': text},
|
||||
);
|
||||
|
||||
final notes =
|
||||
(response.data as List).map((e) => NoteModel.fromJson(e)).toList();
|
||||
|
||||
return document.copyWith(notes: notes);
|
||||
} on DioException catch (exception) {
|
||||
throw exception.unravel(
|
||||
orElse: const PaperlessApiException(ErrorCode.addNoteFailed),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,13 +24,15 @@ class PaperlessServerStatsApiImpl implements PaperlessServerStatsApi {
|
||||
"/api/remote_version/",
|
||||
options: Options(validateStatus: (status) => status == 200),
|
||||
);
|
||||
var version = response.data["version"] as String;
|
||||
if (version == _fallbackVersion) {
|
||||
version = response.headers.value('x-version') ?? _fallbackVersion;
|
||||
}
|
||||
final latestVersion = response.data["version"] as String;
|
||||
final version = response.headers
|
||||
.value(PaperlessServerInformationModel.versionHeader) ??
|
||||
_fallbackVersion;
|
||||
final updateAvailable = response.data["update_available"] as bool;
|
||||
return PaperlessServerInformationModel(
|
||||
apiVersion: int.parse(response.headers.value('x-api-version')!),
|
||||
apiVersion: int.parse(response.headers
|
||||
.value(PaperlessServerInformationModel.apiVersionHeader)!),
|
||||
latestVersion: latestVersion,
|
||||
version: version,
|
||||
isUpdateAvailable: updateAvailable,
|
||||
);
|
||||
|
||||
@@ -22,6 +22,8 @@ dependencies:
|
||||
jiffy: ^5.0.0
|
||||
freezed_annotation: ^2.4.1
|
||||
hive: ^2.2.3
|
||||
mockito: ^5.4.4
|
||||
http_mock_adapter: ^0.6.1
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:http_mock_adapter/http_mock_adapter.dart';
|
||||
import 'package:mockito/mockito.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
|
||||
void main() {
|
||||
group('AuthenticationApi with DioHttpErrorIncerceptor', () {
|
||||
late PaperlessAuthenticationApi authenticationApi;
|
||||
late DioAdapter mockAdapter;
|
||||
const token = "abcde";
|
||||
const invalidCredentialsServerMessage =
|
||||
"Unable to log in with provided credentials.";
|
||||
|
||||
setUp(() {
|
||||
final dio = Dio()..interceptors.add(DioHttpErrorInterceptor());
|
||||
authenticationApi = PaperlessAuthenticationApiImpl(dio);
|
||||
mockAdapter = DioAdapter(dio: dio);
|
||||
// Valid credentials
|
||||
mockAdapter.onPost(
|
||||
"/api/token/",
|
||||
data: {
|
||||
"username": "username",
|
||||
"password": "password",
|
||||
},
|
||||
(server) => server.reply(200, {"token": token}),
|
||||
);
|
||||
// Invalid credentials
|
||||
mockAdapter.onPost(
|
||||
"/api/token/",
|
||||
data: {
|
||||
"username": "wrongUsername",
|
||||
"password": "wrongPassword",
|
||||
},
|
||||
(server) => server.reply(400, {
|
||||
"non_field_errors": [invalidCredentialsServerMessage]
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
// tearDown(() {});
|
||||
test(
|
||||
'should return a valid token when logging in with valid credentials',
|
||||
() {
|
||||
expect(
|
||||
authenticationApi.login(
|
||||
username: "username",
|
||||
password: "password",
|
||||
),
|
||||
completion(token),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'should throw a PaperlessFormValidationException containing a reason '
|
||||
'when logging in with invalid credentials',
|
||||
() {
|
||||
expect(
|
||||
authenticationApi.login(
|
||||
username: "wrongUsername",
|
||||
password: "wrongPassword",
|
||||
),
|
||||
throwsA(isA<PaperlessFormValidationException>().having(
|
||||
(e) => e.unspecificErrorMessage(),
|
||||
"non-field specific error message",
|
||||
equals(invalidCredentialsServerMessage),
|
||||
)),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'should return an error when logging in with invalid credentials',
|
||||
() {
|
||||
expect(
|
||||
authenticationApi.login(
|
||||
username: "wrongUsername",
|
||||
password: "wrongPassword",
|
||||
),
|
||||
throwsA(isA<PaperlessFormValidationException>().having(
|
||||
(e) => e.unspecificErrorMessage(),
|
||||
"non-field specific error message",
|
||||
equals(invalidCredentialsServerMessage),
|
||||
)),
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
|
||||
void main() {
|
||||
group('Validate parsing logic from [SavedView] to [DocumentFilter]:', () {
|
||||
group('Parsing [SavedView] to [DocumentFilter]:', () {
|
||||
test('Values are correctly parsed if set.', () {
|
||||
expect(
|
||||
SavedView.fromJson({
|
||||
@@ -64,7 +64,7 @@ void main() {
|
||||
]
|
||||
}).toDocumentFilter(),
|
||||
equals(
|
||||
DocumentFilter.initial.copyWith(
|
||||
DocumentFilter(
|
||||
correspondent: const SetIdQueryParameter(id: 42),
|
||||
documentType: const SetIdQueryParameter(id: 69),
|
||||
storagePath: const SetIdQueryParameter(id: 14),
|
||||
@@ -83,6 +83,7 @@ void main() {
|
||||
sortField: SortField.created,
|
||||
sortOrder: SortOrder.descending,
|
||||
query: const TextQuery.extended("Never gonna give you up"),
|
||||
selectedView: 1,
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -99,7 +100,11 @@ void main() {
|
||||
"sort_reverse": true,
|
||||
"filter_rules": [],
|
||||
}).toDocumentFilter(),
|
||||
equals(DocumentFilter.initial),
|
||||
equals(
|
||||
const DocumentFilter(
|
||||
selectedView: 1,
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -130,11 +135,12 @@ void main() {
|
||||
},
|
||||
],
|
||||
}).toDocumentFilter();
|
||||
final expected = DocumentFilter.initial.copyWith(
|
||||
correspondent: const NotAssignedIdQueryParameter(),
|
||||
documentType: const NotAssignedIdQueryParameter(),
|
||||
storagePath: const NotAssignedIdQueryParameter(),
|
||||
tags: const NotAssignedTagsQuery(),
|
||||
const expected = DocumentFilter(
|
||||
correspondent: NotAssignedIdQueryParameter(),
|
||||
documentType: NotAssignedIdQueryParameter(),
|
||||
storagePath: NotAssignedIdQueryParameter(),
|
||||
tags: NotAssignedTagsQuery(),
|
||||
selectedView: 1,
|
||||
);
|
||||
expect(
|
||||
actual,
|
||||
@@ -148,6 +154,7 @@ void main() {
|
||||
expect(
|
||||
SavedView.fromDocumentFilter(
|
||||
DocumentFilter(
|
||||
selectedView: 1,
|
||||
correspondent: const SetIdQueryParameter(id: 1),
|
||||
documentType: const SetIdQueryParameter(id: 2),
|
||||
storagePath: const SetIdQueryParameter(id: 3),
|
||||
@@ -173,6 +180,7 @@ void main() {
|
||||
),
|
||||
equals(
|
||||
SavedView(
|
||||
id: 1,
|
||||
name: "test_name",
|
||||
showOnDashboard: false,
|
||||
showInSidebar: false,
|
||||
@@ -101,10 +101,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: collection
|
||||
sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687
|
||||
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.17.2"
|
||||
version: "1.18.0"
|
||||
colorfilter_generator:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -244,10 +244,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3"
|
||||
sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.9.1"
|
||||
version: "1.10.0"
|
||||
paperless_document_scanner:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -376,18 +376,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stack_trace
|
||||
sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5
|
||||
sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.11.0"
|
||||
version: "1.11.1"
|
||||
stream_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stream_channel
|
||||
sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8"
|
||||
sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
version: "2.1.2"
|
||||
stream_transform:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -416,10 +416,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8"
|
||||
sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.6.0"
|
||||
version: "0.6.1"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -440,10 +440,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web
|
||||
sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10
|
||||
sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.4-beta"
|
||||
version: "0.3.0"
|
||||
win32:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -469,5 +469,5 @@ packages:
|
||||
source: hosted
|
||||
version: "6.3.0"
|
||||
sdks:
|
||||
dart: ">=3.1.0 <4.0.0"
|
||||
dart: ">=3.2.0-194.0.dev <4.0.0"
|
||||
flutter: ">=3.13.0"
|
||||
|
||||
64
pubspec.lock
64
pubspec.lock
@@ -221,10 +221,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: collection
|
||||
sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687
|
||||
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.17.2"
|
||||
version: "1.18.0"
|
||||
color:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -848,6 +848,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
http_mock_adapter:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: http_mock_adapter
|
||||
sha256: "46399c78bd4a0af071978edd8c502d7aeeed73b5fb9860bca86b5ed647a63c1b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.6.1"
|
||||
http_multi_server:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1014,7 +1022,7 @@ packages:
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
markdown:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: markdown
|
||||
sha256: acf35edccc0463a9d7384e437c015a3535772e09714cf60e07eeef3a15870dcd
|
||||
@@ -1041,10 +1049,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3"
|
||||
sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.9.1"
|
||||
version: "1.10.0"
|
||||
mime:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1060,6 +1068,14 @@ packages:
|
||||
relative: true
|
||||
source: path
|
||||
version: "0.0.1"
|
||||
mockito:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: mockito
|
||||
sha256: "6841eed20a7befac0ce07df8116c8b8233ed1f4486a7647c7fc5a02ae6163917"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.4.4"
|
||||
mocktail:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1223,8 +1239,8 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "packages/pdfx"
|
||||
ref: HEAD
|
||||
resolved-ref: "11f7dee82b58ca4f483c753f06bbdc91b34a0793"
|
||||
ref: "4be9de9ffed5398fd7d5f44bbb07dcd3d3f1711b"
|
||||
resolved-ref: "4be9de9ffed5398fd7d5f44bbb07dcd3d3f1711b"
|
||||
url: "https://github.com/ScerIO/packages.flutter"
|
||||
source: git
|
||||
version: "2.5.0"
|
||||
@@ -1288,10 +1304,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: platform
|
||||
sha256: "4a451831508d7d6ca779f7ac6e212b4023dd5a7d08a27a63da33756410e32b76"
|
||||
sha256: ae68c7bfcd7383af3629daafb32fb4e8681c7154428da4febcff06200585f102
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.0"
|
||||
version: "3.1.2"
|
||||
plugin_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1637,18 +1653,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stack_trace
|
||||
sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5
|
||||
sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.11.0"
|
||||
version: "1.11.1"
|
||||
stream_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stream_channel
|
||||
sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8"
|
||||
sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
version: "2.1.2"
|
||||
stream_transform:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1693,26 +1709,26 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test
|
||||
sha256: "13b41f318e2a5751c3169137103b60c584297353d4b1761b66029bae6411fe46"
|
||||
sha256: a1f7595805820fcc05e5c52e3a231aedd0b72972cb333e8c738a8b1239448b6f
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.24.3"
|
||||
version: "1.24.9"
|
||||
test_api:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8"
|
||||
sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.6.0"
|
||||
version: "0.6.1"
|
||||
test_core:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_core
|
||||
sha256: "99806e9e6d95c7b059b7a0fc08f07fc53fabe54a829497f0d9676299f1e8637e"
|
||||
sha256: a757b14fc47507060a162cc2530d9a4a2f92f5100a952c7443b5cad5ef5b106a
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.5.3"
|
||||
version: "0.5.9"
|
||||
time:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1877,10 +1893,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vm_service
|
||||
sha256: c620a6f783fa22436da68e42db7ebbf18b8c44b9a46ab911f666ff09ffd9153f
|
||||
sha256: c538be99af830f478718b51630ec1b6bee5e74e52c8a802d328d9e71d35d2583
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "11.7.1"
|
||||
version: "11.10.0"
|
||||
watcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1893,10 +1909,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web
|
||||
sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10
|
||||
sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.4-beta"
|
||||
version: "0.3.0"
|
||||
web_socket_channel:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -1994,5 +2010,5 @@ packages:
|
||||
source: hosted
|
||||
version: "3.1.2"
|
||||
sdks:
|
||||
dart: ">=3.1.0 <4.0.0"
|
||||
dart: ">=3.2.0-194.0.dev <4.0.0"
|
||||
flutter: ">=3.13.0"
|
||||
|
||||
@@ -15,7 +15,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
||||
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
|
||||
# Read more about iOS versioning at
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
version: 3.1.8+403
|
||||
version: 3.2.0+404
|
||||
|
||||
environment:
|
||||
sdk: ">=3.1.0 <4.0.0"
|
||||
@@ -103,8 +103,10 @@ dependencies:
|
||||
# camerawesome: ^2.0.0-dev.1
|
||||
pdfx:
|
||||
git:
|
||||
url: "https://github.com/ScerIO/packages.flutter"
|
||||
url: 'https://github.com/ScerIO/packages.flutter'
|
||||
ref: '4be9de9ffed5398fd7d5f44bbb07dcd3d3f1711b'
|
||||
path: packages/pdfx
|
||||
markdown: ^7.1.1
|
||||
|
||||
dependency_overrides:
|
||||
intl: ^0.18.1
|
||||
|
||||
@@ -1,23 +1,20 @@
|
||||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
set -Euo pipefail
|
||||
|
||||
__script_dir=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )
|
||||
readonly __script_dir
|
||||
|
||||
pushd "$__script_dir/../"
|
||||
|
||||
pushd packages/paperless_api
|
||||
flutter packages pub get
|
||||
dart run build_runner build --delete-conflicting-outputs
|
||||
popd
|
||||
|
||||
pushd packages/mock_server
|
||||
flutter packages pub get
|
||||
popd
|
||||
for dir in packages/*/ # list directories in the form "/tmp/dirname/"
|
||||
do
|
||||
pushd $dir
|
||||
echo "Installing dependencies for $dir"
|
||||
flutter packages pub get
|
||||
dart run build_runner build --delete-conflicting-outputs
|
||||
popd
|
||||
done
|
||||
|
||||
flutter packages pub get
|
||||
flutter gen-l10n
|
||||
dart run build_runner build --delete-conflicting-outputs
|
||||
|
||||
popd
|
||||
|
||||
dart run build_runner build --delete-conflicting-outputs
|
||||
10
scripts/upload_translation_source.sh
Normal file
10
scripts/upload_translation_source.sh
Normal file
@@ -0,0 +1,10 @@
|
||||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
__script_dir=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )
|
||||
readonly __script_dir
|
||||
|
||||
cd "$__script_dir/../"
|
||||
echo "Uploading source translation file..."
|
||||
crowdin upload sources --identity=crowdin_credentials.yml --preserve-hierarchy
|
||||
flutter packages pub get
|
||||
Reference in New Issue
Block a user