Merge pull request #346 from astubenbord/feature/notes

Feature: Notes and bugfixes
This commit is contained in:
Anton Stubenbord
2024-01-06 20:54:18 +01:00
committed by GitHub
80 changed files with 3732 additions and 726 deletions

View File

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

View File

@@ -0,0 +1,3 @@
- Neues Feature: Notizen
- Neue Sprache: Italienisch
- Mehere Fehlerbehebungen

View File

@@ -0,0 +1,3 @@
- New feature: Notes
- New language: Italian
- Multiple bugfixes

8
build.yaml Normal file
View File

@@ -0,0 +1,8 @@
targets:
$default:
builders:
mockito|mockBuilder:
generate_for:
- lib/**.dart
- test/**.dart
- integration_test/**.dart

View File

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

View File

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

View 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;
}
}

View File

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

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View 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();
}
}

View File

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

View File

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

View File

@@ -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: [

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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!"
} }

View File

@@ -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!"
} }

View File

@@ -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!"
} }

View File

@@ -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!"
} }

View File

@@ -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!"
} }

View File

@@ -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
View File

File diff suppressed because it is too large Load Diff

View File

@@ -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!"
} }

View File

@@ -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
View File

File diff suppressed because it is too large Load Diff

View File

@@ -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!"
} }

View File

@@ -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!"
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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