mirror of
https://github.com/Xevion/paperless-mobile.git
synced 2025-12-11 08:08:00 -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:name="${applicationName}"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:requestLegacyExternalStorage="true"
|
android:requestLegacyExternalStorage="true"
|
||||||
>
|
>
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
@@ -135,6 +135,7 @@
|
|||||||
android:mimeType="application/vnd.openxmlformats-officedocument.presentationml.presentation" />
|
android:mimeType="application/vnd.openxmlformats-officedocument.presentationml.presentation" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
|
|
||||||
<!-- .xls -->
|
<!-- .xls -->
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.SEND" />
|
<action android:name="android.intent.action.SEND" />
|
||||||
@@ -162,11 +163,22 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
<!-- END Snippet from https://github.com/qcasey/paperless_share -->
|
<!-- END Snippet from https://github.com/qcasey/paperless_share -->
|
||||||
|
|
||||||
|
|
||||||
</activity>
|
</activity>
|
||||||
<!-- Don't delete the meta-data below. This is used by the Flutter tool to generate
|
<!-- Don't delete the meta-data below. This is used by the Flutter tool to generate
|
||||||
GeneratedPluginRegistrant.java -->
|
GeneratedPluginRegistrant.java -->
|
||||||
<meta-data android:name="flutterEmbedding" android:value="2" />
|
<meta-data android:name="flutterEmbedding" android:value="2" />
|
||||||
</application>
|
</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.CAMERA" />
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
<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 'dart:io';
|
||||||
// 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 '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 {
|
import 'src/mocks/mock_paperless_api.dart';
|
||||||
// final t = await initializeTestingFramework(languageCode: 'de');
|
|
||||||
|
|
||||||
// const testServerUrl = 'https://example.com';
|
class MockConnectivityStatusService extends Mock
|
||||||
// const testUsername = 'user';
|
implements ConnectivityStatusService {}
|
||||||
// const testPassword = 'pass';
|
|
||||||
|
|
||||||
// final serverAddressField = find.byKey(const ValueKey('login-server-address'));
|
class MockLocalAuthService extends Mock implements LocalAuthenticationService {}
|
||||||
// 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'));
|
|
||||||
|
|
||||||
// testWidgets('Test successful login flow', (WidgetTester tester) async {
|
class MockSessionManager extends Mock implements SessionManager {}
|
||||||
// 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"));
|
|
||||||
|
|
||||||
// await getIt<ConnectivityCubit>().initialize();
|
class MockLocalNotificationService extends Mock
|
||||||
// await getIt<ApplicationSettingsCubit>().initialize();
|
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;
|
final hiveDirectory = await getTemporaryDirectory();
|
||||||
// await tester.pumpAndSettle();
|
|
||||||
|
|
||||||
// await tester.enterText(serverAddressField, testServerUrl);
|
late ConnectivityStatusService connectivityStatusService;
|
||||||
// await tester.pumpAndSettle();
|
late MockPaperlessApiFactory paperlessApiFactory;
|
||||||
|
late AuthenticationCubit authenticationCubit;
|
||||||
|
late LocalNotificationService localNotificationService;
|
||||||
|
late SessionManager sessionManager;
|
||||||
|
final localAuthService = MockLocalAuthService();
|
||||||
|
|
||||||
// await tester.enterText(usernameField, testUsername);
|
setUp(() async {
|
||||||
// await tester.pumpAndSettle();
|
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 initializeDefaultParameters();
|
||||||
// await tester.pumpAndSettle();
|
|
||||||
|
|
||||||
// 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(
|
await tester.enterText(
|
||||||
// username: testUsername,
|
find.byKey(TestKeys.login.serverAddressFormField),
|
||||||
// password: testPassword,
|
testServerUrl,
|
||||||
// )).called(1);
|
);
|
||||||
// });
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
// testWidgets('Test login validation missing password',
|
await tester.press(find.byKey(TestKeys.login.continueButton));
|
||||||
// (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())
|
await tester.pumpAndSettle();
|
||||||
// .thenAnswer((realInvocation) async => ApplicationSettingsState(
|
expect(
|
||||||
// preferredLocaleSubtag: 'en',
|
find.byKey(TestKeys.login.usernameFormField),
|
||||||
// preferredThemeMode: ThemeMode.light,
|
findsOneWidget,
|
||||||
// isLocalAuthenticationEnabled: false,
|
);
|
||||||
// preferredViewType: ViewType.list,
|
|
||||||
// showInboxOnStartup: false,
|
|
||||||
// ));
|
|
||||||
|
|
||||||
// await getIt<ConnectivityCubit>().initialize();
|
await tester.enterText(
|
||||||
// await getIt<ApplicationSettingsCubit>().initialize();
|
find.byKey(TestKeys.login.usernameFormField),
|
||||||
// });
|
testUsername,
|
||||||
// // Mocked classes
|
);
|
||||||
|
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;
|
expect(
|
||||||
// await tester.pumpAndSettle();
|
find.byKey(TestKeys.login.loggingInScreen),
|
||||||
|
findsOneWidget,
|
||||||
// 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,
|
|
||||||
// );
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
|
|||||||
@@ -1,7 +1,16 @@
|
|||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:integration_test/integration_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/generated/l10n/app_localizations.dart';
|
||||||
|
import 'package:paperless_mobile/main.dart'
|
||||||
|
show initializeDefaultParameters, AppEntrypoint;
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
|
||||||
Future<TestingFrameworkVariables> initializeTestingFramework(
|
Future<TestingFrameworkVariables> initializeTestingFramework(
|
||||||
{String languageCode = 'en'}) async {
|
{String languageCode = 'en'}) async {
|
||||||
@@ -26,11 +35,3 @@ class TestingFrameworkVariables {
|
|||||||
required this.translations,
|
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 PackageInfo packageInfo;
|
||||||
late final AndroidDeviceInfo? androidInfo;
|
late final AndroidDeviceInfo? androidInfo;
|
||||||
late final IosDeviceInfo? iosInfo;
|
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 localUserAccount = 'localUserAccount';
|
||||||
static const localUserAppState = 'localUserAppState';
|
static const localUserAppState = 'localUserAppState';
|
||||||
static const hosts = 'hosts';
|
static const hosts = 'hosts';
|
||||||
|
static const hintStateBox = 'hintStateBox';
|
||||||
|
|
||||||
static List<String> get all => [
|
static List<String> get all => [
|
||||||
globalSettings,
|
globalSettings,
|
||||||
localUserCredentials,
|
localUserCredentials,
|
||||||
localUserAccount,
|
localUserAccount,
|
||||||
localUserAppState,
|
localUserAppState,
|
||||||
|
hintStateBox,
|
||||||
hosts,
|
hosts,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,4 +54,5 @@ extension HiveBoxAccessors on HiveInterface {
|
|||||||
box<LocalUserAppState>(HiveBoxes.localUserAppState);
|
box<LocalUserAppState>(HiveBoxes.localUserAppState);
|
||||||
Box<GlobalSettings> get globalSettingsBox =>
|
Box<GlobalSettings> get globalSettingsBox =>
|
||||||
box<GlobalSettings>(HiveBoxes.globalSettings);
|
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({
|
Widget paddedSymmetrically({
|
||||||
double horizontal = 0.0,
|
double horizontal = 0.0,
|
||||||
double vertical = 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(
|
return Padding(
|
||||||
padding: EdgeInsets.symmetric(horizontal: horizontal, vertical: vertical),
|
padding: insets,
|
||||||
child: this,
|
child: this,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
|
|
||||||
class LanguageHeaderInterceptor extends Interceptor {
|
class LanguageHeaderInterceptor extends Interceptor {
|
||||||
String preferredLocaleSubtag;
|
final String Function() preferredLocaleSubtagBuilder;
|
||||||
LanguageHeaderInterceptor(this.preferredLocaleSubtag);
|
LanguageHeaderInterceptor(this.preferredLocaleSubtagBuilder);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
|
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
|
||||||
late String languages;
|
late String languages;
|
||||||
if (preferredLocaleSubtag == "en") {
|
if (preferredLocaleSubtagBuilder() == "en") {
|
||||||
languages = "en";
|
languages = "en";
|
||||||
} else {
|
} 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});
|
options.headers.addAll({"Accept-Language": languages});
|
||||||
handler.next(options);
|
handler.next(options);
|
||||||
|
|||||||
@@ -9,4 +9,9 @@ class InfoMessageException implements Exception {
|
|||||||
this.message,
|
this.message,
|
||||||
this.stackTrace,
|
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/dio.dart';
|
||||||
import 'package:dio/io.dart';
|
|
||||||
import 'package:flutter/material.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: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
|
abstract interface class SessionManager implements ChangeNotifier {
|
||||||
/// an underlying [Dio] client which is injected into all services
|
Dio get client;
|
||||||
/// 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
void updateSettings({
|
void updateSettings({
|
||||||
String? baseUrl,
|
String? baseUrl,
|
||||||
String? authToken,
|
String? authToken,
|
||||||
ClientCertificate? clientCertificate,
|
ClientCertificate? clientCertificate,
|
||||||
}) {
|
});
|
||||||
if (clientCertificate != null) {
|
void resetSettings();
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
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/global/os_error_codes.dart';
|
||||||
import 'package:paperless_mobile/core/interceptor/server_reachability_error_interceptor.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.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/client_certificate.dart';
|
||||||
import 'package:paperless_mobile/features/login/model/reachability_status.dart';
|
import 'package:paperless_mobile/features/login/model/reachability_status.dart';
|
||||||
import 'package:rxdart/subjects.dart';
|
import 'package:rxdart/subjects.dart';
|
||||||
@@ -79,7 +80,7 @@ class ConnectivityStatusServiceImpl implements ConnectivityStatusService {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
SessionManager manager =
|
SessionManager manager =
|
||||||
SessionManager([ServerReachabilityErrorInterceptor()])
|
SessionManagerImpl([ServerReachabilityErrorInterceptor()])
|
||||||
..updateSettings(clientCertificate: clientCertificate)
|
..updateSettings(clientCertificate: clientCertificate)
|
||||||
..client.options.connectTimeout = const Duration(seconds: 5)
|
..client.options.connectTimeout = const Duration(seconds: 5)
|
||||||
..client.options.receiveTimeout = 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
|
'Could not load custom field.', //TODO: INTL
|
||||||
ErrorCode.customFieldDeleteFailed =>
|
ErrorCode.customFieldDeleteFailed =>
|
||||||
'Could not delete custom field, please try again.', //TODO: INTL
|
'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 'dart:collection';
|
||||||
|
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:flutter/cupertino.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||||
|
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:paperless_mobile/core/extensions/flutter_extensions.dart';
|
import 'package:paperless_mobile/core/extensions/flutter_extensions.dart';
|
||||||
import 'package:paperless_mobile/features/landing/view/widgets/mime_types_pie_chart.dart';
|
import 'package:paperless_mobile/features/landing/view/widgets/mime_types_pie_chart.dart';
|
||||||
@@ -83,7 +85,6 @@ class _FormBuilderLocalizedDatePickerState
|
|||||||
|
|
||||||
final _textFieldControls =
|
final _textFieldControls =
|
||||||
LinkedList<_NeighbourAwareDateInputSegmentControls>();
|
LinkedList<_NeighbourAwareDateInputSegmentControls>();
|
||||||
String? _error;
|
|
||||||
bool _temporarilyDisableListeners = false;
|
bool _temporarilyDisableListeners = false;
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -184,10 +185,7 @@ class _FormBuilderLocalizedDatePickerState
|
|||||||
// Imitate the functionality of the validator function in "normal" form fields.
|
// 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.
|
// The error is shown on the outer decorator as if this was a regular text input.
|
||||||
// Errors are cleared after the next user interaction.
|
// Errors are cleared after the next user interaction.
|
||||||
final error = _validateDate(value);
|
// final error = _validateDate(value);
|
||||||
setState(() {
|
|
||||||
_error = error;
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||||
initialValue: widget.initialValue != null
|
initialValue: widget.initialValue != null
|
||||||
@@ -201,7 +199,7 @@ class _FormBuilderLocalizedDatePickerState
|
|||||||
child: InputDecorator(
|
child: InputDecorator(
|
||||||
textAlignVertical: TextAlignVertical.bottom,
|
textAlignVertical: TextAlignVertical.bottom,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
errorText: _error,
|
errorText: field.errorText,
|
||||||
labelText: widget.labelText,
|
labelText: widget.labelText,
|
||||||
suffixIcon: Row(
|
suffixIcon: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
@@ -271,16 +269,10 @@ class _FormBuilderLocalizedDatePickerState
|
|||||||
if (d.day != date.day && d.month != date.month && d.year != date.year) {
|
if (d.day != date.day && d.month != date.month && d.year != date.year) {
|
||||||
return "Invalid date.";
|
return "Invalid date.";
|
||||||
}
|
}
|
||||||
if (d.isBefore(widget.firstDate)) {
|
if (d.isBefore(widget.firstDate) || d.isAfter(widget.lastDate)) {
|
||||||
final formattedDateHint =
|
return S.of(context)!.dateOutOfRange(widget.firstDate, widget.lastDate);
|
||||||
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.";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -332,6 +324,7 @@ class _FormBuilderLocalizedDatePickerState
|
|||||||
_DateInputSegment.year => fieldValue.copyWith(year: number),
|
_DateInputSegment.year => fieldValue.copyWith(year: number),
|
||||||
};
|
};
|
||||||
field.setValue(newValue);
|
field.setValue(newValue);
|
||||||
|
field.validate();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
inputFormatters: [
|
inputFormatters: [
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ class HintCard extends StatelessWidget {
|
|||||||
const Padding(padding: EdgeInsets.only(bottom: 24)),
|
const Padding(padding: EdgeInsets.only(bottom: 24)),
|
||||||
],
|
],
|
||||||
).padded(),
|
).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 = {
|
const _versionNumbers = {
|
||||||
|
"4043": "3.2.0",
|
||||||
"4033": "3.1.8",
|
"4033": "3.1.8",
|
||||||
"4023": "3.1.7",
|
"4023": "3.1.7",
|
||||||
"4013": "3.1.6",
|
"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(
|
Future<void> assignAsn(
|
||||||
DocumentModel document, {
|
DocumentModel document, {
|
||||||
int? asn,
|
int? asn,
|
||||||
@@ -270,4 +311,17 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
|
|||||||
_notifier.removeListener(this);
|
_notifier.removeListener(this);
|
||||||
await super.close();
|
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_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_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_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_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_permissions_widget.dart';
|
||||||
import 'package:paperless_mobile/features/document_details/view/widgets/document_share_button.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());
|
debugPrint(disableAnimations.toString());
|
||||||
final hasMultiUserSupport =
|
final hasMultiUserSupport =
|
||||||
context.watch<LocalUserAccount>().hasMultiUserSupport;
|
context.watch<LocalUserAccount>().hasMultiUserSupport;
|
||||||
final tabLength = 4 + (hasMultiUserSupport ? 1 : 0);
|
final tabLength = 5 + (hasMultiUserSupport ? 1 : 0);
|
||||||
return AnnotatedRegion(
|
return AnnotatedRegion(
|
||||||
value: buildOverlayStyle(
|
value: buildOverlayStyle(
|
||||||
Theme.of(context),
|
Theme.of(context),
|
||||||
@@ -160,6 +161,7 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
|||||||
bottom: ColoredTabBar(
|
bottom: ColoredTabBar(
|
||||||
tabBar: TabBar(
|
tabBar: TabBar(
|
||||||
isScrollable: true,
|
isScrollable: true,
|
||||||
|
tabAlignment: TabAlignment.start,
|
||||||
tabs: [
|
tabs: [
|
||||||
Tab(
|
Tab(
|
||||||
child: Text(
|
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)
|
if (hasMultiUserSupport)
|
||||||
Tab(
|
Tab(
|
||||||
child: Text(
|
child: Text(
|
||||||
"Permissions",
|
S.of(context)!.permissions,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Theme.of(context)
|
color: Theme.of(context)
|
||||||
.colorScheme
|
.colorScheme
|
||||||
@@ -229,67 +255,103 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
|||||||
context.read(),
|
context.read(),
|
||||||
documentId: widget.id,
|
documentId: widget.id,
|
||||||
),
|
),
|
||||||
child: Padding(
|
child: TabBarView(
|
||||||
padding: const EdgeInsets.symmetric(
|
children: [
|
||||||
vertical: 16,
|
CustomScrollView(
|
||||||
horizontal: 16,
|
slivers: [
|
||||||
),
|
SliverOverlapInjector(
|
||||||
child: TabBarView(
|
handle: NestedScrollView
|
||||||
children: [
|
.sliverOverlapAbsorberHandleFor(context),
|
||||||
CustomScrollView(
|
),
|
||||||
slivers: [
|
switch (state.status) {
|
||||||
SliverOverlapInjector(
|
LoadingStatus.loaded => DocumentOverviewWidget(
|
||||||
handle: NestedScrollView
|
document: state.document!,
|
||||||
.sliverOverlapAbsorberHandleFor(context),
|
itemSpacing: _itemSpacing,
|
||||||
),
|
queryString:
|
||||||
switch (state.status) {
|
widget.titleAndContentQueryString,
|
||||||
LoadingStatus.loaded =>
|
).paddedSymmetrically(
|
||||||
DocumentOverviewWidget(
|
vertical: 16,
|
||||||
document: state.document!,
|
sliver: true,
|
||||||
itemSpacing: _itemSpacing,
|
),
|
||||||
queryString:
|
LoadingStatus.error => _buildErrorState(),
|
||||||
widget.titleAndContentQueryString,
|
_ => _buildLoadingState(),
|
||||||
),
|
},
|
||||||
LoadingStatus.error => _buildErrorState(),
|
],
|
||||||
_ => _buildLoadingState(),
|
),
|
||||||
},
|
CustomScrollView(
|
||||||
],
|
slivers: [
|
||||||
),
|
SliverOverlapInjector(
|
||||||
CustomScrollView(
|
handle: NestedScrollView
|
||||||
slivers: [
|
.sliverOverlapAbsorberHandleFor(context),
|
||||||
SliverOverlapInjector(
|
),
|
||||||
handle: NestedScrollView
|
switch (state.status) {
|
||||||
.sliverOverlapAbsorberHandleFor(context),
|
LoadingStatus.loaded => DocumentContentWidget(
|
||||||
),
|
document: state.document!,
|
||||||
switch (state.status) {
|
queryString:
|
||||||
LoadingStatus.loaded => DocumentContentWidget(
|
widget.titleAndContentQueryString,
|
||||||
document: state.document!,
|
).paddedSymmetrically(
|
||||||
queryString:
|
vertical: 16,
|
||||||
widget.titleAndContentQueryString,
|
sliver: true,
|
||||||
),
|
),
|
||||||
LoadingStatus.error => _buildErrorState(),
|
LoadingStatus.error => _buildErrorState(),
|
||||||
_ => _buildLoadingState(),
|
_ => _buildLoadingState(),
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
CustomScrollView(
|
CustomScrollView(
|
||||||
slivers: [
|
slivers: [
|
||||||
SliverOverlapInjector(
|
SliverOverlapInjector(
|
||||||
handle: NestedScrollView
|
handle: NestedScrollView
|
||||||
.sliverOverlapAbsorberHandleFor(context),
|
.sliverOverlapAbsorberHandleFor(context),
|
||||||
),
|
),
|
||||||
switch (state.status) {
|
switch (state.status) {
|
||||||
LoadingStatus.loaded =>
|
LoadingStatus.loaded => DocumentMetaDataWidget(
|
||||||
DocumentMetaDataWidget(
|
document: state.document!,
|
||||||
document: state.document!,
|
itemSpacing: _itemSpacing,
|
||||||
itemSpacing: _itemSpacing,
|
metaData: state.metaData!,
|
||||||
metaData: state.metaData!,
|
).paddedSymmetrically(
|
||||||
),
|
vertical: 16,
|
||||||
LoadingStatus.error => _buildErrorState(),
|
sliver: true,
|
||||||
_ => _buildLoadingState(),
|
),
|
||||||
},
|
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(
|
CustomScrollView(
|
||||||
controller: _pagingScrollController,
|
controller: _pagingScrollController,
|
||||||
slivers: [
|
slivers: [
|
||||||
@@ -297,33 +359,27 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
|||||||
handle: NestedScrollView
|
handle: NestedScrollView
|
||||||
.sliverOverlapAbsorberHandleFor(context),
|
.sliverOverlapAbsorberHandleFor(context),
|
||||||
),
|
),
|
||||||
SimilarDocumentsView(
|
switch (state.status) {
|
||||||
pagingScrollController:
|
LoadingStatus.loaded =>
|
||||||
_pagingScrollController,
|
DocumentPermissionsWidget(
|
||||||
),
|
document: state.document!,
|
||||||
|
).paddedSymmetrically(
|
||||||
|
vertical: 16,
|
||||||
|
sliver: true,
|
||||||
|
),
|
||||||
|
LoadingStatus.error => _buildErrorState(),
|
||||||
|
_ => _buildLoadingState(),
|
||||||
|
}
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
if (hasMultiUserSupport)
|
]
|
||||||
CustomScrollView(
|
.map(
|
||||||
controller: _pagingScrollController,
|
(child) => Padding(
|
||||||
slivers: [
|
padding: EdgeInsets.symmetric(horizontal: 16),
|
||||||
SliverOverlapInjector(
|
child: child,
|
||||||
handle: NestedScrollView
|
|
||||||
.sliverOverlapAbsorberHandleFor(
|
|
||||||
context),
|
|
||||||
),
|
|
||||||
switch (state.status) {
|
|
||||||
LoadingStatus.loaded =>
|
|
||||||
DocumentPermissionsWidget(
|
|
||||||
document: state.document!,
|
|
||||||
),
|
|
||||||
LoadingStatus.error => _buildErrorState(),
|
|
||||||
_ => _buildLoadingState(),
|
|
||||||
}
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
],
|
)
|
||||||
),
|
.toList(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -25,54 +25,51 @@ class DocumentMetaDataWidget extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final currentUser = context.watch<LocalUserAccount>().paperlessUser;
|
final currentUser = context.watch<LocalUserAccount>().paperlessUser;
|
||||||
|
|
||||||
return SliverList(
|
return SliverList.list(
|
||||||
delegate: SliverChildListDelegate(
|
children: [
|
||||||
[
|
if (currentUser.canEditDocuments)
|
||||||
if (currentUser.canEditDocuments)
|
ArchiveSerialNumberField(
|
||||||
ArchiveSerialNumberField(
|
document: document,
|
||||||
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),
|
).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(
|
DetailsItem.text(
|
||||||
DateFormat.yMMMMd(Localizations.localeOf(context).toString())
|
document.originalFileName!,
|
||||||
.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,
|
|
||||||
context: context,
|
context: context,
|
||||||
label: S.of(context)!.originalMD5Checksum,
|
label: S.of(context)!.originalMD5Checksum,
|
||||||
).paddedOnly(bottom: itemSpacing),
|
).paddedOnly(bottom: itemSpacing),
|
||||||
DetailsItem.text(
|
DetailsItem.text(
|
||||||
formatBytes(metaData.originalSize, 2),
|
metaData.originalChecksum,
|
||||||
context: context,
|
context: context,
|
||||||
label: S.of(context)!.originalFileSize,
|
label: S.of(context)!.originalMD5Checksum,
|
||||||
).paddedOnly(bottom: itemSpacing),
|
).paddedOnly(bottom: itemSpacing),
|
||||||
DetailsItem.text(
|
DetailsItem.text(
|
||||||
metaData.originalMimeType,
|
formatBytes(metaData.originalSize, 2),
|
||||||
context: context,
|
context: context,
|
||||||
label: S.of(context)!.originalMIMEType,
|
label: S.of(context)!.originalFileSize,
|
||||||
).paddedOnly(bottom: itemSpacing),
|
).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,
|
initialValue: initialCreatedAtDate,
|
||||||
labelText: S.of(context)!.createdAt,
|
labelText: S.of(context)!.createdAt,
|
||||||
firstDate: DateTime(1970, 1, 1),
|
firstDate: DateTime(1970, 1, 1),
|
||||||
lastDate: DateTime.now(),
|
lastDate: DateTime(2100, 1, 1),
|
||||||
locale: Localizations.localeOf(context),
|
locale: Localizations.localeOf(context),
|
||||||
prefixIcon: Icon(Icons.calendar_today),
|
prefixIcon: Icon(Icons.calendar_today),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -54,7 +54,9 @@ class DocumentScannerCubit extends Cubit<DocumentScannerState> {
|
|||||||
|
|
||||||
Future<void> removeScan(File file) async {
|
Future<void> removeScan(File file) async {
|
||||||
try {
|
try {
|
||||||
await file.delete();
|
if (await file.exists()) {
|
||||||
|
await file.delete();
|
||||||
|
}
|
||||||
} catch (error, stackTrace) {
|
} catch (error, stackTrace) {
|
||||||
throw InfoMessageException(
|
throw InfoMessageException(
|
||||||
code: ErrorCode.scanRemoveFailed,
|
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/hive/hive_config.dart';
|
||||||
import 'package:paperless_mobile/core/database/tables/global_settings.dart';
|
import 'package:paperless_mobile/core/database/tables/global_settings.dart';
|
||||||
import 'package:paperless_mobile/core/global/constants.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/core/service/file_service.dart';
|
||||||
import 'package:paperless_mobile/features/app_drawer/view/app_drawer.dart';
|
import 'package:paperless_mobile/features/app_drawer/view/app_drawer.dart';
|
||||||
import 'package:paperless_mobile/features/document_scan/cubit/document_scanner_cubit.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]);
|
.removeScan(scans[index]);
|
||||||
} on PaperlessApiException catch (error, stackTrace) {
|
} on PaperlessApiException catch (error, stackTrace) {
|
||||||
showErrorMessage(context, error, stackTrace);
|
showErrorMessage(context, error, stackTrace);
|
||||||
|
} on InfoMessageException catch (error, stackTrace) {
|
||||||
|
showInfoMessage(context, error, stackTrace);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
index: index,
|
index: index,
|
||||||
|
|||||||
@@ -222,7 +222,7 @@ class _DocumentUploadPreparationPageState
|
|||||||
FormBuilderLocalizedDatePicker(
|
FormBuilderLocalizedDatePicker(
|
||||||
name: DocumentModel.createdKey,
|
name: DocumentModel.createdKey,
|
||||||
firstDate: DateTime(1970, 1, 1),
|
firstDate: DateTime(1970, 1, 1),
|
||||||
lastDate: DateTime.now(),
|
lastDate: DateTime(2100, 1, 1),
|
||||||
locale: Localizations.localeOf(context),
|
locale: Localizations.localeOf(context),
|
||||||
labelText: S.of(context)!.createdAt + " *",
|
labelText: S.of(context)!.createdAt + " *",
|
||||||
allowUnset: true,
|
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/selection/view_type_selection_widget.dart';
|
||||||
import 'package:paperless_mobile/features/documents/view/widgets/sort_documents_button.dart';
|
import 'package:paperless_mobile/features/documents/view/widgets/sort_documents_button.dart';
|
||||||
import 'package:paperless_mobile/features/labels/cubit/label_cubit.dart';
|
import 'package:paperless_mobile/features/labels/cubit/label_cubit.dart';
|
||||||
|
import 'package:paperless_mobile/features/logging/data/logger.dart';
|
||||||
import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart';
|
import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart';
|
||||||
import 'package:paperless_mobile/features/tasks/model/pending_tasks_notifier.dart';
|
import 'package:paperless_mobile/features/tasks/model/pending_tasks_notifier.dart';
|
||||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
@@ -308,8 +309,18 @@ class _DocumentsPageState extends State<DocumentsPage> {
|
|||||||
// Listen for scroll notifications to load new data.
|
// Listen for scroll notifications to load new data.
|
||||||
// Scroll controller does not work here due to nestedscrollview limitations.
|
// Scroll controller does not work here due to nestedscrollview limitations.
|
||||||
final offset = notification.metrics.pixels;
|
final offset = notification.metrics.pixels;
|
||||||
if (offset > 128 && _savedViewsExpansionController.isExpanded) {
|
try {
|
||||||
_savedViewsExpansionController.collapse();
|
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;
|
final max = notification.metrics.maxScrollExtent;
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import 'package:flutter/widgets.dart';
|
|||||||
import 'package:hive_flutter/adapters.dart';
|
import 'package:hive_flutter/adapters.dart';
|
||||||
import 'package:hydrated_bloc/hydrated_bloc.dart';
|
import 'package:hydrated_bloc/hydrated_bloc.dart';
|
||||||
import 'package:paperless_api/paperless_api.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_config.dart';
|
||||||
import 'package:paperless_mobile/core/database/hive/hive_extensions.dart';
|
import 'package:paperless_mobile/core/database/hive/hive_extensions.dart';
|
||||||
import 'package:paperless_mobile/core/database/tables/global_settings.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/database/tables/user_credentials.dart';
|
||||||
import 'package:paperless_mobile/core/factory/paperless_api_factory.dart';
|
import 'package:paperless_mobile/core/factory/paperless_api_factory.dart';
|
||||||
import 'package:paperless_mobile/core/interceptor/language_header.interceptor.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/data/logger.dart';
|
||||||
import 'package:paperless_mobile/features/logging/utils/redaction_utils.dart';
|
import 'package:paperless_mobile/features/logging/utils/redaction_utils.dart';
|
||||||
import 'package:paperless_mobile/core/model/info_message_exception.dart';
|
import 'package:paperless_mobile/core/model/info_message_exception.dart';
|
||||||
@@ -83,7 +86,7 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
|||||||
AuthenticatingStage.persistingLocalUserData));
|
AuthenticatingStage.persistingLocalUserData));
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} on PaperlessApiException catch (exception, stackTrace) {
|
||||||
emit(
|
emit(
|
||||||
AuthenticationErrorState(
|
AuthenticationErrorState(
|
||||||
serverUrl: serverUrl,
|
serverUrl: serverUrl,
|
||||||
@@ -207,8 +210,8 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
|||||||
methodName: 'switchAccount',
|
methodName: 'switchAccount',
|
||||||
);
|
);
|
||||||
|
|
||||||
final sessionManager = SessionManager([
|
final SessionManager sessionManager = SessionManagerImpl([
|
||||||
LanguageHeaderInterceptor(locale),
|
LanguageHeaderInterceptor(() => locale),
|
||||||
]);
|
]);
|
||||||
await _addUser(
|
await _addUser(
|
||||||
localUserId,
|
localUserId,
|
||||||
@@ -462,14 +465,12 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
|||||||
|
|
||||||
final authApi = _apiFactory.createAuthenticationApi(sessionManager.client);
|
final authApi = _apiFactory.createAuthenticationApi(sessionManager.client);
|
||||||
|
|
||||||
|
await onPerformLogin?.call();
|
||||||
logger.fd(
|
logger.fd(
|
||||||
"Fetching bearer token from the server...",
|
"Fetching bearer token from the server...",
|
||||||
className: runtimeType.toString(),
|
className: runtimeType.toString(),
|
||||||
methodName: '_addUser',
|
methodName: '_addUser',
|
||||||
);
|
);
|
||||||
|
|
||||||
await onPerformLogin?.call();
|
|
||||||
|
|
||||||
final token = await authApi.login(
|
final token = await authApi.login(
|
||||||
username: credentials.username!,
|
username: credentials.username!,
|
||||||
password: credentials.password!,
|
password: credentials.password!,
|
||||||
@@ -486,7 +487,6 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
|||||||
clientCertificate: clientCert,
|
clientCertificate: clientCert,
|
||||||
authToken: token,
|
authToken: token,
|
||||||
);
|
);
|
||||||
|
|
||||||
final userAccountBox =
|
final userAccountBox =
|
||||||
Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount);
|
Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount);
|
||||||
final userStateBox =
|
final userStateBox =
|
||||||
@@ -586,12 +586,14 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
|||||||
clientCertificate: clientCert,
|
clientCertificate: clientCert,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
logger.fd(
|
logger.fd(
|
||||||
"User credentials successfully saved.",
|
"User credentials successfully saved.",
|
||||||
className: runtimeType.toString(),
|
className: runtimeType.toString(),
|
||||||
methodName: '_addUser',
|
methodName: '_addUser',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
final hostsBox = Hive.box<String>(HiveBoxes.hosts);
|
final hostsBox = Hive.box<String>(HiveBoxes.hosts);
|
||||||
if (!hostsBox.values.contains(serverUrl)) {
|
if (!hostsBox.values.contains(serverUrl)) {
|
||||||
await hostsBox.add(serverUrl);
|
await hostsBox.add(serverUrl);
|
||||||
@@ -618,12 +620,19 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
|||||||
try {
|
try {
|
||||||
final response = await dio.get(
|
final response = await dio.get(
|
||||||
"/api/",
|
"/api/",
|
||||||
options: Options(
|
options: Options(sendTimeout: timeout),
|
||||||
sendTimeout: timeout,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
final apiVersion =
|
int apiVersion =
|
||||||
int.parse(response.headers.value('x-api-version') ?? "3");
|
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(
|
logger.fd(
|
||||||
"Successfully retrieved API version ($apiVersion).",
|
"Successfully retrieved API version ($apiVersion).",
|
||||||
className: runtimeType.toString(),
|
className: runtimeType.toString(),
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/cupertino.dart';
|
|
||||||
import 'package:flutter/gestures.dart';
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_animate/flutter_animate.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/login_form_credentials.dart';
|
||||||
import 'package:paperless_mobile/features/login/model/reachability_status.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/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/server_address_form_field.dart';
|
||||||
import 'package:paperless_mobile/features/login/view/widgets/form_fields/user_credentials_form_field.dart';
|
import 'package:paperless_mobile/features/login/view/widgets/form_fields/user_credentials_form_field.dart';
|
||||||
import 'package:paperless_mobile/generated/assets.gen.dart';
|
import 'package:paperless_mobile/generated/assets.gen.dart';
|
||||||
@@ -44,6 +42,7 @@ class AddAccountPage extends StatefulWidget {
|
|||||||
final bool showLocalAccounts;
|
final bool showLocalAccounts;
|
||||||
|
|
||||||
final Widget? bottomLeftButton;
|
final Widget? bottomLeftButton;
|
||||||
|
|
||||||
const AddAccountPage({
|
const AddAccountPage({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.onSubmit,
|
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/core/database/hive/hive_config.dart';
|
||||||
|
|
||||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
|
import 'package:paperless_mobile/keys.dart';
|
||||||
|
|
||||||
class ServerAddressFormField extends StatefulWidget {
|
class ServerAddressFormField extends StatefulWidget {
|
||||||
static const String fkServerAddress = "serverAddress";
|
static const String fkServerAddress = "serverAddress";
|
||||||
@@ -59,7 +60,7 @@ class _ServerAddressFormFieldState extends State<ServerAddressFormField>
|
|||||||
maxWidth: MediaQuery.sizeOf(context).width - 40,
|
maxWidth: MediaQuery.sizeOf(context).width - 40,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
key: const ValueKey('login-server-address'),
|
key: TestKeys.login.serverAddressFormField,
|
||||||
optionsBuilder: (textEditingValue) {
|
optionsBuilder: (textEditingValue) {
|
||||||
return Hive.box<String>(HiveBoxes.hosts)
|
return Hive.box<String>(HiveBoxes.hosts)
|
||||||
.values
|
.values
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import 'package:flutter/cupertino.dart';
|
||||||
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
import 'package:paperless_mobile/features/settings/view/widgets/app_logs_tile.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/features/settings/view/widgets/user_settings_builder.dart';
|
||||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
|
|
||||||
class SettingsPage extends StatelessWidget {
|
class SettingsPage extends StatelessWidget {
|
||||||
const SettingsPage({super.key});
|
const SettingsPage({super.key});
|
||||||
@@ -80,15 +83,49 @@ class SettingsPage extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
final serverData = snapshot.data!;
|
final serverData = snapshot.data!;
|
||||||
return Text(
|
return Column(
|
||||||
S.of(context)!.paperlessServerVersion +
|
mainAxisSize: MainAxisSize.min,
|
||||||
' ' +
|
children: [
|
||||||
serverData.version.toString() +
|
Text(
|
||||||
' (API v${serverData.apiVersion})',
|
S.of(context)!.paperlessServerVersion +
|
||||||
style: Theme.of(context).textTheme.labelSmall?.copyWith(
|
' ' +
|
||||||
color: Theme.of(context).colorScheme.secondary,
|
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),
|
'pl': LanguageOption('Polska', true),
|
||||||
'ca': LanguageOption('Català', true),
|
'ca': LanguageOption('Català', true),
|
||||||
'ru': LanguageOption('Русский', true),
|
'ru': LanguageOption('Русский', true),
|
||||||
|
'it': LanguageOption('Italiano', true),
|
||||||
};
|
};
|
||||||
|
|
||||||
@override
|
@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": {
|
"@testingConnection": {
|
||||||
"description": "Text shown while the app tries to establish a connection to the specified host."
|
"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": {
|
"@testingConnection": {
|
||||||
"description": "Text shown while the app tries to establish a connection to the specified host."
|
"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": {
|
"@testingConnection": {
|
||||||
"description": "Text shown while the app tries to establish a connection to the specified host."
|
"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": {
|
"@testingConnection": {
|
||||||
"description": "Text shown while the app tries to establish a connection to the specified host."
|
"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",
|
"misc": "Otros",
|
||||||
"loggingOut": "Cerrando sesión...",
|
"loggingOut": "Cerrando sesión...",
|
||||||
"testingConnection": "Testing connection...",
|
"testingConnection": "Probando conexión...",
|
||||||
"@testingConnection": {
|
"@testingConnection": {
|
||||||
"description": "Text shown while the app tries to establish a connection to the specified host."
|
"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": {
|
"@confirmAction": {
|
||||||
"description": "Typically used as a title to confirm a previously selected action"
|
"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": "{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": {
|
"@bulkEditTagsAddMessage": {
|
||||||
"description": "Message of the confirmation dialog when bulk adding tags."
|
"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."
|
"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!}}",
|
"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.}}",
|
"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.}}",
|
"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.}}",
|
"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": {
|
"@defaultDownloadFileType": {
|
||||||
"description": "Label indicating the default filetype to download (one of archived, original and always ask)"
|
"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": {
|
"@defaultShareFileType": {
|
||||||
"description": "Label indicating the default filetype to share (one of archived, original and always ask)"
|
"description": "Label indicating the default filetype to share (one of archived, original and always ask)"
|
||||||
},
|
},
|
||||||
@@ -861,7 +861,7 @@
|
|||||||
"@loginRequiredPermissionsHint": {
|
"@loginRequiredPermissionsHint": {
|
||||||
"description": "Hint shown on the login page informing the user of the required permissions to use the app."
|
"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": {
|
"@missingPermissions": {
|
||||||
"description": "Message shown in a snackbar when a user without the reequired permissions performs an action."
|
"description": "Message shown in a snackbar when a user without the reequired permissions performs an action."
|
||||||
},
|
},
|
||||||
@@ -873,156 +873,176 @@
|
|||||||
"@donate": {
|
"@donate": {
|
||||||
"description": "Label of the in-app donate button"
|
"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": {
|
"@donationDialogContent": {
|
||||||
"description": "Text displayed in the donation dialog"
|
"description": "Text displayed in the donation dialog"
|
||||||
},
|
},
|
||||||
"noDocumentsFound": "No documents found.",
|
"noDocumentsFound": "Aucun document trouvé.",
|
||||||
"@noDocumentsFound": {
|
"@noDocumentsFound": {
|
||||||
"description": "Message shown when no documents were found."
|
"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": {
|
"@couldNotDeleteCorrespondent": {
|
||||||
"description": "Message shown in snackbar when a correspondent could not be deleted."
|
"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": {
|
"@couldNotDeleteDocumentType": {
|
||||||
"description": "Message shown when a document type could not be deleted"
|
"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": {
|
"@couldNotDeleteTag": {
|
||||||
"description": "Message shown when a tag could not be deleted"
|
"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": {
|
"@couldNotDeleteStoragePath": {
|
||||||
"description": "Message shown when a storage path could not be deleted"
|
"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": {
|
"@couldNotUpdateCorrespondent": {
|
||||||
"description": "Message shown when a correspondent could not be updated"
|
"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": {
|
"@couldNotUpdateDocumentType": {
|
||||||
"description": "Message shown when a document type could not be updated"
|
"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": {
|
"@couldNotUpdateTag": {
|
||||||
"description": "Message shown when a tag could not be updated"
|
"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": {
|
"@couldNotLoadServerInformation": {
|
||||||
"description": "Message shown when the server information could not be loaded"
|
"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": {
|
"@couldNotLoadStatistics": {
|
||||||
"description": "Message shown when the server statistics could not be loaded"
|
"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": {
|
"@couldNotLoadUISettings": {
|
||||||
"description": "Message shown when the UI settings could not be loaded"
|
"description": "Message shown when the UI settings could not be loaded"
|
||||||
},
|
},
|
||||||
"couldNotLoadTasks": "Could not load tasks.",
|
"couldNotLoadTasks": "Impossible de charger les tâches.",
|
||||||
"@couldNotLoadTasks": {
|
"@couldNotLoadTasks": {
|
||||||
"description": "Message shown when the tasks (e.g. document consumed) could not be loaded"
|
"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": {
|
"@userNotFound": {
|
||||||
"description": "Message shown when the specified user (e.g. by id) could not be found"
|
"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": {
|
"@couldNotUpdateSavedView": {
|
||||||
"description": "Message shown when a saved view could not be updated"
|
"description": "Message shown when a saved view could not be updated"
|
||||||
},
|
},
|
||||||
"couldNotUpdateStoragePath": "Could not update storage path, please try again.",
|
"couldNotUpdateStoragePath": "Impossible de mettre à jour le chemin de stockage, veuillez réessayer.",
|
||||||
"savedViewSuccessfullyUpdated": "Saved view successfully updated.",
|
"savedViewSuccessfullyUpdated": "Vue enregistrée mise à jour avec succès.",
|
||||||
"@savedViewSuccessfullyUpdated": {
|
"@savedViewSuccessfullyUpdated": {
|
||||||
"description": "Message shown when a saved view was successfully updated."
|
"description": "Message shown when a saved view was successfully updated."
|
||||||
},
|
},
|
||||||
"discardChanges": "Discard changes?",
|
"discardChanges": "Annuler les modifications ?",
|
||||||
"@discardChanges": {
|
"@discardChanges": {
|
||||||
"description": "Title of the alert dialog shown when a user tries to close a view with unsaved changes."
|
"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": {
|
"@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."
|
"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": {
|
"@createFromCurrentFilter": {
|
||||||
"description": "Tooltip of the \"New saved view\" button"
|
"description": "Tooltip of the \"New saved view\" button"
|
||||||
},
|
},
|
||||||
"home": "Home",
|
"home": "Accueil",
|
||||||
"@home": {
|
"@home": {
|
||||||
"description": "Label of the \"Home\" route"
|
"description": "Label of the \"Home\" route"
|
||||||
},
|
},
|
||||||
"welcomeUser": "Welcome, {name}!",
|
"welcomeUser": "Bienvenue {name} !",
|
||||||
"@welcomeUser": {
|
"@welcomeUser": {
|
||||||
"description": "Top message shown on the home page"
|
"description": "Top message shown on the home page"
|
||||||
},
|
},
|
||||||
"statistics": "Statistics",
|
"statistics": "Statistiques",
|
||||||
"documentsInInbox": "Documents in inbox",
|
"documentsInInbox": "Documents dans la boîte de réception",
|
||||||
"totalDocuments": "Total documents",
|
"totalDocuments": "Nombre total de documents",
|
||||||
"totalCharacters": "Total characters",
|
"totalCharacters": "Nombre total de caractères",
|
||||||
"showAll": "Show all",
|
"showAll": "Tout afficher",
|
||||||
"@showAll": {
|
"@showAll": {
|
||||||
"description": "Button label shown on a saved view preview to open this view in the documents page"
|
"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": {
|
"@userAlreadyExists": {
|
||||||
"description": "Error message shown when the user tries to add an already existing account."
|
"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": {
|
"@youDidNotSaveAnyViewsYet": {
|
||||||
"description": "Message shown when there are no saved views yet."
|
"description": "Message shown when there are no saved views yet."
|
||||||
},
|
},
|
||||||
"tryAgain": "Try again",
|
"tryAgain": "Veuillez réessayer",
|
||||||
"discardFile": "Discard file?",
|
"discardFile": "Abandonner le fichier ?",
|
||||||
"discard": "Discard",
|
"discard": "Abandonner",
|
||||||
"backToLogin": "Back to login",
|
"backToLogin": "Retour à la page de connexion",
|
||||||
"skipEditingReceivedFiles": "Skip editing received files",
|
"skipEditingReceivedFiles": "Passer l'édition des fichiers reçus",
|
||||||
"uploadWithoutPromptingUploadForm": "Always upload without prompting the upload form when sharing files with the app.",
|
"uploadWithoutPromptingUploadForm": "Toujours mettre en ligne sans montrer le formulaire de mise en ligne lors du partage de fichiers avec l'application.",
|
||||||
"authenticatingDots": "Authenticating...",
|
"authenticatingDots": "Authentification en cours...",
|
||||||
"@authenticatingDots": {
|
"@authenticatingDots": {
|
||||||
"description": "Message shown when the app is authenticating the user"
|
"description": "Message shown when the app is authenticating the user"
|
||||||
},
|
},
|
||||||
"persistingUserInformation": "Persisting user information...",
|
"persistingUserInformation": "Sauvegarde des informations utilisateur...",
|
||||||
"fetchingUserInformation": "Fetching user information...",
|
"fetchingUserInformation": "Récupération des informations utilisateur...",
|
||||||
"@fetchingUserInformation": {
|
"@fetchingUserInformation": {
|
||||||
"description": "Message shown when the app loads user data from the server"
|
"description": "Message shown when the app loads user data from the server"
|
||||||
},
|
},
|
||||||
"restoringSession": "Restoring session...",
|
"restoringSession": "Restauration de la session...",
|
||||||
"@restoringSession": {
|
"@restoringSession": {
|
||||||
"description": "Message shown when the user opens the app and the previous user is tried to be authenticated and logged in"
|
"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": {
|
"@documentsAssigned": {
|
||||||
"description": "Text shown with a correspondent, document type etc. to indicate the number of documents this filter will maximally yield."
|
"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": {
|
"@discardChangesWarning": {
|
||||||
"description": "Warning message shown when the user tries to close a route without saving the changes."
|
"description": "Warning message shown when the user tries to close a route without saving the changes."
|
||||||
},
|
},
|
||||||
"changelog": "Changelog",
|
"changelog": "Notes de version",
|
||||||
"noLogsFoundOn": "No logs found on {date}.",
|
"noLogsFoundOn": "Aucun journal trouvé sur {date}.",
|
||||||
"logfileBottomReached": "You have reached the bottom of this logfile.",
|
"logfileBottomReached": "Vous avez atteint le bas de ce fichier journal.",
|
||||||
"appLogs": "App logs {date}",
|
"appLogs": "Journaux d'application {date}",
|
||||||
"saveLogsToFile": "Save logs to file",
|
"saveLogsToFile": "Enregistrer le fichier journal",
|
||||||
"copyToClipboard": "Copy to clipboard",
|
"copyToClipboard": "Copier dans le presse-papier",
|
||||||
"couldNotLoadLogfileFrom": "Could not load logfile from {date}.",
|
"couldNotLoadLogfileFrom": "Impossible de charger le fichier journal depuis {date}.",
|
||||||
"loadingLogsFrom": "Loading logs from {date}...",
|
"loadingLogsFrom": "Chargement des journaux depuis {date}...",
|
||||||
"clearLogs": "Clear logs from {date}",
|
"clearLogs": "Effacer les journaux de {date}",
|
||||||
"showPdf": "Show PDF",
|
"showPdf": "Afficher le PDF",
|
||||||
"@showPdf": {
|
"@showPdf": {
|
||||||
"description": "Tooltip shown on the \"show pdf\" button on the document edit page"
|
"description": "Tooltip shown on the \"show pdf\" button on the document edit page"
|
||||||
},
|
},
|
||||||
"hidePdf": "Hide PDF",
|
"hidePdf": "Masquer le PDF",
|
||||||
"@hidePdf": {
|
"@hidePdf": {
|
||||||
"description": "Tooltip shown on the \"show pdf\" icon button on the document edit page"
|
"description": "Tooltip shown on the \"show pdf\" icon button on the document edit page"
|
||||||
},
|
},
|
||||||
"misc": "Sonstige",
|
"misc": "Sonstige",
|
||||||
"loggingOut": "Logging out...",
|
"loggingOut": "Déconnexion...",
|
||||||
"testingConnection": "Testing connection...",
|
"testingConnection": "Vérifier la connexion...",
|
||||||
"@testingConnection": {
|
"@testingConnection": {
|
||||||
"description": "Text shown while the app tries to establish a connection to the specified host."
|
"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": {
|
"@testingConnection": {
|
||||||
"description": "Text shown while the app tries to establish a connection to the specified host."
|
"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": {
|
"@testingConnection": {
|
||||||
"description": "Text shown while the app tries to establish a connection to the specified host."
|
"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": {
|
"@testingConnection": {
|
||||||
"description": "Text shown while the app tries to establish a connection to the specified host."
|
"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": {
|
"@testingConnection": {
|
||||||
"description": "Text shown while the app tries to establish a connection to the specified host."
|
"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/connectivity_cubit.dart';
|
||||||
import 'package:paperless_mobile/core/bloc/my_bloc_observer.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_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/global_settings.dart';
|
||||||
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
|
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
|
||||||
import 'package:paperless_mobile/core/database/tables/local_user_app_state.dart';
|
import 'package:paperless_mobile/core/database/tables/local_user_app_state.dart';
|
||||||
@@ -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.dart';
|
||||||
import 'package:paperless_mobile/core/factory/paperless_api_factory_impl.dart';
|
import 'package:paperless_mobile/core/factory/paperless_api_factory_impl.dart';
|
||||||
import 'package:paperless_mobile/core/interceptor/language_header.interceptor.dart';
|
import 'package:paperless_mobile/core/interceptor/language_header.interceptor.dart';
|
||||||
import 'package:paperless_mobile/core/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/formatted_printer.dart';
|
||||||
import 'package:paperless_mobile/features/logging/data/logger.dart';
|
import 'package:paperless_mobile/features/logging/data/logger.dart';
|
||||||
import 'package:paperless_mobile/features/logging/data/mirrored_file_output.dart';
|
import 'package:paperless_mobile/features/logging/data/mirrored_file_output.dart';
|
||||||
@@ -105,64 +107,36 @@ Future<void> performMigrations() async {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _initHive() async {
|
Future<void> initializeDefaultParameters() async {
|
||||||
await Hive.initFlutter();
|
Bloc.observer = MyBlocObserver();
|
||||||
await performMigrations();
|
await FileService.instance.initialize();
|
||||||
registerHiveAdapters();
|
logger = l.Logger(
|
||||||
await Hive.openBox<LocalUserAccount>(HiveBoxes.localUserAccount);
|
output: MirroredFileOutput(),
|
||||||
await Hive.openBox<LocalUserAppState>(HiveBoxes.localUserAppState);
|
printer: FormattedPrinter(),
|
||||||
await Hive.openBox<String>(HiveBoxes.hosts);
|
level: l.Level.trace,
|
||||||
final globalSettingsBox =
|
filter: l.ProductionFilter(),
|
||||||
await Hive.openBox<GlobalSettings>(HiveBoxes.globalSettings);
|
);
|
||||||
|
|
||||||
if (!globalSettingsBox.hasValue) {
|
packageInfo = await PackageInfo.fromPlatform();
|
||||||
await globalSettingsBox.setValue(
|
|
||||||
GlobalSettings(preferredLocaleSubtag: defaultPreferredLocale.toString()),
|
if (Platform.isAndroid) {
|
||||||
);
|
androidInfo = await DeviceInfoPlugin().androidInfo;
|
||||||
}
|
}
|
||||||
|
if (Platform.isIOS) {
|
||||||
|
iosInfo = await DeviceInfoPlugin().iosInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
await findSystemLocale();
|
||||||
}
|
}
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
runZonedGuarded(() 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 widgetsBinding = WidgetsFlutterBinding.ensureInitialized();
|
||||||
final globalSettingsBox =
|
final hiveDirectory = await getApplicationDocumentsDirectory();
|
||||||
Hive.box<GlobalSettings>(HiveBoxes.globalSettings);
|
final defaultLocale = defaultPreferredLocale.languageCode;
|
||||||
final globalSettings = globalSettingsBox.getValue()!;
|
await initializeDefaultParameters();
|
||||||
|
await initHive(hiveDirectory, defaultLocale);
|
||||||
await findSystemLocale();
|
await performMigrations();
|
||||||
|
|
||||||
final connectivityStatusService = ConnectivityStatusServiceImpl(
|
final connectivityStatusService = ConnectivityStatusServiceImpl(
|
||||||
Connectivity(),
|
Connectivity(),
|
||||||
@@ -178,10 +152,10 @@ void main() async {
|
|||||||
FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding);
|
FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding);
|
||||||
|
|
||||||
final languageHeaderInterceptor = LanguageHeaderInterceptor(
|
final languageHeaderInterceptor = LanguageHeaderInterceptor(
|
||||||
globalSettings.preferredLocaleSubtag,
|
() => Hive.globalSettingsBox.getValue()!.preferredLocaleSubtag,
|
||||||
);
|
);
|
||||||
// Manages security context, required for self signed client certificates
|
// Manages security context, required for self signed client certificates
|
||||||
final sessionManager = SessionManager([
|
final SessionManager sessionManager = SessionManagerImpl([
|
||||||
PrettyDioLogger(
|
PrettyDioLogger(
|
||||||
compact: true,
|
compact: true,
|
||||||
responseBody: false,
|
responseBody: false,
|
||||||
@@ -194,21 +168,9 @@ void main() async {
|
|||||||
languageHeaderInterceptor,
|
languageHeaderInterceptor,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Initialize Blocs/Cubits
|
|
||||||
final connectivityCubit = ConnectivityCubit(connectivityStatusService);
|
|
||||||
|
|
||||||
// Load application settings and stored authentication data
|
|
||||||
await connectivityCubit.initialize();
|
|
||||||
|
|
||||||
final localNotificationService = LocalNotificationService();
|
final localNotificationService = LocalNotificationService();
|
||||||
await localNotificationService.initialize();
|
await localNotificationService.initialize();
|
||||||
|
|
||||||
//Update language header in interceptor on language change.
|
|
||||||
globalSettingsBox.listenable().addListener(() {
|
|
||||||
languageHeaderInterceptor.preferredLocaleSubtag =
|
|
||||||
globalSettings.preferredLocaleSubtag;
|
|
||||||
});
|
|
||||||
|
|
||||||
final apiFactory = PaperlessApiFactoryImpl(sessionManager);
|
final apiFactory = PaperlessApiFactoryImpl(sessionManager);
|
||||||
final authenticationCubit = AuthenticationCubit(
|
final authenticationCubit = AuthenticationCubit(
|
||||||
localAuthService,
|
localAuthService,
|
||||||
@@ -218,33 +180,19 @@ void main() async {
|
|||||||
localNotificationService,
|
localNotificationService,
|
||||||
);
|
);
|
||||||
runApp(
|
runApp(
|
||||||
MultiProvider(
|
AppEntrypoint(
|
||||||
providers: [
|
sessionManager: sessionManager,
|
||||||
ChangeNotifierProvider.value(value: sessionManager),
|
apiFactory: apiFactory,
|
||||||
Provider<LocalAuthenticationService>.value(value: localAuthService),
|
authenticationCubit: authenticationCubit,
|
||||||
Provider<ConnectivityStatusService>.value(
|
connectivityStatusService: connectivityStatusService,
|
||||||
value: connectivityStatusService),
|
localNotificationService: localNotificationService,
|
||||||
Provider<LocalNotificationService>.value(
|
localAuthService: localAuthService,
|
||||||
value: localNotificationService),
|
|
||||||
Provider.value(value: DocumentChangedNotifier()),
|
|
||||||
],
|
|
||||||
child: MultiProvider(
|
|
||||||
providers: [
|
|
||||||
Provider<ConnectivityCubit>.value(value: connectivityCubit),
|
|
||||||
Provider.value(value: authenticationCubit),
|
|
||||||
],
|
|
||||||
child: GoRouterShell(
|
|
||||||
apiFactory: apiFactory,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}, (error, stackTrace) {
|
}, (error, stackTrace) {
|
||||||
if (error is StateError &&
|
if (error is StateError &&
|
||||||
error.message.contains("Cannot emit new states")) {
|
error.message.contains("Cannot emit new states")) {
|
||||||
{
|
return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// Catches all unexpected/uncaught errors and prints them to the console.
|
// Catches all unexpected/uncaught errors and prints them to the console.
|
||||||
final message = switch (error) {
|
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 {
|
class GoRouterShell extends StatefulWidget {
|
||||||
final PaperlessApiFactory apiFactory;
|
final PaperlessApiFactory apiFactory;
|
||||||
const GoRouterShell({super.key, required this.apiFactory});
|
|
||||||
|
const GoRouterShell({
|
||||||
|
super.key,
|
||||||
|
required this.apiFactory,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<GoRouterShell> createState() => _GoRouterShellState();
|
State<GoRouterShell> createState() => _GoRouterShellState();
|
||||||
@@ -396,7 +387,7 @@ class _GoRouterShellState extends State<GoRouterShell> {
|
|||||||
dynamicScheme: darkDynamic,
|
dynamicScheme: darkDynamic,
|
||||||
preferredColorScheme: settings.preferredColorSchemeOption,
|
preferredColorScheme: settings.preferredColorSchemeOption,
|
||||||
),
|
),
|
||||||
themeMode: settings.preferredThemeMode,
|
themeMode: settings.preferredThemeMode,
|
||||||
supportedLocales: const [
|
supportedLocales: const [
|
||||||
Locale('en'),
|
Locale('en'),
|
||||||
Locale('de'),
|
Locale('de'),
|
||||||
@@ -408,6 +399,7 @@ class _GoRouterShellState extends State<GoRouterShell> {
|
|||||||
Locale('pl'),
|
Locale('pl'),
|
||||||
Locale('ru'),
|
Locale('ru'),
|
||||||
Locale('tr'),
|
Locale('tr'),
|
||||||
|
Locale('it'),
|
||||||
],
|
],
|
||||||
localeResolutionCallback: (locale, supportedLocales) {
|
localeResolutionCallback: (locale, supportedLocales) {
|
||||||
if (locale == null) {
|
if (locale == null) {
|
||||||
|
|||||||
@@ -26,4 +26,5 @@ class R {
|
|||||||
static const loggingOut = "loggingOut";
|
static const loggingOut = "loggingOut";
|
||||||
static const restoringSession = "restoringSession";
|
static const restoringSession = "restoringSession";
|
||||||
static const addAccount = 'addAccount';
|
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/verify_identity_page.dart';
|
||||||
import 'package:paperless_mobile/features/login/view/widgets/login_transition_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/generated/l10n/app_localizations.dart';
|
||||||
|
import 'package:paperless_mobile/keys.dart';
|
||||||
import 'package:paperless_mobile/routing/navigation_keys.dart';
|
import 'package:paperless_mobile/routing/navigation_keys.dart';
|
||||||
import 'package:paperless_mobile/routing/routes.dart';
|
import 'package:paperless_mobile/routing/routes.dart';
|
||||||
part 'login_route.g.dart';
|
part 'login_route.g.dart';
|
||||||
@@ -108,6 +109,7 @@ class AuthenticatingRoute extends GoRouteData {
|
|||||||
};
|
};
|
||||||
return NoTransitionPage(
|
return NoTransitionPage(
|
||||||
child: LoginTransitionPage(
|
child: LoginTransitionPage(
|
||||||
|
key: TestKeys.login.loggingInScreen,
|
||||||
text: text,
|
text: text,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ part 'authenticated_route.g.dart';
|
|||||||
TypedGoRoute<DocumentDetailsRoute>(
|
TypedGoRoute<DocumentDetailsRoute>(
|
||||||
path: "details/:id",
|
path: "details/:id",
|
||||||
name: R.documentDetails,
|
name: R.documentDetails,
|
||||||
|
routes: [],
|
||||||
),
|
),
|
||||||
TypedGoRoute<EditDocumentRoute>(
|
TypedGoRoute<EditDocumentRoute>(
|
||||||
path: "edit",
|
path: "edit",
|
||||||
|
|||||||
@@ -4,3 +4,4 @@ export 'src/models/models.dart';
|
|||||||
export 'src/modules/modules.dart';
|
export 'src/modules/modules.dart';
|
||||||
export 'src/converters/converters.dart';
|
export 'src/converters/converters.dart';
|
||||||
export 'config/hive/hive_type_ids.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;
|
return queryParams;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
// @override
|
||||||
String toString() => toQueryParameters().toString();
|
// String toString() => toQueryParameters().toString();
|
||||||
|
|
||||||
DocumentFilter copyWith({
|
DocumentFilter copyWith({
|
||||||
int? pageSize,
|
int? pageSize,
|
||||||
@@ -249,9 +249,4 @@ class DocumentFilter extends Equatable {
|
|||||||
moreLike,
|
moreLike,
|
||||||
selectedView,
|
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:equatable/equatable.dart';
|
||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
import 'package:json_annotation/json_annotation.dart';
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
import 'package:paperless_api/paperless_api.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/converters/local_date_time_json_converter.dart';
|
||||||
import 'package:paperless_api/src/models/custom_field_model.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';
|
import 'package:paperless_api/src/models/search_hit.dart';
|
||||||
|
|
||||||
part 'document_model.g.dart';
|
part 'document_model.g.dart';
|
||||||
@@ -48,8 +48,9 @@ class DocumentModel extends Equatable {
|
|||||||
|
|
||||||
final int? owner;
|
final int? owner;
|
||||||
final bool? userCanChange;
|
final bool? userCanChange;
|
||||||
|
final Iterable<NoteModel> notes;
|
||||||
|
|
||||||
// Only present if full_perms=true
|
/// Only present if full_perms=true
|
||||||
final Permissions? permissions;
|
final Permissions? permissions;
|
||||||
final Iterable<CustomFieldModel> customFields;
|
final Iterable<CustomFieldModel> customFields;
|
||||||
|
|
||||||
@@ -72,6 +73,7 @@ class DocumentModel extends Equatable {
|
|||||||
this.userCanChange,
|
this.userCanChange,
|
||||||
this.permissions,
|
this.permissions,
|
||||||
this.customFields = const [],
|
this.customFields = const [],
|
||||||
|
this.notes = const [],
|
||||||
});
|
});
|
||||||
|
|
||||||
factory DocumentModel.fromJson(Map<String, dynamic> json) =>
|
factory DocumentModel.fromJson(Map<String, dynamic> json) =>
|
||||||
@@ -94,6 +96,9 @@ class DocumentModel extends Equatable {
|
|||||||
String? archivedFileName,
|
String? archivedFileName,
|
||||||
int? Function()? owner,
|
int? Function()? owner,
|
||||||
bool? userCanChange,
|
bool? userCanChange,
|
||||||
|
Iterable<NoteModel>? notes,
|
||||||
|
Permissions? permissions,
|
||||||
|
Iterable<CustomFieldModel>? customFields,
|
||||||
}) {
|
}) {
|
||||||
return DocumentModel(
|
return DocumentModel(
|
||||||
id: id,
|
id: id,
|
||||||
@@ -114,6 +119,9 @@ class DocumentModel extends Equatable {
|
|||||||
archivedFileName: archivedFileName ?? this.archivedFileName,
|
archivedFileName: archivedFileName ?? this.archivedFileName,
|
||||||
owner: owner != null ? owner() : this.owner,
|
owner: owner != null ? owner() : this.owner,
|
||||||
userCanChange: userCanChange ?? this.userCanChange,
|
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,
|
archivedFileName,
|
||||||
owner,
|
owner,
|
||||||
userCanChange,
|
userCanChange,
|
||||||
|
customFields,
|
||||||
|
notes,
|
||||||
|
permissions,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,7 +82,6 @@ class FilterRule with EquatableMixin {
|
|||||||
assert(filter.tags is IdsTagsQuery);
|
assert(filter.tags is IdsTagsQuery);
|
||||||
return filter.copyWith(
|
return filter.copyWith(
|
||||||
tags: switch (filter.tags) {
|
tags: switch (filter.tags) {
|
||||||
// TODO: Handle this case.
|
|
||||||
IdsTagsQuery(include: var i, exclude: var e) => IdsTagsQuery(
|
IdsTagsQuery(include: var i, exclude: var e) => IdsTagsQuery(
|
||||||
include: [...i, int.parse(value!)],
|
include: [...i, int.parse(value!)],
|
||||||
exclude: e,
|
exclude: e,
|
||||||
|
|||||||
@@ -28,3 +28,4 @@ export 'task/task.dart';
|
|||||||
export 'task/task_status.dart';
|
export 'task/task_status.dart';
|
||||||
export 'user_model.dart';
|
export 'user_model.dart';
|
||||||
export 'exception/exceptions.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,
|
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
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
@@ -71,5 +80,7 @@ enum ErrorCode {
|
|||||||
updateSavedViewError,
|
updateSavedViewError,
|
||||||
customFieldCreateFailed,
|
customFieldCreateFailed,
|
||||||
customFieldLoadFailed,
|
customFieldLoadFailed,
|
||||||
customFieldDeleteFailed;
|
customFieldDeleteFailed,
|
||||||
|
deleteNoteFailed,
|
||||||
|
addNoteFailed;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ class PaperlessServerInformationModel {
|
|||||||
static const String versionHeader = 'x-version';
|
static const String versionHeader = 'x-version';
|
||||||
static const String apiVersionHeader = 'x-api-version';
|
static const String apiVersionHeader = 'x-api-version';
|
||||||
final String version;
|
final String version;
|
||||||
|
final String latestVersion;
|
||||||
final int apiVersion;
|
final int apiVersion;
|
||||||
final bool isUpdateAvailable;
|
final bool isUpdateAvailable;
|
||||||
|
|
||||||
@@ -11,9 +12,11 @@ class PaperlessServerInformationModel {
|
|||||||
required this.version,
|
required this.version,
|
||||||
required this.apiVersion,
|
required this.apiVersion,
|
||||||
required this.isUpdateAvailable,
|
required this.isUpdateAvailable,
|
||||||
|
required this.latestVersion,
|
||||||
});
|
});
|
||||||
|
|
||||||
int compareToOtherVersion(String other) {
|
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';
|
part 'date_range_query.g.dart';
|
||||||
|
|
||||||
sealed class DateRangeQuery {
|
sealed class DateRangeQuery with EquatableMixin {
|
||||||
const DateRangeQuery();
|
const DateRangeQuery();
|
||||||
|
|
||||||
Map<String, String> toQueryParameter(DateRangeQueryField field);
|
Map<String, String> toQueryParameter(DateRangeQueryField field);
|
||||||
@@ -28,10 +28,13 @@ class UnsetDateRangeQuery extends DateRangeQuery {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
bool matches(DateTime dt) => true;
|
bool matches(DateTime dt) => true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@HiveType(typeId: PaperlessApiHiveTypeIds.relativeDateRangeQuery)
|
@HiveType(typeId: PaperlessApiHiveTypeIds.relativeDateRangeQuery)
|
||||||
class RelativeDateRangeQuery extends DateRangeQuery with EquatableMixin {
|
class RelativeDateRangeQuery extends DateRangeQuery {
|
||||||
@HiveField(0)
|
@HiveField(0)
|
||||||
final int offset;
|
final int offset;
|
||||||
@HiveField(1)
|
@HiveField(1)
|
||||||
@@ -84,7 +87,7 @@ class RelativeDateRangeQuery extends DateRangeQuery with EquatableMixin {
|
|||||||
|
|
||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
@HiveType(typeId: PaperlessApiHiveTypeIds.absoluteDateRangeQuery)
|
@HiveType(typeId: PaperlessApiHiveTypeIds.absoluteDateRangeQuery)
|
||||||
class AbsoluteDateRangeQuery extends DateRangeQuery with EquatableMixin {
|
class AbsoluteDateRangeQuery extends DateRangeQuery {
|
||||||
@LocalDateTimeJsonConverter()
|
@LocalDateTimeJsonConverter()
|
||||||
@HiveField(0)
|
@HiveField(0)
|
||||||
final DateTime? after;
|
final DateTime? after;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import 'package:paperless_api/config/hive/hive_type_ids.dart';
|
|||||||
|
|
||||||
part 'id_query_parameter.g.dart';
|
part 'id_query_parameter.g.dart';
|
||||||
|
|
||||||
sealed class IdQueryParameter {
|
sealed class IdQueryParameter with EquatableMixin {
|
||||||
const IdQueryParameter();
|
const IdQueryParameter();
|
||||||
Map<String, String> toQueryParameter(String field);
|
Map<String, String> toQueryParameter(String field);
|
||||||
bool matches(int? id);
|
bool matches(int? id);
|
||||||
@@ -23,6 +23,9 @@ class UnsetIdQueryParameter extends IdQueryParameter {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
bool matches(int? id) => true;
|
bool matches(int? id) => true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// @HiveType(typeId: PaperlessApiHiveTypeIds.notAssignedIdQueryParameter)
|
// @HiveType(typeId: PaperlessApiHiveTypeIds.notAssignedIdQueryParameter)
|
||||||
@@ -36,6 +39,8 @@ class NotAssignedIdQueryParameter extends IdQueryParameter {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
bool matches(int? id) => id == null;
|
bool matches(int? id) => id == null;
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// @HiveType(typeId: PaperlessApiHiveTypeIds.anyAssignedIdQueryParameter)
|
// @HiveType(typeId: PaperlessApiHiveTypeIds.anyAssignedIdQueryParameter)
|
||||||
@@ -48,6 +53,8 @@ class AnyAssignedIdQueryParameter extends IdQueryParameter {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
bool matches(int? id) => id != null;
|
bool matches(int? id) => id != null;
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@HiveType(typeId: PaperlessApiHiveTypeIds.setIdQueryParameter)
|
@HiveType(typeId: PaperlessApiHiveTypeIds.setIdQueryParameter)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import 'package:paperless_api/config/hive/hive_type_ids.dart';
|
|||||||
|
|
||||||
part 'tags_query.g.dart';
|
part 'tags_query.g.dart';
|
||||||
|
|
||||||
sealed class TagsQuery {
|
sealed class TagsQuery with EquatableMixin {
|
||||||
const TagsQuery();
|
const TagsQuery();
|
||||||
Map<String, String> toQueryParameter();
|
Map<String, String> toQueryParameter();
|
||||||
bool matches(Iterable<int> ids);
|
bool matches(Iterable<int> ids);
|
||||||
@@ -20,10 +20,13 @@ class NotAssignedTagsQuery extends TagsQuery {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
bool matches(Iterable<int> ids) => ids.isEmpty;
|
bool matches(Iterable<int> ids) => ids.isEmpty;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@HiveType(typeId: PaperlessApiHiveTypeIds.anyAssignedTagsQuery)
|
@HiveType(typeId: PaperlessApiHiveTypeIds.anyAssignedTagsQuery)
|
||||||
class AnyAssignedTagsQuery extends TagsQuery with EquatableMixin {
|
class AnyAssignedTagsQuery extends TagsQuery {
|
||||||
@HiveField(0)
|
@HiveField(0)
|
||||||
final List<int> tagIds;
|
final List<int> tagIds;
|
||||||
const AnyAssignedTagsQuery({
|
const AnyAssignedTagsQuery({
|
||||||
@@ -54,7 +57,7 @@ class AnyAssignedTagsQuery extends TagsQuery with EquatableMixin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@HiveType(typeId: PaperlessApiHiveTypeIds.idsTagsQuery)
|
@HiveType(typeId: PaperlessApiHiveTypeIds.idsTagsQuery)
|
||||||
class IdsTagsQuery extends TagsQuery with EquatableMixin {
|
class IdsTagsQuery extends TagsQuery {
|
||||||
@HiveField(0)
|
@HiveField(0)
|
||||||
final List<int> include;
|
final List<int> include;
|
||||||
@HiveField(1)
|
@HiveField(1)
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'package:equatable/equatable.dart';
|
||||||
import 'package:hive/hive.dart';
|
import 'package:hive/hive.dart';
|
||||||
import 'package:json_annotation/json_annotation.dart';
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
import 'package:paperless_api/config/hive/hive_type_ids.dart';
|
import 'package:paperless_api/config/hive/hive_type_ids.dart';
|
||||||
@@ -91,6 +92,11 @@ class TextQuery {
|
|||||||
return other.queryText == queryText && other.queryType == queryType;
|
return other.queryText == queryText && other.queryType == queryType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return "TextQuery($queryText, $queryType)";
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode => Object.hash(queryText, queryType);
|
int get hashCode => Object.hash(queryText, queryType);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,4 @@
|
|||||||
import 'package:paperless_api/src/models/exception/exceptions.dart';
|
|
||||||
|
|
||||||
abstract class PaperlessAuthenticationApi {
|
abstract class PaperlessAuthenticationApi {
|
||||||
///
|
|
||||||
/// @throws [PaperlessUnauthorizedException]
|
|
||||||
///
|
|
||||||
Future<String> login({
|
Future<String> login({
|
||||||
required String username,
|
required String username,
|
||||||
required String password,
|
required String password,
|
||||||
|
|||||||
@@ -37,6 +37,11 @@ class PaperlessAuthenticationApiImpl implements PaperlessAuthenticationApi {
|
|||||||
// return AuthenticationTemporaryRedirect(redirectUrl!);
|
// return AuthenticationTemporaryRedirect(redirectUrl!);
|
||||||
} on DioException catch (exception) {
|
} on DioException catch (exception) {
|
||||||
throw exception.unravel();
|
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<DocumentModel> find(int id);
|
||||||
Future<int> delete(DocumentModel doc);
|
Future<int> delete(DocumentModel doc);
|
||||||
Future<DocumentMetaData> getMetaData(int id);
|
Future<DocumentMetaData> getMetaData(int id);
|
||||||
|
Future<DocumentModel> deleteNote(DocumentModel document, int noteId);
|
||||||
Future<Iterable<int>> bulkAction(BulkAction action);
|
Future<Iterable<int>> bulkAction(BulkAction action);
|
||||||
Future<Uint8List> getPreview(int docId);
|
Future<Uint8List> getPreview(int docId);
|
||||||
String getThumbnailUrl(int docId);
|
String getThumbnailUrl(int docId);
|
||||||
@@ -35,4 +36,7 @@ abstract class PaperlessDocumentsApi {
|
|||||||
Future<FieldSuggestions> findSuggestions(DocumentModel document);
|
Future<FieldSuggestions> findSuggestions(DocumentModel document);
|
||||||
|
|
||||||
Future<List<String>> autocomplete(String query, [int limit = 10]);
|
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/",
|
"/api/remote_version/",
|
||||||
options: Options(validateStatus: (status) => status == 200),
|
options: Options(validateStatus: (status) => status == 200),
|
||||||
);
|
);
|
||||||
var version = response.data["version"] as String;
|
final latestVersion = response.data["version"] as String;
|
||||||
if (version == _fallbackVersion) {
|
final version = response.headers
|
||||||
version = response.headers.value('x-version') ?? _fallbackVersion;
|
.value(PaperlessServerInformationModel.versionHeader) ??
|
||||||
}
|
_fallbackVersion;
|
||||||
final updateAvailable = response.data["update_available"] as bool;
|
final updateAvailable = response.data["update_available"] as bool;
|
||||||
return PaperlessServerInformationModel(
|
return PaperlessServerInformationModel(
|
||||||
apiVersion: int.parse(response.headers.value('x-api-version')!),
|
apiVersion: int.parse(response.headers
|
||||||
|
.value(PaperlessServerInformationModel.apiVersionHeader)!),
|
||||||
|
latestVersion: latestVersion,
|
||||||
version: version,
|
version: version,
|
||||||
isUpdateAvailable: updateAvailable,
|
isUpdateAvailable: updateAvailable,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ dependencies:
|
|||||||
jiffy: ^5.0.0
|
jiffy: ^5.0.0
|
||||||
freezed_annotation: ^2.4.1
|
freezed_annotation: ^2.4.1
|
||||||
hive: ^2.2.3
|
hive: ^2.2.3
|
||||||
|
mockito: ^5.4.4
|
||||||
|
http_mock_adapter: ^0.6.1
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
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';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
group('Validate parsing logic from [SavedView] to [DocumentFilter]:', () {
|
group('Parsing [SavedView] to [DocumentFilter]:', () {
|
||||||
test('Values are correctly parsed if set.', () {
|
test('Values are correctly parsed if set.', () {
|
||||||
expect(
|
expect(
|
||||||
SavedView.fromJson({
|
SavedView.fromJson({
|
||||||
@@ -64,7 +64,7 @@ void main() {
|
|||||||
]
|
]
|
||||||
}).toDocumentFilter(),
|
}).toDocumentFilter(),
|
||||||
equals(
|
equals(
|
||||||
DocumentFilter.initial.copyWith(
|
DocumentFilter(
|
||||||
correspondent: const SetIdQueryParameter(id: 42),
|
correspondent: const SetIdQueryParameter(id: 42),
|
||||||
documentType: const SetIdQueryParameter(id: 69),
|
documentType: const SetIdQueryParameter(id: 69),
|
||||||
storagePath: const SetIdQueryParameter(id: 14),
|
storagePath: const SetIdQueryParameter(id: 14),
|
||||||
@@ -83,6 +83,7 @@ void main() {
|
|||||||
sortField: SortField.created,
|
sortField: SortField.created,
|
||||||
sortOrder: SortOrder.descending,
|
sortOrder: SortOrder.descending,
|
||||||
query: const TextQuery.extended("Never gonna give you up"),
|
query: const TextQuery.extended("Never gonna give you up"),
|
||||||
|
selectedView: 1,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -99,7 +100,11 @@ void main() {
|
|||||||
"sort_reverse": true,
|
"sort_reverse": true,
|
||||||
"filter_rules": [],
|
"filter_rules": [],
|
||||||
}).toDocumentFilter(),
|
}).toDocumentFilter(),
|
||||||
equals(DocumentFilter.initial),
|
equals(
|
||||||
|
const DocumentFilter(
|
||||||
|
selectedView: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -130,11 +135,12 @@ void main() {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
}).toDocumentFilter();
|
}).toDocumentFilter();
|
||||||
final expected = DocumentFilter.initial.copyWith(
|
const expected = DocumentFilter(
|
||||||
correspondent: const NotAssignedIdQueryParameter(),
|
correspondent: NotAssignedIdQueryParameter(),
|
||||||
documentType: const NotAssignedIdQueryParameter(),
|
documentType: NotAssignedIdQueryParameter(),
|
||||||
storagePath: const NotAssignedIdQueryParameter(),
|
storagePath: NotAssignedIdQueryParameter(),
|
||||||
tags: const NotAssignedTagsQuery(),
|
tags: NotAssignedTagsQuery(),
|
||||||
|
selectedView: 1,
|
||||||
);
|
);
|
||||||
expect(
|
expect(
|
||||||
actual,
|
actual,
|
||||||
@@ -148,6 +154,7 @@ void main() {
|
|||||||
expect(
|
expect(
|
||||||
SavedView.fromDocumentFilter(
|
SavedView.fromDocumentFilter(
|
||||||
DocumentFilter(
|
DocumentFilter(
|
||||||
|
selectedView: 1,
|
||||||
correspondent: const SetIdQueryParameter(id: 1),
|
correspondent: const SetIdQueryParameter(id: 1),
|
||||||
documentType: const SetIdQueryParameter(id: 2),
|
documentType: const SetIdQueryParameter(id: 2),
|
||||||
storagePath: const SetIdQueryParameter(id: 3),
|
storagePath: const SetIdQueryParameter(id: 3),
|
||||||
@@ -173,6 +180,7 @@ void main() {
|
|||||||
),
|
),
|
||||||
equals(
|
equals(
|
||||||
SavedView(
|
SavedView(
|
||||||
|
id: 1,
|
||||||
name: "test_name",
|
name: "test_name",
|
||||||
showOnDashboard: false,
|
showOnDashboard: false,
|
||||||
showInSidebar: false,
|
showInSidebar: false,
|
||||||
@@ -101,10 +101,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: collection
|
name: collection
|
||||||
sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687
|
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.17.2"
|
version: "1.18.0"
|
||||||
colorfilter_generator:
|
colorfilter_generator:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -244,10 +244,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: meta
|
name: meta
|
||||||
sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3"
|
sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.9.1"
|
version: "1.10.0"
|
||||||
paperless_document_scanner:
|
paperless_document_scanner:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -376,18 +376,18 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: stack_trace
|
name: stack_trace
|
||||||
sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5
|
sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.11.0"
|
version: "1.11.1"
|
||||||
stream_channel:
|
stream_channel:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: stream_channel
|
name: stream_channel
|
||||||
sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8"
|
sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.1"
|
version: "2.1.2"
|
||||||
stream_transform:
|
stream_transform:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -416,10 +416,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test_api
|
name: test_api
|
||||||
sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8"
|
sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.6.0"
|
version: "0.6.1"
|
||||||
typed_data:
|
typed_data:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -440,10 +440,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: web
|
name: web
|
||||||
sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10
|
sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.1.4-beta"
|
version: "0.3.0"
|
||||||
win32:
|
win32:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -469,5 +469,5 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "6.3.0"
|
version: "6.3.0"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.1.0 <4.0.0"
|
dart: ">=3.2.0-194.0.dev <4.0.0"
|
||||||
flutter: ">=3.13.0"
|
flutter: ">=3.13.0"
|
||||||
|
|||||||
64
pubspec.lock
64
pubspec.lock
@@ -221,10 +221,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: collection
|
name: collection
|
||||||
sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687
|
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.17.2"
|
version: "1.18.0"
|
||||||
color:
|
color:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -848,6 +848,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.1"
|
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:
|
http_multi_server:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1014,7 +1022,7 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "1.2.0"
|
version: "1.2.0"
|
||||||
markdown:
|
markdown:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: markdown
|
name: markdown
|
||||||
sha256: acf35edccc0463a9d7384e437c015a3535772e09714cf60e07eeef3a15870dcd
|
sha256: acf35edccc0463a9d7384e437c015a3535772e09714cf60e07eeef3a15870dcd
|
||||||
@@ -1041,10 +1049,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: meta
|
name: meta
|
||||||
sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3"
|
sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.9.1"
|
version: "1.10.0"
|
||||||
mime:
|
mime:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1060,6 +1068,14 @@ packages:
|
|||||||
relative: true
|
relative: true
|
||||||
source: path
|
source: path
|
||||||
version: "0.0.1"
|
version: "0.0.1"
|
||||||
|
mockito:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: mockito
|
||||||
|
sha256: "6841eed20a7befac0ce07df8116c8b8233ed1f4486a7647c7fc5a02ae6163917"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "5.4.4"
|
||||||
mocktail:
|
mocktail:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1223,8 +1239,8 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
path: "packages/pdfx"
|
path: "packages/pdfx"
|
||||||
ref: HEAD
|
ref: "4be9de9ffed5398fd7d5f44bbb07dcd3d3f1711b"
|
||||||
resolved-ref: "11f7dee82b58ca4f483c753f06bbdc91b34a0793"
|
resolved-ref: "4be9de9ffed5398fd7d5f44bbb07dcd3d3f1711b"
|
||||||
url: "https://github.com/ScerIO/packages.flutter"
|
url: "https://github.com/ScerIO/packages.flutter"
|
||||||
source: git
|
source: git
|
||||||
version: "2.5.0"
|
version: "2.5.0"
|
||||||
@@ -1288,10 +1304,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: platform
|
name: platform
|
||||||
sha256: "4a451831508d7d6ca779f7ac6e212b4023dd5a7d08a27a63da33756410e32b76"
|
sha256: ae68c7bfcd7383af3629daafb32fb4e8681c7154428da4febcff06200585f102
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.0"
|
version: "3.1.2"
|
||||||
plugin_platform_interface:
|
plugin_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1637,18 +1653,18 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: stack_trace
|
name: stack_trace
|
||||||
sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5
|
sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.11.0"
|
version: "1.11.1"
|
||||||
stream_channel:
|
stream_channel:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: stream_channel
|
name: stream_channel
|
||||||
sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8"
|
sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.1"
|
version: "2.1.2"
|
||||||
stream_transform:
|
stream_transform:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1693,26 +1709,26 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test
|
name: test
|
||||||
sha256: "13b41f318e2a5751c3169137103b60c584297353d4b1761b66029bae6411fe46"
|
sha256: a1f7595805820fcc05e5c52e3a231aedd0b72972cb333e8c738a8b1239448b6f
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.24.3"
|
version: "1.24.9"
|
||||||
test_api:
|
test_api:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test_api
|
name: test_api
|
||||||
sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8"
|
sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.6.0"
|
version: "0.6.1"
|
||||||
test_core:
|
test_core:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test_core
|
name: test_core
|
||||||
sha256: "99806e9e6d95c7b059b7a0fc08f07fc53fabe54a829497f0d9676299f1e8637e"
|
sha256: a757b14fc47507060a162cc2530d9a4a2f92f5100a952c7443b5cad5ef5b106a
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.5.3"
|
version: "0.5.9"
|
||||||
time:
|
time:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1877,10 +1893,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: vm_service
|
name: vm_service
|
||||||
sha256: c620a6f783fa22436da68e42db7ebbf18b8c44b9a46ab911f666ff09ffd9153f
|
sha256: c538be99af830f478718b51630ec1b6bee5e74e52c8a802d328d9e71d35d2583
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "11.7.1"
|
version: "11.10.0"
|
||||||
watcher:
|
watcher:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1893,10 +1909,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: web
|
name: web
|
||||||
sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10
|
sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.1.4-beta"
|
version: "0.3.0"
|
||||||
web_socket_channel:
|
web_socket_channel:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -1994,5 +2010,5 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.2"
|
version: "3.1.2"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.1.0 <4.0.0"
|
dart: ">=3.2.0-194.0.dev <4.0.0"
|
||||||
flutter: ">=3.13.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.
|
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
|
||||||
# Read more about iOS versioning at
|
# Read more about iOS versioning at
|
||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
version: 3.1.8+403
|
version: 3.2.0+404
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=3.1.0 <4.0.0"
|
sdk: ">=3.1.0 <4.0.0"
|
||||||
@@ -103,8 +103,10 @@ dependencies:
|
|||||||
# camerawesome: ^2.0.0-dev.1
|
# camerawesome: ^2.0.0-dev.1
|
||||||
pdfx:
|
pdfx:
|
||||||
git:
|
git:
|
||||||
url: "https://github.com/ScerIO/packages.flutter"
|
url: 'https://github.com/ScerIO/packages.flutter'
|
||||||
|
ref: '4be9de9ffed5398fd7d5f44bbb07dcd3d3f1711b'
|
||||||
path: packages/pdfx
|
path: packages/pdfx
|
||||||
|
markdown: ^7.1.1
|
||||||
|
|
||||||
dependency_overrides:
|
dependency_overrides:
|
||||||
intl: ^0.18.1
|
intl: ^0.18.1
|
||||||
|
|||||||
@@ -1,23 +1,20 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -Eeuo pipefail
|
set -Euo pipefail
|
||||||
|
|
||||||
__script_dir=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )
|
__script_dir=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )
|
||||||
readonly __script_dir
|
readonly __script_dir
|
||||||
|
|
||||||
pushd "$__script_dir/../"
|
pushd "$__script_dir/../"
|
||||||
|
|
||||||
pushd packages/paperless_api
|
for dir in packages/*/ # list directories in the form "/tmp/dirname/"
|
||||||
flutter packages pub get
|
do
|
||||||
dart run build_runner build --delete-conflicting-outputs
|
pushd $dir
|
||||||
popd
|
echo "Installing dependencies for $dir"
|
||||||
|
flutter packages pub get
|
||||||
pushd packages/mock_server
|
dart run build_runner build --delete-conflicting-outputs
|
||||||
flutter packages pub get
|
popd
|
||||||
popd
|
done
|
||||||
|
|
||||||
flutter packages pub get
|
flutter packages pub get
|
||||||
flutter gen-l10n
|
flutter gen-l10n
|
||||||
dart run build_runner build --delete-conflicting-outputs
|
dart run build_runner build --delete-conflicting-outputs
|
||||||
|
|
||||||
popd
|
|
||||||
|
|
||||||
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