From 12a3d498eb1b2bf7ef019e9f15935e00d848d3d5 Mon Sep 17 00:00:00 2001 From: Eoic Date: Fri, 8 May 2026 23:51:01 +0300 Subject: [PATCH 01/16] fix(commands): incorrect flutter run commands in VS Code tasks --- .gitignore | 3 ++- .vscode/tasks.json | 19 +++++++++++++------ 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/.gitignore b/.gitignore index c5d2d58..8df6c94 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ firebase-debug.log .worktrees/ landing/node_modules/ landing/css/ -test/data/ \ No newline at end of file +test/data/ +.idea/ \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 6e70df3..d396d06 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -1,11 +1,15 @@ { "version": "2.0.0", + "options": { + "cwd": "${workspaceFolder}/app/" + }, "tasks": [ { "label": "Papyrus (web)", "type": "shell", - "command": "flutter run", + "command": "flutter", "args": [ + "run", "-d", "chrome", "--dart-define-from-file", @@ -15,8 +19,9 @@ { "label": "Papyrus (android)", "type": "shell", - "command": "flutter run", + "command": "flutter", "args": [ + "run", "-d", "android", "--dart-define-from-file", @@ -26,8 +31,9 @@ { "label": "Papyrus (iOS)", "type": "shell", - "command": "flutter run", + "command": "flutter", "args": [ + "run", "-d", "ios", "--dart-define-from-file", @@ -35,12 +41,13 @@ ] }, { - "label": "Papyrus (desktop)", + "label": "Papyrus (Linux)", "type": "shell", - "command": "flutter run", + "command": "flutter", "args": [ + "run", "-d", - "desktop", + "linux", "--dart-define-from-file", ".dart_defines" ] From 038a3644883ec4d010c915d74afc9a9d604ae6e8 Mon Sep 17 00:00:00 2001 From: Eoic Date: Sat, 9 May 2026 16:16:55 +0300 Subject: [PATCH 02/16] feat: change auth via self-hosted server --- .vscode/tasks.json | 4 +- app/analysis_options.yaml | 5 +- app/android/app/src/main/AndroidManifest.xml | 10 + .../app/src/main/res/values/strings.xml | 2 - app/ios/Runner/Info.plist | 2 +- app/lib/auth/auth_api_client.dart | 181 +++++++++ app/lib/auth/auth_models.dart | 67 +++ app/lib/auth/auth_repository.dart | 186 +++++++++ app/lib/auth/papyrus_api_config.dart | 26 ++ app/lib/auth/token_store.dart | 56 +++ app/lib/config/app_router.dart | 133 +++--- app/lib/data/data_store.dart | 70 +--- app/lib/data/sample_data.dart | 331 +++------------ app/lib/forms/login_form.dart | 65 ++- app/lib/forms/register_form.dart | 77 ++-- app/lib/main.dart | 39 +- app/lib/models/active_filter.dart | 13 +- app/lib/models/annotation.dart | 50 +-- app/lib/models/book.dart | 34 +- app/lib/models/book_shelf_relation.dart | 25 +- app/lib/models/book_tag_relation.dart | 22 +- app/lib/models/bookmark.dart | 8 +- app/lib/models/daily_activity.dart | 81 +--- app/lib/models/genre_stats.dart | 35 +- app/lib/models/note.dart | 23 +- app/lib/models/reading_goal.dart | 27 +- app/lib/models/reading_session.dart | 8 +- app/lib/models/reading_streak.dart | 19 +- app/lib/models/search_filter.dart | 28 +- app/lib/models/tag.dart | 16 +- app/lib/pages/annotations_page.dart | 98 +---- app/lib/pages/auth/oauth_callback_page.dart | 74 ++++ app/lib/pages/book_details_page.dart | 135 ++----- app/lib/pages/book_edit_page.dart | 274 +++---------- app/lib/pages/bookmarks_page.dart | 77 +--- app/lib/pages/books_page.dart | 7 +- app/lib/pages/dashboard_page.dart | 48 +-- app/lib/pages/developer_options_page.dart | 38 +- app/lib/pages/edit_profile_page.dart | 173 +++----- app/lib/pages/forgot_password_page.dart | 245 +++++++++-- app/lib/pages/goals_page.dart | 77 +--- app/lib/pages/library_page.dart | 197 ++------- app/lib/pages/login_page.dart | 72 ++-- app/lib/pages/notes_page.dart | 67 +-- app/lib/pages/profile_page.dart | 344 ++++------------ app/lib/pages/register_page.dart | 70 ++-- app/lib/pages/shelf_contents_page.dart | 221 ++-------- app/lib/pages/shelves_page.dart | 83 +--- app/lib/pages/statistics_page.dart | 250 +++--------- app/lib/pages/welcome_page.dart | 168 ++------ app/lib/platform/web_redirect.dart | 1 + app/lib/platform/web_redirect_stub.dart | 3 + app/lib/platform/web_redirect_web.dart | 5 + app/lib/providers/annotations_provider.dart | 7 +- app/lib/providers/auth_provider.dart | 344 +++++++++------- app/lib/providers/book_details_provider.dart | 5 +- app/lib/providers/book_edit_provider.dart | 65 +-- app/lib/providers/bookmarks_provider.dart | 15 +- app/lib/providers/dashboard_provider.dart | 58 +-- app/lib/providers/goals_provider.dart | 26 +- app/lib/providers/library_provider.dart | 14 +- app/lib/providers/notes_provider.dart | 13 +- app/lib/providers/preferences_provider.dart | 18 +- app/lib/providers/shelves_provider.dart | 48 +-- app/lib/providers/statistics_provider.dart | 117 +----- app/lib/services/book_import_service.dart | 44 +- app/lib/services/file_metadata_service.dart | 145 ++----- app/lib/services/metadata_service.dart | 32 +- app/lib/themes/app_theme.dart | 319 +++------------ app/lib/utils/book_actions.dart | 6 +- app/lib/utils/bulk_book_actions.dart | 74 +--- app/lib/utils/image_utils.dart | 10 +- app/lib/utils/responsive.dart | 19 +- app/lib/utils/search_query_parser.dart | 24 +- .../add_book/add_book_choice_sheet.dart | 37 +- .../add_book/add_physical_book_sheet.dart | 115 ++---- .../widgets/add_book/import_book_sheet.dart | 97 +---- .../widgets/add_book/isbn_scanner_dialog.dart | 46 +-- .../annotations/annotation_action_sheet.dart | 28 +- app/lib/widgets/auth/auth_branding.dart | 6 +- .../widgets/auth/auth_continue_button.dart | 30 +- app/lib/widgets/auth/auth_hero_panel.dart | 22 +- app/lib/widgets/auth/auth_page_layouts.dart | 41 +- app/lib/widgets/auth/auth_switch_link.dart | 18 +- .../widgets/auth/curved_bottom_clipper.dart | 14 +- app/lib/widgets/book/book.dart | 82 +--- app/lib/widgets/book/book_annotations.dart | 47 +-- app/lib/widgets/book/book_bookmarks.dart | 47 +-- app/lib/widgets/book/book_details.dart | 37 +- app/lib/widgets/book/book_notes.dart | 55 +-- .../book_details/annotation_action_sheet.dart | 19 +- .../widgets/book_details/annotation_card.dart | 41 +- .../book_details/annotation_dialog.dart | 87 +--- .../book_details/book_action_buttons.dart | 24 +- .../book_details/book_cover_image.dart | 43 +- app/lib/widgets/book_details/book_header.dart | 56 +-- .../widgets/book_details/book_info_grid.dart | 37 +- .../book_details/book_progress_bar.dart | 8 +- .../widgets/book_details/bookmark_dialog.dart | 68 +--- .../eink_book_details_tab_bar.dart | 33 +- .../book_details/empty_annotations_state.dart | 25 +- .../book_details/empty_bookmarks_state.dart | 25 +- .../book_details/empty_notes_state.dart | 25 +- .../book_details/note_action_sheet.dart | 23 +- app/lib/widgets/book_details/note_card.dart | 28 +- app/lib/widgets/book_details/note_dialog.dart | 65 +-- .../book_details/update_progress_sheet.dart | 44 +- .../widgets/book_edit/cover_image_picker.dart | 45 +-- .../widgets/book_form/book_date_field.dart | 9 +- .../widgets/book_form/book_text_field.dart | 9 +- .../widgets/book_form/co_author_editor.dart | 23 +- .../book_form/responsive_form_row.dart | 6 +- .../bookmarks/bookmark_action_sheet.dart | 93 +---- .../widgets/bookmarks/bookmark_list_item.dart | 19 +- app/lib/widgets/buttons/google_sign_in.dart | 48 +-- .../context_menu/book_context_menu.dart | 87 +--- .../dashboard/continue_reading_card.dart | 83 +--- .../widgets/dashboard/dashboard_greeting.dart | 31 +- .../widgets/dashboard/quick_stats_widget.dart | 45 +-- .../widgets/dashboard/reading_goal_card.dart | 46 +-- .../dashboard/recently_added_section.dart | 61 +-- .../dashboard/weekly_activity_chart.dart | 62 +-- app/lib/widgets/filter/active_filter_bar.dart | 36 +- .../widgets/filter/filter_bottom_sheet.dart | 117 ++---- app/lib/widgets/filter/filter_dialog.dart | 104 ++--- .../goals/active_goal_details_sheet.dart | 216 ++-------- app/lib/widgets/goals/add_goal_card.dart | 27 +- app/lib/widgets/goals/add_goal_sheet.dart | 153 ++----- .../widgets/goals/completed_goal_chip.dart | 208 ++-------- app/lib/widgets/goals/goal_card.dart | 129 +----- app/lib/widgets/heading.dart | 5 +- app/lib/widgets/input/email_input.dart | 9 +- app/lib/widgets/input/name_input.dart | 5 +- app/lib/widgets/input/password_input.dart | 4 +- app/lib/widgets/input/search_field.dart | 11 +- app/lib/widgets/input/text_input.dart | 14 +- app/lib/widgets/library/book_card.dart | 86 ++-- app/lib/widgets/library/book_grid.dart | 26 +- app/lib/widgets/library/book_list_item.dart | 88 ++-- app/lib/widgets/library/bulk_action_bar.dart | 29 +- .../widgets/library/bulk_status_sheet.dart | 53 +-- app/lib/widgets/library/eink_tab_filter.dart | 17 +- app/lib/widgets/library/library_drawer.dart | 27 +- .../widgets/library/library_filter_chips.dart | 25 +- app/lib/widgets/library/selection_header.dart | 11 +- app/lib/widgets/profile/profile_header.dart | 48 +-- .../widgets/profile/profile_menu_item.dart | 54 +-- .../widgets/profile/profile_stats_card.dart | 30 +- app/lib/widgets/profile_button.dart | 7 +- .../widgets/search/library_search_bar.dart | 81 +--- app/lib/widgets/search_settings.dart | 5 +- app/lib/widgets/settings/settings_row.dart | 45 +-- .../widgets/settings/settings_section.dart | 7 +- app/lib/widgets/shared/book_group_header.dart | 27 +- .../widgets/shared/bottom_sheet_handle.dart | 5 +- .../widgets/shared/bottom_sheet_header.dart | 12 +- app/lib/widgets/shared/eink_page_header.dart | 12 +- app/lib/widgets/shared/empty_state.dart | 21 +- .../widgets/shared/quick_filter_chips.dart | 28 +- app/lib/widgets/shared/view_mode_toggle.dart | 21 +- app/lib/widgets/shell/adaptive_app_shell.dart | 31 +- app/lib/widgets/shell/desktop_sidebar.dart | 115 ++---- app/lib/widgets/shell/eink_bottom_nav.dart | 26 +- app/lib/widgets/shell/mobile_bottom_nav.dart | 7 +- app/lib/widgets/shelves/add_shelf_sheet.dart | 132 ++---- .../widgets/shelves/move_to_shelf_sheet.dart | 94 +---- app/lib/widgets/shelves/shelf_card.dart | 83 +--- .../widgets/statistics/reading_charts.dart | 381 ++++-------------- app/lib/widgets/statistics/stat_card.dart | 74 +--- app/lib/widgets/titled_divider.dart | 21 +- app/lib/widgets/topics/add_topic_sheet.dart | 97 +---- .../widgets/topics/manage_topics_sheet.dart | 130 ++---- .../widgets/topics/topic_detail_sheet.dart | 54 +-- .../flutter/generated_plugin_registrant.cc | 16 +- app/linux/flutter/generated_plugins.cmake | 4 +- .../Flutter/GeneratedPluginRegistrant.swift | 12 +- app/macos/Runner/Info.plist | 11 + app/pubspec.lock | 292 ++++---------- app/pubspec.yaml | 6 +- app/test/auth/auth_api_client_test.dart | 81 ++++ app/test/auth/token_store_test.dart | 38 ++ app/test/config/app_router_test.dart | 102 +++++ app/test/helpers/test_helpers.dart | 35 +- app/test/models/active_filter_test.dart | 69 +--- app/test/models/annotation_test.dart | 38 +- app/test/models/book_test.dart | 37 +- app/test/models/bookmark_test.dart | 18 +- app/test/models/note_test.dart | 41 +- app/test/models/search_filter_test.dart | 305 +++----------- app/test/pages/library_page_test.dart | 51 +-- app/test/providers/auth_provider_test.dart | 125 ++++++ .../providers/book_details_provider_test.dart | 42 +- app/test/providers/library_provider_test.dart | 11 +- .../services/book_import_service_test.dart | 19 +- .../services/file_metadata_service_test.dart | 205 ++++------ app/test/utils/search_query_parser_test.dart | 29 +- .../widgets/book/book_annotations_test.dart | 43 +- .../widgets/book/book_bookmarks_test.dart | 45 +-- app/test/widgets/book/book_notes_test.dart | 11 +- .../book_details/annotation_card_test.dart | 49 +-- .../book_action_buttons_test.dart | 90 +---- .../book_details/book_progress_bar_test.dart | 12 +- .../book_details/bookmark_list_item_test.dart | 28 +- .../empty_annotations_state_test.dart | 26 +- .../empty_bookmarks_state_test.dart | 19 +- .../widgets/book_details/note_card_test.dart | 19 +- app/test/widgets/library/book_card_test.dart | 15 +- app/test/widgets/library/book_grid_test.dart | 10 +- .../widgets/library/book_list_item_test.dart | 23 +- .../widgets/library/library_drawer_test.dart | 32 +- .../library/library_filter_chips_test.dart | 26 +- .../search/library_search_bar_test.dart | 55 +-- .../shared/bottom_sheet_header_test.dart | 13 +- app/test/widgets/shared/empty_state_test.dart | 25 +- .../flutter/generated_plugin_registrant.cc | 12 +- app/windows/flutter/generated_plugins.cmake | 4 +- 216 files changed, 3855 insertions(+), 9150 deletions(-) create mode 100644 app/lib/auth/auth_api_client.dart create mode 100644 app/lib/auth/auth_models.dart create mode 100644 app/lib/auth/auth_repository.dart create mode 100644 app/lib/auth/papyrus_api_config.dart create mode 100644 app/lib/auth/token_store.dart create mode 100644 app/lib/pages/auth/oauth_callback_page.dart create mode 100644 app/lib/platform/web_redirect.dart create mode 100644 app/lib/platform/web_redirect_stub.dart create mode 100644 app/lib/platform/web_redirect_web.dart create mode 100644 app/test/auth/auth_api_client_test.dart create mode 100644 app/test/auth/token_store_test.dart create mode 100644 app/test/config/app_router_test.dart create mode 100644 app/test/providers/auth_provider_test.dart diff --git a/.vscode/tasks.json b/.vscode/tasks.json index d396d06..b8421a6 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -13,7 +13,9 @@ "-d", "chrome", "--dart-define-from-file", - ".dart_defines" + ".dart_defines", + "--web-port", + "3000" ] }, { diff --git a/app/analysis_options.yaml b/app/analysis_options.yaml index f16eead..77e6c8f 100644 --- a/app/analysis_options.yaml +++ b/app/analysis_options.yaml @@ -1,4 +1,7 @@ include: package:flutter_lints/flutter.yaml linter: - rules: \ No newline at end of file + rules: + lines_longer_than_80_chars: false +formatter: + page_width: 120 \ No newline at end of file diff --git a/app/android/app/src/main/AndroidManifest.xml b/app/android/app/src/main/AndroidManifest.xml index 5f22883..0c05a42 100644 --- a/app/android/app/src/main/AndroidManifest.xml +++ b/app/android/app/src/main/AndroidManifest.xml @@ -26,6 +26,16 @@ + + + + + + + + - - 856619753967-uks4vdv502s0kqd8017e7d6b4pvou8jo.apps.googleusercontent.com diff --git a/app/ios/Runner/Info.plist b/app/ios/Runner/Info.plist index 0ed24e5..b653a1b 100644 --- a/app/ios/Runner/Info.plist +++ b/app/ios/Runner/Info.plist @@ -54,7 +54,7 @@ Editor CFBundleURLSchemes - com.googleusercontent.apps.856619753967-c7gsj5tujnukf8ku4a3kk8p8ttdt7qk3 + papyrus diff --git a/app/lib/auth/auth_api_client.dart b/app/lib/auth/auth_api_client.dart new file mode 100644 index 0000000..ad33150 --- /dev/null +++ b/app/lib/auth/auth_api_client.dart @@ -0,0 +1,181 @@ +import 'dart:convert'; + +import 'package:http/http.dart' as http; +import 'package:papyrus/auth/auth_models.dart'; +import 'package:papyrus/auth/papyrus_api_config.dart'; + +class AuthApiException implements Exception { + final int statusCode; + final String message; + final String? code; + + const AuthApiException({required this.statusCode, required this.message, this.code}); + + @override + String toString() => message; +} + +class AuthApiClient { + final PapyrusApiConfig config; + final http.Client _httpClient; + + AuthApiClient({required this.config, http.Client? httpClient}) : _httpClient = httpClient ?? http.Client(); + + Uri googleOAuthStartUri(String redirectUri) { + return config.endpoint('/auth/oauth/google/start', {'redirect_uri': redirectUri}); + } + + Future register({ + required String email, + required String password, + required String displayName, + String clientType = 'mobile', + String? deviceLabel, + }) async { + final json = await _postJson( + config.endpoint('/auth/register'), + body: { + 'email': email, + 'password': password, + 'display_name': displayName, + 'client_type': clientType, + 'device_label': deviceLabel, + }, + ); + + return AuthTokens.fromJson(json); + } + + Future login({ + required String email, + required String password, + String clientType = 'mobile', + String? deviceLabel, + }) async { + final json = await _postJson( + config.endpoint('/auth/login'), + body: {'email': email, 'password': password, 'client_type': clientType, 'device_label': deviceLabel}, + ); + + return AuthTokens.fromJson(json); + } + + Future refresh(String refreshToken) async { + final json = await _postJson(config.endpoint('/auth/refresh'), body: {'refresh_token': refreshToken}); + + return AuthTokens.fromJson(json); + } + + Future exchangeCode({required String code, String clientType = 'mobile', String? deviceLabel}) async { + final json = await _postJson( + config.endpoint('/auth/exchange-code'), + body: {'code': code, 'client_type': clientType, 'device_label': deviceLabel}, + ); + + return AuthTokens.fromJson(json); + } + + Future logout(String accessToken) async { + await _postJson(config.endpoint('/auth/logout'), accessToken: accessToken); + } + + Future currentUser(String accessToken) async { + final json = await _getJson(config.endpoint('/users/me'), accessToken: accessToken); + + return PapyrusUser.fromJson(json); + } + + Future updateCurrentUser({required String accessToken, String? displayName, String? avatarUrl}) async { + final body = {}; + + if (displayName != null) { + body['display_name'] = displayName; + } + + if (avatarUrl != null) { + body['avatar_url'] = avatarUrl; + } + + final json = await _patchJson(config.endpoint('/users/me'), accessToken: accessToken, body: body); + + return PapyrusUser.fromJson(json); + } + + Future forgotPassword(String email) async { + final json = await _postJson(config.endpoint('/auth/forgot-password'), body: {'email': email}); + + return json['message'] as String? ?? 'If the email is registered, a reset link has been sent'; + } + + Future resetPassword({required String token, required String password}) async { + final json = await _postJson(config.endpoint('/auth/reset-password'), body: {'token': token, 'password': password}); + + return json['message'] as String? ?? 'Password has been reset successfully'; + } + + Future verifyEmail(String token) async { + final json = await _postJson(config.endpoint('/auth/verify-email'), body: {'token': token}); + + return json['message'] as String? ?? 'Email verified successfully'; + } + + Future resendVerification(String email) async { + final json = await _postJson(config.endpoint('/auth/resend-verification'), body: {'email': email}); + + return json['message'] as String? ?? 'If the email is registered, a verification link has been sent'; + } + + Future> _getJson(Uri uri, {String? accessToken}) async { + final response = await _httpClient.get(uri, headers: _headers(accessToken)); + + return _decodeResponse(response); + } + + Future> _postJson(Uri uri, {Map? body, String? accessToken}) async { + final response = await _httpClient.post( + uri, + headers: _headers(accessToken), + body: body == null ? null : jsonEncode(body), + ); + + return _decodeResponse(response); + } + + Future> _patchJson( + Uri uri, { + required String accessToken, + required Map body, + }) async { + final response = await _httpClient.patch(uri, headers: _headers(accessToken), body: jsonEncode(body)); + + return _decodeResponse(response); + } + + Map _headers(String? accessToken) { + return { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + if (accessToken != null) 'Authorization': 'Bearer $accessToken', + }; + } + + Map _decodeResponse(http.Response response) { + final decoded = response.body.isEmpty ? {} : jsonDecode(response.body) as Map; + + if (response.statusCode >= 200 && response.statusCode < 300) { + return decoded; + } + + final error = decoded['error']; + + if (error is Map) { + throw AuthApiException( + statusCode: response.statusCode, + code: error['code'] as String?, + message: error['message'] as String? ?? 'Authentication request failed', + ); + } + + throw AuthApiException(statusCode: response.statusCode, message: 'Authentication request failed'); + } +} diff --git a/app/lib/auth/auth_models.dart b/app/lib/auth/auth_models.dart new file mode 100644 index 0000000..4a83969 --- /dev/null +++ b/app/lib/auth/auth_models.dart @@ -0,0 +1,67 @@ +enum AuthStatus { bootstrapping, signedOut, authenticating, signedIn, refreshing, authError } + +class PapyrusUser { + final String userId; + final String? email; + final String displayName; + final String? avatarUrl; + final bool emailVerified; + final DateTime? createdAt; + final DateTime? lastLoginAt; + + const PapyrusUser({ + required this.userId, + required this.email, + required this.displayName, + required this.avatarUrl, + required this.emailVerified, + required this.createdAt, + required this.lastLoginAt, + }); + + factory PapyrusUser.fromJson(Map json) { + return PapyrusUser( + userId: json['user_id'] as String, + email: json['email'] as String?, + displayName: json['display_name'] as String? ?? 'Papyrus User', + avatarUrl: json['avatar_url'] as String?, + emailVerified: json['email_verified'] as bool? ?? false, + createdAt: _parseDateTime(json['created_at']), + lastLoginAt: _parseDateTime(json['last_login_at']), + ); + } +} + +class AuthTokens { + final String accessToken; + final String refreshToken; + final String tokenType; + final int expiresIn; + final PapyrusUser user; + + const AuthTokens({ + required this.accessToken, + required this.refreshToken, + required this.tokenType, + required this.expiresIn, + required this.user, + }); + + factory AuthTokens.fromJson(Map json) { + return AuthTokens( + accessToken: json['access_token'] as String, + refreshToken: json['refresh_token'] as String, + tokenType: json['token_type'] as String? ?? 'Bearer', + expiresIn: json['expires_in'] as int, + user: PapyrusUser.fromJson(json['user'] as Map), + ); + } +} + +DateTime? _parseDateTime(Object? value) { + if (value is! String || value.isEmpty) { + return null; + } + + return DateTime.tryParse(value); +} diff --git a/app/lib/auth/auth_repository.dart b/app/lib/auth/auth_repository.dart new file mode 100644 index 0000000..e8b09c9 --- /dev/null +++ b/app/lib/auth/auth_repository.dart @@ -0,0 +1,186 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter_web_auth_2/flutter_web_auth_2.dart'; +import 'package:papyrus/auth/auth_api_client.dart'; +import 'package:papyrus/auth/auth_models.dart'; +import 'package:papyrus/auth/token_store.dart'; +import 'package:papyrus/platform/web_redirect.dart'; + +class AuthRepository { + static const nativeOAuthRedirectUri = 'papyrus://auth/callback'; + + final AuthApiClient apiClient; + final TokenStore tokenStore; + + Future? _refreshOperation; + + AuthRepository({required this.apiClient, required this.tokenStore}); + + String? get accessToken => tokenStore.accessToken; + + Future bootstrap() async { + final refreshToken = await tokenStore.readRefreshToken(); + + if (refreshToken == null) { + return null; + } + + return refresh(); + } + + Future register({ + required String email, + required String password, + required String displayName, + required String clientType, + String? deviceLabel, + }) async { + final tokens = await apiClient.register( + email: email, + password: password, + displayName: displayName, + clientType: clientType, + deviceLabel: deviceLabel, + ); + + await _save(tokens); + return tokens; + } + + Future login({ + required String email, + required String password, + required String clientType, + String? deviceLabel, + }) async { + final tokens = await apiClient.login( + email: email, + password: password, + clientType: clientType, + deviceLabel: deviceLabel, + ); + + await _save(tokens); + return tokens; + } + + Future refresh() { + _refreshOperation ??= _refresh(); + return _refreshOperation!; + } + + Future _refresh() async { + try { + final refreshToken = await tokenStore.readRefreshToken(); + + if (refreshToken == null) { + throw const AuthApiException(statusCode: 401, message: 'No stored refresh token'); + } + + final tokens = await apiClient.refresh(refreshToken); + await _save(tokens); + return tokens; + } catch (_) { + await tokenStore.clear(); + rethrow; + } finally { + _refreshOperation = null; + } + } + + Future signInWithGoogle({required String clientType, String? deviceLabel}) async { + final redirectUri = kIsWeb ? _webOAuthRedirectUri() : nativeOAuthRedirectUri; + final startUri = apiClient.googleOAuthStartUri(redirectUri); + + if (kIsWeb) { + redirectTo(startUri.toString()); + return null; + } + + final callbackUrl = await FlutterWebAuth2.authenticate(url: startUri.toString(), callbackUrlScheme: 'papyrus'); + final callbackUri = Uri.parse(callbackUrl); + return completeGoogleSignIn(callbackUri, clientType: clientType, deviceLabel: deviceLabel); + } + + Future completeGoogleSignIn(Uri callbackUri, {required String clientType, String? deviceLabel}) async { + final error = callbackUri.queryParameters['error']; + + if (error != null && error.isNotEmpty) { + throw AuthApiException(statusCode: 400, message: error); + } + + final code = callbackUri.queryParameters['code']; + + if (code == null || code.isEmpty) { + throw const AuthApiException(statusCode: 400, message: 'OAuth callback did not include a code'); + } + + final tokens = await apiClient.exchangeCode(code: code, clientType: clientType, deviceLabel: deviceLabel); + + await _save(tokens); + return tokens; + } + + Future logout() async { + final accessToken = tokenStore.accessToken; + + try { + if (accessToken != null) { + await apiClient.logout(accessToken); + } + } finally { + await tokenStore.clear(); + } + } + + Future currentUser() async { + final accessToken = await _requireAccessToken(); + return apiClient.currentUser(accessToken); + } + + Future updateCurrentUser({String? displayName, String? avatarUrl}) async { + final accessToken = await _requireAccessToken(); + + return apiClient.updateCurrentUser(accessToken: accessToken, displayName: displayName, avatarUrl: avatarUrl); + } + + Future forgotPassword(String email) { + return apiClient.forgotPassword(email); + } + + Future resetPassword({required String token, required String password}) { + return apiClient.resetPassword(token: token, password: password); + } + + Future verifyEmail(String token) { + return apiClient.verifyEmail(token); + } + + Future resendVerification(String email) { + return apiClient.resendVerification(email); + } + + Future clearTokens() { + return tokenStore.clear(); + } + + Future _requireAccessToken() async { + final currentAccessToken = tokenStore.accessToken; + + if (currentAccessToken != null) { + return currentAccessToken; + } + + final tokens = await refresh(); + return tokens.accessToken; + } + + Future _save(AuthTokens tokens) { + return tokenStore.saveTokens(accessToken: tokens.accessToken, refreshToken: tokens.refreshToken); + } + + String _webOAuthRedirectUri() { + return Uri.base.replace(path: '/auth/callback', query: '').toString(); + } +} diff --git a/app/lib/auth/papyrus_api_config.dart b/app/lib/auth/papyrus_api_config.dart new file mode 100644 index 0000000..3c849c9 --- /dev/null +++ b/app/lib/auth/papyrus_api_config.dart @@ -0,0 +1,26 @@ +class PapyrusApiConfig { + static const _defaultBaseUrl = 'http://localhost:8080'; + + final Uri serverBaseUri; + final String apiPrefix; + + const PapyrusApiConfig({required this.serverBaseUri, this.apiPrefix = '/v1'}); + + factory PapyrusApiConfig.fromEnvironment() { + const rawBaseUrl = String.fromEnvironment('PAPYRUS_API_BASE_URL', defaultValue: _defaultBaseUrl); + + return PapyrusApiConfig(serverBaseUri: Uri.parse(rawBaseUrl)); + } + + Uri get apiBaseUri { + final normalizedPrefix = apiPrefix.startsWith('/') ? apiPrefix : '/$apiPrefix'; + return serverBaseUri.replace(path: normalizedPrefix); + } + + Uri endpoint(String path, [Map? queryParameters]) { + final normalizedPath = path.startsWith('/') ? path : '/$path'; + final apiPath = '${apiBaseUri.path}$normalizedPath'; + + return apiBaseUri.replace(path: apiPath, queryParameters: queryParameters); + } +} diff --git a/app/lib/auth/token_store.dart b/app/lib/auth/token_store.dart new file mode 100644 index 0000000..78d1162 --- /dev/null +++ b/app/lib/auth/token_store.dart @@ -0,0 +1,56 @@ +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; + +abstract class RefreshTokenStorage { + Future read(); + + Future write(String refreshToken); + + Future delete(); +} + +class SecureRefreshTokenStorage implements RefreshTokenStorage { + static const _refreshTokenKey = 'papyrus_refresh_token'; + + final FlutterSecureStorage _storage; + + const SecureRefreshTokenStorage([this._storage = const FlutterSecureStorage()]); + + @override + Future read() => _storage.read(key: _refreshTokenKey); + + @override + Future write(String refreshToken) { + return _storage.write(key: _refreshTokenKey, value: refreshToken); + } + + @override + Future delete() => _storage.delete(key: _refreshTokenKey); +} + +class TokenStore { + final RefreshTokenStorage _refreshTokenStorage; + + String? _accessToken; + + TokenStore(this._refreshTokenStorage); + + String? get accessToken => _accessToken; + + bool get hasAccessToken => _accessToken != null; + + void setAccessToken(String accessToken) { + _accessToken = accessToken; + } + + Future readRefreshToken() => _refreshTokenStorage.read(); + + Future saveTokens({required String accessToken, required String refreshToken}) async { + _accessToken = accessToken; + await _refreshTokenStorage.write(refreshToken); + } + + Future clear() async { + _accessToken = null; + await _refreshTokenStorage.delete(); + } +} diff --git a/app/lib/config/app_router.dart b/app/lib/config/app_router.dart index 03f4583..c463c08 100644 --- a/app/lib/config/app_router.dart +++ b/app/lib/config/app_router.dart @@ -1,10 +1,10 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; +import 'package:papyrus/auth/auth_models.dart'; +import 'package:papyrus/pages/auth/oauth_callback_page.dart'; import 'package:papyrus/pages/book_details_page.dart'; import 'package:papyrus/providers/auth_provider.dart'; -import 'package:provider/provider.dart'; -import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:papyrus/pages/bookmarks_page.dart'; import 'package:papyrus/pages/book_edit_page.dart'; import 'package:papyrus/pages/dashboard_page.dart'; @@ -26,12 +26,16 @@ import 'package:papyrus/pages/welcome_page.dart'; import 'package:papyrus/widgets/shell/adaptive_app_shell.dart'; class AppRouter { + final AuthProvider authProvider; final rootNavigatorKey = GlobalKey(); final shellNavigatorKey = GlobalKey(); + AppRouter({required this.authProvider}); + late final GoRouter router = GoRouter( debugLogDiagnostics: true, navigatorKey: rootNavigatorKey, + refreshListenable: authProvider, routes: [ GoRoute( path: '/', @@ -42,23 +46,24 @@ class AppRouter { GoRoute( name: 'LOGIN', path: 'login', - pageBuilder: (context, state) => - NoTransitionPage(key: state.pageKey, child: const LoginPage()), + pageBuilder: (context, state) => NoTransitionPage(key: state.pageKey, child: const LoginPage()), ), GoRoute( name: 'REGISTER', path: 'register', - pageBuilder: (context, state) => NoTransitionPage( - key: state.pageKey, - child: const RegisterPage(), - ), + pageBuilder: (context, state) => NoTransitionPage(key: state.pageKey, child: const RegisterPage()), ), GoRoute( name: 'FORGOT_PASSWORD', path: 'forgot-password', + pageBuilder: (context, state) => NoTransitionPage(key: state.pageKey, child: const ForgotPasswordPage()), + ), + GoRoute( + name: 'AUTH_CALLBACK', + path: 'auth/callback', pageBuilder: (context, state) => NoTransitionPage( key: state.pageKey, - child: const ForgotPasswordPage(), + child: OAuthCallbackPage(callbackUri: state.uri), ), ), ], @@ -74,40 +79,26 @@ class AppRouter { GoRoute( name: 'DASHBOARD', path: '/dashboard', - pageBuilder: (context, state) => NoTransitionPage( - key: state.pageKey, - child: const DashboardPage(), - ), + pageBuilder: (context, state) => NoTransitionPage(key: state.pageKey, child: const DashboardPage()), ), // Library and sub-routes GoRoute( name: 'LIBRARY', path: '/library', redirect: (context, state) { - return state.uri.toString() == '/library' - ? '/library/books' - : null; + return state.uri.toString() == '/library' ? '/library/books' : null; }, - pageBuilder: (context, state) => NoTransitionPage( - key: state.pageKey, - child: const LibraryPage(), - ), + pageBuilder: (context, state) => NoTransitionPage(key: state.pageKey, child: const LibraryPage()), routes: [ GoRoute( name: 'BOOKS', path: 'books', - pageBuilder: (context, state) => NoTransitionPage( - key: state.pageKey, - child: const LibraryPage(), - ), + pageBuilder: (context, state) => NoTransitionPage(key: state.pageKey, child: const LibraryPage()), ), GoRoute( name: 'SHELVES', path: 'shelves', - pageBuilder: (context, state) => NoTransitionPage( - key: state.pageKey, - child: const ShelvesPage(), - ), + pageBuilder: (context, state) => NoTransitionPage(key: state.pageKey, child: const ShelvesPage()), routes: [ GoRoute( name: 'SHELF_CONTENTS', @@ -125,34 +116,22 @@ class AppRouter { GoRoute( name: 'BOOKMARKS', path: 'bookmarks', - pageBuilder: (context, state) => NoTransitionPage( - key: state.pageKey, - child: const BookmarksPage(), - ), + pageBuilder: (context, state) => NoTransitionPage(key: state.pageKey, child: const BookmarksPage()), ), GoRoute( name: 'ANNOTATIONS', path: 'annotations', - pageBuilder: (context, state) => NoTransitionPage( - key: state.pageKey, - child: const AnnotationsPage(), - ), + pageBuilder: (context, state) => NoTransitionPage(key: state.pageKey, child: const AnnotationsPage()), ), GoRoute( name: 'NOTES', path: 'notes', - pageBuilder: (context, state) => NoTransitionPage( - key: state.pageKey, - child: const NotesPage(), - ), + pageBuilder: (context, state) => NoTransitionPage(key: state.pageKey, child: const NotesPage()), ), GoRoute( name: 'SEARCH_OPTIONS', path: 'search/options', - pageBuilder: (context, state) => NoTransitionPage( - key: state.pageKey, - child: const SearchOptionsPage(), - ), + pageBuilder: (context, state) => NoTransitionPage(key: state.pageKey, child: const SearchOptionsPage()), ), GoRoute( name: 'BOOK_DETAILS', @@ -182,34 +161,24 @@ class AppRouter { GoRoute( name: 'GOALS', path: '/goals', - pageBuilder: (context, state) => - NoTransitionPage(key: state.pageKey, child: const GoalsPage()), + pageBuilder: (context, state) => NoTransitionPage(key: state.pageKey, child: const GoalsPage()), ), // Statistics GoRoute( name: 'STATISTICS', path: '/statistics', - pageBuilder: (context, state) => NoTransitionPage( - key: state.pageKey, - child: const StatisticsPage(), - ), + pageBuilder: (context, state) => NoTransitionPage(key: state.pageKey, child: const StatisticsPage()), ), // Profile GoRoute( name: 'PROFILE', path: '/profile', - pageBuilder: (context, state) => NoTransitionPage( - key: state.pageKey, - child: const ProfilePage(), - ), + pageBuilder: (context, state) => NoTransitionPage(key: state.pageKey, child: const ProfilePage()), routes: [ GoRoute( name: 'EDIT_PROFILE', path: 'edit', - pageBuilder: (context, state) => NoTransitionPage( - key: state.pageKey, - child: const EditProfilePage(), - ), + pageBuilder: (context, state) => NoTransitionPage(key: state.pageKey, child: const EditProfilePage()), ), ], ), @@ -218,35 +187,41 @@ class AppRouter { GoRoute( name: 'DEVELOPER_OPTIONS', path: '/developer-options', - pageBuilder: (context, state) => NoTransitionPage( - key: state.pageKey, - child: const DeveloperOptionsPage(), - ), + pageBuilder: (context, state) => + NoTransitionPage(key: state.pageKey, child: const DeveloperOptionsPage()), ), ], ), ], redirect: (BuildContext context, GoRouterState state) { - final isOffline = Provider.of( - context, - listen: false, - ).isOfflineMode; + return redirectForPath(state.uri.path); + }, + ); - if (Supabase.instance.client.auth.currentSession == null && !isOffline) { - if (state.uri.toString().contains('/login') || - state.uri.toString().contains('/register') || - state.uri.toString().contains('/forgot-password')) { - return null; - } + String? redirectForPath(String location) { + final isAuthRoute = + location == '/' || + location == '/login' || + location == '/register' || + location == '/forgot-password' || + location == '/auth/callback'; - return '/'; - } + if (authProvider.status == AuthStatus.bootstrapping) { + return null; + } - if (state.uri.toString() == '/') { - return '/library/books'; + if (!authProvider.isSignedIn && !authProvider.isOfflineMode) { + if (isAuthRoute) { + return null; } - return null; - }, - ); + return '/'; + } + + if (location == '/' || location == '/login' || location == '/register') { + return '/library/books'; + } + + return null; + } } diff --git a/app/lib/data/data_store.dart b/app/lib/data/data_store.dart index dae8ae5..43e8e74 100644 --- a/app/lib/data/data_store.dart +++ b/app/lib/data/data_store.dart @@ -53,10 +53,8 @@ class DataStore extends ChangeNotifier { List get bookmarks => _bookmarks.values.toList(); List get readingSessions => _readingSessions.values.toList(); List get readingGoals => _readingGoals.values.toList(); - List get bookShelfRelations => - List.unmodifiable(_bookShelfRelations); - List get bookTagRelations => - List.unmodifiable(_bookTagRelations); + List get bookShelfRelations => List.unmodifiable(_bookShelfRelations); + List get bookTagRelations => List.unmodifiable(_bookTagRelations); // ============================================================ // Book CRUD @@ -94,10 +92,7 @@ class DataStore extends ChangeNotifier { Shelf? getShelf(String id) { final shelf = _shelves[id]; if (shelf == null) return null; - return shelf.copyWith( - bookCount: getBookCountForShelf(id), - coverPreviews: getCoverPreviewsForShelf(id), - ); + return shelf.copyWith(bookCount: getBookCountForShelf(id), coverPreviews: getCoverPreviewsForShelf(id)); } void addShelf(Shelf shelf) { @@ -118,9 +113,7 @@ class DataStore extends ChangeNotifier { /// Get all books in a shelf. List getBooksInShelf(String shelfId) { - final bookIds = _bookShelfRelations - .where((r) => r.shelfId == shelfId) - .map((r) => r.bookId); + final bookIds = _bookShelfRelations.where((r) => r.shelfId == shelfId).map((r) => r.bookId); return bookIds.map((id) => _books[id]).whereType().toList(); } @@ -145,10 +138,7 @@ class DataStore extends ChangeNotifier { /// Get cover previews for a shelf (up to 4 books). List getCoverPreviewsForShelf(String shelfId, {int limit = 4}) { final books = getBooksInShelf(shelfId); - return books - .take(limit) - .map((b) => CoverPreview(url: b.coverUrl, title: b.title)) - .toList(); + return books.take(limit).map((b) => CoverPreview(url: b.coverUrl, title: b.title)).toList(); } // ============================================================ @@ -175,9 +165,7 @@ class DataStore extends ChangeNotifier { /// Get all books with a tag. List getBooksWithTag(String tagId) { - final bookIds = _bookTagRelations - .where((r) => r.tagId == tagId) - .map((r) => r.bookId); + final bookIds = _bookTagRelations.where((r) => r.tagId == tagId).map((r) => r.bookId); return bookIds.map((id) => _books[id]).whereType().toList(); } @@ -334,9 +322,7 @@ class DataStore extends ChangeNotifier { ReadingGoal? getReadingGoal(String id) => _readingGoals[id]; List get activeGoals { - return _readingGoals.values - .where((g) => g.isActive && !g.isArchived) - .toList(); + return _readingGoals.values.where((g) => g.isActive && !g.isArchived).toList(); } List get completedGoals { @@ -363,33 +349,20 @@ class DataStore extends ChangeNotifier { // ============================================================ void addBookToShelf(String bookId, String shelfId) { - final exists = _bookShelfRelations.any( - (r) => r.bookId == bookId && r.shelfId == shelfId, - ); + final exists = _bookShelfRelations.any((r) => r.bookId == bookId && r.shelfId == shelfId); if (!exists) { - _bookShelfRelations.add( - BookShelfRelation( - bookId: bookId, - shelfId: shelfId, - addedAt: DateTime.now(), - ), - ); + _bookShelfRelations.add(BookShelfRelation(bookId: bookId, shelfId: shelfId, addedAt: DateTime.now())); notifyListeners(); } } void removeBookFromShelf(String bookId, String shelfId) { - _bookShelfRelations.removeWhere( - (r) => r.bookId == bookId && r.shelfId == shelfId, - ); + _bookShelfRelations.removeWhere((r) => r.bookId == bookId && r.shelfId == shelfId); notifyListeners(); } List getShelfIdsForBook(String bookId) { - return _bookShelfRelations - .where((r) => r.bookId == bookId) - .map((r) => r.shelfId) - .toList(); + return _bookShelfRelations.where((r) => r.bookId == bookId).map((r) => r.shelfId).toList(); } List getShelvesForBook(String bookId) { @@ -402,33 +375,20 @@ class DataStore extends ChangeNotifier { // ============================================================ void addTagToBook(String bookId, String tagId) { - final exists = _bookTagRelations.any( - (r) => r.bookId == bookId && r.tagId == tagId, - ); + final exists = _bookTagRelations.any((r) => r.bookId == bookId && r.tagId == tagId); if (!exists) { - _bookTagRelations.add( - BookTagRelation( - bookId: bookId, - tagId: tagId, - createdAt: DateTime.now(), - ), - ); + _bookTagRelations.add(BookTagRelation(bookId: bookId, tagId: tagId, createdAt: DateTime.now())); notifyListeners(); } } void removeTagFromBook(String bookId, String tagId) { - _bookTagRelations.removeWhere( - (r) => r.bookId == bookId && r.tagId == tagId, - ); + _bookTagRelations.removeWhere((r) => r.bookId == bookId && r.tagId == tagId); notifyListeners(); } List getTagIdsForBook(String bookId) { - return _bookTagRelations - .where((r) => r.bookId == bookId) - .map((r) => r.tagId) - .toList(); + return _bookTagRelations.where((r) => r.bookId == bookId).map((r) => r.tagId).toList(); } List getTagsForBook(String bookId) { diff --git a/app/lib/data/sample_data.dart b/app/lib/data/sample_data.dart index 0e0042b..e92cbac 100644 --- a/app/lib/data/sample_data.dart +++ b/app/lib/data/sample_data.dart @@ -15,10 +15,8 @@ import 'package:papyrus/models/tag.dart'; class SampleData { SampleData._(); - static DateTime _daysAgo(int days) => - DateTime.now().subtract(Duration(days: days)); - static DateTime _hoursAgo(int hours) => - DateTime.now().subtract(Duration(hours: hours)); + static DateTime _daysAgo(int days) => DateTime.now().subtract(Duration(days: days)); + static DateTime _hoursAgo(int hours) => DateTime.now().subtract(Duration(hours: hours)); // ============================================================ // Books (15 books) @@ -241,8 +239,7 @@ class SampleData { publisher: 'Avery', language: 'en', pageCount: 320, - description: - 'No matter your goals, Atomic Habits offers a proven framework for improving—every day.', + description: 'No matter your goals, Atomic Habits offers a proven framework for improving—every day.', coverUrl: 'https://images-na.ssl-images-amazon.com/images/S/compressed.photo.goodreads.com/books/1655988385i/40121378.jpg', isPhysical: true, @@ -513,8 +510,7 @@ class SampleData { Series( id: 'series-1', name: 'Dune', - description: - 'The epic science fiction saga set on the desert planet Arrakis', + description: 'The epic science fiction saga set on the desert planet Arrakis', author: 'Frank Herbert', totalBooks: 6, isComplete: true, @@ -551,175 +547,51 @@ class SampleData { static List get bookShelfRelations { return [ // Currently reading (shelf-1) - BookShelfRelation( - bookId: 'book-1', - shelfId: 'shelf-1', - addedAt: _daysAgo(30), - ), - BookShelfRelation( - bookId: 'book-3', - shelfId: 'shelf-1', - addedAt: _daysAgo(14), - ), - BookShelfRelation( - bookId: 'book-7', - shelfId: 'shelf-1', - addedAt: _daysAgo(30), - ), - BookShelfRelation( - bookId: 'book-8', - shelfId: 'shelf-1', - addedAt: _daysAgo(21), - ), + BookShelfRelation(bookId: 'book-1', shelfId: 'shelf-1', addedAt: _daysAgo(30)), + BookShelfRelation(bookId: 'book-3', shelfId: 'shelf-1', addedAt: _daysAgo(14)), + BookShelfRelation(bookId: 'book-7', shelfId: 'shelf-1', addedAt: _daysAgo(30)), + BookShelfRelation(bookId: 'book-8', shelfId: 'shelf-1', addedAt: _daysAgo(21)), // Want to read (shelf-2) - BookShelfRelation( - bookId: 'book-6', - shelfId: 'shelf-2', - addedAt: _daysAgo(30), - ), - BookShelfRelation( - bookId: 'book-9', - shelfId: 'shelf-2', - addedAt: _daysAgo(14), - ), - BookShelfRelation( - bookId: 'book-12', - shelfId: 'shelf-2', - addedAt: _daysAgo(7), - ), - BookShelfRelation( - bookId: 'book-13', - shelfId: 'shelf-2', - addedAt: _daysAgo(7), - ), - BookShelfRelation( - bookId: 'book-14', - shelfId: 'shelf-2', - addedAt: _daysAgo(7), - ), - BookShelfRelation( - bookId: 'book-15', - shelfId: 'shelf-2', - addedAt: _daysAgo(30), - ), + BookShelfRelation(bookId: 'book-6', shelfId: 'shelf-2', addedAt: _daysAgo(30)), + BookShelfRelation(bookId: 'book-9', shelfId: 'shelf-2', addedAt: _daysAgo(14)), + BookShelfRelation(bookId: 'book-12', shelfId: 'shelf-2', addedAt: _daysAgo(7)), + BookShelfRelation(bookId: 'book-13', shelfId: 'shelf-2', addedAt: _daysAgo(7)), + BookShelfRelation(bookId: 'book-14', shelfId: 'shelf-2', addedAt: _daysAgo(7)), + BookShelfRelation(bookId: 'book-15', shelfId: 'shelf-2', addedAt: _daysAgo(30)), // Finished (shelf-3) - BookShelfRelation( - bookId: 'book-2', - shelfId: 'shelf-3', - addedAt: _daysAgo(60), - ), - BookShelfRelation( - bookId: 'book-4', - shelfId: 'shelf-3', - addedAt: _daysAgo(280), - ), - BookShelfRelation( - bookId: 'book-10', - shelfId: 'shelf-3', - addedAt: _daysAgo(150), - ), + BookShelfRelation(bookId: 'book-2', shelfId: 'shelf-3', addedAt: _daysAgo(60)), + BookShelfRelation(bookId: 'book-4', shelfId: 'shelf-3', addedAt: _daysAgo(280)), + BookShelfRelation(bookId: 'book-10', shelfId: 'shelf-3', addedAt: _daysAgo(150)), // Technical (shelf-4) - BookShelfRelation( - bookId: 'book-1', - shelfId: 'shelf-4', - addedAt: _daysAgo(90), - ), - BookShelfRelation( - bookId: 'book-2', - shelfId: 'shelf-4', - addedAt: _daysAgo(180), - ), - BookShelfRelation( - bookId: 'book-5', - shelfId: 'shelf-4', - addedAt: _daysAgo(45), - ), - BookShelfRelation( - bookId: 'book-7', - shelfId: 'shelf-4', - addedAt: _daysAgo(60), - ), - BookShelfRelation( - bookId: 'book-11', - shelfId: 'shelf-4', - addedAt: _daysAgo(90), - ), + BookShelfRelation(bookId: 'book-1', shelfId: 'shelf-4', addedAt: _daysAgo(90)), + BookShelfRelation(bookId: 'book-2', shelfId: 'shelf-4', addedAt: _daysAgo(180)), + BookShelfRelation(bookId: 'book-5', shelfId: 'shelf-4', addedAt: _daysAgo(45)), + BookShelfRelation(bookId: 'book-7', shelfId: 'shelf-4', addedAt: _daysAgo(60)), + BookShelfRelation(bookId: 'book-11', shelfId: 'shelf-4', addedAt: _daysAgo(90)), // Fiction (shelf-5) - BookShelfRelation( - bookId: 'book-3', - shelfId: 'shelf-5', - addedAt: _daysAgo(60), - ), - BookShelfRelation( - bookId: 'book-4', - shelfId: 'shelf-5', - addedAt: _daysAgo(365), - ), - BookShelfRelation( - bookId: 'book-6', - shelfId: 'shelf-5', - addedAt: _daysAgo(30), - ), - BookShelfRelation( - bookId: 'book-15', - shelfId: 'shelf-5', - addedAt: _daysAgo(30), - ), + BookShelfRelation(bookId: 'book-3', shelfId: 'shelf-5', addedAt: _daysAgo(60)), + BookShelfRelation(bookId: 'book-4', shelfId: 'shelf-5', addedAt: _daysAgo(365)), + BookShelfRelation(bookId: 'book-6', shelfId: 'shelf-5', addedAt: _daysAgo(30)), + BookShelfRelation(bookId: 'book-15', shelfId: 'shelf-5', addedAt: _daysAgo(30)), // Sci-Fi (shelf-6) - BookShelfRelation( - bookId: 'book-3', - shelfId: 'shelf-6', - addedAt: _daysAgo(60), - ), - BookShelfRelation( - bookId: 'book-9', - shelfId: 'shelf-6', - addedAt: _daysAgo(14), - ), - BookShelfRelation( - bookId: 'book-12', - shelfId: 'shelf-6', - addedAt: _daysAgo(7), - ), - BookShelfRelation( - bookId: 'book-13', - shelfId: 'shelf-6', - addedAt: _daysAgo(7), - ), - BookShelfRelation( - bookId: 'book-14', - shelfId: 'shelf-6', - addedAt: _daysAgo(7), - ), + BookShelfRelation(bookId: 'book-3', shelfId: 'shelf-6', addedAt: _daysAgo(60)), + BookShelfRelation(bookId: 'book-9', shelfId: 'shelf-6', addedAt: _daysAgo(14)), + BookShelfRelation(bookId: 'book-12', shelfId: 'shelf-6', addedAt: _daysAgo(7)), + BookShelfRelation(bookId: 'book-13', shelfId: 'shelf-6', addedAt: _daysAgo(7)), + BookShelfRelation(bookId: 'book-14', shelfId: 'shelf-6', addedAt: _daysAgo(7)), // Non-Fiction (shelf-7) - BookShelfRelation( - bookId: 'book-8', - shelfId: 'shelf-7', - addedAt: _daysAgo(45), - ), - BookShelfRelation( - bookId: 'book-10', - shelfId: 'shelf-7', - addedAt: _daysAgo(200), - ), + BookShelfRelation(bookId: 'book-8', shelfId: 'shelf-7', addedAt: _daysAgo(45)), + BookShelfRelation(bookId: 'book-10', shelfId: 'shelf-7', addedAt: _daysAgo(200)), // Reference (shelf-8) - BookShelfRelation( - bookId: 'book-5', - shelfId: 'shelf-8', - addedAt: _daysAgo(45), - ), - BookShelfRelation( - bookId: 'book-11', - shelfId: 'shelf-8', - addedAt: _daysAgo(90), - ), + BookShelfRelation(bookId: 'book-5', shelfId: 'shelf-8', addedAt: _daysAgo(45)), + BookShelfRelation(bookId: 'book-11', shelfId: 'shelf-8', addedAt: _daysAgo(90)), ]; } @@ -730,99 +602,31 @@ class SampleData { static List get bookTagRelations { return [ // Programming (tag-1) - BookTagRelation( - bookId: 'book-1', - tagId: 'tag-1', - createdAt: _daysAgo(90), - ), - BookTagRelation( - bookId: 'book-2', - tagId: 'tag-1', - createdAt: _daysAgo(180), - ), - BookTagRelation( - bookId: 'book-5', - tagId: 'tag-1', - createdAt: _daysAgo(45), - ), - BookTagRelation( - bookId: 'book-7', - tagId: 'tag-1', - createdAt: _daysAgo(60), - ), - BookTagRelation( - bookId: 'book-11', - tagId: 'tag-1', - createdAt: _daysAgo(90), - ), + BookTagRelation(bookId: 'book-1', tagId: 'tag-1', createdAt: _daysAgo(90)), + BookTagRelation(bookId: 'book-2', tagId: 'tag-1', createdAt: _daysAgo(180)), + BookTagRelation(bookId: 'book-5', tagId: 'tag-1', createdAt: _daysAgo(45)), + BookTagRelation(bookId: 'book-7', tagId: 'tag-1', createdAt: _daysAgo(60)), + BookTagRelation(bookId: 'book-11', tagId: 'tag-1', createdAt: _daysAgo(90)), // Science Fiction (tag-2) - BookTagRelation( - bookId: 'book-3', - tagId: 'tag-2', - createdAt: _daysAgo(60), - ), - BookTagRelation( - bookId: 'book-9', - tagId: 'tag-2', - createdAt: _daysAgo(14), - ), - BookTagRelation( - bookId: 'book-12', - tagId: 'tag-2', - createdAt: _daysAgo(7), - ), - BookTagRelation( - bookId: 'book-13', - tagId: 'tag-2', - createdAt: _daysAgo(7), - ), - BookTagRelation( - bookId: 'book-14', - tagId: 'tag-2', - createdAt: _daysAgo(7), - ), + BookTagRelation(bookId: 'book-3', tagId: 'tag-2', createdAt: _daysAgo(60)), + BookTagRelation(bookId: 'book-9', tagId: 'tag-2', createdAt: _daysAgo(14)), + BookTagRelation(bookId: 'book-12', tagId: 'tag-2', createdAt: _daysAgo(7)), + BookTagRelation(bookId: 'book-13', tagId: 'tag-2', createdAt: _daysAgo(7)), + BookTagRelation(bookId: 'book-14', tagId: 'tag-2', createdAt: _daysAgo(7)), // Classic (tag-3) - BookTagRelation( - bookId: 'book-3', - tagId: 'tag-3', - createdAt: _daysAgo(60), - ), - BookTagRelation( - bookId: 'book-4', - tagId: 'tag-3', - createdAt: _daysAgo(365), - ), - BookTagRelation( - bookId: 'book-6', - tagId: 'tag-3', - createdAt: _daysAgo(30), - ), - BookTagRelation( - bookId: 'book-12', - tagId: 'tag-3', - createdAt: _daysAgo(7), - ), + BookTagRelation(bookId: 'book-3', tagId: 'tag-3', createdAt: _daysAgo(60)), + BookTagRelation(bookId: 'book-4', tagId: 'tag-3', createdAt: _daysAgo(365)), + BookTagRelation(bookId: 'book-6', tagId: 'tag-3', createdAt: _daysAgo(30)), + BookTagRelation(bookId: 'book-12', tagId: 'tag-3', createdAt: _daysAgo(7)), // Productivity (tag-4) - BookTagRelation( - bookId: 'book-10', - tagId: 'tag-4', - createdAt: _daysAgo(200), - ), + BookTagRelation(bookId: 'book-10', tagId: 'tag-4', createdAt: _daysAgo(200)), // Fantasy (tag-5) - BookTagRelation( - bookId: 'book-6', - tagId: 'tag-5', - createdAt: _daysAgo(30), - ), - BookTagRelation( - bookId: 'book-15', - tagId: 'tag-5', - createdAt: _daysAgo(30), - ), + BookTagRelation(bookId: 'book-6', tagId: 'tag-5', createdAt: _daysAgo(30)), + BookTagRelation(bookId: 'book-15', tagId: 'tag-5', createdAt: _daysAgo(30)), ]; } @@ -868,11 +672,7 @@ class SampleData { selectedText: 'DRY—Don\'t Repeat Yourself. Every piece of knowledge must have a single, unambiguous, authoritative representation within a system.', color: HighlightColor.pink, - location: const BookLocation( - chapter: 2, - pageNumber: 58, - percentage: 0.19, - ), + location: const BookLocation(chapter: 2, pageNumber: 58, percentage: 0.19), createdAt: _daysAgo(20), ), @@ -880,28 +680,17 @@ class SampleData { Annotation( id: 'ann-4', bookId: 'book-2', - selectedText: - 'Clean code is simple and direct. Clean code reads like well-written prose.', + selectedText: 'Clean code is simple and direct. Clean code reads like well-written prose.', color: HighlightColor.yellow, - location: const BookLocation( - chapter: 1, - chapterTitle: 'Clean Code', - pageNumber: 12, - percentage: 0.03, - ), + location: const BookLocation(chapter: 1, chapterTitle: 'Clean Code', pageNumber: 12, percentage: 0.03), createdAt: _daysAgo(90), ), Annotation( id: 'ann-5', bookId: 'book-2', - selectedText: - 'The ratio of time spent reading versus writing is well over 10 to 1.', + selectedText: 'The ratio of time spent reading versus writing is well over 10 to 1.', color: HighlightColor.orange, - location: const BookLocation( - chapter: 1, - pageNumber: 18, - percentage: 0.05, - ), + location: const BookLocation(chapter: 1, pageNumber: 18, percentage: 0.05), note: 'This is why readability matters so much', createdAt: _daysAgo(88), ), @@ -913,11 +702,7 @@ class SampleData { selectedText: 'I must not fear. Fear is the mind-killer. Fear is the little-death that brings total obliteration.', color: HighlightColor.purple, - location: const BookLocation( - chapter: 1, - pageNumber: 8, - percentage: 0.01, - ), + location: const BookLocation(chapter: 1, pageNumber: 8, percentage: 0.01), note: 'The Litany Against Fear - iconic!', createdAt: _daysAgo(12), ), diff --git a/app/lib/forms/login_form.dart b/app/lib/forms/login_form.dart index 5412bde..c69d473 100644 --- a/app/lib/forms/login_form.dart +++ b/app/lib/forms/login_form.dart @@ -1,10 +1,11 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; +import 'package:papyrus/providers/auth_provider.dart'; import 'package:papyrus/widgets/buttons/google_sign_in.dart'; import 'package:papyrus/widgets/input/email_input.dart'; import 'package:papyrus/widgets/input/password_input.dart'; import 'package:papyrus/widgets/titled_divider.dart'; -import 'package:supabase_flutter/supabase_flutter.dart'; +import 'package:provider/provider.dart'; class LoginForm extends StatefulWidget { const LoginForm({super.key}); @@ -17,13 +18,11 @@ class _LoginForm extends State { bool isLoginDisabled = false; final formKey = GlobalKey(); - final emailController = TextEditingController( - text: "karolis.strazdas.sso@gmail.com", - ); + final emailController = TextEditingController(text: "karolis.strazdas.sso@gmail.com"); final passwordController = TextEditingController(text: ""); - Future signIn() async { - return Supabase.instance.client.auth.signInWithPassword( + Future signIn() async { + return context.read().login( email: emailController.text.trim(), password: passwordController.text.trim(), ); @@ -38,21 +37,28 @@ class _LoginForm extends State { showDialog( context: context, barrierDismissible: false, - builder: (context) => const Center( - child: SizedBox( - width: 150, - height: 150, - child: CircularProgressIndicator(strokeWidth: 8), - ), - ), + builder: (context) => + const Center(child: SizedBox(width: 150, height: 150, child: CircularProgressIndicator(strokeWidth: 8))), ); try { - await signIn(); + final success = await signIn(); if (!mounted) return; setState(() => isLoginDisabled = false); Navigator.of(context).pop(); - context.goNamed('LIBRARY'); + + if (success) { + context.goNamed('LIBRARY'); + return; + } + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + duration: const Duration(seconds: 5), + content: Text(context.read().error ?? 'Incorrect username or password.'), + backgroundColor: Theme.of(context).colorScheme.error, + ), + ); } catch (e) { if (!mounted) return; setState(() => isLoginDisabled = false); @@ -79,24 +85,15 @@ class _LoginForm extends State { children: [ Align( alignment: Alignment.centerLeft, - child: Text( - "Sign in", - style: Theme.of(context).textTheme.headlineMedium, - ), + child: Text("Sign in", style: Theme.of(context).textTheme.headlineMedium), ), const SizedBox(height: 16), EmailInput(labelText: "Email address", controller: emailController), const SizedBox(height: 24), - PasswordInput( - labelText: "Password", - controller: passwordController, - ), + PasswordInput(labelText: "Password", controller: passwordController), Align( alignment: Alignment.centerRight, - child: TextButton( - onPressed: () {}, - child: const Text("Forgot your password?"), - ), + child: TextButton(onPressed: () {}, child: const Text("Forgot your password?")), ), ElevatedButton( onPressed: isLoginDisabled ? null : _handleLogin, @@ -104,14 +101,7 @@ class _LoginForm extends State { minimumSize: WidgetStatePropertyAll(Size.fromHeight(50)), elevation: WidgetStatePropertyAll(2.0), ), - child: const Row( - children: [ - Spacer(), - Text("Continue"), - Spacer(), - Icon(Icons.arrow_right), - ], - ), + child: const Row(children: [Spacer(), Text("Continue"), Spacer(), Icon(Icons.arrow_right)]), ), Expanded( child: Column( @@ -124,10 +114,7 @@ class _LoginForm extends State { mainAxisAlignment: MainAxisAlignment.center, children: [ const Text("Don't have an account?"), - TextButton( - onPressed: () => context.go("/register"), - child: const Text("Sign up"), - ), + TextButton(onPressed: () => context.go("/register"), child: const Text("Sign up")), ], ), ], diff --git a/app/lib/forms/register_form.dart b/app/lib/forms/register_form.dart index 426197b..379cf72 100644 --- a/app/lib/forms/register_form.dart +++ b/app/lib/forms/register_form.dart @@ -1,10 +1,12 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; +import 'package:papyrus/providers/auth_provider.dart'; import 'package:papyrus/widgets/buttons/google_sign_in.dart'; import 'package:papyrus/widgets/input/email_input.dart'; +import 'package:papyrus/widgets/input/name_input.dart'; import 'package:papyrus/widgets/input/password_input.dart'; import 'package:papyrus/widgets/titled_divider.dart'; -import 'package:supabase_flutter/supabase_flutter.dart'; +import 'package:provider/provider.dart'; class RegisterForm extends StatefulWidget { const RegisterForm({super.key}); @@ -17,12 +19,14 @@ class _RegisterForm extends State { bool isRegisterDisabled = false; final formKey = GlobalKey(); + final displayNameController = TextEditingController(); final emailController = TextEditingController(); final passwordController = TextEditingController(); final repeatPasswordController = TextEditingController(); - Future signUp() async { - return Supabase.instance.client.auth.signUp( + Future signUp() async { + return context.read().register( + displayName: displayNameController.text.trim(), email: emailController.text.trim(), password: passwordController.text, ); @@ -37,34 +41,36 @@ class _RegisterForm extends State { showDialog( context: context, barrierDismissible: false, - builder: (context) => const Center( - child: SizedBox( - width: 150, - height: 150, - child: CircularProgressIndicator(strokeWidth: 8), - ), - ), + builder: (context) => + const Center(child: SizedBox(width: 150, height: 150, child: CircularProgressIndicator(strokeWidth: 8))), ); try { - await signUp(); + final success = await signUp(); if (!mounted) return; setState(() => isRegisterDisabled = false); Navigator.of(context).pop(); - context.goNamed("LIBRARY"); + + if (success) { + context.goNamed("LIBRARY"); + return; + } + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + duration: const Duration(seconds: 5), + content: Text(context.read().error ?? "Account creation failed."), + backgroundColor: Theme.of(context).colorScheme.error, + ), + ); } catch (e) { if (!mounted) return; setState(() => isRegisterDisabled = false); Navigator.of(context).pop(); - var errorMessage = "Account creation failed."; - if (e is AuthException && - e.message.toLowerCase().contains('user already registered')) { - errorMessage = "Account with this email already exists."; - } ScaffoldMessenger.of(context).showSnackBar( SnackBar( duration: const Duration(seconds: 5), - content: Text(errorMessage), + content: const Text("Account creation failed."), backgroundColor: Theme.of(context).colorScheme.error, ), ); @@ -83,18 +89,14 @@ class _RegisterForm extends State { children: [ Align( alignment: Alignment.centerLeft, - child: Text( - "Sign up", - style: Theme.of(context).textTheme.headlineMedium, - ), + child: Text("Sign up", style: Theme.of(context).textTheme.headlineMedium), ), const SizedBox(height: 16), + NameInput(labelText: "Display name", controller: displayNameController), + const SizedBox(height: 24), EmailInput(labelText: "Email address", controller: emailController), const SizedBox(height: 24), - PasswordInput( - labelText: "Password", - controller: passwordController, - ), + PasswordInput(labelText: "Password", controller: passwordController), const SizedBox(height: 24), PasswordInput( labelText: "Repeat password", @@ -113,14 +115,7 @@ class _RegisterForm extends State { minimumSize: WidgetStatePropertyAll(Size.fromHeight(46)), elevation: WidgetStatePropertyAll(2.0), ), - child: const Row( - children: [ - Spacer(), - Text("Continue"), - Spacer(), - Icon(Icons.arrow_right), - ], - ), + child: const Row(children: [Spacer(), Text("Continue"), Spacer(), Icon(Icons.arrow_right)]), ), Expanded( child: Column( @@ -133,10 +128,7 @@ class _RegisterForm extends State { mainAxisAlignment: MainAxisAlignment.center, children: [ const Text("Already have an account?"), - TextButton( - onPressed: () => context.go("/login"), - child: const Text("Sign in"), - ), + TextButton(onPressed: () => context.go("/login"), child: const Text("Sign in")), ], ), ], @@ -147,4 +139,13 @@ class _RegisterForm extends State { ), ); } + + @override + void dispose() { + displayNameController.dispose(); + emailController.dispose(); + passwordController.dispose(); + repeatPasswordController.dispose(); + super.dispose(); + } } diff --git a/app/lib/main.dart b/app/lib/main.dart index 1ac9374..f488da0 100644 --- a/app/lib/main.dart +++ b/app/lib/main.dart @@ -1,4 +1,8 @@ import 'package:flutter/material.dart'; +import 'package:papyrus/auth/auth_api_client.dart'; +import 'package:papyrus/auth/auth_repository.dart'; +import 'package:papyrus/auth/papyrus_api_config.dart'; +import 'package:papyrus/auth/token_store.dart'; import 'package:papyrus/data/data_store.dart'; import 'package:papyrus/data/sample_data.dart'; import 'package:papyrus/providers/auth_provider.dart'; @@ -8,16 +12,11 @@ import 'package:papyrus/providers/sidebar_provider.dart'; import 'package:papyrus/themes/app_theme.dart'; import 'package:provider/provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import 'package:supabase_flutter/supabase_flutter.dart'; import 'config/app_router.dart'; Future main() async { WidgetsFlutterBinding.ensureInitialized(); - await Supabase.initialize( - url: const String.fromEnvironment('SUPABASE_URL'), - anonKey: const String.fromEnvironment('SUPABASE_ANON_KEY'), - ); final prefs = await SharedPreferences.getInstance(); runApp(Papyrus(prefs: prefs)); } @@ -32,7 +31,29 @@ class Papyrus extends StatefulWidget { } class _PapyrusState extends State { - late final AppRouter _appRouter = AppRouter(); + late final AuthProvider _authProvider; + late final AppRouter _appRouter; + + @override + void initState() { + super.initState(); + + final apiConfig = PapyrusApiConfig.fromEnvironment(); + final tokenStore = TokenStore(const SecureRefreshTokenStorage()); + final authRepository = AuthRepository( + apiClient: AuthApiClient(config: apiConfig), + tokenStore: tokenStore, + ); + + _authProvider = AuthProvider(widget.prefs, repository: authRepository); + _appRouter = AppRouter(authProvider: _authProvider); + } + + @override + void dispose() { + _authProvider.dispose(); + super.dispose(); + } @override Widget build(BuildContext context) { @@ -56,12 +77,10 @@ class _PapyrusState extends State { ), ), // Auth and UI state providers - ChangeNotifierProvider(create: (_) => AuthProvider(widget.prefs)), + ChangeNotifierProvider.value(value: _authProvider), ChangeNotifierProvider(create: (_) => SidebarProvider()), ChangeNotifierProvider(create: (_) => LibraryProvider()), - ChangeNotifierProvider( - create: (_) => PreferencesProvider(widget.prefs), - ), + ChangeNotifierProvider(create: (_) => PreferencesProvider(widget.prefs)), ], child: Consumer( builder: (context, preferencesProvider, child) { diff --git a/app/lib/models/active_filter.dart b/app/lib/models/active_filter.dart index e08d054..ef9c0c1 100644 --- a/app/lib/models/active_filter.dart +++ b/app/lib/models/active_filter.dart @@ -16,21 +16,12 @@ class ActiveFilter { /// Icon to display with the filter final String? iconName; - const ActiveFilter({ - required this.type, - required this.label, - required this.value, - this.queryString, - this.iconName, - }); + const ActiveFilter({required this.type, required this.label, required this.value, this.queryString, this.iconName}); @override bool operator ==(Object other) { if (identical(this, other)) return true; - return other is ActiveFilter && - other.type == type && - other.label == label && - other.value == value; + return other is ActiveFilter && other.type == type && other.label == label && other.value == value; } @override diff --git a/app/lib/models/annotation.dart b/app/lib/models/annotation.dart index eba8be5..60e6104 100644 --- a/app/lib/models/annotation.dart +++ b/app/lib/models/annotation.dart @@ -67,12 +67,7 @@ class BookLocation { final int pageNumber; final double? percentage; - const BookLocation({ - this.chapter, - this.chapterTitle, - required this.pageNumber, - this.percentage, - }); + const BookLocation({this.chapter, this.chapterTitle, required this.pageNumber, this.percentage}); /// Get a display-friendly location string. String get displayLocation { @@ -93,12 +88,7 @@ class BookLocation { return 'Page $pageNumber'; } - BookLocation copyWith({ - int? chapter, - String? chapterTitle, - int? pageNumber, - double? percentage, - }) { + BookLocation copyWith({int? chapter, String? chapterTitle, int? pageNumber, double? percentage}) { return BookLocation( chapter: chapter ?? this.chapter, chapterTitle: chapterTitle ?? this.chapterTitle, @@ -190,9 +180,7 @@ class Annotation { ), note: json['note'] as String?, createdAt: DateTime.parse(json['created_at'] as String), - updatedAt: json['updated_at'] != null - ? DateTime.parse(json['updated_at'] as String) - : null, + updatedAt: json['updated_at'] != null ? DateTime.parse(json['updated_at'] as String) : null, ); } @@ -254,11 +242,7 @@ class Annotation { selectedText: 'DRY—Don\'t Repeat Yourself. Every piece of knowledge must have a single, unambiguous, authoritative representation within a system.', color: HighlightColor.pink, - location: const BookLocation( - chapter: 2, - pageNumber: 58, - percentage: 0.19, - ), + location: const BookLocation(chapter: 2, pageNumber: 58, percentage: 0.19), createdAt: now.subtract(const Duration(days: 2)), ), ]; @@ -268,28 +252,17 @@ class Annotation { Annotation( id: 'ann-2-1', bookId: bookId, - selectedText: - 'Clean code is simple and direct. Clean code reads like well-written prose.', + selectedText: 'Clean code is simple and direct. Clean code reads like well-written prose.', color: HighlightColor.yellow, - location: const BookLocation( - chapter: 1, - chapterTitle: 'Clean Code', - pageNumber: 12, - percentage: 0.03, - ), + location: const BookLocation(chapter: 1, chapterTitle: 'Clean Code', pageNumber: 12, percentage: 0.03), createdAt: now.subtract(const Duration(days: 10)), ), Annotation( id: 'ann-2-2', bookId: bookId, - selectedText: - 'The ratio of time spent reading versus writing is well over 10 to 1.', + selectedText: 'The ratio of time spent reading versus writing is well over 10 to 1.', color: HighlightColor.orange, - location: const BookLocation( - chapter: 1, - pageNumber: 18, - percentage: 0.05, - ), + location: const BookLocation(chapter: 1, pageNumber: 18, percentage: 0.05), note: 'This is why readability matters so much', createdAt: now.subtract(const Duration(days: 9)), ), @@ -302,12 +275,7 @@ class Annotation { bookId: bookId, selectedText: 'Program to an interface, not an implementation.', color: HighlightColor.purple, - location: const BookLocation( - chapter: 1, - chapterTitle: 'Introduction', - pageNumber: 32, - percentage: 0.08, - ), + location: const BookLocation(chapter: 1, chapterTitle: 'Introduction', pageNumber: 32, percentage: 0.08), note: 'Fundamental principle of OOP', createdAt: now.subtract(const Duration(days: 20)), ), diff --git a/app/lib/models/book.dart b/app/lib/models/book.dart index 01d51e4..8da3fa8 100644 --- a/app/lib/models/book.dart +++ b/app/lib/models/book.dart @@ -313,36 +313,24 @@ class Book { title: json['title'] as String, subtitle: json['subtitle'] as String?, author: json['author'] as String, - coAuthors: - (json['co_authors'] as List?) - ?.map((e) => e as String) - .toList() ?? - [], + coAuthors: (json['co_authors'] as List?)?.map((e) => e as String).toList() ?? [], isbn: json['isbn'] as String?, isbn13: json['isbn13'] as String?, - publicationDate: json['publication_date'] != null - ? DateTime.parse(json['publication_date'] as String) - : null, + publicationDate: json['publication_date'] != null ? DateTime.parse(json['publication_date'] as String) : null, publisher: json['publisher'] as String?, language: json['language'] as String?, pageCount: json['page_count'] as int?, description: json['description'] as String?, coverUrl: json['cover_image_url'] as String?, filePath: json['file_path'] as String?, - fileFormat: json['file_format'] != null - ? BookFormat.values.byName(json['file_format'] as String) - : null, + fileFormat: json['file_format'] != null ? BookFormat.values.byName(json['file_format'] as String) : null, fileSize: json['file_size'] as int?, fileHash: json['file_hash'] as String?, isPhysical: json['is_physical'] as bool? ?? false, physicalLocation: json['physical_location'] as String?, lentTo: json['lent_to'] as String?, - lentAt: json['lent_at'] != null - ? DateTime.parse(json['lent_at'] as String) - : null, - readingStatus: ReadingStatus.values.byName( - json['reading_status'] as String? ?? 'notStarted', - ), + lentAt: json['lent_at'] != null ? DateTime.parse(json['lent_at'] as String) : null, + readingStatus: ReadingStatus.values.byName(json['reading_status'] as String? ?? 'notStarted'), currentPage: json['current_page'] as int?, currentPosition: (json['current_position'] as num?)?.toDouble() ?? 0.0, currentCfi: json['current_cfi'] as String?, @@ -353,15 +341,9 @@ class Book { seriesName: json['series_name'] as String?, seriesNumber: (json['series_number'] as num?)?.toDouble(), addedAt: DateTime.parse(json['added_at'] as String), - startedAt: json['started_at'] != null - ? DateTime.parse(json['started_at'] as String) - : null, - completedAt: json['completed_at'] != null - ? DateTime.parse(json['completed_at'] as String) - : null, - lastReadAt: json['last_read_at'] != null - ? DateTime.parse(json['last_read_at'] as String) - : null, + startedAt: json['started_at'] != null ? DateTime.parse(json['started_at'] as String) : null, + completedAt: json['completed_at'] != null ? DateTime.parse(json['completed_at'] as String) : null, + lastReadAt: json['last_read_at'] != null ? DateTime.parse(json['last_read_at'] as String) : null, ); } } diff --git a/app/lib/models/book_shelf_relation.dart b/app/lib/models/book_shelf_relation.dart index 9b97217..9530b0c 100644 --- a/app/lib/models/book_shelf_relation.dart +++ b/app/lib/models/book_shelf_relation.dart @@ -5,20 +5,10 @@ class BookShelfRelation { final DateTime addedAt; final int sortOrder; - const BookShelfRelation({ - required this.bookId, - required this.shelfId, - required this.addedAt, - this.sortOrder = 0, - }); + const BookShelfRelation({required this.bookId, required this.shelfId, required this.addedAt, this.sortOrder = 0}); /// Create a copy with updated fields. - BookShelfRelation copyWith({ - String? bookId, - String? shelfId, - DateTime? addedAt, - int? sortOrder, - }) { + BookShelfRelation copyWith({String? bookId, String? shelfId, DateTime? addedAt, int? sortOrder}) { return BookShelfRelation( bookId: bookId ?? this.bookId, shelfId: shelfId ?? this.shelfId, @@ -29,12 +19,7 @@ class BookShelfRelation { /// Convert to JSON for API/storage. Map toJson() { - return { - 'book_id': bookId, - 'shelf_id': shelfId, - 'added_at': addedAt.toIso8601String(), - 'sort_order': sortOrder, - }; + return {'book_id': bookId, 'shelf_id': shelfId, 'added_at': addedAt.toIso8601String(), 'sort_order': sortOrder}; } /// Create from JSON. @@ -50,9 +35,7 @@ class BookShelfRelation { @override bool operator ==(Object other) { if (identical(this, other)) return true; - return other is BookShelfRelation && - other.bookId == bookId && - other.shelfId == shelfId; + return other is BookShelfRelation && other.bookId == bookId && other.shelfId == shelfId; } @override diff --git a/app/lib/models/book_tag_relation.dart b/app/lib/models/book_tag_relation.dart index c33d55f..7c96909 100644 --- a/app/lib/models/book_tag_relation.dart +++ b/app/lib/models/book_tag_relation.dart @@ -4,18 +4,10 @@ class BookTagRelation { final String tagId; final DateTime createdAt; - const BookTagRelation({ - required this.bookId, - required this.tagId, - required this.createdAt, - }); + const BookTagRelation({required this.bookId, required this.tagId, required this.createdAt}); /// Create a copy with updated fields. - BookTagRelation copyWith({ - String? bookId, - String? tagId, - DateTime? createdAt, - }) { + BookTagRelation copyWith({String? bookId, String? tagId, DateTime? createdAt}) { return BookTagRelation( bookId: bookId ?? this.bookId, tagId: tagId ?? this.tagId, @@ -25,11 +17,7 @@ class BookTagRelation { /// Convert to JSON for API/storage. Map toJson() { - return { - 'book_id': bookId, - 'tag_id': tagId, - 'created_at': createdAt.toIso8601String(), - }; + return {'book_id': bookId, 'tag_id': tagId, 'created_at': createdAt.toIso8601String()}; } /// Create from JSON. @@ -44,9 +32,7 @@ class BookTagRelation { @override bool operator ==(Object other) { if (identical(this, other)) return true; - return other is BookTagRelation && - other.bookId == bookId && - other.tagId == tagId; + return other is BookTagRelation && other.bookId == bookId && other.tagId == tagId; } @override diff --git a/app/lib/models/bookmark.dart b/app/lib/models/bookmark.dart index 55aa519..1835f55 100644 --- a/app/lib/models/bookmark.dart +++ b/app/lib/models/bookmark.dart @@ -71,12 +71,8 @@ class Bookmark { id: id ?? this.id, bookId: bookId ?? this.bookId, position: position ?? this.position, - pageNumber: identical(pageNumber, _sentinel) - ? this.pageNumber - : pageNumber as int?, - chapterTitle: identical(chapterTitle, _sentinel) - ? this.chapterTitle - : chapterTitle as String?, + pageNumber: identical(pageNumber, _sentinel) ? this.pageNumber : pageNumber as int?, + chapterTitle: identical(chapterTitle, _sentinel) ? this.chapterTitle : chapterTitle as String?, note: identical(note, _sentinel) ? this.note : note as String?, colorHex: colorHex ?? this.colorHex, createdAt: createdAt ?? this.createdAt, diff --git a/app/lib/models/daily_activity.dart b/app/lib/models/daily_activity.dart index ae080d9..21a1a3f 100644 --- a/app/lib/models/daily_activity.dart +++ b/app/lib/models/daily_activity.dart @@ -24,25 +24,9 @@ class DailyActivity { /// Full day name (e.g., "Monday", "Tuesday"). String get dayName => _weekdayFull[date.weekday - 1]; - static const _weekdayShort = [ - 'Mon', - 'Tue', - 'Wed', - 'Thu', - 'Fri', - 'Sat', - 'Sun', - ]; + static const _weekdayShort = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; static const _weekdayInitial = ['M', 'T', 'W', 'T', 'F', 'S', 'S']; - static const _weekdayFull = [ - 'Monday', - 'Tuesday', - 'Wednesday', - 'Thursday', - 'Friday', - 'Saturday', - 'Sunday', - ]; + static const _weekdayFull = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']; /// Formatted reading time (e.g., "45m", "1h 30m"). String get readingTimeLabel { @@ -59,18 +43,11 @@ class DailyActivity { /// Whether this is today. bool get isToday { final now = DateTime.now(); - return date.year == now.year && - date.month == now.month && - date.day == now.day; + return date.year == now.year && date.month == now.month && date.day == now.day; } /// Create a copy with updated fields. - DailyActivity copyWith({ - DateTime? date, - int? readingMinutes, - int? pagesRead, - List? booksRead, - }) { + DailyActivity copyWith({DateTime? date, int? readingMinutes, int? pagesRead, List? booksRead}) { return DailyActivity( date: date ?? this.date, readingMinutes: readingMinutes ?? this.readingMinutes, @@ -85,42 +62,17 @@ class DailyActivity { final monday = now.subtract(Duration(days: now.weekday - 1)); return [ - DailyActivity( - date: monday, - readingMinutes: 45, - pagesRead: 32, - booksRead: ['1'], - ), + DailyActivity(date: monday, readingMinutes: 45, pagesRead: 32, booksRead: ['1']), DailyActivity( date: monday.add(const Duration(days: 1)), readingMinutes: 52, pagesRead: 38, booksRead: ['1', '3'], ), - DailyActivity( - date: monday.add(const Duration(days: 2)), - readingMinutes: 30, - pagesRead: 22, - booksRead: ['3'], - ), - DailyActivity( - date: monday.add(const Duration(days: 3)), - readingMinutes: 48, - pagesRead: 35, - booksRead: ['3'], - ), - DailyActivity( - date: monday.add(const Duration(days: 4)), - readingMinutes: 15, - pagesRead: 10, - booksRead: ['3'], - ), - DailyActivity( - date: monday.add(const Duration(days: 5)), - readingMinutes: 0, - pagesRead: 0, - booksRead: [], - ), + DailyActivity(date: monday.add(const Duration(days: 2)), readingMinutes: 30, pagesRead: 22, booksRead: ['3']), + DailyActivity(date: monday.add(const Duration(days: 3)), readingMinutes: 48, pagesRead: 35, booksRead: ['3']), + DailyActivity(date: monday.add(const Duration(days: 4)), readingMinutes: 15, pagesRead: 10, booksRead: ['3']), + DailyActivity(date: monday.add(const Duration(days: 5)), readingMinutes: 0, pagesRead: 0, booksRead: []), DailyActivity( date: monday.add(const Duration(days: 6)), readingMinutes: 55, @@ -135,21 +87,14 @@ class DailyActivity { final now = DateTime.now(); final monday = now.subtract(Duration(days: now.weekday - 1)); - return List.generate( - 7, - (index) => DailyActivity( - date: monday.add(Duration(days: index)), - readingMinutes: 0, - ), - ); + return List.generate(7, (index) => DailyActivity(date: monday.add(Duration(days: index)), readingMinutes: 0)); } } /// Extension for calculating weekly statistics. extension WeeklyActivityStats on List { /// Total reading minutes for the week. - int get totalMinutes => - fold(0, (sum, activity) => sum + activity.readingMinutes); + int get totalMinutes => fold(0, (sum, activity) => sum + activity.readingMinutes); /// Average reading minutes per day. int get averageMinutes => isEmpty ? 0 : totalMinutes ~/ length; @@ -175,7 +120,5 @@ extension WeeklyActivityStats on List { } /// Maximum reading minutes in a single day. - int get maxMinutes => isEmpty - ? 0 - : map((a) => a.readingMinutes).reduce((a, b) => a > b ? a : b); + int get maxMinutes => isEmpty ? 0 : map((a) => a.readingMinutes).reduce((a, b) => a > b ? a : b); } diff --git a/app/lib/models/genre_stats.dart b/app/lib/models/genre_stats.dart index 619bec1..dec2450 100644 --- a/app/lib/models/genre_stats.dart +++ b/app/lib/models/genre_stats.dart @@ -12,42 +12,17 @@ class GenreStats { /// Color for chart rendering (hex string). final String? colorHex; - const GenreStats({ - required this.genre, - required this.bookCount, - required this.percentage, - this.colorHex, - }); + const GenreStats({required this.genre, required this.bookCount, required this.percentage, this.colorHex}); /// Percentage as an integer (0-100). int get percentageInt => (percentage * 100).round(); /// Sample genre statistics for development and testing. static List get sample => const [ - GenreStats( - genre: 'Fiction', - bookCount: 45, - percentage: 0.45, - colorHex: '#5654A8', - ), - GenreStats( - genre: 'Non-fiction', - bookCount: 30, - percentage: 0.30, - colorHex: '#7A5368', - ), - GenreStats( - genre: 'History', - bookCount: 15, - percentage: 0.15, - colorHex: '#006B5B', - ), - GenreStats( - genre: 'Other', - bookCount: 10, - percentage: 0.10, - colorHex: '#8B8B8B', - ), + GenreStats(genre: 'Fiction', bookCount: 45, percentage: 0.45, colorHex: '#5654A8'), + GenreStats(genre: 'Non-fiction', bookCount: 30, percentage: 0.30, colorHex: '#7A5368'), + GenreStats(genre: 'History', bookCount: 15, percentage: 0.15, colorHex: '#006B5B'), + GenreStats(genre: 'Other', bookCount: 10, percentage: 0.10, colorHex: '#8B8B8B'), ]; /// Empty list for no data. diff --git a/app/lib/models/note.dart b/app/lib/models/note.dart index 756bb36..d039e34 100644 --- a/app/lib/models/note.dart +++ b/app/lib/models/note.dart @@ -39,20 +39,7 @@ class Note { /// Get formatted date string for display. String get formattedDate { final date = updatedAt ?? createdAt; - final months = [ - 'Jan', - 'Feb', - 'Mar', - 'Apr', - 'May', - 'Jun', - 'Jul', - 'Aug', - 'Sep', - 'Oct', - 'Nov', - 'Dec', - ]; + final months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; return '${months[date.month - 1]} ${date.day}, ${date.year}'; } @@ -122,14 +109,10 @@ class Note { percentage: (json['percentage'] as num?)?.toDouble(), ) : null, - tags: - (json['tags'] as List?)?.map((e) => e as String).toList() ?? - [], + tags: (json['tags'] as List?)?.map((e) => e as String).toList() ?? [], isPinned: json['is_pinned'] as bool? ?? false, createdAt: DateTime.parse(json['created_at'] as String), - updatedAt: json['updated_at'] != null - ? DateTime.parse(json['updated_at'] as String) - : null, + updatedAt: json['updated_at'] != null ? DateTime.parse(json['updated_at'] as String) : null, ); } diff --git a/app/lib/models/reading_goal.dart b/app/lib/models/reading_goal.dart index fa7ee7e..7117e0e 100644 --- a/app/lib/models/reading_goal.dart +++ b/app/lib/models/reading_goal.dart @@ -112,20 +112,7 @@ class ReadingGoal { } String _formatDateRange() { - const months = [ - 'Jan', - 'Feb', - 'Mar', - 'Apr', - 'May', - 'Jun', - 'Jul', - 'Aug', - 'Sep', - 'Oct', - 'Nov', - 'Dec', - ]; + const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; final start = '${months[startDate.month - 1]} ${startDate.day}'; final end = '${months[endDate.month - 1]} ${endDate.day}'; if (startDate.year != endDate.year) { @@ -146,9 +133,7 @@ class ReadingGoal { /// Full goal description. String get description { if (goalDescription != null) return goalDescription!; - final valueStr = type == GoalType.minutes - ? formatDuration(targetValue) - : '$targetValue $typeLabel'; + final valueStr = type == GoalType.minutes ? formatDuration(targetValue) : '$targetValue $typeLabel'; if (isCustomPeriod) return 'Read $valueStr'; return 'Read $valueStr $periodLabel'; } @@ -164,9 +149,7 @@ class ReadingGoal { if (isCompleted) { return 'Goal completed!'; } - final remainStr = type == GoalType.minutes - ? formatDuration(remaining) - : '$remaining $typeLabel'; + final remainStr = type == GoalType.minutes ? formatDuration(remaining) : '$remaining $typeLabel'; return '$remainStr to go'; } @@ -249,9 +232,7 @@ class ReadingGoal { isRecurring: json['is_recurring'] as bool? ?? true, streak: json['streak'] as int? ?? 0, isArchived: json['is_archived'] as bool? ?? false, - completedAt: json['completed_at'] != null - ? DateTime.parse(json['completed_at'] as String) - : null, + completedAt: json['completed_at'] != null ? DateTime.parse(json['completed_at'] as String) : null, ); } diff --git a/app/lib/models/reading_session.dart b/app/lib/models/reading_session.dart index 7f9dd4d..97e12b8 100644 --- a/app/lib/models/reading_session.dart +++ b/app/lib/models/reading_session.dart @@ -102,13 +102,9 @@ class ReadingSession { id: json['id'] as String, bookId: json['book_id'] as String, startTime: DateTime.parse(json['start_time'] as String), - endTime: json['end_time'] != null - ? DateTime.parse(json['end_time'] as String) - : null, + endTime: json['end_time'] != null ? DateTime.parse(json['end_time'] as String) : null, startPosition: (json['start_position'] as num).toDouble(), - endPosition: json['end_position'] != null - ? (json['end_position'] as num).toDouble() - : null, + endPosition: json['end_position'] != null ? (json['end_position'] as num).toDouble() : null, pagesRead: json['pages_read'] as int?, deviceType: json['device_type'] as String?, deviceName: json['device_name'] as String?, diff --git a/app/lib/models/reading_streak.dart b/app/lib/models/reading_streak.dart index 8f07dc8..59e88ae 100644 --- a/app/lib/models/reading_streak.dart +++ b/app/lib/models/reading_streak.dart @@ -20,8 +20,7 @@ class ReadingStreak { }); /// Percentage of days this month with reading activity. - double get monthlyPercentage => - totalDaysInMonth > 0 ? daysThisMonth / totalDaysInMonth : 0.0; + double get monthlyPercentage => totalDaysInMonth > 0 ? daysThisMonth / totalDaysInMonth : 0.0; /// Whether currently on a streak. bool get hasActiveStreak => currentStreak > 0; @@ -30,18 +29,10 @@ class ReadingStreak { bool get isCurrentBest => currentStreak >= bestStreak && currentStreak > 0; /// Sample streak data for development and testing. - static ReadingStreak get sample => const ReadingStreak( - currentStreak: 5, - bestStreak: 21, - daysThisMonth: 18, - totalDaysInMonth: 26, - ); + static ReadingStreak get sample => + const ReadingStreak(currentStreak: 5, bestStreak: 21, daysThisMonth: 18, totalDaysInMonth: 26); /// Empty streak (no reading activity). - static ReadingStreak get empty => const ReadingStreak( - currentStreak: 0, - bestStreak: 0, - daysThisMonth: 0, - totalDaysInMonth: 30, - ); + static ReadingStreak get empty => + const ReadingStreak(currentStreak: 0, bestStreak: 0, daysThisMonth: 0, totalDaysInMonth: 30); } diff --git a/app/lib/models/search_filter.dart b/app/lib/models/search_filter.dart index 46876b0..a2be0b4 100644 --- a/app/lib/models/search_filter.dart +++ b/app/lib/models/search_filter.dart @@ -7,11 +7,7 @@ class SearchFilter { final SearchOperator operator; final String value; - const SearchFilter({ - required this.field, - required this.operator, - required this.value, - }); + const SearchFilter({required this.field, required this.operator, required this.value}); /// Check if a book matches this filter. bool matches(Book book, {DataStore? dataStore}) { @@ -47,17 +43,9 @@ class SearchFilter { case SearchField.format: return book.formatLabel.toLowerCase(); case SearchField.shelf: - return dataStore - ?.getShelvesForBook(book.id) - .map((s) => s.name) - .join(',') ?? - book.shelves.join(','); + return dataStore?.getShelvesForBook(book.id).map((s) => s.name).join(',') ?? book.shelves.join(','); case SearchField.topic: - return dataStore - ?.getTagsForBook(book.id) - .map((t) => t.name) - .join(',') ?? - book.topics.join(','); + return dataStore?.getTagsForBook(book.id).map((t) => t.name).join(',') ?? book.topics.join(','); case SearchField.status: if (book.isFinished) return 'finished'; if (book.isReading) return 'reading'; @@ -106,11 +94,7 @@ class SearchQuery { final List operators; final List notFilters; - const SearchQuery({ - this.filters = const [], - this.operators = const [], - this.notFilters = const [], - }); + const SearchQuery({this.filters = const [], this.operators = const [], this.notFilters = const []}); /// Check if a book matches this query. bool matches(Book book, {DataStore? dataStore}) { @@ -127,9 +111,7 @@ class SearchQuery { for (int i = 1; i < filters.length; i++) { final filterResult = filters[i].matches(book, dataStore: dataStore); - final op = i - 1 < operators.length - ? operators[i - 1] - : LogicalOperator.and; + final op = i - 1 < operators.length ? operators[i - 1] : LogicalOperator.and; if (op == LogicalOperator.and) { result = result && filterResult; diff --git a/app/lib/models/tag.dart b/app/lib/models/tag.dart index 6ad438f..b8b826b 100644 --- a/app/lib/models/tag.dart +++ b/app/lib/models/tag.dart @@ -8,13 +8,7 @@ class Tag { final String? description; final DateTime createdAt; - const Tag({ - required this.id, - required this.name, - required this.colorHex, - this.description, - required this.createdAt, - }); + const Tag({required this.id, required this.name, required this.colorHex, this.description, required this.createdAt}); /// Get the color from hex string. Color get color { @@ -27,13 +21,7 @@ class Tag { } /// Create a copy with updated fields. - Tag copyWith({ - String? id, - String? name, - String? colorHex, - String? description, - DateTime? createdAt, - }) { + Tag copyWith({String? id, String? name, String? colorHex, String? description, DateTime? createdAt}) { return Tag( id: id ?? this.id, name: name ?? this.name, diff --git a/app/lib/pages/annotations_page.dart b/app/lib/pages/annotations_page.dart index 88d2c69..f145cd4 100644 --- a/app/lib/pages/annotations_page.dart +++ b/app/lib/pages/annotations_page.dart @@ -70,10 +70,7 @@ class _AnnotationsPageState extends State { // MOBILE LAYOUT // ============================================================================ - Widget _buildMobileLayout( - BuildContext context, - AnnotationsProvider provider, - ) { + Widget _buildMobileLayout(BuildContext context, AnnotationsProvider provider) { return Scaffold( key: _scaffoldKey, drawer: const LibraryDrawer(currentPath: '/library/annotations'), @@ -82,11 +79,7 @@ class _AnnotationsPageState extends State { children: [ // Row 1: Menu + Search + Sort Padding( - padding: const EdgeInsets.only( - top: Spacing.md, - left: Spacing.md, - right: Spacing.md, - ), + padding: const EdgeInsets.only(top: Spacing.md, left: Spacing.md, right: Spacing.md), child: Row( children: [ IconButton( @@ -122,21 +115,14 @@ class _AnnotationsPageState extends State { // DESKTOP LAYOUT // ============================================================================ - Widget _buildDesktopLayout( - BuildContext context, - AnnotationsProvider provider, - ) { + Widget _buildDesktopLayout(BuildContext context, AnnotationsProvider provider) { return Scaffold( body: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Header row Container( - padding: const EdgeInsets.only( - top: Spacing.lg, - left: Spacing.lg, - right: Spacing.lg, - ), + padding: const EdgeInsets.only(top: Spacing.lg, left: Spacing.lg, right: Spacing.lg), child: Row( children: [ Expanded(child: _buildSearchField(provider)), @@ -190,26 +176,10 @@ class _AnnotationsPageState extends State { tooltip: 'Sort annotations', onSelected: provider.setSortOption, itemBuilder: (context) => [ - _buildSortMenuItem( - AnnotationSortOption.dateNewest, - 'Newest first', - provider.sortOption, - ), - _buildSortMenuItem( - AnnotationSortOption.dateOldest, - 'Oldest first', - provider.sortOption, - ), - _buildSortMenuItem( - AnnotationSortOption.bookTitle, - 'By book title', - provider.sortOption, - ), - _buildSortMenuItem( - AnnotationSortOption.position, - 'By position', - provider.sortOption, - ), + _buildSortMenuItem(AnnotationSortOption.dateNewest, 'Newest first', provider.sortOption), + _buildSortMenuItem(AnnotationSortOption.dateOldest, 'Oldest first', provider.sortOption), + _buildSortMenuItem(AnnotationSortOption.bookTitle, 'By book title', provider.sortOption), + _buildSortMenuItem(AnnotationSortOption.position, 'By position', provider.sortOption), ], ); } @@ -224,12 +194,7 @@ class _AnnotationsPageState extends State { child: Row( children: [ Expanded(child: Text(label)), - if (option == current) - Icon( - Icons.check, - size: IconSizes.small, - color: Theme.of(context).colorScheme.primary, - ), + if (option == current) Icon(Icons.check, size: IconSizes.small, color: Theme.of(context).colorScheme.primary), ], ), ); @@ -263,10 +228,7 @@ class _AnnotationsPageState extends State { avatar: Container( width: 12, height: 12, - decoration: BoxDecoration( - color: highlightColor.accentColor, - shape: BoxShape.circle, - ), + decoration: BoxDecoration(color: highlightColor.accentColor, shape: BoxShape.circle), ), onSelected: (_) => provider.toggleColorFilter(highlightColor), ), @@ -304,10 +266,7 @@ class _AnnotationsPageState extends State { return _buildAnnotationList(context, provider); } - Widget _buildAnnotationList( - BuildContext context, - AnnotationsProvider provider, - ) { + Widget _buildAnnotationList(BuildContext context, AnnotationsProvider provider) { final groups = provider.annotationsByBook; final items = []; final screenWidth = MediaQuery.of(context).size.width; @@ -340,8 +299,7 @@ class _AnnotationsPageState extends State { annotation: annotation, showActionMenu: isDesktop, onTap: () => _navigateToBook(context, annotation.bookId), - onLongPress: () => - _onAnnotationActions(context, provider, annotation), + onLongPress: () => _onAnnotationActions(context, provider, annotation), ), ); } @@ -362,15 +320,8 @@ class _AnnotationsPageState extends State { context.goNamed('BOOK_DETAILS', pathParameters: {'bookId': bookId}); } - void _onAnnotationActions( - BuildContext context, - AnnotationsProvider provider, - Annotation annotation, - ) async { - final action = await AnnotationActionSheet.show( - context, - annotation: annotation, - ); + void _onAnnotationActions(BuildContext context, AnnotationsProvider provider, Annotation annotation) async { + final action = await AnnotationActionSheet.show(context, annotation: annotation); if (action == null || !mounted) return; @@ -382,14 +333,8 @@ class _AnnotationsPageState extends State { } } - void _onEditAnnotationNote( - AnnotationsProvider provider, - Annotation annotation, - ) async { - final note = await AnnotationNoteSheet.show( - context, - annotation: annotation, - ); + void _onEditAnnotationNote(AnnotationsProvider provider, Annotation annotation) async { + final note = await AnnotationNoteSheet.show(context, annotation: annotation); if (!mounted) return; if (note != null) { @@ -397,16 +342,9 @@ class _AnnotationsPageState extends State { } } - void _onDeleteAnnotation( - AnnotationsProvider provider, - Annotation annotation, - ) async { + void _onDeleteAnnotation(AnnotationsProvider provider, Annotation annotation) async { final bookTitle = provider.getBookTitle(annotation.bookId); - final confirmed = await DeleteAnnotationDialog.show( - context, - annotation: annotation, - bookTitle: bookTitle, - ); + final confirmed = await DeleteAnnotationDialog.show(context, annotation: annotation, bookTitle: bookTitle); if (confirmed && mounted) { provider.deleteAnnotation(annotation.id); } diff --git a/app/lib/pages/auth/oauth_callback_page.dart b/app/lib/pages/auth/oauth_callback_page.dart new file mode 100644 index 0000000..6af38f5 --- /dev/null +++ b/app/lib/pages/auth/oauth_callback_page.dart @@ -0,0 +1,74 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:papyrus/providers/auth_provider.dart'; +import 'package:papyrus/themes/design_tokens.dart'; +import 'package:provider/provider.dart'; + +class OAuthCallbackPage extends StatefulWidget { + final Uri callbackUri; + + const OAuthCallbackPage({super.key, required this.callbackUri}); + + @override + State createState() => _OAuthCallbackPageState(); +} + +class _OAuthCallbackPageState extends State { + String? _errorMessage; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) => _completeSignIn()); + } + + Future _completeSignIn() async { + final authProvider = context.read(); + final success = await authProvider.completeGoogleSignIn(widget.callbackUri); + + if (!mounted) { + return; + } + + if (success) { + context.goNamed('LIBRARY'); + return; + } + + setState(() { + _errorMessage = authProvider.error ?? 'Google sign-in failed.'; + }); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Scaffold( + body: Center( + child: Padding( + padding: const EdgeInsets.all(Spacing.xl), + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 420), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (_errorMessage == null) ...[ + const CircularProgressIndicator(), + const SizedBox(height: Spacing.lg), + Text('Completing sign-in...', style: theme.textTheme.titleMedium, textAlign: TextAlign.center), + ] else ...[ + Icon(Icons.error_outline, color: theme.colorScheme.error, size: IconSizes.large), + const SizedBox(height: Spacing.md), + Text(_errorMessage!, style: theme.textTheme.titleMedium, textAlign: TextAlign.center), + const SizedBox(height: Spacing.lg), + FilledButton(onPressed: () => context.go('/login'), child: const Text('Back to sign in')), + ], + ], + ), + ), + ), + ), + ); + } +} diff --git a/app/lib/pages/book_details_page.dart b/app/lib/pages/book_details_page.dart index 50bd76b..0437135 100644 --- a/app/lib/pages/book_details_page.dart +++ b/app/lib/pages/book_details_page.dart @@ -17,8 +17,7 @@ import 'package:papyrus/widgets/book_details/annotation_action_sheet.dart'; import 'package:papyrus/widgets/book_details/note_action_sheet.dart'; import 'package:papyrus/widgets/book_details/note_dialog.dart'; import 'package:papyrus/widgets/book_details/update_progress_sheet.dart'; -import 'package:papyrus/widgets/annotations/annotation_action_sheet.dart' - as annotation_sheets; +import 'package:papyrus/widgets/annotations/annotation_action_sheet.dart' as annotation_sheets; import 'package:papyrus/widgets/bookmarks/bookmark_action_sheet.dart'; import 'package:provider/provider.dart'; @@ -32,8 +31,7 @@ class BookDetailsPage extends StatefulWidget { State createState() => _BookDetailsPageState(); } -class _BookDetailsPageState extends State - with SingleTickerProviderStateMixin { +class _BookDetailsPageState extends State with SingleTickerProviderStateMixin { late BookDetailsProvider _provider; late TabController _tabController; @@ -102,10 +100,7 @@ class _BookDetailsPageState extends State Widget _buildLoadingState(BuildContext context) { return Scaffold( - appBar: AppBar( - leading: const BackButton(), - title: const Text('Loading...'), - ), + appBar: AppBar(leading: const BackButton(), title: const Text('Loading...')), body: const Center(child: CircularProgressIndicator()), ); } @@ -119,10 +114,7 @@ class _BookDetailsPageState extends State children: [ const Icon(Icons.error_outline, size: 64), const SizedBox(height: Spacing.md), - Text( - 'Failed to load book', - style: Theme.of(context).textTheme.titleLarge, - ), + Text('Failed to load book', style: Theme.of(context).textTheme.titleLarge), const SizedBox(height: Spacing.sm), Text(error), const SizedBox(height: Spacing.lg), @@ -142,35 +134,23 @@ class _BookDetailsPageState extends State Widget _buildNotFoundState(BuildContext context) { return Scaffold( - appBar: AppBar( - leading: const BackButton(), - title: const Text('Not found'), - ), + appBar: AppBar(leading: const BackButton(), title: const Text('Not found')), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.menu_book_outlined, size: 64), const SizedBox(height: Spacing.md), - Text( - 'Book not found', - style: Theme.of(context).textTheme.titleLarge, - ), + Text('Book not found', style: Theme.of(context).textTheme.titleLarge), const SizedBox(height: Spacing.lg), - FilledButton( - onPressed: () => context.go('/library/books'), - child: const Text('Back to library'), - ), + FilledButton(onPressed: () => context.go('/library/books'), child: const Text('Back to library')), ], ), ), ); } - Widget _buildDesktopLayout( - BuildContext context, - BookDetailsProvider provider, - ) { + Widget _buildDesktopLayout(BuildContext context, BookDetailsProvider provider) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -206,10 +186,7 @@ class _BookDetailsPageState extends State ); } - Widget _buildDesktopTabBar( - BuildContext context, - BookDetailsProvider provider, - ) { + Widget _buildDesktopTabBar(BuildContext context, BookDetailsProvider provider) { final colorScheme = Theme.of(context).colorScheme; return Container( @@ -230,10 +207,7 @@ class _BookDetailsPageState extends State ); } - Widget _buildMobileLayout( - BuildContext context, - BookDetailsProvider provider, - ) { + Widget _buildMobileLayout(BuildContext context, BookDetailsProvider provider) { return Scaffold( appBar: AppBar( leading: const BackButton(), @@ -242,9 +216,7 @@ class _BookDetailsPageState extends State PopupMenuButton( icon: const Icon(Icons.more_vert), onSelected: _onMenuAction, - itemBuilder: (context) => [ - const PopupMenuItem(value: 'delete', child: Text('Delete')), - ], + itemBuilder: (context) => [const PopupMenuItem(value: 'delete', child: Text('Delete'))], ), ], ), @@ -298,11 +270,7 @@ class _BookDetailsPageState extends State onAddAnnotation: _onAddAnnotation, onAnnotationActions: _onAnnotationActions, ), - BookNotes( - notes: provider.notes, - onAddNote: _onAddNote, - onNoteActions: _onNoteActions, - ), + BookNotes(notes: provider.notes, onAddNote: _onAddNote, onNoteActions: _onNoteActions), ], ), ), @@ -337,11 +305,7 @@ class _BookDetailsPageState extends State onAnnotationActions: _onAnnotationActions, ); case BookDetailsTab.notes: - return BookNotes( - notes: provider.notes, - onAddNote: _onAddNote, - onNoteActions: _onNoteActions, - ); + return BookNotes(notes: provider.notes, onAddNote: _onAddNote, onNoteActions: _onNoteActions); } } @@ -350,24 +314,15 @@ class _BookDetailsPageState extends State switch (provider.selectedTab) { case BookDetailsTab.notes: - return FloatingActionButton( - onPressed: _onAddNote, - child: const Icon(Icons.add), - ); + return FloatingActionButton(onPressed: _onAddNote, child: const Icon(Icons.add)); case BookDetailsTab.bookmarks: if (isPhysical) { - return FloatingActionButton( - onPressed: _onAddBookmark, - child: const Icon(Icons.add), - ); + return FloatingActionButton(onPressed: _onAddBookmark, child: const Icon(Icons.add)); } return null; case BookDetailsTab.annotations: if (isPhysical) { - return FloatingActionButton( - onPressed: _onAddAnnotation, - child: const Icon(Icons.add), - ); + return FloatingActionButton(onPressed: _onAddAnnotation, child: const Icon(Icons.add)); } return null; case BookDetailsTab.details: @@ -384,9 +339,7 @@ class _BookDetailsPageState extends State onSave: (page, position) { _provider.updatePageProgress(page, position); if (mounted) { - ScaffoldMessenger.of( - context, - ).showSnackBar(const SnackBar(content: Text('Progress updated'))); + ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Progress updated'))); } }, ); @@ -394,17 +347,12 @@ class _BookDetailsPageState extends State void _onContinueReading() { // TODO: Navigate to reader - ScaffoldMessenger.of( - context, - ).showSnackBar(const SnackBar(content: Text('Opening book reader...'))); + ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Opening book reader...'))); } void _onEdit() { if (_provider.book != null) { - context.pushNamed( - 'BOOK_EDIT', - pathParameters: {'bookId': _provider.book!.id}, - ); + context.pushNamed('BOOK_EDIT', pathParameters: {'bookId': _provider.book!.id}); } } @@ -415,9 +363,7 @@ class _BookDetailsPageState extends State if (note != null && mounted) { _provider.addNote(note); - ScaffoldMessenger.of( - context, - ).showSnackBar(const SnackBar(content: Text('Note added'))); + ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Note added'))); } } @@ -432,25 +378,18 @@ class _BookDetailsPageState extends State if (bookmark != null && mounted) { _provider.addBookmark(bookmark); - ScaffoldMessenger.of( - context, - ).showSnackBar(const SnackBar(content: Text('Bookmark added'))); + ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Bookmark added'))); } } void _onAddAnnotation() async { if (_provider.book == null) return; - final annotation = await AnnotationDialog.show( - context, - bookId: _provider.book!.id, - ); + final annotation = await AnnotationDialog.show(context, bookId: _provider.book!.id); if (annotation != null && mounted) { _provider.addAnnotation(annotation); - ScaffoldMessenger.of( - context, - ).showSnackBar(const SnackBar(content: Text('Annotation added'))); + ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Annotation added'))); } } @@ -470,17 +409,11 @@ class _BookDetailsPageState extends State void _onEditNote(Note note) async { if (_provider.book == null) return; - final updatedNote = await NoteDialog.show( - context, - bookId: _provider.book!.id, - existingNote: note, - ); + final updatedNote = await NoteDialog.show(context, bookId: _provider.book!.id, existingNote: note); if (updatedNote != null && mounted) { _provider.updateNote(note.id, updatedNote); - ScaffoldMessenger.of( - context, - ).showSnackBar(const SnackBar(content: Text('Note updated'))); + ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Note updated'))); } } @@ -489,17 +422,12 @@ class _BookDetailsPageState extends State if (confirmed && mounted) { _provider.deleteNote(note.id); - ScaffoldMessenger.of( - context, - ).showSnackBar(const SnackBar(content: Text('Note deleted'))); + ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Note deleted'))); } } void _onAnnotationActions(Annotation annotation) async { - final action = await AnnotationActionSheet.show( - context, - annotation: annotation, - ); + final action = await AnnotationActionSheet.show(context, annotation: annotation); if (action == null || !mounted) return; @@ -512,10 +440,7 @@ class _BookDetailsPageState extends State } void _onEditAnnotationNote(Annotation annotation) async { - final note = await annotation_sheets.AnnotationNoteSheet.show( - context, - annotation: annotation, - ); + final note = await annotation_sheets.AnnotationNoteSheet.show(context, annotation: annotation); if (!mounted) return; if (note != null) { @@ -580,9 +505,7 @@ class _BookDetailsPageState extends State switch (action) { case 'delete': // TODO: Confirm and delete - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Delete functionality coming soon')), - ); + ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Delete functionality coming soon'))); } } } diff --git a/app/lib/pages/book_edit_page.dart b/app/lib/pages/book_edit_page.dart index b095938..61eba4a 100644 --- a/app/lib/pages/book_edit_page.dart +++ b/app/lib/pages/book_edit_page.dart @@ -80,23 +80,17 @@ class _BookEditPageState extends State { ? DateFormat.yMMMMd().format(book.publicationDate!) : ''; _seriesNameController.text = book.seriesName ?? ''; - _seriesNumberController.text = book.seriesNumber != null - ? _formatSeriesNumber(book.seriesNumber!) - : ''; + _seriesNumberController.text = book.seriesNumber != null ? _formatSeriesNumber(book.seriesNumber!) : ''; _physicalLocationController.text = book.physicalLocation ?? ''; _lentToController.text = book.lentTo ?? ''; - _lentAtController.text = book.lentAt != null - ? DateFormat.yMMMMd().format(book.lentAt!) - : ''; + _lentAtController.text = book.lentAt != null ? DateFormat.yMMMMd().format(book.lentAt!) : ''; setState(() { _coAuthors = List.from(book.coAuthors); }); } String _formatSeriesNumber(double number) { - return number == number.roundToDouble() - ? number.toInt().toString() - : number.toString(); + return number == number.roundToDouble() ? number.toInt().toString() : number.toString(); } @override @@ -121,8 +115,7 @@ class _BookEditPageState extends State { super.dispose(); } - bool get _isDesktop => - MediaQuery.of(context).size.width >= Breakpoints.desktopSmall; + bool get _isDesktop => MediaQuery.of(context).size.width >= Breakpoints.desktopSmall; @override Widget build(BuildContext context) { @@ -144,11 +137,7 @@ class _BookEditPageState extends State { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon( - Icons.error_outline, - size: 64, - color: Theme.of(context).colorScheme.error, - ), + Icon(Icons.error_outline, size: 64, color: Theme.of(context).colorScheme.error), const SizedBox(height: Spacing.md), Text(provider.error ?? 'Book not found'), const SizedBox(height: Spacing.lg), @@ -180,23 +169,15 @@ class _BookEditPageState extends State { : Scaffold( appBar: AppBar( title: const Text('Edit book'), - leading: IconButton( - icon: const Icon(Icons.arrow_back), - onPressed: () => _handleCancel(context), - ), + leading: IconButton(icon: const Icon(Icons.arrow_back), onPressed: () => _handleCancel(context)), actions: [ TextButton( - onPressed: provider.canSave - ? () => _handleSave(context) - : null, + onPressed: provider.canSave ? () => _handleSave(context) : null, child: const Text('Save'), ), ], ), - body: Form( - key: _formKey, - child: _buildMobileLayout(context, provider), - ), + body: Form(key: _formKey, child: _buildMobileLayout(context, provider)), ), ); }, @@ -208,10 +189,7 @@ class _BookEditPageState extends State { // LAYOUTS // ============================================================================ - Widget _buildDesktopScaffold( - BuildContext context, - BookEditProvider provider, - ) { + Widget _buildDesktopScaffold(BuildContext context, BookEditProvider provider) { return Form(key: _formKey, child: _buildDesktopLayout(context, provider)); } @@ -249,13 +227,7 @@ class _BookEditPageState extends State { children: [ _buildSectionCard( title: 'Cover', - children: [ - _buildCoverSection( - context, - provider, - isDesktop: true, - ), - ], + children: [_buildCoverSection(context, provider, isDesktop: true)], ), _buildSectionCard( title: 'Fetch metadata', @@ -269,11 +241,7 @@ class _BookEditPageState extends State { Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, - children: _buildFormSections( - context, - provider, - skipMetadata: true, - ), + children: _buildFormSections(context, provider, skipMetadata: true), ), ), ], @@ -288,9 +256,7 @@ class _BookEditPageState extends State { child: Align( alignment: Alignment.centerRight, child: FilledButton( - onPressed: provider.canSave - ? () => _handleSave(context) - : null, + onPressed: provider.canSave ? () => _handleSave(context) : null, child: const Text('Save'), ), ), @@ -302,11 +268,7 @@ class _BookEditPageState extends State { ); } - List _buildFormSections( - BuildContext context, - BookEditProvider provider, { - bool skipMetadata = false, - }) { + List _buildFormSections(BuildContext context, BookEditProvider provider, {bool skipMetadata = false}) { return [ _buildBasicInfoSection(context, provider), _buildPublicationSection(), @@ -315,40 +277,22 @@ class _BookEditPageState extends State { Card( margin: EdgeInsets.zero, child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: Spacing.md, - vertical: Spacing.xs, - ), + padding: const EdgeInsets.symmetric(horizontal: Spacing.md, vertical: Spacing.xs), child: _buildPhysicalBookSection(context, provider), ), ), if (!skipMetadata) - _buildSectionCard( - title: 'Fetch metadata', - children: [_buildMetadataSection(context, provider)], - ), + _buildSectionCard(title: 'Fetch metadata', children: [_buildMetadataSection(context, provider)]), ]; } - Widget _buildBasicInfoSection( - BuildContext context, - BookEditProvider provider, - ) { + Widget _buildBasicInfoSection(BuildContext context, BookEditProvider provider) { return _buildSectionCard( title: 'Basic information', children: [ - BookTextField( - controller: _titleController, - label: 'Title', - required: true, - onChanged: _provider.updateTitle, - ), + BookTextField(controller: _titleController, label: 'Title', required: true, onChanged: _provider.updateTitle), const SizedBox(height: Spacing.md), - BookTextField( - controller: _subtitleController, - label: 'Subtitle', - onChanged: _provider.updateSubtitle, - ), + BookTextField(controller: _subtitleController, label: 'Subtitle', onChanged: _provider.updateSubtitle), const SizedBox(height: Spacing.md), BookTextField( controller: _descriptionController, @@ -389,16 +333,8 @@ class _BookEditPageState extends State { ResponsiveFormRow( isDesktop: _isDesktop, children: [ - BookTextField( - controller: _publisherController, - label: 'Publisher', - onChanged: _provider.updatePublisher, - ), - BookTextField( - controller: _languageController, - label: 'Language', - onChanged: _provider.updateLanguage, - ), + BookTextField(controller: _publisherController, label: 'Publisher', onChanged: _provider.updatePublisher), + BookTextField(controller: _languageController, label: 'Language', onChanged: _provider.updateLanguage), ], ), const SizedBox(height: Spacing.md), @@ -433,16 +369,8 @@ class _BookEditPageState extends State { ResponsiveFormRow( isDesktop: _isDesktop, children: [ - BookTextField( - controller: _isbnController, - label: 'ISBN', - onChanged: _provider.updateIsbn, - ), - BookTextField( - controller: _isbn13Controller, - label: 'ISBN-13', - onChanged: _provider.updateIsbn13, - ), + BookTextField(controller: _isbnController, label: 'ISBN', onChanged: _provider.updateIsbn), + BookTextField(controller: _isbn13Controller, label: 'ISBN-13', onChanged: _provider.updateIsbn13), ], ), ], @@ -464,9 +392,7 @@ class _BookEditPageState extends State { BookTextField( controller: _seriesNumberController, label: 'Number in series', - keyboardType: const TextInputType.numberWithOptions( - decimal: true, - ), + keyboardType: const TextInputType.numberWithOptions(decimal: true), onChanged: (value) { final number = double.tryParse(value); _provider.updateSeriesNumber(number); @@ -482,10 +408,7 @@ class _BookEditPageState extends State { // SECTION CARD // ============================================================================ - Widget _buildSectionCard({ - required String title, - required List children, - }) { + Widget _buildSectionCard({required String title, required List children}) { return Card( margin: const EdgeInsets.only(bottom: Spacing.xs), child: Padding( @@ -493,12 +416,7 @@ class _BookEditPageState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - title, - style: Theme.of( - context, - ).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600), - ), + Text(title, style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600)), const SizedBox(height: Spacing.md), ...children, ], @@ -517,12 +435,7 @@ class _BookEditPageState extends State { return Row( children: [ - Text( - 'Rating', - style: Theme.of( - context, - ).textTheme.bodySmall?.copyWith(color: colorScheme.onSurfaceVariant), - ), + Text('Rating', style: Theme.of(context).textTheme.bodySmall?.copyWith(color: colorScheme.onSurfaceVariant)), const SizedBox(width: Spacing.sm), ...List.generate(5, (index) { final starValue = index + 1; @@ -535,9 +448,7 @@ class _BookEditPageState extends State { padding: const EdgeInsets.symmetric(horizontal: 2), child: Icon( isSelected ? Icons.star_rounded : Icons.star_outline_rounded, - color: isSelected - ? colorScheme.primary - : colorScheme.onSurfaceVariant, + color: isSelected ? colorScheme.primary : colorScheme.onSurfaceVariant, size: 28, ), ), @@ -551,10 +462,7 @@ class _BookEditPageState extends State { // PHYSICAL BOOK SECTION // ============================================================================ - Widget _buildPhysicalBookSection( - BuildContext context, - BookEditProvider provider, - ) { + Widget _buildPhysicalBookSection(BuildContext context, BookEditProvider provider) { final isPhysical = provider.editedBook?.isPhysical ?? false; return Column( @@ -577,11 +485,7 @@ class _BookEditPageState extends State { ResponsiveFormRow( isDesktop: _isDesktop, children: [ - BookTextField( - controller: _lentToController, - label: 'Lent to', - onChanged: _provider.updateLentTo, - ), + BookTextField(controller: _lentToController, label: 'Lent to', onChanged: _provider.updateLentTo), BookDateField( controller: _lentAtController, label: 'Lent at', @@ -599,11 +503,7 @@ class _BookEditPageState extends State { // COVER SECTION // ============================================================================ - Widget _buildCoverSection( - BuildContext context, - BookEditProvider provider, { - required bool isDesktop, - }) { + Widget _buildCoverSection(BuildContext context, BookEditProvider provider, {required bool isDesktop}) { return CoverImagePicker( initialUrl: provider.editedBook?.coverUrl, initialBytes: provider.coverImageBytes, @@ -617,10 +517,7 @@ class _BookEditPageState extends State { // METADATA SECTION // ============================================================================ - Widget _buildMetadataSection( - BuildContext context, - BookEditProvider provider, - ) { + Widget _buildMetadataSection(BuildContext context, BookEditProvider provider) { final colorScheme = Theme.of(context).colorScheme; return Column( @@ -629,14 +526,8 @@ class _BookEditPageState extends State { // Source selector SegmentedButton( segments: const [ - ButtonSegment( - value: MetadataSource.openLibrary, - label: Text('Open Library'), - ), - ButtonSegment( - value: MetadataSource.googleBooks, - label: Text('Google Books'), - ), + ButtonSegment(value: MetadataSource.openLibrary, label: Text('Open Library')), + ButtonSegment(value: MetadataSource.googleBooks, label: Text('Google Books')), ], selected: {provider.selectedSource}, onSelectionChanged: (selection) { @@ -655,21 +546,13 @@ class _BookEditPageState extends State { decoration: InputDecoration( labelText: 'Search', hintText: 'Title, author, or ISBN', - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(AppRadius.md), - ), + border: OutlineInputBorder(borderRadius: BorderRadius.circular(AppRadius.md)), prefixIcon: const Icon(Icons.search), suffixIcon: IconButton( icon: provider.isFetching - ? const SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator(strokeWidth: 2), - ) + ? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2)) : const Icon(Icons.arrow_forward), - onPressed: provider.isFetching - ? null - : () => _searchMetadata(provider), + onPressed: provider.isFetching ? null : () => _searchMetadata(provider), ), ), onFieldSubmitted: (_) => _searchMetadata(provider), @@ -702,24 +585,15 @@ class _BookEditPageState extends State { // Results if (provider.fetchedResults.isNotEmpty) ...[ const SizedBox(height: Spacing.md), - Text( - '${provider.fetchedResults.length} result(s)', - style: Theme.of(context).textTheme.bodySmall, - ), + Text('${provider.fetchedResults.length} result(s)', style: Theme.of(context).textTheme.bodySmall), const SizedBox(height: Spacing.sm), - ...provider.fetchedResults.map( - (result) => _buildResultCard(context, result, provider), - ), + ...provider.fetchedResults.map((result) => _buildResultCard(context, result, provider)), ], ], ); } - Widget _buildResultCard( - BuildContext context, - BookMetadataResult result, - BookEditProvider provider, - ) { + Widget _buildResultCard(BuildContext context, BookMetadataResult result, BookEditProvider provider) { final colorScheme = Theme.of(context).colorScheme; final textTheme = Theme.of(context).textTheme; @@ -747,18 +621,11 @@ class _BookEditPageState extends State { child: Image.network( result.coverUrl!, fit: BoxFit.cover, - errorBuilder: (_, e, s) => Icon( - Icons.menu_book, - size: 20, - color: colorScheme.onSurfaceVariant, - ), + errorBuilder: (_, e, s) => + Icon(Icons.menu_book, size: 20, color: colorScheme.onSurfaceVariant), ), ) - : Icon( - Icons.menu_book, - size: 20, - color: colorScheme.onSurfaceVariant, - ), + : Icon(Icons.menu_book, size: 20, color: colorScheme.onSurfaceVariant), ), const SizedBox(width: Spacing.sm), // Info @@ -768,9 +635,7 @@ class _BookEditPageState extends State { children: [ Text( result.title ?? 'Unknown', - style: textTheme.bodyMedium?.copyWith( - fontWeight: FontWeight.w500, - ), + style: textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w500), maxLines: 2, overflow: TextOverflow.ellipsis, ), @@ -778,9 +643,7 @@ class _BookEditPageState extends State { const SizedBox(height: 2), Text( result.primaryAuthor, - style: textTheme.bodySmall?.copyWith( - color: colorScheme.onSurfaceVariant, - ), + style: textTheme.bodySmall?.copyWith(color: colorScheme.onSurfaceVariant), maxLines: 1, overflow: TextOverflow.ellipsis, ), @@ -789,28 +652,18 @@ class _BookEditPageState extends State { Row( children: [ Container( - padding: const EdgeInsets.symmetric( - horizontal: 6, - vertical: 2, - ), + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: colorScheme.secondaryContainer, borderRadius: BorderRadius.circular(4), ), child: Text( result.sourceLabel, - style: textTheme.labelSmall?.copyWith( - color: colorScheme.onSecondaryContainer, - ), + style: textTheme.labelSmall?.copyWith(color: colorScheme.onSecondaryContainer), ), ), const Spacer(), - Text( - 'Tap to apply', - style: textTheme.labelSmall?.copyWith( - color: colorScheme.primary, - ), - ), + Text('Tap to apply', style: textTheme.labelSmall?.copyWith(color: colorScheme.primary)), ], ), ], @@ -862,10 +715,7 @@ class _BookEditPageState extends State { } ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Applied metadata from ${result.sourceLabel}'), - behavior: SnackBarBehavior.floating, - ), + SnackBar(content: Text('Applied metadata from ${result.sourceLabel}'), behavior: SnackBarBehavior.floating), ); } @@ -874,18 +724,10 @@ class _BookEditPageState extends State { context: context, builder: (ctx) => AlertDialog( title: const Text('Discard changes?'), - content: const Text( - 'You have unsaved changes. Are you sure you want to discard them?', - ), + content: const Text('You have unsaved changes. Are you sure you want to discard them?'), actions: [ - TextButton( - onPressed: () => Navigator.pop(ctx, false), - child: const Text('Keep editing'), - ), - FilledButton( - onPressed: () => Navigator.pop(ctx, true), - child: const Text('Discard'), - ), + TextButton(onPressed: () => Navigator.pop(ctx, false), child: const Text('Keep editing')), + FilledButton(onPressed: () => Navigator.pop(ctx, true), child: const Text('Discard')), ], ), ); @@ -913,10 +755,7 @@ class _BookEditPageState extends State { Future _handleSave(BuildContext context) async { if (!_formKey.currentState!.validate()) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Please fix the errors before saving'), - behavior: SnackBarBehavior.floating, - ), + const SnackBar(content: Text('Please fix the errors before saving'), behavior: SnackBarBehavior.floating), ); return; } @@ -925,12 +764,9 @@ class _BookEditPageState extends State { if (!mounted || !context.mounted) return; if (success) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Book updated'), - behavior: SnackBarBehavior.floating, - ), - ); + ScaffoldMessenger.of( + context, + ).showSnackBar(const SnackBar(content: Text('Book updated'), behavior: SnackBarBehavior.floating)); _navigateToBookDetails(context); } else { ScaffoldMessenger.of(context).showSnackBar( diff --git a/app/lib/pages/bookmarks_page.dart b/app/lib/pages/bookmarks_page.dart index 05978db..04f790d 100644 --- a/app/lib/pages/bookmarks_page.dart +++ b/app/lib/pages/bookmarks_page.dart @@ -89,11 +89,7 @@ class _BookmarksPageState extends State { children: [ // Row 1: Menu + Search + Sort Padding( - padding: const EdgeInsets.only( - top: Spacing.md, - left: Spacing.md, - right: Spacing.md, - ), + padding: const EdgeInsets.only(top: Spacing.md, left: Spacing.md, right: Spacing.md), child: Row( children: [ IconButton( @@ -136,11 +132,7 @@ class _BookmarksPageState extends State { children: [ // Header row Container( - padding: const EdgeInsets.only( - top: Spacing.lg, - left: Spacing.lg, - right: Spacing.lg, - ), + padding: const EdgeInsets.only(top: Spacing.lg, left: Spacing.lg, right: Spacing.lg), child: Row( children: [ Expanded(child: _buildSearchField(provider)), @@ -194,26 +186,10 @@ class _BookmarksPageState extends State { tooltip: 'Sort bookmarks', onSelected: provider.setSortOption, itemBuilder: (context) => [ - _buildSortMenuItem( - BookmarkSortOption.dateNewest, - 'Newest first', - provider.sortOption, - ), - _buildSortMenuItem( - BookmarkSortOption.dateOldest, - 'Oldest first', - provider.sortOption, - ), - _buildSortMenuItem( - BookmarkSortOption.bookTitle, - 'By book title', - provider.sortOption, - ), - _buildSortMenuItem( - BookmarkSortOption.position, - 'By position', - provider.sortOption, - ), + _buildSortMenuItem(BookmarkSortOption.dateNewest, 'Newest first', provider.sortOption), + _buildSortMenuItem(BookmarkSortOption.dateOldest, 'Oldest first', provider.sortOption), + _buildSortMenuItem(BookmarkSortOption.bookTitle, 'By book title', provider.sortOption), + _buildSortMenuItem(BookmarkSortOption.position, 'By position', provider.sortOption), ], ); } @@ -228,12 +204,7 @@ class _BookmarksPageState extends State { child: Row( children: [ Expanded(child: Text(label)), - if (option == current) - Icon( - Icons.check, - size: IconSizes.small, - color: Theme.of(context).colorScheme.primary, - ), + if (option == current) Icon(Icons.check, size: IconSizes.small, color: Theme.of(context).colorScheme.primary), ], ), ); @@ -258,9 +229,7 @@ class _BookmarksPageState extends State { // Color chips ...Bookmark.availableColors.map((hex) { final isSelected = provider.activeColors.contains(hex); - final color = Color( - int.parse('FF${hex.replaceFirst('#', '')}', radix: 16), - ); + final color = Color(int.parse('FF${hex.replaceFirst('#', '')}', radix: 16)); final name = _colorNames[hex] ?? 'Unknown'; return Padding( @@ -271,10 +240,7 @@ class _BookmarksPageState extends State { avatar: Container( width: 12, height: 12, - decoration: BoxDecoration( - color: color, - shape: BoxShape.circle, - ), + decoration: BoxDecoration(color: color, shape: BoxShape.circle), ), onSelected: (_) => provider.toggleColorFilter(hex), ), @@ -346,8 +312,7 @@ class _BookmarksPageState extends State { bookTitle: provider.getBookTitle(bookmark.bookId), showActionMenu: isDesktop, onTap: () => _navigateToBook(context, bookmark.bookId), - onLongPress: () => - _onBookmarkActions(context, provider, bookmark), + onLongPress: () => _onBookmarkActions(context, provider, bookmark), ), ); } @@ -368,11 +333,7 @@ class _BookmarksPageState extends State { context.goNamed('BOOK_DETAILS', pathParameters: {'bookId': bookId}); } - void _onBookmarkActions( - BuildContext context, - BookmarksProvider provider, - Bookmark bookmark, - ) async { + void _onBookmarkActions(BuildContext context, BookmarksProvider provider, Bookmark bookmark) async { final action = await BookmarkActionSheet.show(context, bookmark: bookmark); if (action == null || !mounted) return; @@ -387,10 +348,7 @@ class _BookmarksPageState extends State { } } - void _onEditBookmarkNote( - BookmarksProvider provider, - Bookmark bookmark, - ) async { + void _onEditBookmarkNote(BookmarksProvider provider, Bookmark bookmark) async { final note = await BookmarkNoteSheet.show(context, bookmark: bookmark); if (!mounted) return; @@ -399,10 +357,7 @@ class _BookmarksPageState extends State { } } - void _onChangeBookmarkColor( - BookmarksProvider provider, - Bookmark bookmark, - ) async { + void _onChangeBookmarkColor(BookmarksProvider provider, Bookmark bookmark) async { final colorHex = await BookmarkColorSheet.show(context, bookmark: bookmark); if (colorHex != null && mounted) { provider.updateBookmarkColor(bookmark.id, colorHex); @@ -411,11 +366,7 @@ class _BookmarksPageState extends State { void _onDeleteBookmark(BookmarksProvider provider, Bookmark bookmark) async { final bookTitle = provider.getBookTitle(bookmark.bookId); - final confirmed = await DeleteBookmarkDialog.show( - context, - bookmark: bookmark, - bookTitle: bookTitle, - ); + final confirmed = await DeleteBookmarkDialog.show(context, bookmark: bookmark, bookTitle: bookTitle); if (confirmed && mounted) { provider.deleteBookmark(bookmark.id); } diff --git a/app/lib/pages/books_page.dart b/app/lib/pages/books_page.dart index 3d54ff7..5d1a0ca 100644 --- a/app/lib/pages/books_page.dart +++ b/app/lib/pages/books_page.dart @@ -60,12 +60,7 @@ class _AllBooksState extends State { children: [ ...books .asMap() - .map( - (index, data) => MapEntry( - index, - widgets.Book(id: index.toString(), data: data), - ), - ) + .map((index, data) => MapEntry(index, widgets.Book(id: index.toString(), data: data))) .values, ], ), diff --git a/app/lib/pages/dashboard_page.dart b/app/lib/pages/dashboard_page.dart index 108482c..93b88ad 100644 --- a/app/lib/pages/dashboard_page.dart +++ b/app/lib/pages/dashboard_page.dart @@ -54,9 +54,7 @@ class _DashboardPageState extends State { final isDesktop = screenWidth >= Breakpoints.desktopSmall; if (provider.isLoading) { - return const Scaffold( - body: Center(child: CircularProgressIndicator()), - ); + return const Scaffold(body: Center(child: CircularProgressIndicator())); } if (isDesktop) { @@ -149,10 +147,7 @@ class _DashboardPageState extends State { child: Row( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Expanded( - flex: 65, - child: _buildRecentlyAddedCard(context, provider), - ), + Expanded(flex: 65, child: _buildRecentlyAddedCard(context, provider)), const SizedBox(width: Spacing.lg), Expanded( flex: 35, @@ -183,19 +178,9 @@ class _DashboardPageState extends State { child: Row( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Expanded( - child: ContinueReadingCard( - book: provider.currentBook, - isDesktop: true, - ), - ), + Expanded(child: ContinueReadingCard(book: provider.currentBook, isDesktop: true)), const SizedBox(width: Spacing.lg), - Expanded( - child: ReadingGoalCard( - goals: provider.activeGoals, - isDesktop: true, - ), - ), + Expanded(child: ReadingGoalCard(goals: provider.activeGoals, isDesktop: true)), ], ), ); @@ -211,10 +196,7 @@ class _DashboardPageState extends State { } /// Wraps the recently added section in a bordered card for mobile layout. - Widget _buildMobileRecentlyAddedCard( - BuildContext context, - DashboardProvider provider, - ) { + Widget _buildMobileRecentlyAddedCard(BuildContext context, DashboardProvider provider) { final colorScheme = Theme.of(context).colorScheme; return Container( @@ -222,10 +204,7 @@ class _DashboardPageState extends State { decoration: BoxDecoration( color: colorScheme.surfaceContainerLow, borderRadius: BorderRadius.circular(AppRadius.lg), - border: Border.all( - color: colorScheme.outlineVariant, - width: BorderWidths.thin, - ), + border: Border.all(color: colorScheme.outlineVariant, width: BorderWidths.thin), ), clipBehavior: Clip.antiAlias, child: RecentlyAddedSection(books: provider.recentlyAdded), @@ -233,10 +212,7 @@ class _DashboardPageState extends State { } /// Wraps the recently added section in a bordered card for desktop layout. - Widget _buildRecentlyAddedCard( - BuildContext context, - DashboardProvider provider, - ) { + Widget _buildRecentlyAddedCard(BuildContext context, DashboardProvider provider) { final colorScheme = Theme.of(context).colorScheme; return Container( @@ -244,15 +220,9 @@ class _DashboardPageState extends State { decoration: BoxDecoration( color: colorScheme.surfaceContainerLow, borderRadius: BorderRadius.circular(AppRadius.lg), - border: Border.all( - color: colorScheme.outlineVariant, - width: BorderWidths.thin, - ), - ), - child: RecentlyAddedSection( - books: provider.recentlyAdded, - isDesktop: true, + border: Border.all(color: colorScheme.outlineVariant, width: BorderWidths.thin), ), + child: RecentlyAddedSection(books: provider.recentlyAdded, isDesktop: true), ); } } diff --git a/app/lib/pages/developer_options_page.dart b/app/lib/pages/developer_options_page.dart index a520c49..c222629 100644 --- a/app/lib/pages/developer_options_page.dart +++ b/app/lib/pages/developer_options_page.dart @@ -26,10 +26,7 @@ class DeveloperOptionsPage extends StatelessWidget { return Scaffold( appBar: AppBar(title: const Text('Developer options')), body: SafeArea( - child: ListView( - padding: const EdgeInsets.all(Spacing.md), - children: [], - ), + child: ListView(padding: const EdgeInsets.all(Spacing.md), children: []), ), ); } @@ -43,10 +40,7 @@ class DeveloperOptionsPage extends StatelessWidget { _buildEinkHeader(context), const Divider(color: Colors.black, height: 1), Expanded( - child: ListView( - padding: const EdgeInsets.all(Spacing.pageMarginsEink), - children: [], - ), + child: ListView(padding: const EdgeInsets.all(Spacing.pageMarginsEink), children: []), ), ], ), @@ -65,21 +59,14 @@ class DeveloperOptionsPage extends StatelessWidget { width: TouchTargets.einkMin, height: TouchTargets.einkMin, child: Center( - child: Text( - '<', - style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold), - ), + child: Text('<', style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold)), ), ), ), const SizedBox(width: Spacing.sm), const Text( 'DEVELOPER OPTIONS', - style: TextStyle( - fontSize: 24, - fontWeight: FontWeight.bold, - letterSpacing: 1, - ), + style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, letterSpacing: 1), ), ], ), @@ -91,31 +78,20 @@ class DeveloperOptionsPage extends StatelessWidget { width: 56, height: 32, decoration: BoxDecoration( - border: Border.all( - color: Colors.black, - width: BorderWidths.einkDefault, - ), + border: Border.all(color: Colors.black, width: BorderWidths.einkDefault), ), child: Row( children: [ Expanded( child: Container( color: isOn ? Colors.black : Colors.white, - child: Center( - child: isOn - ? const Icon(Icons.check, color: Colors.white, size: 16) - : null, - ), + child: Center(child: isOn ? const Icon(Icons.check, color: Colors.white, size: 16) : null), ), ), Expanded( child: Container( color: isOn ? Colors.white : Colors.black, - child: Center( - child: !isOn - ? const Icon(Icons.close, color: Colors.white, size: 16) - : null, - ), + child: Center(child: !isOn ? const Icon(Icons.close, color: Colors.white, size: 16) : null), ), ), ], diff --git a/app/lib/pages/edit_profile_page.dart b/app/lib/pages/edit_profile_page.dart index b98deb8..23bdc68 100644 --- a/app/lib/pages/edit_profile_page.dart +++ b/app/lib/pages/edit_profile_page.dart @@ -3,12 +3,13 @@ import 'dart:typed_data'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; +import 'package:papyrus/providers/auth_provider.dart'; import 'package:papyrus/themes/design_tokens.dart'; -import 'package:supabase_flutter/supabase_flutter.dart'; +import 'package:provider/provider.dart'; /// Page for editing user profile information (display name and avatar). /// -/// Reads current values from Supabase Auth and writes back on save. +/// Reads current values from Papyrus auth and writes back on save. /// Mobile: full-screen form with AppBar save action. /// Desktop: top-centered card within the adaptive shell. class EditProfilePage extends StatefulWidget { @@ -31,8 +32,8 @@ class _EditProfilePageState extends State { @override void initState() { super.initState(); - final user = Supabase.instance.client.auth.currentUser; - final displayName = user?.userMetadata?['full_name'] as String? ?? ''; + final user = context.read().user; + final displayName = user?.displayName ?? ''; _nameController = TextEditingController(text: displayName); } @@ -43,8 +44,8 @@ class _EditProfilePageState extends State { } bool get _hasChanges { - final user = Supabase.instance.client.auth.currentUser; - final currentName = user?.userMetadata?['full_name'] as String? ?? ''; + final user = context.read().user; + final currentName = user?.displayName ?? ''; final nameChanged = _nameController.text.trim() != currentName; return nameChanged || _pickedImageBytes != null || _photoRemoved; } @@ -57,10 +58,7 @@ class _EditProfilePageState extends State { return Scaffold( appBar: AppBar( title: const Text('Edit profile'), - leading: IconButton( - icon: const Icon(Icons.close), - onPressed: () => _handleBack(context), - ), + leading: IconButton(icon: const Icon(Icons.close), onPressed: () => _handleBack(context)), actions: [ Padding( padding: const EdgeInsets.only(right: Spacing.sm), @@ -70,21 +68,14 @@ class _EditProfilePageState extends State { ? const SizedBox( width: 20, height: 20, - child: CircularProgressIndicator( - strokeWidth: 2, - color: Colors.white, - ), + child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white), ) : const Text('Save'), ), ), ], ), - body: SafeArea( - child: isDesktop - ? _buildDesktopBody(context) - : _buildMobileBody(context), - ), + body: SafeArea(child: isDesktop ? _buildDesktopBody(context) : _buildMobileBody(context)), ); } @@ -93,10 +84,7 @@ class _EditProfilePageState extends State { // =========================================================================== Widget _buildMobileBody(BuildContext context) { - return SingleChildScrollView( - padding: const EdgeInsets.all(Spacing.md), - child: _buildForm(context), - ); + return SingleChildScrollView(padding: const EdgeInsets.all(Spacing.md), child: _buildForm(context)); } Widget _buildDesktopBody(BuildContext context) { @@ -104,10 +92,7 @@ class _EditProfilePageState extends State { alignment: Alignment.topCenter, child: SingleChildScrollView( padding: const EdgeInsets.all(Spacing.xl), - child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 480), - child: _buildForm(context), - ), + child: ConstrainedBox(constraints: const BoxConstraints(maxWidth: 480), child: _buildForm(context)), ), ); } @@ -130,18 +115,10 @@ class _EditProfilePageState extends State { const SizedBox(height: Spacing.xl), // Error banner - if (_errorMessage != null) ...[ - _buildErrorBanner(context), - const SizedBox(height: Spacing.md), - ], + if (_errorMessage != null) ...[_buildErrorBanner(context), const SizedBox(height: Spacing.md)], // Display name - Text( - 'Display name', - style: textTheme.bodyMedium?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), + Text('Display name', style: textTheme.bodyMedium?.copyWith(color: colorScheme.onSurfaceVariant)), const SizedBox(height: Spacing.sm), TextFormField( controller: _nameController, @@ -163,18 +140,9 @@ class _EditProfilePageState extends State { const SizedBox(height: Spacing.lg), // Email (read-only) - Text( - 'Email', - style: textTheme.bodyMedium?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), + Text('Email', style: textTheme.bodyMedium?.copyWith(color: colorScheme.onSurfaceVariant)), const SizedBox(height: Spacing.sm), - TextFormField( - initialValue: _getEmail(), - enabled: false, - decoration: const InputDecoration(), - ), + TextFormField(initialValue: _getEmail(), enabled: false, decoration: const InputDecoration()), ], ), ); @@ -187,25 +155,13 @@ class _EditProfilePageState extends State { return Container( width: double.infinity, padding: const EdgeInsets.all(Spacing.md), - decoration: BoxDecoration( - color: colorScheme.errorContainer, - borderRadius: BorderRadius.circular(AppRadius.md), - ), + decoration: BoxDecoration(color: colorScheme.errorContainer, borderRadius: BorderRadius.circular(AppRadius.md)), child: Row( children: [ - Icon( - Icons.error_outline, - color: colorScheme.onErrorContainer, - size: IconSizes.medium, - ), + Icon(Icons.error_outline, color: colorScheme.onErrorContainer, size: IconSizes.medium), const SizedBox(width: Spacing.sm), Expanded( - child: Text( - _errorMessage!, - style: textTheme.bodyMedium?.copyWith( - color: colorScheme.onErrorContainer, - ), - ), + child: Text(_errorMessage!, style: textTheme.bodyMedium?.copyWith(color: colorScheme.onErrorContainer)), ), ], ), @@ -227,10 +183,7 @@ class _EditProfilePageState extends State { Container( width: size, height: size, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: colorScheme.primaryContainer, - ), + decoration: BoxDecoration(shape: BoxShape.circle, color: colorScheme.primaryContainer), clipBehavior: Clip.antiAlias, child: _buildAvatarImage(context, size), ), @@ -245,11 +198,7 @@ class _EditProfilePageState extends State { color: colorScheme.primary, border: Border.all(color: colorScheme.surface, width: 2), ), - child: Icon( - Icons.camera_alt, - size: IconSizes.small, - color: colorScheme.onPrimary, - ), + child: Icon(Icons.camera_alt, size: IconSizes.small, color: colorScheme.onPrimary), ), ), ], @@ -263,10 +212,9 @@ class _EditProfilePageState extends State { final initialsWidget = Center( child: Text( _initials, - style: Theme.of(context).textTheme.headlineMedium?.copyWith( - color: colorScheme.onPrimaryContainer, - fontSize: size * 0.35, - ), + style: Theme.of( + context, + ).textTheme.headlineMedium?.copyWith(color: colorScheme.onPrimaryContainer, fontSize: size * 0.35), ), ); @@ -283,9 +231,7 @@ class _EditProfilePageState extends State { // Show existing network photo if not removed. if (!_photoRemoved) { - final photoUrl = - Supabase.instance.client.auth.currentUser?.userMetadata?['avatar_url'] - as String?; + final photoUrl = context.read().user?.avatarUrl; if (photoUrl != null && photoUrl.isNotEmpty) { return Image.network( photoUrl, @@ -303,9 +249,7 @@ class _EditProfilePageState extends State { bool get _hasExistingPhoto { if (_photoRemoved) return false; if (_pickedImageBytes != null) return true; - final photoUrl = - Supabase.instance.client.auth.currentUser?.userMetadata?['avatar_url'] - as String?; + final photoUrl = context.read().user?.avatarUrl; return photoUrl != null && photoUrl.isNotEmpty; } @@ -326,14 +270,8 @@ class _EditProfilePageState extends State { ), if (_hasExistingPhoto) ListTile( - leading: Icon( - Icons.delete_outline, - color: Theme.of(context).colorScheme.error, - ), - title: Text( - 'Remove photo', - style: TextStyle(color: Theme.of(context).colorScheme.error), - ), + leading: Icon(Icons.delete_outline, color: Theme.of(context).colorScheme.error), + title: Text('Remove photo', style: TextStyle(color: Theme.of(context).colorScheme.error)), onTap: () { Navigator.pop(sheetContext); setState(() { @@ -350,10 +288,7 @@ class _EditProfilePageState extends State { Future _pickImage() async { try { - final result = await FilePicker.platform.pickFiles( - type: FileType.image, - withData: true, - ); + final result = await FilePicker.platform.pickFiles(type: FileType.image, withData: true); if (result != null && result.files.single.bytes != null) { setState(() { _pickedImageBytes = result.files.single.bytes; @@ -362,9 +297,7 @@ class _EditProfilePageState extends State { } } catch (e) { if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Could not open image picker')), - ); + ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Could not open image picker'))); } } } @@ -382,8 +315,10 @@ class _EditProfilePageState extends State { }); try { - final client = Supabase.instance.client; - if (client.auth.currentUser == null) { + final authProvider = context.read(); + final user = authProvider.user; + + if (user == null) { setState(() { _errorMessage = 'Not signed in'; _isSaving = false; @@ -392,28 +327,23 @@ class _EditProfilePageState extends State { } final newName = _nameController.text.trim(); - final currentName = - client.auth.currentUser?.userMetadata?['full_name'] as String? ?? ''; - - final Map data = {}; - if (newName != currentName) data['full_name'] = newName; - // Note: uploading a new photo requires a storage backend which isn't - // configured yet. Picked images are shown as a local preview but won't - // persist across devices until storage is set up. - if (_photoRemoved) data['avatar_url'] = null; - - if (data.isNotEmpty) { - await client.auth.updateUser(UserAttributes(data: data)); + final currentName = user.displayName; + + if (newName != currentName) { + final success = await authProvider.updateProfile(displayName: newName); + + if (!success) { + setState(() { + _errorMessage = authProvider.error ?? 'Failed to update profile'; + _isSaving = false; + }); + return; + } } if (context.mounted) { context.pop(); } - } on AuthException catch (e) { - setState(() { - _errorMessage = e.message; - _isSaving = false; - }); } catch (e) { setState(() { _errorMessage = 'Failed to update profile'; @@ -428,14 +358,9 @@ class _EditProfilePageState extends State { context: context, builder: (dialogContext) => AlertDialog( title: const Text('Discard changes?'), - content: const Text( - 'You have unsaved changes. Are you sure you want to discard them?', - ), + content: const Text('You have unsaved changes. Are you sure you want to discard them?'), actions: [ - TextButton( - onPressed: () => Navigator.pop(dialogContext), - child: const Text('Keep editing'), - ), + TextButton(onPressed: () => Navigator.pop(dialogContext), child: const Text('Keep editing')), FilledButton( onPressed: () { Navigator.pop(dialogContext); @@ -456,7 +381,7 @@ class _EditProfilePageState extends State { // =========================================================================== String _getEmail() { - final email = Supabase.instance.client.auth.currentUser?.email; + final email = context.read().user?.email; if (email == null || email.trim().isEmpty) return 'No email provided'; return email; } diff --git a/app/lib/pages/forgot_password_page.dart b/app/lib/pages/forgot_password_page.dart index b1d86fb..e92c0e9 100644 --- a/app/lib/pages/forgot_password_page.dart +++ b/app/lib/pages/forgot_password_page.dart @@ -1,12 +1,14 @@ import 'package:flutter/material.dart'; -import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:go_router/go_router.dart'; +import 'package:papyrus/providers/auth_provider.dart'; import 'package:papyrus/themes/design_tokens.dart'; import 'package:papyrus/utils/responsive.dart'; import 'package:papyrus/widgets/auth/auth_continue_button.dart'; import 'package:papyrus/widgets/auth/auth_page_layouts.dart'; import 'package:papyrus/widgets/auth/auth_switch_link.dart'; import 'package:papyrus/widgets/input/email_input.dart'; +import 'package:papyrus/widgets/input/password_input.dart'; +import 'package:provider/provider.dart'; /// Forgot password page for the Papyrus book management application. /// Provides responsive layouts for mobile and desktop displays. @@ -19,16 +21,30 @@ class ForgotPasswordPage extends StatefulWidget { class _ForgotPasswordPageState extends State { final _formKey = GlobalKey(); + final _resetFormKey = GlobalKey(); final _emailController = TextEditingController(); + final _tokenController = TextEditingController(); + final _passwordController = TextEditingController(); + final _confirmPasswordController = TextEditingController(); final _emailFocusNode = FocusNode(); + final _tokenFocusNode = FocusNode(); + final _passwordFocusNode = FocusNode(); + final _confirmPasswordFocusNode = FocusNode(); bool _isLoading = false; bool _emailSent = false; + bool _passwordReset = false; @override void dispose() { _emailController.dispose(); + _tokenController.dispose(); + _passwordController.dispose(); + _confirmPasswordController.dispose(); _emailFocusNode.dispose(); + _tokenFocusNode.dispose(); + _passwordFocusNode.dispose(); + _confirmPasswordFocusNode.dispose(); super.dispose(); } @@ -44,28 +60,54 @@ class _ForgotPasswordPageState extends State { ScaffoldMessenger.of(context).hideCurrentSnackBar(); try { - await Supabase.instance.client.auth.resetPasswordForEmail( - _emailController.text.trim(), - ); + final message = await context.read().forgotPassword(_emailController.text.trim()); if (!mounted) return; + setState(() { _isLoading = false; _emailSent = true; }); - } on AuthException catch (e) { + + if (message != null) { + _showSuccessSnackBar(message); + } + } catch (e) { + if (!mounted) return; + setState(() => _isLoading = false); + _showErrorSnackBar('An error occurred. Please try again.'); + } + } + + Future _handleSetNewPassword() async { + if (_isLoading) return; + + FocusScope.of(context).unfocus(); + + if (!_resetFormKey.currentState!.validate()) return; + + setState(() => _isLoading = true); + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + + try { + final message = await context.read().resetPassword( + token: _tokenController.text.trim(), + password: _passwordController.text, + ); + if (!mounted) return; - String message; - final msg = e.message.toLowerCase(); - if (msg.contains('too many requests')) { - message = 'Too many attempts. Please try again later.'; - } else { - message = 'Failed to send reset email. Please try again.'; + if (message == null) { + _showErrorSnackBar(context.read().error ?? 'Failed to reset password.'); + setState(() => _isLoading = false); + return; } - setState(() => _isLoading = false); - _showErrorSnackBar(message); + setState(() { + _isLoading = false; + _passwordReset = true; + }); + _showSuccessSnackBar(message); } catch (e) { if (!mounted) return; setState(() => _isLoading = false); @@ -73,6 +115,32 @@ class _ForgotPasswordPageState extends State { } } + String? _validateConfirmPassword(String? value) { + if (value != _passwordController.text) { + return 'Passwords do not match'; + } + + return null; + } + + String? _validatePasswordStrength(String? value) { + if (value == null || value.length < 8) { + return 'Minimum 8 characters'; + } + + return null; + } + + void _showSuccessSnackBar(String message) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + duration: const Duration(seconds: 5), + content: Text(message), + backgroundColor: Theme.of(context).colorScheme.primary, + ), + ); + } + void _showErrorSnackBar(String message) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( @@ -88,8 +156,26 @@ class _ForgotPasswordPageState extends State { } Widget _buildForm({required bool isDesktop}) { + if (_passwordReset) { + return const _PasswordResetConfirmation(); + } + if (_emailSent) { - return _EmailSentConfirmation(email: _emailController.text.trim()); + return _EmailSentConfirmation( + email: _emailController.text.trim(), + formKey: _resetFormKey, + tokenController: _tokenController, + passwordController: _passwordController, + confirmPasswordController: _confirmPasswordController, + tokenFocusNode: _tokenFocusNode, + passwordFocusNode: _passwordFocusNode, + confirmPasswordFocusNode: _confirmPasswordFocusNode, + isLoading: _isLoading, + isDesktop: isDesktop, + onSubmit: _handleSetNewPassword, + validatePasswordStrength: _validatePasswordStrength, + validateConfirmPassword: _validateConfirmPassword, + ); } return _ForgotPasswordForm( formKey: _formKey, @@ -104,11 +190,7 @@ class _ForgotPasswordPageState extends State { List _buildFooter() { return [ const SizedBox(height: Spacing.md), - AuthSwitchLink( - promptText: 'Remember your password?', - actionText: 'Sign in', - onPressed: _navigateToLogin, - ), + AuthSwitchLink(promptText: 'Remember your password?', actionText: 'Sign in', onPressed: _navigateToLogin), ]; } @@ -171,11 +253,7 @@ class _ForgotPasswordForm extends StatelessWidget { onEditingComplete: onSubmit, ), const SizedBox(height: Spacing.lg), - AuthContinueButton( - isLoading: isLoading, - onPressed: onSubmit, - isDesktop: isDesktop, - ), + AuthContinueButton(isLoading: isLoading, onPressed: onSubmit, isDesktop: isDesktop), ], ), ); @@ -189,8 +267,34 @@ class _ForgotPasswordForm extends StatelessWidget { /// Confirmation view shown after the reset email has been sent. class _EmailSentConfirmation extends StatelessWidget { final String email; + final GlobalKey formKey; + final TextEditingController tokenController; + final TextEditingController passwordController; + final TextEditingController confirmPasswordController; + final FocusNode tokenFocusNode; + final FocusNode passwordFocusNode; + final FocusNode confirmPasswordFocusNode; + final bool isLoading; + final bool isDesktop; + final VoidCallback onSubmit; + final String? Function(String?) validatePasswordStrength; + final String? Function(String?) validateConfirmPassword; - const _EmailSentConfirmation({required this.email}); + const _EmailSentConfirmation({ + required this.email, + required this.formKey, + required this.tokenController, + required this.passwordController, + required this.confirmPasswordController, + required this.tokenFocusNode, + required this.passwordFocusNode, + required this.confirmPasswordFocusNode, + required this.isLoading, + required this.isDesktop, + required this.onSubmit, + required this.validatePasswordStrength, + required this.validateConfirmPassword, + }); @override Widget build(BuildContext context) { @@ -200,34 +304,95 @@ class _EmailSentConfirmation extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisSize: MainAxisSize.min, children: [ - Icon( - Icons.mark_email_read_outlined, - size: 72, - color: theme.colorScheme.primary, - ), + Icon(Icons.mark_email_read_outlined, size: 72, color: theme.colorScheme.primary), const SizedBox(height: Spacing.sm), Text( 'Check your email', - style: theme.textTheme.titleLarge?.copyWith( - fontWeight: FontWeight.w600, - color: theme.colorScheme.onSurface, - ), + style: theme.textTheme.titleLarge?.copyWith(fontWeight: FontWeight.w600, color: theme.colorScheme.onSurface), textAlign: TextAlign.center, ), const SizedBox(height: Spacing.sm), Text( - 'We sent a password reset link to $email', - style: theme.textTheme.bodyMedium?.copyWith( - color: theme.colorScheme.onSurfaceVariant, - ), + 'We sent a password reset token to $email', + style: theme.textTheme.bodyMedium?.copyWith(color: theme.colorScheme.onSurfaceVariant), textAlign: TextAlign.center, ), const SizedBox(height: Spacing.sm), Text( "If you don't see the email, check your spam folder.", - style: theme.textTheme.bodyMedium?.copyWith( - color: theme.colorScheme.onSurfaceVariant, + style: theme.textTheme.bodyMedium?.copyWith(color: theme.colorScheme.onSurfaceVariant), + textAlign: TextAlign.center, + ), + const SizedBox(height: Spacing.lg), + Form( + key: formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + TextFormField( + controller: tokenController, + focusNode: tokenFocusNode, + textInputAction: TextInputAction.next, + decoration: const InputDecoration(labelText: 'Reset token'), + validator: (value) { + if (value == null || value.trim().isEmpty) { + return 'Reset token is required'; + } + + return null; + }, + onEditingComplete: () => passwordFocusNode.requestFocus(), + ), + const SizedBox(height: Spacing.md), + PasswordInput( + labelText: 'New password', + controller: passwordController, + focusNode: passwordFocusNode, + textInputAction: TextInputAction.next, + extraValidator: validatePasswordStrength, + onEditingComplete: () => confirmPasswordFocusNode.requestFocus(), + ), + const SizedBox(height: Spacing.md), + PasswordInput( + labelText: 'Confirm new password', + controller: confirmPasswordController, + focusNode: confirmPasswordFocusNode, + textInputAction: TextInputAction.done, + extraValidator: validateConfirmPassword, + onFieldSubmitted: (_) => onSubmit(), + ), + const SizedBox(height: Spacing.lg), + AuthContinueButton(isLoading: isLoading, onPressed: onSubmit, isDesktop: isDesktop), + ], ), + ), + ], + ); + } +} + +class _PasswordResetConfirmation extends StatelessWidget { + const _PasswordResetConfirmation(); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.check_circle_outline, size: 72, color: theme.colorScheme.primary), + const SizedBox(height: Spacing.sm), + Text( + 'Password reset', + style: theme.textTheme.titleLarge?.copyWith(fontWeight: FontWeight.w600, color: theme.colorScheme.onSurface), + textAlign: TextAlign.center, + ), + const SizedBox(height: Spacing.sm), + Text( + 'You can now sign in with your new password.', + style: theme.textTheme.bodyMedium?.copyWith(color: theme.colorScheme.onSurfaceVariant), textAlign: TextAlign.center, ), ], diff --git a/app/lib/pages/goals_page.dart b/app/lib/pages/goals_page.dart index 484b80d..99fc6de 100644 --- a/app/lib/pages/goals_page.dart +++ b/app/lib/pages/goals_page.dart @@ -106,10 +106,7 @@ class _GoalsPageState extends State { ...provider.activeGoals.map( (goal) => Padding( padding: const EdgeInsets.only(bottom: Spacing.md), - child: GoalCard( - goal: goal, - onTap: () => _showGoalDetails(context, goal), - ), + child: GoalCard(goal: goal, onTap: () => _showGoalDetails(context, goal)), ), ), ], @@ -127,13 +124,8 @@ class _GoalsPageState extends State { children: provider.completedGoals .map( (goal) => Padding( - padding: const EdgeInsets.only( - bottom: Spacing.md, - ), - child: GoalCard( - goal: goal, - onTap: () => _showGoalDetails(context, goal), - ), + padding: const EdgeInsets.only(bottom: Spacing.md), + child: GoalCard(goal: goal, onTap: () => _showGoalDetails(context, goal)), ), ) .toList(), @@ -192,10 +184,7 @@ class _GoalsPageState extends State { _buildCompletedHeader(context, provider.completedGoals.length), const SizedBox(height: Spacing.md), if (!_completedCollapsed) - Opacity( - opacity: 0.6, - child: _buildGoalGrid(context, provider.completedGoals), - ), + Opacity(opacity: 0.6, child: _buildGoalGrid(context, provider.completedGoals)), ], ], ), @@ -225,11 +214,7 @@ class _GoalsPageState extends State { ), itemCount: goals.length, itemBuilder: (context, index) { - return GoalCard( - goal: goals[index], - isDesktop: true, - onTap: () => _showGoalDetails(context, goals[index]), - ); + return GoalCard(goal: goals[index], isDesktop: true, onTap: () => _showGoalDetails(context, goals[index])); }, ); }, @@ -250,27 +235,15 @@ class _GoalsPageState extends State { return Row( children: [ Expanded( - child: CompactStatCard( - value: '${provider.activeGoals.length}', - label: 'Active', - isDesktop: isDesktop, - ), + child: CompactStatCard(value: '${provider.activeGoals.length}', label: 'Active', isDesktop: isDesktop), ), const SizedBox(width: Spacing.md), Expanded( - child: CompactStatCard( - value: '${provider.completedGoals.length}', - label: 'Completed', - isDesktop: isDesktop, - ), + child: CompactStatCard(value: '${provider.completedGoals.length}', label: 'Completed', isDesktop: isDesktop), ), const SizedBox(width: Spacing.md), Expanded( - child: CompactStatCard( - value: '${_getBestStreak(provider)}', - label: 'Best streak', - isDesktop: isDesktop, - ), + child: CompactStatCard(value: '${_getBestStreak(provider)}', label: 'Best streak', isDesktop: isDesktop), ), ], ); @@ -295,17 +268,9 @@ class _GoalsPageState extends State { children: [ Text('Completed goals', style: textTheme.titleMedium), const SizedBox(width: Spacing.sm), - Text( - '($count)', - style: textTheme.titleMedium?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), + Text('($count)', style: textTheme.titleMedium?.copyWith(color: colorScheme.onSurfaceVariant)), const Spacer(), - Icon( - _completedCollapsed ? Icons.expand_more : Icons.expand_less, - color: colorScheme.onSurfaceVariant, - ), + Icon(_completedCollapsed ? Icons.expand_more : Icons.expand_less, color: colorScheme.onSurfaceVariant), ], ), ), @@ -325,23 +290,13 @@ class _GoalsPageState extends State { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon( - Icons.flag_outlined, - size: 64, - color: colorScheme.onSurfaceVariant, - ), + Icon(Icons.flag_outlined, size: 64, color: colorScheme.onSurfaceVariant), const SizedBox(height: Spacing.lg), - Text( - 'No goals yet', - style: textTheme.headlineSmall, - textAlign: TextAlign.center, - ), + Text('No goals yet', style: textTheme.headlineSmall, textAlign: TextAlign.center), const SizedBox(height: Spacing.sm), Text( 'Create your first reading goal to track your progress', - style: textTheme.bodyMedium?.copyWith( - color: colorScheme.onSurfaceVariant, - ), + style: textTheme.bodyMedium?.copyWith(color: colorScheme.onSurfaceVariant), textAlign: TextAlign.center, ), const SizedBox(height: Spacing.lg), @@ -377,11 +332,7 @@ class _GoalsPageState extends State { void _showGoalDetails(BuildContext context, ReadingGoal goal) { if (goal.isCompleted) { - CompletedGoalChip.showDetailsSheet( - context, - goal: goal, - onDelete: () => _provider.deleteGoal(goal.id), - ); + CompletedGoalChip.showDetailsSheet(context, goal: goal, onDelete: () => _provider.deleteGoal(goal.id)); return; } diff --git a/app/lib/pages/library_page.dart b/app/lib/pages/library_page.dart index dc8be81..66fca51 100644 --- a/app/lib/pages/library_page.dart +++ b/app/lib/pages/library_page.dart @@ -58,18 +58,14 @@ class _LibraryPageState extends State { if (provider.searchQuery.isNotEmpty) { final searchQuery = SearchQueryParser.parse(provider.searchQuery); if (searchQuery.isNotEmpty) { - books = books - .where((book) => searchQuery.matches(book, dataStore: dataStore)) - .toList(); + books = books.where((book) => searchQuery.matches(book, dataStore: dataStore)).toList(); } } // Apply category filters (quick filters from chips) if (!provider.isFilterActive(LibraryFilterType.all)) { if (provider.isFilterActive(LibraryFilterType.favorites)) { - books = books - .where((book) => provider.isBookFavorite(book.id, book.isFavorite)) - .toList(); + books = books.where((book) => provider.isBookFavorite(book.id, book.isFavorite)).toList(); } if (provider.isFilterActive(LibraryFilterType.reading)) { books = books.where((book) => book.isReading).toList(); @@ -78,28 +74,16 @@ class _LibraryPageState extends State { books = books.where((book) => book.isFinished).toList(); } if (provider.isFilterActive(LibraryFilterType.unread)) { - books = books - .where((book) => book.readingStatus == ReadingStatus.notStarted) - .toList(); + books = books.where((book) => book.readingStatus == ReadingStatus.notStarted).toList(); } - if (provider.isFilterActive(LibraryFilterType.shelves) && - provider.selectedShelf != null) { + if (provider.isFilterActive(LibraryFilterType.shelves) && provider.selectedShelf != null) { books = books - .where( - (book) => dataStore - .getShelvesForBook(book.id) - .any((s) => s.name == provider.selectedShelf), - ) + .where((book) => dataStore.getShelvesForBook(book.id).any((s) => s.name == provider.selectedShelf)) .toList(); } - if (provider.isFilterActive(LibraryFilterType.topics) && - provider.selectedTopic != null) { + if (provider.isFilterActive(LibraryFilterType.topics) && provider.selectedTopic != null) { books = books - .where( - (book) => dataStore - .getTagsForBook(book.id) - .any((t) => t.name == provider.selectedTopic), - ) + .where((book) => dataStore.getTagsForBook(book.id).any((t) => t.name == provider.selectedTopic)) .toList(); } } @@ -111,11 +95,7 @@ class _LibraryPageState extends State { // MOBILE LAYOUT // ============================================================================ - Widget _buildMobileLayout( - BuildContext context, - List books, - LibraryProvider libraryProvider, - ) { + Widget _buildMobileLayout(BuildContext context, List books, LibraryProvider libraryProvider) { final isSelectionMode = libraryProvider.isSelectionMode; return Scaffold( @@ -126,19 +106,13 @@ class _LibraryPageState extends State { children: [ // Header: selection header or normal header Padding( - padding: const EdgeInsets.only( - top: Spacing.md, - left: Spacing.md, - right: Spacing.md, - ), + padding: const EdgeInsets.only(top: Spacing.md, left: Spacing.md, right: Spacing.md), child: isSelectionMode ? SelectionHeader( selectedCount: libraryProvider.selectedCount, totalCount: books.length, onClose: libraryProvider.exitSelectionMode, - onSelectAll: () => libraryProvider.selectAll( - books.map((b) => b.id).toList(), - ), + onSelectAll: () => libraryProvider.selectAll(books.map((b) => b.id).toList()), onDeselectAll: libraryProvider.deselectAll, ) : Row( @@ -166,19 +140,15 @@ class _LibraryPageState extends State { // View toggle row if (!isSelectionMode) Padding( - padding: const EdgeInsets.only( - left: Spacing.md, - right: Spacing.md, - bottom: Spacing.md, - ), + padding: const EdgeInsets.only(left: Spacing.md, right: Spacing.md, bottom: Spacing.md), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( '${books.length} ${books.length == 1 ? 'book' : 'books'}', - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), + style: Theme.of( + context, + ).textTheme.bodyMedium?.copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant), ), _buildViewToggle(libraryProvider), ], @@ -191,24 +161,15 @@ class _LibraryPageState extends State { ? _buildEmptyState() : libraryProvider.isListView ? _buildBookList(context, books) - : BookGrid( - books: books, - onBookTap: (book) => - _navigateToBookDetails(context, book), - ), + : BookGrid(books: books, onBookTap: (book) => _navigateToBookDetails(context, book)), ), ], ), ), floatingActionButton: isSelectionMode ? null - : FloatingActionButton( - onPressed: () => AddBookChoiceSheet.show(context), - child: const Icon(Icons.add), - ), - bottomNavigationBar: isSelectionMode - ? buildMobileBottomActionBar(context, libraryProvider) - : null, + : FloatingActionButton(onPressed: () => AddBookChoiceSheet.show(context), child: const Icon(Icons.add)), + bottomNavigationBar: isSelectionMode ? buildMobileBottomActionBar(context, libraryProvider) : null, ); } @@ -283,15 +244,9 @@ class _LibraryPageState extends State { filterOptions: filterOptions, initialFilters: AppliedFilters.fromQueryString( libraryProvider.searchQuery, - filterReading: libraryProvider.isFilterActive( - LibraryFilterType.reading, - ), - filterFavorites: libraryProvider.isFilterActive( - LibraryFilterType.favorites, - ), - filterFinished: libraryProvider.isFilterActive( - LibraryFilterType.finished, - ), + filterReading: libraryProvider.isFilterActive(LibraryFilterType.reading), + filterFavorites: libraryProvider.isFilterActive(LibraryFilterType.favorites), + filterFinished: libraryProvider.isFilterActive(LibraryFilterType.finished), filterUnread: libraryProvider.isFilterActive(LibraryFilterType.unread), shelf: libraryProvider.selectedShelf, topic: libraryProvider.selectedTopic, @@ -318,15 +273,9 @@ class _LibraryPageState extends State { filterOptions: filterOptions, initialFilters: AppliedFilters.fromQueryString( libraryProvider.searchQuery, - filterReading: libraryProvider.isFilterActive( - LibraryFilterType.reading, - ), - filterFavorites: libraryProvider.isFilterActive( - LibraryFilterType.favorites, - ), - filterFinished: libraryProvider.isFilterActive( - LibraryFilterType.finished, - ), + filterReading: libraryProvider.isFilterActive(LibraryFilterType.reading), + filterFavorites: libraryProvider.isFilterActive(LibraryFilterType.favorites), + filterFinished: libraryProvider.isFilterActive(LibraryFilterType.finished), filterUnread: libraryProvider.isFilterActive(LibraryFilterType.unread), shelf: libraryProvider.selectedShelf, topic: libraryProvider.selectedTopic, @@ -378,8 +327,7 @@ class _LibraryPageState extends State { Widget _buildSearchBar(LibraryProvider libraryProvider) { final activeFilters = _buildActiveFilters(libraryProvider); - final isDesktop = - MediaQuery.of(context).size.width >= Breakpoints.desktopSmall; + final isDesktop = MediaQuery.of(context).size.width >= Breakpoints.desktopSmall; return LibrarySearchBar( initialQuery: libraryProvider.searchQuery, @@ -391,9 +339,7 @@ class _LibraryPageState extends State { libraryProvider.setSearchQuery(query); } }, - onFilterTap: () => isDesktop - ? _showFilterDialog(context) - : _showFilterBottomSheet(context), + onFilterTap: () => isDesktop ? _showFilterDialog(context) : _showFilterBottomSheet(context), ); } @@ -404,9 +350,7 @@ class _LibraryPageState extends State { Widget _buildViewToggle(LibraryProvider libraryProvider) { return ViewModeToggle( isGridView: libraryProvider.isGridView, - onChanged: (isGrid) => libraryProvider.setViewMode( - isGrid ? LibraryViewMode.grid : LibraryViewMode.list, - ), + onChanged: (isGrid) => libraryProvider.setViewMode(isGrid ? LibraryViewMode.grid : LibraryViewMode.list), ); } @@ -425,54 +369,18 @@ class _LibraryPageState extends State { tooltip: 'Sort books', onSelected: provider.setSortOption, itemBuilder: (context) => [ - _buildSortMenuItem( - LibrarySortOption.dateAddedNewest, - 'Date added (newest)', - provider.sortOption, - ), - _buildSortMenuItem( - LibrarySortOption.dateAddedOldest, - 'Date added (oldest)', - provider.sortOption, - ), + _buildSortMenuItem(LibrarySortOption.dateAddedNewest, 'Date added (newest)', provider.sortOption), + _buildSortMenuItem(LibrarySortOption.dateAddedOldest, 'Date added (oldest)', provider.sortOption), const PopupMenuDivider(), - _buildSortMenuItem( - LibrarySortOption.titleAZ, - 'Title (A\u2013Z)', - provider.sortOption, - ), - _buildSortMenuItem( - LibrarySortOption.titleZA, - 'Title (Z\u2013A)', - provider.sortOption, - ), + _buildSortMenuItem(LibrarySortOption.titleAZ, 'Title (A\u2013Z)', provider.sortOption), + _buildSortMenuItem(LibrarySortOption.titleZA, 'Title (Z\u2013A)', provider.sortOption), const PopupMenuDivider(), - _buildSortMenuItem( - LibrarySortOption.authorAZ, - 'Author (A\u2013Z)', - provider.sortOption, - ), - _buildSortMenuItem( - LibrarySortOption.authorZA, - 'Author (Z\u2013A)', - provider.sortOption, - ), + _buildSortMenuItem(LibrarySortOption.authorAZ, 'Author (A\u2013Z)', provider.sortOption), + _buildSortMenuItem(LibrarySortOption.authorZA, 'Author (Z\u2013A)', provider.sortOption), const PopupMenuDivider(), - _buildSortMenuItem( - LibrarySortOption.lastRead, - 'Last read', - provider.sortOption, - ), - _buildSortMenuItem( - LibrarySortOption.rating, - 'Rating', - provider.sortOption, - ), - _buildSortMenuItem( - LibrarySortOption.progress, - 'Progress', - provider.sortOption, - ), + _buildSortMenuItem(LibrarySortOption.lastRead, 'Last read', provider.sortOption), + _buildSortMenuItem(LibrarySortOption.rating, 'Rating', provider.sortOption), + _buildSortMenuItem(LibrarySortOption.progress, 'Progress', provider.sortOption), ], ); } @@ -490,20 +398,14 @@ class _LibraryPageState extends State { Icon( Icons.check, size: IconSizes.small, - color: option == current - ? Theme.of(context).colorScheme.primary - : Colors.transparent, + color: option == current ? Theme.of(context).colorScheme.primary : Colors.transparent, ), ], ), ); } - Widget _buildDesktopLayout( - BuildContext context, - List books, - LibraryProvider libraryProvider, - ) { + Widget _buildDesktopLayout(BuildContext context, List books, LibraryProvider libraryProvider) { const double controlHeight = 40.0; final isSelectionMode = libraryProvider.isSelectionMode; @@ -523,19 +425,13 @@ class _LibraryPageState extends State { children: [ // Header row Container( - padding: const EdgeInsets.only( - top: Spacing.lg, - left: Spacing.lg, - right: Spacing.lg, - ), + padding: const EdgeInsets.only(top: Spacing.lg, left: Spacing.lg, right: Spacing.lg), child: isSelectionMode ? SelectionHeader( selectedCount: libraryProvider.selectedCount, totalCount: books.length, onClose: libraryProvider.exitSelectionMode, - onSelectAll: () => libraryProvider.selectAll( - books.map((b) => b.id).toList(), - ), + onSelectAll: () => libraryProvider.selectAll(books.map((b) => b.id).toList()), onDeselectAll: libraryProvider.deselectAll, actions: buildBulkActionBar(context, libraryProvider), ) @@ -549,9 +445,7 @@ class _LibraryPageState extends State { children: [ Row( children: [ - Expanded( - child: _buildSearchBar(libraryProvider), - ), + Expanded(child: _buildSearchBar(libraryProvider)), const SizedBox(width: Spacing.sm), _buildSortButton(libraryProvider), ], @@ -591,11 +485,7 @@ class _LibraryPageState extends State { ? _buildEmptyState() : libraryProvider.isListView ? _buildBookList(context, books) - : BookGrid( - books: books, - onBookTap: (book) => - _navigateToBookDetails(context, book), - ), + : BookGrid(books: books, onBookTap: (book) => _navigateToBookDetails(context, book)), ), ], ), @@ -613,10 +503,7 @@ class _LibraryPageState extends State { itemCount: books.length, itemBuilder: (context, index) { final book = books[index]; - final isFavorite = libraryProvider.isBookFavorite( - book.id, - book.isFavorite, - ); + final isFavorite = libraryProvider.isBookFavorite(book.id, book.isFavorite); return BookListItem( book: book, isFavorite: isFavorite, diff --git a/app/lib/pages/login_page.dart b/app/lib/pages/login_page.dart index 6964a28..52e786e 100644 --- a/app/lib/pages/login_page.dart +++ b/app/lib/pages/login_page.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:go_router/go_router.dart'; +import 'package:papyrus/providers/auth_provider.dart'; import 'package:papyrus/themes/design_tokens.dart'; import 'package:papyrus/utils/responsive.dart'; import 'package:papyrus/widgets/auth/auth_continue_button.dart'; @@ -10,9 +10,8 @@ import 'package:papyrus/widgets/buttons/google_sign_in.dart'; import 'package:papyrus/widgets/input/email_input.dart'; import 'package:papyrus/widgets/input/password_input.dart'; import 'package:papyrus/widgets/titled_divider.dart'; +import 'package:provider/provider.dart'; -/// Login page for the Papyrus book management application. -/// Provides responsive layouts for mobile and desktop displays. class LoginPage extends StatefulWidget { const LoginPage({super.key}); @@ -39,43 +38,40 @@ class _LoginPageState extends State { } Future _handleLogin() async { - if (_isLoading) return; + if (_isLoading) { + return; + } - // Hide keyboard FocusScope.of(context).unfocus(); - if (!_formKey.currentState!.validate()) return; + if (!_formKey.currentState!.validate()) { + return; + } setState(() => _isLoading = true); ScaffoldMessenger.of(context).hideCurrentSnackBar(); try { - await Supabase.instance.client.auth.signInWithPassword( + final success = await context.read().login( email: _emailController.text.trim(), password: _passwordController.text.trim(), ); - if (!mounted) return; + if (!mounted) { + return; + } + + if (!success) { + _showErrorSnackBar(context.read().error ?? 'Incorrect email or password.'); + return; + } + context.goNamed('LIBRARY'); - } on AuthException catch (e) { - if (!mounted) return; - - String message; - final msg = e.message.toLowerCase(); - if (msg.contains('invalid login credentials') || - msg.contains('invalid email or password')) { - message = 'Incorrect email or password.'; - } else if (msg.contains('email not confirmed')) { - message = 'Please verify your email before signing in.'; - } else if (msg.contains('too many requests')) { - message = 'Too many attempts. Please try again later.'; - } else { - message = 'Incorrect email or password.'; + } catch (error) { + if (!mounted) { + return; } - _showErrorSnackBar(message); - } catch (e) { - if (!mounted) return; _showErrorSnackBar('An error occurred. Please try again.'); } finally { if (mounted) { @@ -103,11 +99,7 @@ class _LoginPageState extends State { const TitledDivider(title: 'Or continue with'), const GoogleSignInButton(title: 'Sign in with Google'), const SizedBox(height: Spacing.md), - AuthSwitchLink( - promptText: "Don't have an account?", - actionText: 'Sign up', - onPressed: _navigateToRegister, - ), + AuthSwitchLink(promptText: "Don't have an account?", actionText: 'Sign up', onPressed: _navigateToRegister), ]; } @@ -148,11 +140,6 @@ class _LoginPageState extends State { } } -// ============================================================================= -// LOGIN FORM -// ============================================================================= - -/// Login-specific form with email and password fields. class _LoginForm extends StatelessWidget { final GlobalKey formKey; final TextEditingController emailController; @@ -204,21 +191,12 @@ class _LoginForm extends StatelessWidget { onPressed: () { context.go('/forgot-password'); }, - style: TextButton.styleFrom( - foregroundColor: Theme.of(context).colorScheme.primary, - ), - child: const Text( - 'Forgot password?', - style: TextStyle(fontWeight: FontWeight.w500), - ), + style: TextButton.styleFrom(foregroundColor: Theme.of(context).colorScheme.primary), + child: const Text('Forgot password?', style: TextStyle(fontWeight: FontWeight.w500)), ), ), const SizedBox(height: Spacing.sm), - AuthContinueButton( - isLoading: isLoading, - onPressed: onLogin, - isDesktop: isDesktop, - ), + AuthContinueButton(isLoading: isLoading, onPressed: onLogin, isDesktop: isDesktop), ], ), ); diff --git a/app/lib/pages/notes_page.dart b/app/lib/pages/notes_page.dart index 5bc658e..095bbe6 100644 --- a/app/lib/pages/notes_page.dart +++ b/app/lib/pages/notes_page.dart @@ -79,11 +79,7 @@ class _NotesPageState extends State { children: [ // Row 1: Menu + Search + Sort Padding( - padding: const EdgeInsets.only( - top: Spacing.md, - left: Spacing.md, - right: Spacing.md, - ), + padding: const EdgeInsets.only(top: Spacing.md, left: Spacing.md, right: Spacing.md), child: Row( children: [ IconButton( @@ -103,8 +99,7 @@ class _NotesPageState extends State { const SizedBox(height: Spacing.md), // Tag filter chips - if (provider.hasNotes && provider.allTags.isNotEmpty) - _buildTagFilterChips(provider), + if (provider.hasNotes && provider.allTags.isNotEmpty) _buildTagFilterChips(provider), const SizedBox(height: Spacing.sm), @@ -127,11 +122,7 @@ class _NotesPageState extends State { children: [ // Header row Container( - padding: const EdgeInsets.only( - top: Spacing.lg, - left: Spacing.lg, - right: Spacing.lg, - ), + padding: const EdgeInsets.only(top: Spacing.lg, left: Spacing.lg, right: Spacing.lg), child: Row( children: [ Expanded(child: _buildSearchField(provider)), @@ -143,8 +134,7 @@ class _NotesPageState extends State { const SizedBox(height: Spacing.md), // Tag filter chips - if (provider.hasNotes && provider.allTags.isNotEmpty) - _buildTagFilterChips(provider), + if (provider.hasNotes && provider.allTags.isNotEmpty) _buildTagFilterChips(provider), // Content Expanded(child: _buildContent(context, provider)), @@ -186,46 +176,21 @@ class _NotesPageState extends State { tooltip: 'Sort notes', onSelected: provider.setSortOption, itemBuilder: (context) => [ - _buildSortMenuItem( - NoteSortOption.dateNewest, - 'Newest first', - provider.sortOption, - ), - _buildSortMenuItem( - NoteSortOption.dateOldest, - 'Oldest first', - provider.sortOption, - ), - _buildSortMenuItem( - NoteSortOption.bookTitle, - 'By book title', - provider.sortOption, - ), - _buildSortMenuItem( - NoteSortOption.pinnedFirst, - 'Pinned first', - provider.sortOption, - ), + _buildSortMenuItem(NoteSortOption.dateNewest, 'Newest first', provider.sortOption), + _buildSortMenuItem(NoteSortOption.dateOldest, 'Oldest first', provider.sortOption), + _buildSortMenuItem(NoteSortOption.bookTitle, 'By book title', provider.sortOption), + _buildSortMenuItem(NoteSortOption.pinnedFirst, 'Pinned first', provider.sortOption), ], ); } - PopupMenuItem _buildSortMenuItem( - NoteSortOption option, - String label, - NoteSortOption current, - ) { + PopupMenuItem _buildSortMenuItem(NoteSortOption option, String label, NoteSortOption current) { return PopupMenuItem( value: option, child: Row( children: [ Expanded(child: Text(label)), - if (option == current) - Icon( - Icons.check, - size: IconSizes.small, - color: Theme.of(context).colorScheme.primary, - ), + if (option == current) Icon(Icons.check, size: IconSizes.small, color: Theme.of(context).colorScheme.primary), ], ), ); @@ -345,21 +310,13 @@ class _NotesPageState extends State { context.goNamed('BOOK_DETAILS', pathParameters: {'bookId': bookId}); } - void _showNoteActions( - BuildContext context, - NotesProvider provider, - Note note, - ) async { + void _showNoteActions(BuildContext context, NotesProvider provider, Note note) async { final action = await NoteActionSheet.show(context, note: note); if (!mounted || action == null) return; switch (action) { case NoteAction.edit: - final updatedNote = await NoteDialog.show( - this.context, - bookId: note.bookId, - existingNote: note, - ); + final updatedNote = await NoteDialog.show(this.context, bookId: note.bookId, existingNote: note); if (updatedNote != null && mounted) { provider.updateNote(updatedNote); } diff --git a/app/lib/pages/profile_page.dart b/app/lib/pages/profile_page.dart index aae02ca..e46bb9b 100644 --- a/app/lib/pages/profile_page.dart +++ b/app/lib/pages/profile_page.dart @@ -3,7 +3,6 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:papyrus/providers/auth_provider.dart'; import 'package:papyrus/providers/preferences_provider.dart'; -import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:papyrus/themes/design_tokens.dart'; import 'package:papyrus/widgets/settings/settings_row.dart'; import 'package:papyrus/widgets/settings/settings_section.dart'; @@ -104,11 +103,7 @@ class _ProfilePageState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ const SettingsSectionHeader(title: 'Appearance'), - SettingsRow( - label: 'Theme', - value: _getThemeLabel(prefs.themeModePref), - onTap: () => _showThemePicker(context), - ), + SettingsRow(label: 'Theme', value: _getThemeLabel(prefs.themeModePref), onTap: () => _showThemePicker(context)), ], ); } @@ -120,11 +115,7 @@ class _ProfilePageState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ const SettingsSectionHeader(title: 'Reading'), - SettingsRow( - label: 'Default font', - value: prefs.defaultFont, - onTap: () => _showFontPicker(context), - ), + SettingsRow(label: 'Default font', value: prefs.defaultFont, onTap: () => _showFontPicker(context)), SettingsRow( label: 'Line spacing', value: _capitalize(prefs.lineSpacing), @@ -209,11 +200,7 @@ class _ProfilePageState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ const SettingsSectionHeader(title: 'Storage & sync'), - SettingsRow( - label: 'Storage backend', - value: prefs.storageBackend, - onTap: () => _showStoragePicker(context), - ), + SettingsRow(label: 'Storage backend', value: prefs.storageBackend, onTap: () => _showStoragePicker(context)), SettingsRow( label: 'Sync server', value: prefs.serverUrl.isEmpty ? 'Not connected' : prefs.serverUrl, @@ -376,12 +363,7 @@ class _ProfilePageState extends State { label: 'Accessibility', section: _ProfileSection.accessibility, ), - _buildNavItem( - context, - icon: Icons.info_outline, - label: 'About', - section: _ProfileSection.about, - ), + _buildNavItem(context, icon: Icons.info_outline, label: 'About', section: _ProfileSection.about), if (kDebugMode) _buildNavItem( context, @@ -430,9 +412,7 @@ class _ProfilePageState extends State { : isSelected ? colorScheme.onPrimaryContainer : null; - final bgColor = isSelected - ? colorScheme.primaryContainer - : Colors.transparent; + final bgColor = isSelected ? colorScheme.primaryContainer : Colors.transparent; return Padding( padding: const EdgeInsets.symmetric(vertical: 2), @@ -449,10 +429,7 @@ class _ProfilePageState extends State { }, borderRadius: BorderRadius.circular(AppRadius.md), child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: Spacing.md, - vertical: Spacing.sm + 2, - ), + padding: const EdgeInsets.symmetric(horizontal: Spacing.md, vertical: Spacing.sm + 2), child: Row( children: [ Icon(icon, color: iconColor, size: IconSizes.medium), @@ -462,9 +439,7 @@ class _ProfilePageState extends State { label, style: textTheme.bodyMedium?.copyWith( color: textColor, - fontWeight: isSelected - ? FontWeight.w600 - : FontWeight.normal, + fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal, ), ), ), @@ -573,12 +548,7 @@ class _ProfilePageState extends State { children: [ Text(_getDisplayName(), style: textTheme.headlineSmall), const SizedBox(height: Spacing.xs), - Text( - _getEmail(), - style: textTheme.bodyMedium?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), + Text(_getEmail(), style: textTheme.bodyMedium?.copyWith(color: colorScheme.onSurfaceVariant)), const SizedBox(height: Spacing.md), Align( alignment: Alignment.centerLeft, @@ -607,11 +577,7 @@ class _ProfilePageState extends State { SettingsCard( title: 'Connected accounts', children: [ - SettingsRow( - label: 'Google', - value: _isGoogleLinked() ? 'Connected' : 'Not connected', - onTap: () {}, - ), + SettingsRow(label: 'Google', value: _isGoogleLinked() ? 'Connected' : 'Not connected', onTap: () {}), ], ), const SizedBox(height: Spacing.lg), @@ -636,12 +602,7 @@ class _ProfilePageState extends State { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - 'Theme', - style: textTheme.bodyMedium?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), + Text('Theme', style: textTheme.bodyMedium?.copyWith(color: colorScheme.onSurfaceVariant)), const SizedBox(height: Spacing.sm), _buildRadioTile('Light', 'light'), _buildRadioTile('Dark', 'dark'), @@ -667,9 +628,7 @@ class _ProfilePageState extends State { decoration: BoxDecoration( shape: BoxShape.circle, border: Border.all( - color: isSelected - ? Theme.of(context).colorScheme.primary - : Theme.of(context).colorScheme.outline, + color: isSelected ? Theme.of(context).colorScheme.primary : Theme.of(context).colorScheme.outline, width: 2, ), ), @@ -678,10 +637,7 @@ class _ProfilePageState extends State { child: Container( width: 10, height: 10, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: Theme.of(context).colorScheme.primary, - ), + decoration: BoxDecoration(shape: BoxShape.circle, color: Theme.of(context).colorScheme.primary), ), ) : null, @@ -723,12 +679,7 @@ class _ProfilePageState extends State { onChanged: (value) => prefs.defaultFont = value, ), const SizedBox(height: Spacing.lg), - Text( - 'Default font size', - style: textTheme.bodyMedium?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), + Text('Default font size', style: textTheme.bodyMedium?.copyWith(color: colorScheme.onSurfaceVariant)), Row( children: [ Expanded( @@ -740,13 +691,7 @@ class _ProfilePageState extends State { onChanged: (value) => prefs.defaultFontSize = value, ), ), - SizedBox( - width: 48, - child: Text( - '${prefs.defaultFontSize.toInt()}px', - style: textTheme.bodyMedium, - ), - ), + SizedBox(width: 48, child: Text('${prefs.defaultFontSize.toInt()}px', style: textTheme.bodyMedium)), ], ), const SizedBox(height: Spacing.md), @@ -754,11 +699,7 @@ class _ProfilePageState extends State { context, label: 'Line spacing', value: prefs.lineSpacing, - options: const { - 'compact': 'Compact', - 'normal': 'Normal', - 'relaxed': 'Relaxed', - }, + options: const {'compact': 'Compact', 'normal': 'Normal', 'relaxed': 'Relaxed'}, onChanged: (value) => prefs.lineSpacing = value, ), const SizedBox(height: Spacing.md), @@ -774,11 +715,7 @@ class _ProfilePageState extends State { context, label: 'Margins', value: prefs.margins, - options: const { - 'small': 'Small', - 'medium': 'Medium', - 'large': 'Large', - }, + options: const {'small': 'Small', 'medium': 'Medium', 'large': 'Large'}, onChanged: (value) => prefs.margins = value, ), ], @@ -791,10 +728,7 @@ class _ProfilePageState extends State { context, label: 'Reading mode', value: prefs.readingMode, - options: const { - 'paginated': 'Paginated', - 'scroll': 'Continuous scroll', - }, + options: const {'paginated': 'Paginated', 'scroll': 'Continuous scroll'}, onChanged: (value) => prefs.readingMode = value, ), const SizedBox(height: Spacing.md), @@ -806,10 +740,7 @@ class _ProfilePageState extends State { ], ), const SizedBox(height: Spacing.lg), - SettingsCard( - title: 'Annotations', - children: [_buildHighlightColorField(context)], - ), + SettingsCard(title: 'Annotations', children: [_buildHighlightColorField(context)]), const SizedBox(height: Spacing.lg), SettingsCard( children: [SettingsRow(label: 'Reading profiles', onTap: () {})], @@ -834,12 +765,7 @@ class _ProfilePageState extends State { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - 'Default highlight color', - style: textTheme.bodyMedium?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), + Text('Default highlight color', style: textTheme.bodyMedium?.copyWith(color: colorScheme.onSurfaceVariant)), const SizedBox(height: Spacing.sm), Row( children: highlightColors.entries.map((entry) { @@ -858,9 +784,7 @@ class _ProfilePageState extends State { ? Border.all(color: colorScheme.primary, width: 3) : Border.all(color: colorScheme.outline, width: 1), ), - child: isSelected - ? Icon(Icons.check, size: 18, color: colorScheme.primary) - : null, + child: isSelected ? Icon(Icons.check, size: 18, color: colorScheme.primary) : null, ), ), ); @@ -885,11 +809,7 @@ class _ProfilePageState extends State { context, label: 'Default view mode', value: prefs.defaultViewMode, - options: const { - 'grid': 'Grid', - 'list': 'List', - 'compact': 'Compact', - }, + options: const {'grid': 'Grid', 'list': 'List', 'compact': 'Compact'}, onChanged: (value) => prefs.defaultViewMode = value, ), const SizedBox(height: Spacing.md), @@ -897,13 +817,7 @@ class _ProfilePageState extends State { context, label: 'Default sort order', value: prefs.defaultSortOrder, - options: const [ - 'title', - 'author', - 'date_added', - 'last_read', - 'rating', - ], + options: const ['title', 'author', 'date_added', 'last_read', 'rating'], labels: const { 'title': 'Title', 'author': 'Author', @@ -923,10 +837,7 @@ class _ProfilePageState extends State { context, label: 'Metadata source', value: prefs.metadataSource, - options: const { - 'Open Library': 'Open Library', - 'Google Books': 'Google Books', - }, + options: const {'Open Library': 'Open Library', 'Google Books': 'Google Books'}, onChanged: (value) => prefs.metadataSource = value, ), const SizedBox(height: Spacing.md), @@ -1010,39 +921,22 @@ class _ProfilePageState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - 'Server', - style: textTheme.bodyMedium?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), + Text('Server', style: textTheme.bodyMedium?.copyWith(color: colorScheme.onSurfaceVariant)), const SizedBox(height: Spacing.xs), - Text( - prefs.serverUrl.isEmpty - ? 'Not connected' - : prefs.serverUrl, - style: textTheme.bodyLarge, - ), + Text(prefs.serverUrl.isEmpty ? 'Not connected' : prefs.serverUrl, style: textTheme.bodyLarge), ], ), ), Container( - padding: const EdgeInsets.symmetric( - horizontal: Spacing.sm, - vertical: Spacing.xs, - ), + padding: const EdgeInsets.symmetric(horizontal: Spacing.sm, vertical: Spacing.xs), decoration: BoxDecoration( - color: prefs.serverUrl.isEmpty - ? colorScheme.errorContainer - : colorScheme.primaryContainer, + color: prefs.serverUrl.isEmpty ? colorScheme.errorContainer : colorScheme.primaryContainer, borderRadius: BorderRadius.circular(AppRadius.sm), ), child: Text( prefs.serverUrl.isEmpty ? 'Offline' : 'Connected', style: textTheme.labelSmall?.copyWith( - color: prefs.serverUrl.isEmpty - ? colorScheme.onErrorContainer - : colorScheme.onPrimaryContainer, + color: prefs.serverUrl.isEmpty ? colorScheme.onErrorContainer : colorScheme.onPrimaryContainer, ), ), ), @@ -1053,10 +947,7 @@ class _ProfilePageState extends State { context, label: 'Server type', value: prefs.serverType, - options: const { - 'official': 'Official', - 'self-hosted': 'Self-hosted', - }, + options: const {'official': 'Official', 'self-hosted': 'Self-hosted'}, onChanged: (value) => prefs.serverType = value, ), const SizedBox(height: Spacing.md), @@ -1084,33 +975,21 @@ class _ProfilePageState extends State { context, label: 'Conflict resolution', value: prefs.conflictResolution, - options: const { - 'server': 'Server wins', - 'client': 'Client wins', - 'ask': 'Ask me', - }, + options: const {'server': 'Server wins', 'client': 'Client wins', 'ask': 'Ask me'}, onChanged: (value) => prefs.conflictResolution = value, ), const SizedBox(height: Spacing.md), Padding( - padding: const EdgeInsets.symmetric( - horizontal: Spacing.sm, - vertical: Spacing.xs, - ), + padding: const EdgeInsets.symmetric(horizontal: Spacing.sm, vertical: Spacing.xs), child: Text( 'Last sync: 2 min ago', - style: textTheme.bodySmall?.copyWith( - color: colorScheme.onSurfaceVariant, - ), + style: textTheme.bodySmall?.copyWith(color: colorScheme.onSurfaceVariant), ), ), const SizedBox(height: Spacing.sm), Align( alignment: Alignment.centerLeft, - child: OutlinedButton( - onPressed: () {}, - child: const Text('Sync now'), - ), + child: OutlinedButton(onPressed: () {}, child: const Text('Sync now')), ), ], ), @@ -1139,9 +1018,9 @@ class _ProfilePageState extends State { child: Text( 'Help improve Papyrus by sharing anonymous usage statistics. ' 'No personal data or reading content is collected.', - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), + style: Theme.of( + context, + ).textTheme.bodySmall?.copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant), ), ), ], @@ -1177,17 +1056,13 @@ class _ProfilePageState extends State { onChanged: (value) => prefs.reduceAnimations = value, ), Padding( - padding: const EdgeInsets.only( - left: Spacing.sm, - right: Spacing.sm, - bottom: Spacing.md, - ), + padding: const EdgeInsets.only(left: Spacing.sm, right: Spacing.sm, bottom: Spacing.md), child: Text( 'Minimizes motion effects throughout the app. ' 'Separate from e-ink mode.', - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), + style: Theme.of( + context, + ).textTheme.bodySmall?.copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant), ), ), SettingsToggleRow( @@ -1196,16 +1071,12 @@ class _ProfilePageState extends State { onChanged: (value) => prefs.dyslexiaFont = value, ), Padding( - padding: const EdgeInsets.only( - left: Spacing.sm, - right: Spacing.sm, - bottom: Spacing.md, - ), + padding: const EdgeInsets.only(left: Spacing.sm, right: Spacing.sm, bottom: Spacing.md), child: Text( 'Use OpenDyslexic font across the app interface.', - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), + style: Theme.of( + context, + ).textTheme.bodySmall?.copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant), ), ), ], @@ -1251,25 +1122,18 @@ class _ProfilePageState extends State { // ============================================================================ String _getDisplayName() { - final user = Supabase.instance.client.auth.currentUser; - return (user?.userMetadata?['full_name'] as String?) ?? 'Anonymous User'; + final user = context.watch().user; + return user?.displayName ?? 'Anonymous User'; } String _getEmail() { - final user = Supabase.instance.client.auth.currentUser; - final email = user?.email; + final email = context.watch().user?.email; if (email == null || email.trim().isEmpty) return 'No email provided'; return email; } String? _getAvatarUrl() { - return Supabase - .instance - .client - .auth - .currentUser - ?.userMetadata?['avatar_url'] - as String?; + return context.watch().user?.avatarUrl; } String get _initials { @@ -1282,10 +1146,7 @@ class _ProfilePageState extends State { } bool _isGoogleLinked() { - final user = Supabase.instance.client.auth.currentUser; - if (user == null) return false; - return user.appMetadata['provider'] == 'google' || - (user.identities?.any((i) => i.provider == 'google') ?? false); + return false; } // ============================================================================ @@ -1305,10 +1166,7 @@ class _ProfilePageState extends State { title: const Text('Log out'), content: const Text('Are you sure you want to log out?'), actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: const Text('Cancel'), - ), + TextButton(onPressed: () => Navigator.pop(context), child: const Text('Cancel')), FilledButton( onPressed: () { Navigator.pop(context); @@ -1326,11 +1184,7 @@ class _ProfilePageState extends State { } void _showLicenses(BuildContext context) { - showLicensePage( - context: context, - applicationName: 'Papyrus', - applicationVersion: '1.0.0', - ); + showLicensePage(context: context, applicationName: 'Papyrus', applicationVersion: '1.0.0'); } // ============================================================================ @@ -1351,12 +1205,7 @@ class _ProfilePageState extends State { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - label, - style: textTheme.bodyMedium?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), + Text(label, style: textTheme.bodyMedium?.copyWith(color: colorScheme.onSurfaceVariant)), const SizedBox(height: Spacing.sm), DropdownMenu( initialSelection: value, @@ -1386,21 +1235,13 @@ class _ProfilePageState extends State { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - label, - style: textTheme.bodyMedium?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), + Text(label, style: textTheme.bodyMedium?.copyWith(color: colorScheme.onSurfaceVariant)), const SizedBox(height: Spacing.sm), SizedBox( width: double.infinity, child: SegmentedButton( segments: options.entries.map((entry) { - return ButtonSegment( - value: entry.key, - label: Text(entry.value), - ); + return ButtonSegment(value: entry.key, label: Text(entry.value)); }).toList(), selected: {value}, onSelectionChanged: (selected) { @@ -1452,12 +1293,7 @@ class _ProfilePageState extends State { _showPickerSheet( context, - items: [ - ('Light', 'light'), - ('Dark', 'dark'), - ('E-ink', 'eink'), - ('System', 'system'), - ], + items: [('Light', 'light'), ('Dark', 'dark'), ('E-ink', 'eink'), ('System', 'system')], selected: prefs.themeModePref, onSelected: (value) => prefs.themeModePref = value, ); @@ -1487,11 +1323,7 @@ class _ProfilePageState extends State { _showPickerSheet( context, - items: [ - ('Compact', 'compact'), - ('Normal', 'normal'), - ('Relaxed', 'relaxed'), - ], + items: [('Compact', 'compact'), ('Normal', 'normal'), ('Relaxed', 'relaxed')], selected: prefs.lineSpacing, onSelected: (value) => prefs.lineSpacing = value, ); @@ -1541,10 +1373,7 @@ class _ProfilePageState extends State { _showPickerSheet( context, - items: [ - ('Open Library', 'Open Library'), - ('Google Books', 'Google Books'), - ], + items: [('Open Library', 'Open Library'), ('Google Books', 'Google Books')], selected: prefs.metadataSource, onSelected: (value) => prefs.metadataSource = value, ); @@ -1555,12 +1384,7 @@ class _ProfilePageState extends State { _showPickerSheet( context, - items: [ - ('Markdown', 'Markdown'), - ('PDF', 'PDF'), - ('TXT', 'TXT'), - ('HTML', 'HTML'), - ], + items: [('Markdown', 'Markdown'), ('PDF', 'PDF'), ('TXT', 'TXT'), ('HTML', 'HTML')], selected: prefs.annotationExportFormat, onSelected: (value) => prefs.annotationExportFormat = value, ); @@ -1571,11 +1395,7 @@ class _ProfilePageState extends State { _showPickerSheet( context, - items: [ - ('Local', 'Local'), - ('Cloud', 'Cloud'), - ('Self-hosted', 'Self-hosted'), - ], + items: [('Local', 'Local'), ('Cloud', 'Cloud'), ('Self-hosted', 'Self-hosted')], selected: prefs.storageBackend, onSelected: (value) => prefs.storageBackend = value, ); @@ -1638,26 +1458,17 @@ class _ProfilePageState extends State { children: [ _buildAvatar(context, size: avatarSize), const SizedBox(height: Spacing.md), - Text( - _getDisplayName(), - style: textTheme.headlineSmall, - textAlign: TextAlign.center, - ), + Text(_getDisplayName(), style: textTheme.headlineSmall, textAlign: TextAlign.center), const SizedBox(height: Spacing.xs), Text( _getEmail(), - style: textTheme.bodyMedium?.copyWith( - color: colorScheme.onSurfaceVariant, - ), + style: textTheme.bodyMedium?.copyWith(color: colorScheme.onSurfaceVariant), textAlign: TextAlign.center, ), const SizedBox(height: Spacing.md), SizedBox( width: 200, - child: OutlinedButton( - onPressed: () => _navigateToEditProfile(context), - child: const Text('Edit profile'), - ), + child: OutlinedButton(onPressed: () => _navigateToEditProfile(context), child: const Text('Edit profile')), ), ], ); @@ -1671,10 +1482,7 @@ class _ProfilePageState extends State { return Container( width: size, height: size, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(size / 2), - color: colorScheme.primaryContainer, - ), + decoration: BoxDecoration(borderRadius: BorderRadius.circular(size / 2), color: colorScheme.primaryContainer), clipBehavior: Clip.antiAlias, child: avatarUrl != null && avatarUrl.isNotEmpty ? Image.network( @@ -1693,10 +1501,7 @@ class _ProfilePageState extends State { : Center( child: Text( _initials, - style: textTheme.headlineMedium?.copyWith( - color: colorScheme.onPrimaryContainer, - fontSize: size * 0.35, - ), + style: textTheme.headlineMedium?.copyWith(color: colorScheme.onPrimaryContainer, fontSize: size * 0.35), ), ), ); @@ -1712,9 +1517,7 @@ class _ProfilePageState extends State { }) { final colorScheme = Theme.of(context).colorScheme; final textTheme = Theme.of(context).textTheme; - final iconColor = isDestructive - ? colorScheme.error - : colorScheme.onSurfaceVariant; + final iconColor = isDestructive ? colorScheme.error : colorScheme.onSurfaceVariant; final textColor = isDestructive ? colorScheme.error : null; return Material( @@ -1723,10 +1526,7 @@ class _ProfilePageState extends State { onTap: onTap, borderRadius: BorderRadius.circular(AppRadius.sm), child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: Spacing.sm, - vertical: Spacing.md, - ), + padding: const EdgeInsets.symmetric(horizontal: Spacing.sm, vertical: Spacing.md), child: Row( children: [ Container( @@ -1742,17 +1542,9 @@ class _ProfilePageState extends State { ), const SizedBox(width: Spacing.md), Expanded( - child: Text( - label, - style: textTheme.bodyLarge?.copyWith(color: textColor), - ), + child: Text(label, style: textTheme.bodyLarge?.copyWith(color: textColor)), ), - if (showChevron) - Icon( - Icons.chevron_right, - color: colorScheme.onSurfaceVariant, - size: IconSizes.medium, - ), + if (showChevron) Icon(Icons.chevron_right, color: colorScheme.onSurfaceVariant, size: IconSizes.medium), ], ), ), diff --git a/app/lib/pages/register_page.dart b/app/lib/pages/register_page.dart index 6fe780c..a71e177 100644 --- a/app/lib/pages/register_page.dart +++ b/app/lib/pages/register_page.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:go_router/go_router.dart'; +import 'package:papyrus/providers/auth_provider.dart'; import 'package:papyrus/themes/design_tokens.dart'; import 'package:papyrus/utils/responsive.dart'; import 'package:papyrus/widgets/auth/auth_continue_button.dart'; @@ -11,9 +11,8 @@ import 'package:papyrus/widgets/input/email_input.dart'; import 'package:papyrus/widgets/input/name_input.dart'; import 'package:papyrus/widgets/input/password_input.dart'; import 'package:papyrus/widgets/titled_divider.dart'; +import 'package:provider/provider.dart'; -/// Register page for the Papyrus book management application. -/// Provides responsive layouts for mobile and desktop displays. class RegisterPage extends StatefulWidget { const RegisterPage({super.key}); @@ -48,43 +47,41 @@ class _RegisterPageState extends State { } Future _handleRegister() async { - if (_isLoading) return; + if (_isLoading) { + return; + } - // Hide keyboard FocusScope.of(context).unfocus(); - if (!_formKey.currentState!.validate()) return; + if (!_formKey.currentState!.validate()) { + return; + } setState(() => _isLoading = true); ScaffoldMessenger.of(context).hideCurrentSnackBar(); try { - await Supabase.instance.client.auth.signUp( + final success = await context.read().register( email: _emailController.text.trim(), password: _passwordController.text.trim(), - data: {'full_name': _displayNameController.text.trim()}, + displayName: _displayNameController.text.trim(), ); - if (!mounted) return; - context.goNamed('LIBRARY'); - } on AuthException catch (e) { - if (!mounted) return; + if (!mounted) { + return; + } + + if (!success) { + _showErrorSnackBar(context.read().error ?? 'Registration failed. Please try again.'); + return; + } - String message; - final msg = e.message.toLowerCase(); - if (msg.contains('user already registered') || - msg.contains('already been registered')) { - message = 'An account already exists with this email.'; - } else if (msg.contains('password should be at least') || - msg.contains('weak password')) { - message = 'Password is too weak. Please use a stronger password.'; - } else { - message = 'Registration failed. Please try again.'; + context.goNamed('LIBRARY'); + } catch (error) { + if (!mounted) { + return; } - _showErrorSnackBar(message); - } catch (e) { - if (!mounted) return; _showErrorSnackBar('An error occurred. Please try again.'); } finally { if (mounted) { @@ -111,6 +108,7 @@ class _RegisterPageState extends State { if (value != _passwordController.text) { return 'Passwords do not match'; } + return null; } @@ -118,6 +116,7 @@ class _RegisterPageState extends State { if (value != null && value.length < 8) { return 'Minimum 8 characters'; } + return null; } @@ -126,11 +125,7 @@ class _RegisterPageState extends State { const TitledDivider(title: 'Or continue with'), const GoogleSignInButton(title: 'Sign up with Google'), const SizedBox(height: Spacing.md), - AuthSwitchLink( - promptText: 'Already have an account?', - actionText: 'Sign in', - onPressed: _navigateToLogin, - ), + AuthSwitchLink(promptText: 'Already have an account?', actionText: 'Sign in', onPressed: _navigateToLogin), ]; } @@ -138,7 +133,7 @@ class _RegisterPageState extends State { Widget build(BuildContext context) { return ResponsiveBuilder( mobile: (context) => MobileAuthLayout( - heading: 'Create account', + heading: 'Create an account', subtitle: 'Sign up for a new account to get started', form: _RegisterForm( formKey: _formKey, @@ -159,7 +154,7 @@ class _RegisterPageState extends State { footer: _buildFooter(), ), desktop: (context) => DesktopAuthLayout( - heading: 'Create account', + heading: 'Create an account', subtitle: 'Sign up for a new account to get started', form: _RegisterForm( formKey: _formKey, @@ -183,11 +178,6 @@ class _RegisterPageState extends State { } } -// ============================================================================= -// REGISTER FORM -// ============================================================================= - -/// Register-specific form with display name, email, password, and confirm password fields. class _RegisterForm extends StatelessWidget { final GlobalKey formKey; final TextEditingController displayNameController; @@ -263,11 +253,7 @@ class _RegisterForm extends StatelessWidget { extraValidator: validateConfirmPassword, ), const SizedBox(height: Spacing.lg), - AuthContinueButton( - isLoading: isLoading, - onPressed: onRegister, - isDesktop: isDesktop, - ), + AuthContinueButton(isLoading: isLoading, onPressed: onRegister, isDesktop: isDesktop), ], ), ); diff --git a/app/lib/pages/shelf_contents_page.dart b/app/lib/pages/shelf_contents_page.dart index 63d4b69..8b7f354 100644 --- a/app/lib/pages/shelf_contents_page.dart +++ b/app/lib/pages/shelf_contents_page.dart @@ -107,21 +107,14 @@ class _ShelfContentsPageState extends State { // MOBILE LAYOUT // ============================================================================ - Widget _buildMobileLayout( - BuildContext context, - Shelf shelf, - ShelvesProvider provider, - ) { + Widget _buildMobileLayout(BuildContext context, Shelf shelf, ShelvesProvider provider) { final libraryProvider = context.watch(); final childShelves = provider.getChildShelves(shelf.id); final books = provider.getFilteredBooksForShelf( shelf.id, isFavorite: (bookId) { final book = context.read().getBook(bookId); - return libraryProvider.isBookFavorite( - bookId, - book?.isFavorite ?? false, - ); + return libraryProvider.isBookFavorite(bookId, book?.isFavorite ?? false); }, ); @@ -138,31 +131,20 @@ class _ShelfContentsPageState extends State { children: [ // Row 1: Selection header or Back + Search + Sort Padding( - padding: const EdgeInsets.only( - top: Spacing.md, - left: Spacing.md, - right: Spacing.md, - ), + padding: const EdgeInsets.only(top: Spacing.md, left: Spacing.md, right: Spacing.md), child: isSelectionMode ? SelectionHeader( selectedCount: libraryProvider.selectedCount, totalCount: books.length, onClose: libraryProvider.exitSelectionMode, - onSelectAll: () => libraryProvider.selectAll( - books.map((b) => b.id).toList(), - ), + onSelectAll: () => libraryProvider.selectAll(books.map((b) => b.id).toList()), onDeselectAll: libraryProvider.deselectAll, ) : Row( children: [ - IconButton( - icon: const Icon(Icons.arrow_back), - onPressed: () => context.go('/library/shelves'), - ), + IconButton(icon: const Icon(Icons.arrow_back), onPressed: () => context.go('/library/shelves')), const SizedBox(width: Spacing.xs), - Expanded( - child: _buildSearchBar(provider, isDesktop: false), - ), + Expanded(child: _buildSearchBar(provider, isDesktop: false)), const SizedBox(width: Spacing.sm), _buildSortButton(provider), ], @@ -173,37 +155,25 @@ class _ShelfContentsPageState extends State { // Row 2: Shelf info + View toggle (hidden during selection) if (!isSelectionMode) Padding( - padding: const EdgeInsets.only( - left: Spacing.md, - right: Spacing.md, - bottom: Spacing.md, - ), + padding: const EdgeInsets.only(left: Spacing.md, right: Spacing.md, bottom: Spacing.md), child: Row( children: [ Expanded( child: Row( children: [ - Icon( - shelf.displayIcon, - size: IconSizes.small, - color: shelfColor, - ), + Icon(shelf.displayIcon, size: IconSizes.small, color: shelfColor), const SizedBox(width: Spacing.xs), Flexible( child: Text( shelf.name, overflow: TextOverflow.ellipsis, - style: textTheme.bodyMedium?.copyWith( - fontWeight: FontWeight.w600, - ), + style: textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w600), ), ), const SizedBox(width: Spacing.sm), Text( '\u00b7 ${books.length} ${books.length == 1 ? 'book' : 'books'}', - style: textTheme.bodyMedium?.copyWith( - color: colorScheme.onSurfaceVariant, - ), + style: textTheme.bodyMedium?.copyWith(color: colorScheme.onSurfaceVariant), ), ], ), @@ -214,21 +184,11 @@ class _ShelfContentsPageState extends State { ), ), // Content grid/list - Expanded( - child: _buildContent( - context, - childShelves, - books, - provider, - libraryProvider, - ), - ), + Expanded(child: _buildContent(context, childShelves, books, provider, libraryProvider)), ], ), ), - bottomNavigationBar: isSelectionMode - ? buildMobileBottomActionBar(context, libraryProvider) - : null, + bottomNavigationBar: isSelectionMode ? buildMobileBottomActionBar(context, libraryProvider) : null, ); } @@ -236,21 +196,14 @@ class _ShelfContentsPageState extends State { // DESKTOP LAYOUT // ============================================================================ - Widget _buildDesktopLayout( - BuildContext context, - Shelf shelf, - ShelvesProvider provider, - ) { + Widget _buildDesktopLayout(BuildContext context, Shelf shelf, ShelvesProvider provider) { final libraryProvider = context.watch(); final childShelves = provider.getChildShelves(shelf.id); final books = provider.getFilteredBooksForShelf( shelf.id, isFavorite: (bookId) { final book = context.read().getBook(bookId); - return libraryProvider.isBookFavorite( - bookId, - book?.isFavorite ?? false, - ); + return libraryProvider.isBookFavorite(bookId, book?.isFavorite ?? false); }, ); @@ -272,19 +225,13 @@ class _ShelfContentsPageState extends State { children: [ // Header Container( - padding: const EdgeInsets.only( - top: Spacing.lg, - left: Spacing.lg, - right: Spacing.lg, - ), + padding: const EdgeInsets.only(top: Spacing.lg, left: Spacing.lg, right: Spacing.lg), child: isSelectionMode ? SelectionHeader( selectedCount: libraryProvider.selectedCount, totalCount: books.length, onClose: libraryProvider.exitSelectionMode, - onSelectAll: () => libraryProvider.selectAll( - books.map((b) => b.id).toList(), - ), + onSelectAll: () => libraryProvider.selectAll(books.map((b) => b.id).toList()), onDeselectAll: libraryProvider.deselectAll, actions: buildBulkActionBar(context, libraryProvider), ) @@ -298,35 +245,20 @@ class _ShelfContentsPageState extends State { children: [ Row( children: [ - Expanded( - child: _buildSearchBar( - provider, - isDesktop: true, - ), - ), + Expanded(child: _buildSearchBar(provider, isDesktop: true)), const SizedBox(width: Spacing.sm), _buildSortButton(provider), ], ), const SizedBox(height: Spacing.md), - Row( - children: [ - const Spacer(), - _buildViewToggle(provider), - ], - ), + Row(children: [const Spacer(), _buildViewToggle(provider)]), ], ); } return Row( children: [ - Expanded( - child: _buildSearchBar( - provider, - isDesktop: true, - ), - ), + Expanded(child: _buildSearchBar(provider, isDesktop: true)), const SizedBox(width: Spacing.md), _buildSortButton(provider), const SizedBox(width: Spacing.md), @@ -339,15 +271,7 @@ class _ShelfContentsPageState extends State { // Filter chips _buildFilterChips(provider, horizontalPadding: Spacing.lg), // Content grid/list - Expanded( - child: _buildContent( - context, - childShelves, - books, - provider, - libraryProvider, - ), - ), + Expanded(child: _buildContent(context, childShelves, books, provider, libraryProvider)), ], ), ), @@ -366,9 +290,7 @@ class _ShelfContentsPageState extends State { onQueryChanged: provider.setBookSearchQuery, initialQuery: provider.bookSearchQuery, activeFilterCount: activeFilterCount, - onFilterTap: () => isDesktop - ? _showFilterDialog(context, provider) - : _showFilterBottomSheet(context, provider), + onFilterTap: () => isDesktop ? _showFilterDialog(context, provider) : _showFilterBottomSheet(context, provider), ); } @@ -386,11 +308,7 @@ class _ShelfContentsPageState extends State { ); } - PopupMenuItem _buildSortMenuItem( - BookSortOption option, - String label, - ShelvesProvider provider, - ) { + PopupMenuItem _buildSortMenuItem(BookSortOption option, String label, ShelvesProvider provider) { return PopupMenuItem( value: option, child: Row( @@ -399,9 +317,7 @@ class _ShelfContentsPageState extends State { Icon( Icons.check, size: IconSizes.small, - color: option == provider.bookSortOption - ? Theme.of(context).colorScheme.primary - : Colors.transparent, + color: option == provider.bookSortOption ? Theme.of(context).colorScheme.primary : Colors.transparent, ), ], ), @@ -411,9 +327,7 @@ class _ShelfContentsPageState extends State { Widget _buildViewToggle(ShelvesProvider provider) { return ViewModeToggle( isGridView: provider.isGridView, - onChanged: (isGrid) => provider.setViewMode( - isGrid ? ShelvesViewMode.grid : ShelvesViewMode.list, - ), + onChanged: (isGrid) => provider.setViewMode(isGrid ? ShelvesViewMode.grid : ShelvesViewMode.list), ); } @@ -425,31 +339,19 @@ class _ShelfContentsPageState extends State { (type: BookFilterType.all, label: 'All', icon: Icons.apps), (type: BookFilterType.reading, label: 'Reading', icon: Icons.auto_stories), (type: BookFilterType.favorites, label: 'Favorites', icon: Icons.favorite), - ( - type: BookFilterType.finished, - label: 'Finished', - icon: Icons.check_circle, - ), + (type: BookFilterType.finished, label: 'Finished', icon: Icons.check_circle), (type: BookFilterType.unread, label: 'Unread', icon: Icons.book), ]; - Widget _buildFilterChips( - ShelvesProvider provider, { - double? horizontalPadding, - }) { + Widget _buildFilterChips(ShelvesProvider provider, {double? horizontalPadding}) { return QuickFilterChips( horizontalPadding: horizontalPadding, filters: _quickFilters .map( - (f) => QuickFilterChipData( - label: f.label, - icon: f.icon, - isSelected: provider.isBookFilterActive(f.type), - ), + (f) => QuickFilterChipData(label: f.label, icon: f.icon, isSelected: provider.isBookFilterActive(f.type)), ) .toList(), - onFilterTapped: (index) => - provider.toggleBookFilter(_quickFilters[index].type), + onFilterTapped: (index) => provider.toggleBookFilter(_quickFilters[index].type), ); } @@ -458,18 +360,14 @@ class _ShelfContentsPageState extends State { // ============================================================================ int _countActiveAdvancedFilters(ShelvesProvider provider) { - if (provider.bookSearchQuery.isEmpty || - !provider.bookSearchQuery.contains(':')) { + if (provider.bookSearchQuery.isEmpty || !provider.bookSearchQuery.contains(':')) { return 0; } final query = SearchQueryParser.parse(provider.bookSearchQuery); return query.filters.where((f) => f.field.name != 'any').length; } - Future _showFilterBottomSheet( - BuildContext context, - ShelvesProvider provider, - ) async { + Future _showFilterBottomSheet(BuildContext context, ShelvesProvider provider) async { final dataStore = context.read(); final filterOptions = FilterOptions.fromBooks( dataStore.books, @@ -494,10 +392,7 @@ class _ShelfContentsPageState extends State { } } - Future _showFilterDialog( - BuildContext context, - ShelvesProvider provider, - ) async { + Future _showFilterDialog(BuildContext context, ShelvesProvider provider) async { final dataStore = context.read(); final filterOptions = FilterOptions.fromBooks( dataStore.books, @@ -522,10 +417,7 @@ class _ShelfContentsPageState extends State { } } - void _applyShelfFilterResult( - AppliedFilters result, - ShelvesProvider provider, - ) { + void _applyShelfFilterResult(AppliedFilters result, ShelvesProvider provider) { // Apply quick filters if (result.filterReading) { provider.addBookFilter(BookFilterType.reading); @@ -579,22 +471,10 @@ class _ShelfContentsPageState extends State { } if (provider.isListView) { - return _buildListContent( - context, - childShelves, - books, - provider, - libraryProvider, - ); + return _buildListContent(context, childShelves, books, provider, libraryProvider); } - return _buildGridContent( - context, - childShelves, - books, - provider, - libraryProvider, - ); + return _buildGridContent(context, childShelves, books, provider, libraryProvider); } Widget _buildGridContent( @@ -630,11 +510,7 @@ class _ShelfContentsPageState extends State { } return GridView.builder( - padding: const EdgeInsets.only( - left: Spacing.md, - right: Spacing.md, - bottom: Spacing.md, - ), + padding: const EdgeInsets.only(left: Spacing.md, right: Spacing.md, bottom: Spacing.md), gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: crossAxisCount, mainAxisSpacing: spacing, @@ -645,17 +521,11 @@ class _ShelfContentsPageState extends State { itemBuilder: (context, index) { if (index < childShelves.length) { final shelf = childShelves[index]; - return ShelfCard( - shelf: shelf, - onTap: () => context.go('/library/shelves/${shelf.id}'), - ); + return ShelfCard(shelf: shelf, onTap: () => context.go('/library/shelves/${shelf.id}')); } final book = books[index - childShelves.length]; - final isFavorite = libraryProvider.isBookFavorite( - book.id, - book.isFavorite, - ); + final isFavorite = libraryProvider.isBookFavorite(book.id, book.isFavorite); final isSelectionMode = libraryProvider.isSelectionMode; return BookCard( book: book, @@ -663,10 +533,8 @@ class _ShelfContentsPageState extends State { isSelectionMode: isSelectionMode, isSelected: libraryProvider.isBookSelected(book.id), onSelectToggle: () => libraryProvider.toggleBookSelection(book.id), - onEnterSelectionMode: () => - libraryProvider.enterSelectionMode(book.id), - onToggleFavorite: (current) => - libraryProvider.toggleFavorite(book.id, current), + onEnterSelectionMode: () => libraryProvider.enterSelectionMode(book.id), + onToggleFavorite: (current) => libraryProvider.toggleFavorite(book.id, current), onTap: () => context.go('/library/details/${book.id}'), ); }, @@ -688,18 +556,11 @@ class _ShelfContentsPageState extends State { itemBuilder: (context, index) { if (index < childShelves.length) { final shelf = childShelves[index]; - return ShelfCard( - shelf: shelf, - isListItem: true, - onTap: () => context.go('/library/shelves/${shelf.id}'), - ); + return ShelfCard(shelf: shelf, isListItem: true, onTap: () => context.go('/library/shelves/${shelf.id}')); } final book = books[index - childShelves.length]; - final isFavorite = libraryProvider.isBookFavorite( - book.id, - book.isFavorite, - ); + final isFavorite = libraryProvider.isBookFavorite(book.id, book.isFavorite); return BookListItem( book: book, isFavorite: isFavorite, diff --git a/app/lib/pages/shelves_page.dart b/app/lib/pages/shelves_page.dart index 61bf7d3..79f104b 100644 --- a/app/lib/pages/shelves_page.dart +++ b/app/lib/pages/shelves_page.dart @@ -94,11 +94,7 @@ class _ShelvesPageState extends State { children: [ // Row 1: Menu + Search + Sort Padding( - padding: const EdgeInsets.only( - top: Spacing.md, - left: Spacing.md, - right: Spacing.md, - ), + padding: const EdgeInsets.only(top: Spacing.md, left: Spacing.md, right: Spacing.md), child: Row( children: [ IconButton( @@ -118,19 +114,15 @@ class _ShelvesPageState extends State { SizedBox(height: Spacing.md), // Row 2: Count + View toggle Padding( - padding: const EdgeInsets.only( - left: Spacing.md, - right: Spacing.md, - bottom: Spacing.md, - ), + padding: const EdgeInsets.only(left: Spacing.md, right: Spacing.md, bottom: Spacing.md), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( '${provider.shelves.length} ${provider.shelves.length == 1 ? 'shelf' : 'shelves'}', - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), + style: Theme.of( + context, + ).textTheme.bodyMedium?.copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant), ), _buildViewToggle(provider), ], @@ -167,11 +159,7 @@ class _ShelvesPageState extends State { children: [ // Header row Container( - padding: const EdgeInsets.only( - top: Spacing.lg, - left: Spacing.lg, - right: Spacing.lg, - ), + padding: const EdgeInsets.only(top: Spacing.lg, left: Spacing.lg, right: Spacing.lg), child: LayoutBuilder( builder: (context, constraints) { final useCompactLayout = constraints.maxWidth < 800; @@ -263,25 +251,13 @@ class _ShelvesPageState extends State { itemBuilder: (context) => [ _buildSortMenuItem(ShelfSortOption.name, 'Name', provider), _buildSortMenuItem(ShelfSortOption.bookCount, 'Book count', provider), - _buildSortMenuItem( - ShelfSortOption.dateCreated, - 'Date created', - provider, - ), - _buildSortMenuItem( - ShelfSortOption.dateModified, - 'Date modified', - provider, - ), + _buildSortMenuItem(ShelfSortOption.dateCreated, 'Date created', provider), + _buildSortMenuItem(ShelfSortOption.dateModified, 'Date modified', provider), ], ); } - PopupMenuItem _buildSortMenuItem( - ShelfSortOption option, - String label, - ShelvesProvider provider, - ) { + PopupMenuItem _buildSortMenuItem(ShelfSortOption option, String label, ShelvesProvider provider) { return PopupMenuItem( value: option, child: Row( @@ -290,9 +266,7 @@ class _ShelvesPageState extends State { Icon( Icons.check, size: IconSizes.small, - color: option == provider.shelfSortOption - ? Theme.of(context).colorScheme.primary - : Colors.transparent, + color: option == provider.shelfSortOption ? Theme.of(context).colorScheme.primary : Colors.transparent, ), ], ), @@ -302,9 +276,7 @@ class _ShelvesPageState extends State { Widget _buildViewToggle(ShelvesProvider provider) { return ViewModeToggle( isGridView: provider.isGridView, - onChanged: (isGrid) => provider.setViewMode( - isGrid ? ShelvesViewMode.grid : ShelvesViewMode.list, - ), + onChanged: (isGrid) => provider.setViewMode(isGrid ? ShelvesViewMode.grid : ShelvesViewMode.list), ); } @@ -406,12 +378,7 @@ class _ShelvesPageState extends State { AddShelfSheet.show( context, onSave: (name, description, colorHex, icon) { - _provider.createShelf( - name: name, - description: description, - colorHex: colorHex, - icon: icon, - ); + _provider.createShelf(name: name, description: description, colorHex: colorHex, icon: icon); }, ); } @@ -421,13 +388,7 @@ class _ShelvesPageState extends State { context, shelf: shelf, onSave: (name, description, colorHex, icon) { - _provider.updateShelf( - shelfId: shelf.id, - name: name, - description: description, - colorHex: colorHex, - icon: icon, - ); + _provider.updateShelf(shelfId: shelf.id, name: name, description: description, colorHex: colorHex, icon: icon); }, ); } @@ -441,9 +402,7 @@ class _ShelvesPageState extends State { showModalBottomSheet( context: context, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(AppRadius.xl)), - ), + shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(AppRadius.xl))), builder: (context) => SafeArea( child: Padding( padding: const EdgeInsets.symmetric(vertical: Spacing.md), @@ -458,9 +417,7 @@ class _ShelvesPageState extends State { padding: const EdgeInsets.symmetric(horizontal: Spacing.lg), child: Text( shelf.name, - style: Theme.of( - context, - ).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.w600), + style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.w600), ), ), const SizedBox(height: Spacing.md), @@ -475,10 +432,7 @@ class _ShelvesPageState extends State { ), ListTile( leading: Icon(Icons.delete_outlined, color: colorScheme.error), - title: Text( - 'Delete shelf', - style: TextStyle(color: colorScheme.error), - ), + title: Text('Delete shelf', style: TextStyle(color: colorScheme.error)), onTap: () { Navigator.of(context).pop(); _confirmDeleteShelf(context, shelf); @@ -500,10 +454,7 @@ class _ShelvesPageState extends State { title: const Text('Delete shelf'), content: Text('Delete "${shelf.name}"? Books will not be deleted.'), actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: const Text('Cancel'), - ), + TextButton(onPressed: () => Navigator.of(context).pop(), child: const Text('Cancel')), FilledButton( onPressed: () { Navigator.of(context).pop(); diff --git a/app/lib/pages/statistics_page.dart b/app/lib/pages/statistics_page.dart index 3eb064b..48861be 100644 --- a/app/lib/pages/statistics_page.dart +++ b/app/lib/pages/statistics_page.dart @@ -114,13 +114,10 @@ class _StatisticsPageState extends State { ), const SizedBox(height: Spacing.lg), // Books per month (for year/all time views) - if (provider.selectedPeriod == StatsPeriod.year || - provider.selectedPeriod == StatsPeriod.allTime) ...[ + if (provider.selectedPeriod == StatsPeriod.year || provider.selectedPeriod == StatsPeriod.allTime) ...[ StatSectionCard( title: 'Books per month', - child: BooksPerMonthChart( - monthlyStats: provider.monthlyStats, - ), + child: BooksPerMonthChart(monthlyStats: provider.monthlyStats), ), const SizedBox(height: Spacing.lg), ], @@ -136,19 +133,12 @@ class _StatisticsPageState extends State { ); } - Widget _buildPeriodSegmentedButton( - BuildContext context, - StatisticsProvider provider, - ) { + Widget _buildPeriodSegmentedButton(BuildContext context, StatisticsProvider provider) { final colorScheme = Theme.of(context).colorScheme; final textTheme = Theme.of(context).textTheme; - final periods = StatsPeriod.values - .where((p) => p != StatsPeriod.custom) - .toList(); - final selectedPeriod = provider.selectedPeriod == StatsPeriod.custom - ? StatsPeriod.week - : provider.selectedPeriod; + final periods = StatsPeriod.values.where((p) => p != StatsPeriod.custom).toList(); + final selectedPeriod = provider.selectedPeriod == StatsPeriod.custom ? StatsPeriod.week : provider.selectedPeriod; return Row( children: [ @@ -156,10 +146,7 @@ class _StatisticsPageState extends State { child: Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(AppRadius.lg), - border: Border.all( - color: colorScheme.outlineVariant, - width: BorderWidths.thin, - ), + border: Border.all(color: colorScheme.outlineVariant, width: BorderWidths.thin), ), child: ClipRRect( borderRadius: BorderRadius.circular(AppRadius.lg), @@ -171,9 +158,7 @@ class _StatisticsPageState extends State { child: GestureDetector( onTap: () => provider.setPeriod(periods[i]), child: Container( - padding: const EdgeInsets.symmetric( - vertical: Spacing.sm, - ), + padding: const EdgeInsets.symmetric(vertical: Spacing.sm), color: selectedPeriod == periods[i] ? colorScheme.primaryContainer : colorScheme.surfaceContainerLow, @@ -191,11 +176,7 @@ class _StatisticsPageState extends State { ), ), if (i < periods.length - 1) - VerticalDivider( - width: 1, - thickness: 1, - color: colorScheme.outlineVariant, - ), + VerticalDivider(width: 1, thickness: 1, color: colorScheme.outlineVariant), ], ], ), @@ -208,26 +189,18 @@ class _StatisticsPageState extends State { onPressed: () => _showDateRangePicker(context, provider), icon: const Icon(Icons.date_range_outlined, size: 20), tooltip: 'Custom date range', - style: IconButton.styleFrom( - side: BorderSide(color: colorScheme.outlineVariant), - ), + style: IconButton.styleFrom(side: BorderSide(color: colorScheme.outlineVariant)), ), ], ); } - Widget _buildCustomRangeChip( - BuildContext context, - StatisticsProvider provider, - ) { + Widget _buildCustomRangeChip(BuildContext context, StatisticsProvider provider) { final colorScheme = Theme.of(context).colorScheme; final textTheme = Theme.of(context).textTheme; return Container( - padding: const EdgeInsets.symmetric( - horizontal: Spacing.md, - vertical: Spacing.sm, - ), + padding: const EdgeInsets.symmetric(horizontal: Spacing.md, vertical: Spacing.sm), decoration: BoxDecoration( color: colorScheme.secondaryContainer, borderRadius: BorderRadius.circular(AppRadius.full), @@ -235,74 +208,42 @@ class _StatisticsPageState extends State { child: Row( mainAxisSize: MainAxisSize.min, children: [ - Icon( - Icons.date_range, - size: 16, - color: colorScheme.onSecondaryContainer, - ), + Icon(Icons.date_range, size: 16, color: colorScheme.onSecondaryContainer), const SizedBox(width: Spacing.sm), - Text( - provider.periodLabel, - style: textTheme.labelMedium?.copyWith( - color: colorScheme.onSecondaryContainer, - ), - ), + Text(provider.periodLabel, style: textTheme.labelMedium?.copyWith(color: colorScheme.onSecondaryContainer)), const SizedBox(width: Spacing.sm), GestureDetector( onTap: () => provider.setPeriod(StatsPeriod.week), - child: Icon( - Icons.close, - size: 16, - color: colorScheme.onSecondaryContainer, - ), + child: Icon(Icons.close, size: 16, color: colorScheme.onSecondaryContainer), ), ], ), ); } - Widget _buildMobileSummaryCards( - BuildContext context, - StatisticsProvider provider, - ) { + Widget _buildMobileSummaryCards(BuildContext context, StatisticsProvider provider) { return Row( children: [ Expanded( - child: CompactStatCard( - value: provider.totalBooks.toString(), - label: 'Books', - ), + child: CompactStatCard(value: provider.totalBooks.toString(), label: 'Books'), ), const SizedBox(width: Spacing.sm), Expanded( - child: CompactStatCard( - value: provider.pagesRead.toString(), - label: 'Pages', - ), + child: CompactStatCard(value: provider.pagesRead.toString(), label: 'Pages'), ), const SizedBox(width: Spacing.sm), Expanded( - child: CompactStatCard( - value: provider.totalReadingLabel, - label: 'Reading time', - ), + child: CompactStatCard(value: provider.totalReadingLabel, label: 'Reading time'), ), const SizedBox(width: Spacing.sm), Expanded( - child: CompactStatCard( - value: provider.goalsCompleted.toString(), - label: 'Goals', - ), + child: CompactStatCard(value: provider.goalsCompleted.toString(), label: 'Goals'), ), ], ); } - Widget _buildInsightsCard( - BuildContext context, - StatisticsProvider provider, { - bool isDesktop = false, - }) { + Widget _buildInsightsCard(BuildContext context, StatisticsProvider provider, {bool isDesktop = false}) { final colorScheme = Theme.of(context).colorScheme; final sessionStats = provider.sessionStats; final streak = provider.streak; @@ -340,16 +281,11 @@ class _StatisticsPageState extends State { label: 'Avg. daily reading', value: provider.averageReadingLabel, ), - Divider( - height: isDesktop ? Spacing.xl : Spacing.lg, - color: colorScheme.outlineVariant, - ), + Divider(height: isDesktop ? Spacing.xl : Spacing.lg, color: colorScheme.outlineVariant), _buildStreakRow( context, icon: Icons.local_fire_department, - iconColor: streak.hasActiveStreak - ? colorScheme.tertiary - : colorScheme.onSurfaceVariant, + iconColor: streak.hasActiveStreak ? colorScheme.tertiary : colorScheme.onSurfaceVariant, label: 'Current streak', value: '${streak.currentStreak} days', isHighlighted: streak.isCurrentBest, @@ -375,12 +311,7 @@ class _StatisticsPageState extends State { ); } - Widget _buildStatRow( - BuildContext context, { - required IconData icon, - required String label, - required String value, - }) { + Widget _buildStatRow(BuildContext context, {required IconData icon, required String label, required String value}) { final colorScheme = Theme.of(context).colorScheme; final textTheme = Theme.of(context).textTheme; @@ -388,33 +319,18 @@ class _StatisticsPageState extends State { children: [ Icon(icon, size: 20, color: colorScheme.onSurfaceVariant), const SizedBox(width: Spacing.sm), - Text( - label, - style: textTheme.bodyMedium?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), + Text(label, style: textTheme.bodyMedium?.copyWith(color: colorScheme.onSurfaceVariant)), const Spacer(), - Text( - value, - style: textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w600), - ), + Text(value, style: textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w600)), ], ); } - Widget _buildGenreDistributionCard( - BuildContext context, - StatisticsProvider provider, - ) { + Widget _buildGenreDistributionCard(BuildContext context, StatisticsProvider provider) { return _buildGenreBarsCard(context, provider); } - Widget _buildGenreBarsCard( - BuildContext context, - StatisticsProvider provider, { - bool isDesktop = false, - }) { + Widget _buildGenreBarsCard(BuildContext context, StatisticsProvider provider, {bool isDesktop = false}) { final colorScheme = Theme.of(context).colorScheme; final textTheme = Theme.of(context).textTheme; final genres = provider.genreDistribution; @@ -426,9 +342,7 @@ class _StatisticsPageState extends State { child: Center( child: Text( 'No genre data available', - style: textTheme.bodyMedium?.copyWith( - color: colorScheme.onSurfaceVariant, - ), + style: textTheme.bodyMedium?.copyWith(color: colorScheme.onSurfaceVariant), ), ), ); @@ -453,27 +367,15 @@ class _StatisticsPageState extends State { final pct = (genre.percentage * 100).round(); return Padding( - padding: EdgeInsets.only( - bottom: entry.key < genres.length - 1 ? Spacing.md : 0, - ), + padding: EdgeInsets.only(bottom: entry.key < genres.length - 1 ? Spacing.md : 0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text( - genre.genre, - style: textTheme.bodySmall?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), - Text( - '$pct%', - style: textTheme.bodySmall?.copyWith( - fontWeight: FontWeight.w600, - ), - ), + Text(genre.genre, style: textTheme.bodySmall?.copyWith(color: colorScheme.onSurfaceVariant)), + Text('$pct%', style: textTheme.bodySmall?.copyWith(fontWeight: FontWeight.w600)), ], ), const SizedBox(height: 4), @@ -509,12 +411,7 @@ class _StatisticsPageState extends State { children: [ Icon(icon, size: 20, color: iconColor), const SizedBox(width: Spacing.sm), - Text( - label, - style: textTheme.bodyMedium?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), + Text(label, style: textTheme.bodyMedium?.copyWith(color: colorScheme.onSurfaceVariant)), const Spacer(), Text( value, @@ -531,10 +428,7 @@ class _StatisticsPageState extends State { // DESKTOP LAYOUT // ============================================================================ - Widget _buildDesktopLayout( - BuildContext context, - StatisticsProvider provider, - ) { + Widget _buildDesktopLayout(BuildContext context, StatisticsProvider provider) { return Scaffold( body: SafeArea( child: SingleChildScrollView( @@ -586,27 +480,18 @@ class _StatisticsPageState extends State { ), const SizedBox(height: Spacing.lg), // Second row: Genre + Books per month (if applicable) - if (provider.selectedPeriod == StatsPeriod.year || - provider.selectedPeriod == StatsPeriod.allTime) + if (provider.selectedPeriod == StatsPeriod.year || provider.selectedPeriod == StatsPeriod.allTime) IntrinsicHeight( child: Row( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Expanded( - child: _buildDesktopGenreDistributionCard( - context, - provider, - ), - ), + Expanded(child: _buildDesktopGenreDistributionCard(context, provider)), const SizedBox(width: Spacing.lg), Expanded( child: StatSectionCard( title: 'Books per month', isDesktop: true, - child: BooksPerMonthChart( - monthlyStats: provider.monthlyStats, - isDesktop: true, - ), + child: BooksPerMonthChart(monthlyStats: provider.monthlyStats, isDesktop: true), ), ), ], @@ -624,51 +509,29 @@ class _StatisticsPageState extends State { ); } - Widget _buildDesktopSummaryRow( - BuildContext context, - StatisticsProvider provider, - ) { + Widget _buildDesktopSummaryRow(BuildContext context, StatisticsProvider provider) { return Row( children: [ Expanded( - child: CompactStatCard( - value: provider.totalBooks.toString(), - label: 'Books', - isDesktop: true, - ), + child: CompactStatCard(value: provider.totalBooks.toString(), label: 'Books', isDesktop: true), ), const SizedBox(width: Spacing.md), Expanded( - child: CompactStatCard( - value: provider.pagesRead.toString(), - label: 'Pages', - isDesktop: true, - ), + child: CompactStatCard(value: provider.pagesRead.toString(), label: 'Pages', isDesktop: true), ), const SizedBox(width: Spacing.md), Expanded( - child: CompactStatCard( - value: provider.totalReadingLabel, - label: 'Reading time', - isDesktop: true, - ), + child: CompactStatCard(value: provider.totalReadingLabel, label: 'Reading time', isDesktop: true), ), const SizedBox(width: Spacing.md), Expanded( - child: CompactStatCard( - value: provider.goalsCompleted.toString(), - label: 'Goals', - isDesktop: true, - ), + child: CompactStatCard(value: provider.goalsCompleted.toString(), label: 'Goals', isDesktop: true), ), ], ); } - Widget _buildDesktopGenreDistributionCard( - BuildContext context, - StatisticsProvider provider, - ) { + Widget _buildDesktopGenreDistributionCard(BuildContext context, StatisticsProvider provider) { return _buildGenreBarsCard(context, provider, isDesktop: true); } @@ -691,20 +554,11 @@ class _StatisticsPageState extends State { } } - Future _showDateRangePicker( - BuildContext context, - StatisticsProvider provider, - ) async { + Future _showDateRangePicker(BuildContext context, StatisticsProvider provider) async { final now = DateTime.now(); final initialRange = provider.hasCustomRange - ? DateTimeRange( - start: provider.customStartDate!, - end: provider.customEndDate!, - ) - : DateTimeRange( - start: now.subtract(const Duration(days: 30)), - end: now, - ); + ? DateTimeRange(start: provider.customStartDate!, end: provider.customEndDate!) + : DateTimeRange(start: now.subtract(const Duration(days: 30)), end: now); final picked = await showDateRangePicker( context: context, @@ -719,18 +573,10 @@ class _StatisticsPageState extends State { initialEntryMode: DatePickerEntryMode.calendarOnly, builder: (context, child) { return Dialog( - insetPadding: const EdgeInsets.symmetric( - horizontal: Spacing.xl, - vertical: Spacing.xl, - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(AppRadius.xl), - ), + insetPadding: const EdgeInsets.symmetric(horizontal: Spacing.xl, vertical: Spacing.xl), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(AppRadius.xl)), clipBehavior: Clip.antiAlias, - child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 400, maxHeight: 500), - child: child, - ), + child: ConstrainedBox(constraints: const BoxConstraints(maxWidth: 400, maxHeight: 500), child: child), ); }, ); diff --git a/app/lib/pages/welcome_page.dart b/app/lib/pages/welcome_page.dart index 71d005b..83f1df9 100644 --- a/app/lib/pages/welcome_page.dart +++ b/app/lib/pages/welcome_page.dart @@ -33,15 +33,9 @@ class WelcomePage extends StatelessWidget { begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ - (isDark - ? const Color(0xCC18161D) - : const Color(0x885654A8)), - (isDark - ? const Color(0xE61A1720) - : const Color(0xC03E3C8F)), - (isDark - ? const Color(0xF518161C) - : const Color(0xE01F1D2B)), + (isDark ? const Color(0xCC18161D) : const Color(0x885654A8)), + (isDark ? const Color(0xE61A1720) : const Color(0xC03E3C8F)), + (isDark ? const Color(0xF518161C) : const Color(0xE01F1D2B)), ], stops: const [0.0, 0.55, 1.0], ), @@ -52,14 +46,9 @@ class WelcomePage extends StatelessWidget { child: DecoratedBox( decoration: BoxDecoration( gradient: RadialGradient( - center: isDesktop - ? const Alignment(0.75, -0.2) - : const Alignment(0, -0.45), + center: isDesktop ? const Alignment(0.75, -0.2) : const Alignment(0, -0.45), radius: isDesktop ? 1.2 : 1.0, - colors: [ - theme.colorScheme.primary.withValues(alpha: 0.12), - Colors.transparent, - ], + colors: [theme.colorScheme.primary.withValues(alpha: 0.12), Colors.transparent], ), ), ), @@ -68,8 +57,7 @@ class WelcomePage extends StatelessWidget { child: LayoutBuilder( builder: (context, constraints) { if (isDesktop) { - final minDesktopHeight = - constraints.maxHeight - (Spacing.xl * 2); + final minDesktopHeight = constraints.maxHeight - (Spacing.xl * 2); return SingleChildScrollView( padding: const EdgeInsets.symmetric( @@ -77,24 +65,15 @@ class WelcomePage extends StatelessWidget { vertical: Spacing.xl, ), child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: minDesktopHeight > 0 ? minDesktopHeight : 0, - ), - child: _DesktopWelcomeContent( - maxHeight: constraints.maxHeight, - ), + constraints: BoxConstraints(minHeight: minDesktopHeight > 0 ? minDesktopHeight : 0), + child: _DesktopWelcomeContent(maxHeight: constraints.maxHeight), ), ); } return Padding( - padding: const EdgeInsets.symmetric( - horizontal: Spacing.pageMarginsTablet, - vertical: Spacing.md, - ), - child: _MobileWelcomeContent( - maxHeight: constraints.maxHeight - (Spacing.md * 2), - ), + padding: const EdgeInsets.symmetric(horizontal: Spacing.pageMarginsTablet, vertical: Spacing.md), + child: _MobileWelcomeContent(maxHeight: constraints.maxHeight - (Spacing.md * 2)), ); }, ), @@ -126,49 +105,30 @@ class _DesktopWelcomeContent extends StatelessWidget { decoration: BoxDecoration( color: theme.colorScheme.surface.withValues(alpha: 0.94), borderRadius: BorderRadius.circular(32), - border: Border.all( - color: theme.colorScheme.outlineVariant.withValues(alpha: 0.55), - ), + border: Border.all(color: theme.colorScheme.outlineVariant.withValues(alpha: 0.55)), boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.08), - blurRadius: 24, - offset: const Offset(0, 8), - ), + BoxShadow(color: Colors.black.withValues(alpha: 0.08), blurRadius: 24, offset: const Offset(0, 8)), ], ), child: Column( mainAxisSize: MainAxisSize.min, children: [ _WelcomeCopy( - titleStyle: - (isShortDesktop - ? theme.textTheme.headlineLarge - : theme.textTheme.displaySmall) - ?.copyWith( - color: theme.colorScheme.onSurface, - fontFamily: 'MadimiOne', - ), - subtitleStyle: - (isShortDesktop - ? theme.textTheme.bodyLarge - : theme.textTheme.titleLarge) - ?.copyWith( - color: theme.colorScheme.onSurfaceVariant, - height: 1.55, - ), + titleStyle: (isShortDesktop ? theme.textTheme.headlineLarge : theme.textTheme.displaySmall)?.copyWith( + color: theme.colorScheme.onSurface, + fontFamily: 'MadimiOne', + ), + subtitleStyle: (isShortDesktop ? theme.textTheme.bodyLarge : theme.textTheme.titleLarge)?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + height: 1.55, + ), center: true, logoSize: isShortDesktop ? 96 : 120, titleSpacing: isShortDesktop ? 12 : 20, subtitleSpacing: isShortDesktop ? 12 : Spacing.md, ), SizedBox(height: isShortDesktop ? Spacing.lg : Spacing.xl), - _WelcomeActions( - center: true, - compact: isShortDesktop, - useSurfaceColors: true, - isDesktop: true, - ), + _WelcomeActions(center: true, compact: isShortDesktop, useSurfaceColors: true, isDesktop: true), ], ), ), @@ -193,19 +153,14 @@ class _MobileWelcomeContent extends StatelessWidget { children: [ Spacer(flex: isShortScreen ? 2 : 3), _WelcomeCopy( - titleStyle: - (isShortScreen - ? theme.textTheme.headlineMedium - : theme.textTheme.headlineLarge) - ?.copyWith(color: Colors.white, fontFamily: 'MadimiOne'), - subtitleStyle: - (isShortScreen - ? theme.textTheme.bodyMedium - : theme.textTheme.bodyLarge) - ?.copyWith( - color: Colors.white.withValues(alpha: 0.82), - height: isShortScreen ? 1.5 : 1.55, - ), + titleStyle: (isShortScreen ? theme.textTheme.headlineMedium : theme.textTheme.headlineLarge)?.copyWith( + color: Colors.white, + fontFamily: 'MadimiOne', + ), + subtitleStyle: (isShortScreen ? theme.textTheme.bodyMedium : theme.textTheme.bodyLarge)?.copyWith( + color: Colors.white.withValues(alpha: 0.82), + height: isShortScreen ? 1.5 : 1.55, + ), center: true, logoSize: isShortScreen ? 88 : 104, titleSpacing: isShortScreen ? 12 : 20, @@ -242,22 +197,11 @@ class _WelcomeCopy extends StatelessWidget { Widget build(BuildContext context) { return Column( mainAxisSize: MainAxisSize.min, - crossAxisAlignment: center - ? CrossAxisAlignment.center - : CrossAxisAlignment.start, + crossAxisAlignment: center ? CrossAxisAlignment.center : CrossAxisAlignment.start, children: [ - Image.asset( - 'assets/images/logo.png', - width: logoSize, - height: logoSize, - fit: BoxFit.contain, - ), + Image.asset('assets/images/logo.png', width: logoSize, height: logoSize, fit: BoxFit.contain), SizedBox(height: titleSpacing), - Text( - 'Papyrus', - textAlign: center ? TextAlign.center : TextAlign.left, - style: titleStyle, - ), + Text('Papyrus', textAlign: center ? TextAlign.center : TextAlign.left, style: titleStyle), SizedBox(height: subtitleSpacing), Text( 'Manage your library, track reading progress, and read anywhere.', @@ -292,10 +236,7 @@ class _WelcomeActions extends StatelessWidget { const SizedBox(height: Spacing.md), _SignInButton(isDesktop: isDesktop, useSurfaceColors: useSurfaceColors), SizedBox(height: compact ? Spacing.sm : Spacing.sm), - TitledDivider( - title: 'or', - verticalPadding: compact ? Spacing.sm : Spacing.md, - ), + TitledDivider(title: 'or', verticalPadding: compact ? Spacing.sm : Spacing.md), _OfflineModeLink(center: center), ], ); @@ -315,25 +256,18 @@ class _CreateAccountButton extends StatelessWidget { onPressed: () => context.go('/register'), style: ElevatedButton.styleFrom( minimumSize: Size.fromHeight( - isDesktop - ? ComponentSizes.buttonHeightDesktop - : ComponentSizes.buttonHeightMobile, + isDesktop ? ComponentSizes.buttonHeightDesktop : ComponentSizes.buttonHeightMobile, ), backgroundColor: theme.colorScheme.primary, foregroundColor: theme.colorScheme.onPrimary, elevation: AppElevation.level2, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(AppRadius.button), - ), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(AppRadius.button)), padding: const EdgeInsets.symmetric( horizontal: Spacing.buttonPaddingHorizontal, vertical: Spacing.buttonPaddingVertical, ), ), - child: const Text( - 'Create an account', - style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500), - ), + child: const Text('Create an account', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500)), ); } } @@ -342,10 +276,7 @@ class _SignInButton extends StatelessWidget { final bool isDesktop; final bool useSurfaceColors; - const _SignInButton({ - required this.isDesktop, - required this.useSurfaceColors, - }); + const _SignInButton({required this.isDesktop, required this.useSurfaceColors}); @override Widget build(BuildContext context) { @@ -355,31 +286,20 @@ class _SignInButton extends StatelessWidget { onPressed: () => context.go('/login'), style: OutlinedButton.styleFrom( minimumSize: Size.fromHeight( - isDesktop - ? ComponentSizes.buttonHeightDesktop - : ComponentSizes.buttonHeightMobile, + isDesktop ? ComponentSizes.buttonHeightDesktop : ComponentSizes.buttonHeightMobile, ), - foregroundColor: useSurfaceColors - ? theme.colorScheme.onSurface - : Colors.white, + foregroundColor: useSurfaceColors ? theme.colorScheme.onSurface : Colors.white, side: BorderSide( - color: useSurfaceColors - ? theme.colorScheme.outline - : Colors.white.withValues(alpha: 0.38), + color: useSurfaceColors ? theme.colorScheme.outline : Colors.white.withValues(alpha: 0.38), width: BorderWidths.thin, ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(AppRadius.button), - ), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(AppRadius.button)), padding: const EdgeInsets.symmetric( horizontal: Spacing.buttonPaddingHorizontal, vertical: Spacing.buttonPaddingVertical, ), ), - child: const Text( - 'Sign in', - style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500), - ), + child: const Text('Sign in', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500)), ); } } @@ -402,9 +322,7 @@ class _OfflineModeLink extends StatelessWidget { }, style: TextButton.styleFrom( foregroundColor: theme.colorScheme.primary, - textStyle: theme.textTheme.bodyMedium?.copyWith( - fontWeight: FontWeight.w600, - ), + textStyle: theme.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w600), ), child: const Text('Use offline mode'), ), diff --git a/app/lib/platform/web_redirect.dart b/app/lib/platform/web_redirect.dart new file mode 100644 index 0000000..d5e048f --- /dev/null +++ b/app/lib/platform/web_redirect.dart @@ -0,0 +1 @@ +export 'web_redirect_stub.dart' if (dart.library.html) 'web_redirect_web.dart'; diff --git a/app/lib/platform/web_redirect_stub.dart b/app/lib/platform/web_redirect_stub.dart new file mode 100644 index 0000000..650a1fd --- /dev/null +++ b/app/lib/platform/web_redirect_stub.dart @@ -0,0 +1,3 @@ +void redirectTo(String url) { + throw UnsupportedError('Browser redirects are only available on web'); +} diff --git a/app/lib/platform/web_redirect_web.dart b/app/lib/platform/web_redirect_web.dart new file mode 100644 index 0000000..b7acfc6 --- /dev/null +++ b/app/lib/platform/web_redirect_web.dart @@ -0,0 +1,5 @@ +import 'package:web/web.dart' as web; + +void redirectTo(String url) { + web.window.location.assign(url); +} diff --git a/app/lib/providers/annotations_provider.dart b/app/lib/providers/annotations_provider.dart index 6f372c2..e90c06c 100644 --- a/app/lib/providers/annotations_provider.dart +++ b/app/lib/providers/annotations_provider.dart @@ -46,8 +46,7 @@ class AnnotationsProvider extends ChangeNotifier { String get searchQuery => _searchQuery; /// Whether there are any annotations at all (unfiltered). - bool get hasAnnotations => - _dataStore != null && _dataStore!.annotations.isNotEmpty; + bool get hasAnnotations => _dataStore != null && _dataStore!.annotations.isNotEmpty; /// Whether current filters yield results. bool get hasResults => annotations.isNotEmpty; @@ -177,9 +176,7 @@ class AnnotationsProvider extends ChangeNotifier { case AnnotationSortOption.dateOldest: return a.createdAt.compareTo(b.createdAt); case AnnotationSortOption.bookTitle: - return getBookTitle( - a.bookId, - ).toLowerCase().compareTo(getBookTitle(b.bookId).toLowerCase()); + return getBookTitle(a.bookId).toLowerCase().compareTo(getBookTitle(b.bookId).toLowerCase()); case AnnotationSortOption.position: return a.location.pageNumber.compareTo(b.location.pageNumber); } diff --git a/app/lib/providers/auth_provider.dart b/app/lib/providers/auth_provider.dart index 861d29a..1fa5ba3 100644 --- a/app/lib/providers/auth_provider.dart +++ b/app/lib/providers/auth_provider.dart @@ -1,199 +1,183 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; -import 'package:google_sign_in/google_sign_in.dart'; +import 'package:papyrus/auth/auth_api_client.dart'; +import 'package:papyrus/auth/auth_models.dart'; +import 'package:papyrus/auth/auth_repository.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import 'package:supabase_flutter/supabase_flutter.dart'; class AuthProvider extends ChangeNotifier { - final SupabaseClient _client = Supabase.instance.client; + final AuthRepository _repository; final SharedPreferences _prefs; - bool _initialized = false; - late final StreamSubscription _authSubscription; static const _keyOfflineMode = 'offline_mode'; - User? _user; - User? get user => _user; + AuthStatus _status = AuthStatus.bootstrapping; + AuthStatus get status => _status; + + PapyrusUser? _user; + PapyrusUser? get user => _user; bool _isOfflineMode = false; bool get isOfflineMode => _isOfflineMode; - bool _isLoading = false; - bool get isLoading => _isLoading; + bool get isBootstrapping => _status == AuthStatus.bootstrapping; + + bool get isSignedIn => _user != null && _status == AuthStatus.signedIn; + + bool get isLoading { + return _status == AuthStatus.bootstrapping || + _status == AuthStatus.authenticating || + _status == AuthStatus.refreshing; + } String? _error; String? get error => _error; - AuthProvider(this._prefs) { + AuthProvider(this._prefs, {required AuthRepository repository, bool bootstrapOnCreate = true}) + : _repository = repository { _isOfflineMode = _prefs.getBool(_keyOfflineMode) ?? false; - // Listen to Supabase auth state changes - _authSubscription = _client.auth.onAuthStateChange.listen((data) { - _user = data.session?.user; - notifyListeners(); - }); - // Initialize Google Sign-In (not needed on web, where Supabase OAuth handles it) - if (!kIsWeb) { - _initGoogleSignIn(); + if (bootstrapOnCreate) { + unawaited(bootstrap()); } } - @override - void dispose() { - _authSubscription.cancel(); - super.dispose(); - } - - Future _initGoogleSignIn() async { - if (_initialized) return; + Future bootstrap() async { + _setStatus(AuthStatus.bootstrapping); try { - await GoogleSignIn.instance.initialize(); - _initialized = true; - - // Listen to Google Sign-In authentication events - GoogleSignIn.instance.authenticationEvents.listen( - (event) async { - if (event is GoogleSignInAuthenticationEventSignIn) { - await _handleGoogleSignInEvent(event.user); - } else if (event is GoogleSignInAuthenticationEventSignOut) { - // Google signed out; Supabase auth state listener handles state update - } - }, - onError: (error) { - _error = error.toString(); - _isLoading = false; - notifyListeners(); - debugPrint('Google Sign-In Stream Error: $error'); - }, - ); + final tokens = await _repository.bootstrap(); + _user = tokens?.user; + _error = null; + _setStatus(_user == null ? AuthStatus.signedOut : AuthStatus.signedIn); + } catch (error) { + _user = null; + _error = null; + _setStatus(AuthStatus.signedOut); + } + } - // Attempt lightweight authentication (silent sign-in) - unawaited( - GoogleSignIn.instance.attemptLightweightAuthentication()?.then(( - account, - ) async { - if (account != null) { - await _handleGoogleSignInEvent(account); - } - }), + Future register({required String email, required String password, required String displayName}) async { + return _runTokenAction(() { + return _repository.register( + email: email, + password: password, + displayName: displayName, + clientType: _clientType, + deviceLabel: _deviceLabel, ); - } catch (e) { - debugPrint('Google Sign-In initialization error: $e'); - } + }); } - Future _handleGoogleSignInEvent(GoogleSignInAccount account) async { - try { - final idToken = account.authentication.idToken; - - if (idToken != null) { - final response = await _client.auth.signInWithIdToken( - provider: OAuthProvider.google, - idToken: idToken, - ); - _user = response.user; - } - } catch (e) { - _error = e.toString(); - debugPrint('Supabase credential sign-in error: $e'); - } + Future login({required String email, required String password}) async { + return _runTokenAction(() { + return _repository.login(email: email, password: password, clientType: _clientType, deviceLabel: _deviceLabel); + }); } - Future signInWithGoogle() async { - _isLoading = true; + Future signInWithGoogle() async { + _setStatus(AuthStatus.authenticating); _error = null; - notifyListeners(); try { - if (kIsWeb) { - // Web: redirect to Google OAuth via Supabase; user is set via onAuthStateChange after return - await _client.auth.signInWithOAuth(OAuthProvider.google); - } else { - // Mobile/Desktop: use google_sign_in for native dialog, exchange token with Supabase - await _initGoogleSignIn(); - - if (!GoogleSignIn.instance.supportsAuthenticate()) { - // Platform doesn't support authenticate(), rely on lightweight auth - final account = await GoogleSignIn.instance - .attemptLightweightAuthentication(); - if (account != null) { - await _handleGoogleSignInEvent(account); - } else { - _error = 'Sign-in not available on this platform'; - } - } else { - final GoogleSignInAccount account = await GoogleSignIn.instance - .authenticate(); - - final idToken = account.authentication.idToken; - - if (idToken != null) { - final response = await _client.auth.signInWithIdToken( - provider: OAuthProvider.google, - idToken: idToken, - ); - _user = response.user; - } - } - } + final tokens = await _repository.signInWithGoogle(clientType: _clientType, deviceLabel: _deviceLabel); - _isLoading = false; - notifyListeners(); - return _user; - } on AuthException catch (e) { - _error = e.message; - _isLoading = false; - notifyListeners(); - debugPrint('Supabase Auth Error: ${e.message}'); - return null; - } on GoogleSignInException catch (e) { - // Handle user cancellation gracefully - if (e.code == GoogleSignInExceptionCode.canceled) { - _error = null; // Don't show error for user cancellation - } else { - _error = e.description ?? e.code.toString(); + if (tokens == null) { + return false; } - _isLoading = false; - notifyListeners(); - debugPrint('Google Sign-In Error: ${e.code} - ${e.description}'); - return null; - } catch (e) { - _error = e.toString(); - _isLoading = false; - notifyListeners(); - debugPrint('Sign-In Error: $e'); - return null; + + _user = tokens.user; + _isOfflineMode = false; + await _prefs.setBool(_keyOfflineMode, false); + _setStatus(AuthStatus.signedIn); + return true; + } catch (error) { + _user = null; + _error = _messageFor(error); + _setStatus(AuthStatus.authError); + return false; + } + } + + Future completeGoogleSignIn(Uri callbackUri) async { + return _runTokenAction(() { + return _repository.completeGoogleSignIn(callbackUri, clientType: _clientType, deviceLabel: _deviceLabel); + }); + } + + Future refresh() async { + _setStatus(AuthStatus.refreshing); + + try { + final tokens = await _repository.refresh(); + _user = tokens.user; + _error = null; + _setStatus(AuthStatus.signedIn); + return true; + } catch (error) { + _user = null; + _error = _messageFor(error); + _setStatus(AuthStatus.signedOut); + return false; } } Future signOut() async { - _isLoading = true; - notifyListeners(); + _setStatus(AuthStatus.authenticating); try { - await _client.auth.signOut(); + await _repository.logout(); + } catch (error) { + _error = _messageFor(error); + } - if (!kIsWeb && _initialized) { - await GoogleSignIn.instance.signOut(); - } + _user = null; + setOfflineMode(false); + _setStatus(AuthStatus.signedOut); + } - _user = null; + Future updateProfile({required String displayName, String? avatarUrl}) async { + try { + _user = await _repository.updateCurrentUser(displayName: displayName, avatarUrl: avatarUrl); _error = null; - setOfflineMode(false); - } catch (e) { - _error = e.toString(); - debugPrint('Sign-Out Error: $e'); + notifyListeners(); + return true; + } catch (error) { + _error = _messageFor(error); + notifyListeners(); + return false; } + } - _isLoading = false; - notifyListeners(); + Future forgotPassword(String email) { + return _runMessageAction(() => _repository.forgotPassword(email)); + } + + Future resetPassword({required String token, required String password}) { + return _runMessageAction(() { + return _repository.resetPassword(token: token, password: password); + }); + } + + Future verifyEmail(String token) { + return _runMessageAction(() => _repository.verifyEmail(token)); + } + + Future resendVerification(String email) { + return _runMessageAction(() => _repository.resendVerification(email)); } void setOfflineMode(bool value) { _isOfflineMode = value; _prefs.setBool(_keyOfflineMode, value); + + if (value) { + _user = null; + unawaited(_repository.clearTokens()); + _setStatus(AuthStatus.signedOut); + } + notifyListeners(); } @@ -201,4 +185,76 @@ class AuthProvider extends ChangeNotifier { _error = null; notifyListeners(); } + + Future _runTokenAction(Future Function() action) async { + _setStatus(AuthStatus.authenticating); + _error = null; + + try { + final tokens = await action(); + _user = tokens.user; + _isOfflineMode = false; + await _prefs.setBool(_keyOfflineMode, false); + _setStatus(AuthStatus.signedIn); + return true; + } catch (error) { + _error = _messageFor(error); + _setStatus(AuthStatus.authError); + return false; + } + } + + Future _runMessageAction(Future Function() action) async { + _setStatus(AuthStatus.authenticating); + _error = null; + + try { + final message = await action(); + _setStatus(_user == null ? AuthStatus.signedOut : AuthStatus.signedIn); + return message; + } catch (error) { + _error = _messageFor(error); + _setStatus(_user == null ? AuthStatus.authError : AuthStatus.signedIn); + return null; + } + } + + String _messageFor(Object error) { + if (error is AuthApiException) { + return error.message; + } + + return 'Authentication request failed. Please try again.'; + } + + String get _clientType { + if (kIsWeb) { + return 'web'; + } + + switch (defaultTargetPlatform) { + case TargetPlatform.android: + case TargetPlatform.iOS: + return 'mobile'; + case TargetPlatform.macOS: + case TargetPlatform.linux: + case TargetPlatform.windows: + return 'desktop'; + case TargetPlatform.fuchsia: + return 'unknown'; + } + } + + String get _deviceLabel { + if (kIsWeb) { + return 'flutter-web'; + } + + return 'flutter-${defaultTargetPlatform.name}'; + } + + void _setStatus(AuthStatus status) { + _status = status; + notifyListeners(); + } } diff --git a/app/lib/providers/book_details_provider.dart b/app/lib/providers/book_details_provider.dart index 286c18e..982eb8f 100644 --- a/app/lib/providers/book_details_provider.dart +++ b/app/lib/providers/book_details_provider.dart @@ -118,10 +118,7 @@ class BookDetailsProvider extends ChangeNotifier { } // Fallback to sample data if not found in DataStore - foundBook ??= SampleData.books.cast().firstWhere( - (b) => b?.id == bookId, - orElse: () => null, - ); + foundBook ??= SampleData.books.cast().firstWhere((b) => b?.id == bookId, orElse: () => null); if (foundBook == null) { throw Exception('Book not found'); diff --git a/app/lib/providers/book_edit_provider.dart b/app/lib/providers/book_edit_provider.dart index 371680c..060b63b 100644 --- a/app/lib/providers/book_edit_provider.dart +++ b/app/lib/providers/book_edit_provider.dart @@ -28,8 +28,7 @@ class BookEditProvider extends ChangeNotifier { // Cover image state (for uploaded files) Uint8List? _coverImageBytes; - BookEditProvider({MetadataService? metadataService}) - : _metadataService = metadataService ?? MetadataService(); + BookEditProvider({MetadataService? metadataService}) : _metadataService = metadataService ?? MetadataService(); // ============================================================================ // GETTERS @@ -149,9 +148,7 @@ class BookEditProvider extends ChangeNotifier { void updateSubtitle(String? value) { if (_editedBook == null) return; - _editedBook = _editedBook!.copyWith( - subtitle: value?.isEmpty == true ? null : value, - ); + _editedBook = _editedBook!.copyWith(subtitle: value?.isEmpty == true ? null : value); notifyListeners(); } @@ -169,17 +166,13 @@ class BookEditProvider extends ChangeNotifier { void updatePublisher(String? value) { if (_editedBook == null) return; - _editedBook = _editedBook!.copyWith( - publisher: value?.isEmpty == true ? null : value, - ); + _editedBook = _editedBook!.copyWith(publisher: value?.isEmpty == true ? null : value); notifyListeners(); } void updateLanguage(String? value) { if (_editedBook == null) return; - _editedBook = _editedBook!.copyWith( - language: value?.isEmpty == true ? null : value, - ); + _editedBook = _editedBook!.copyWith(language: value?.isEmpty == true ? null : value); notifyListeners(); } @@ -191,35 +184,26 @@ class BookEditProvider extends ChangeNotifier { void updateIsbn(String? value) { if (_editedBook == null) return; - _editedBook = _editedBook!.copyWith( - isbn: value?.isEmpty == true ? null : value, - ); + _editedBook = _editedBook!.copyWith(isbn: value?.isEmpty == true ? null : value); notifyListeners(); } void updateIsbn13(String? value) { if (_editedBook == null) return; - _editedBook = _editedBook!.copyWith( - isbn13: value?.isEmpty == true ? null : value, - ); + _editedBook = _editedBook!.copyWith(isbn13: value?.isEmpty == true ? null : value); notifyListeners(); } void updateDescription(String? value) { if (_editedBook == null) return; - _editedBook = _editedBook!.copyWith( - description: value?.isEmpty == true ? null : value, - ); + _editedBook = _editedBook!.copyWith(description: value?.isEmpty == true ? null : value); notifyListeners(); } void updateCoverUrl(String? value) { if (_editedBook == null) return; final shouldClear = value == null || value.isEmpty; - _editedBook = _editedBook!.copyWith( - coverUrl: shouldClear ? null : value, - clearCoverUrl: shouldClear, - ); + _editedBook = _editedBook!.copyWith(coverUrl: shouldClear ? null : value, clearCoverUrl: shouldClear); // Clear local bytes when URL is set if (value != null && value.isNotEmpty) { _coverImageBytes = null; @@ -251,9 +235,7 @@ class BookEditProvider extends ChangeNotifier { void updateSeriesName(String? value) { if (_editedBook == null) return; - _editedBook = _editedBook!.copyWith( - seriesName: value?.isEmpty == true ? null : value, - ); + _editedBook = _editedBook!.copyWith(seriesName: value?.isEmpty == true ? null : value); notifyListeners(); } @@ -271,17 +253,13 @@ class BookEditProvider extends ChangeNotifier { void updatePhysicalLocation(String? value) { if (_editedBook == null) return; - _editedBook = _editedBook!.copyWith( - physicalLocation: value?.isEmpty == true ? null : value, - ); + _editedBook = _editedBook!.copyWith(physicalLocation: value?.isEmpty == true ? null : value); notifyListeners(); } void updateLentTo(String? value) { if (_editedBook == null) return; - _editedBook = _editedBook!.copyWith( - lentTo: value?.isEmpty == true ? null : value, - ); + _editedBook = _editedBook!.copyWith(lentTo: value?.isEmpty == true ? null : value); notifyListeners(); } @@ -313,9 +291,7 @@ class BookEditProvider extends ChangeNotifier { try { final results = await _metadataService.search(query, _selectedSource); _fetchedResults = results; - _fetchState = results.isEmpty - ? MetadataFetchState.error - : MetadataFetchState.success; + _fetchState = results.isEmpty ? MetadataFetchState.error : MetadataFetchState.success; if (results.isEmpty) { _fetchError = 'No results found'; } @@ -337,14 +313,9 @@ class BookEditProvider extends ChangeNotifier { notifyListeners(); try { - final results = await _metadataService.searchByIsbn( - isbn, - _selectedSource, - ); + final results = await _metadataService.searchByIsbn(isbn, _selectedSource); _fetchedResults = results; - _fetchState = results.isEmpty - ? MetadataFetchState.error - : MetadataFetchState.success; + _fetchState = results.isEmpty ? MetadataFetchState.error : MetadataFetchState.success; if (results.isEmpty) { _fetchError = 'No results found for ISBN'; } @@ -383,12 +354,8 @@ class BookEditProvider extends ChangeNotifier { _editedBook = _editedBook!.copyWith( title: result.title ?? _editedBook!.title, subtitle: result.subtitle ?? _editedBook!.subtitle, - author: result.primaryAuthor.isNotEmpty - ? result.primaryAuthor - : _editedBook!.author, - coAuthors: result.coAuthors.isNotEmpty - ? result.coAuthors - : _editedBook!.coAuthors, + author: result.primaryAuthor.isNotEmpty ? result.primaryAuthor : _editedBook!.author, + coAuthors: result.coAuthors.isNotEmpty ? result.coAuthors : _editedBook!.coAuthors, publisher: result.publisher ?? _editedBook!.publisher, language: result.language ?? _editedBook!.language, pageCount: result.pageCount ?? _editedBook!.pageCount, diff --git a/app/lib/providers/bookmarks_provider.dart b/app/lib/providers/bookmarks_provider.dart index f09be71..c02d7ae 100644 --- a/app/lib/providers/bookmarks_provider.dart +++ b/app/lib/providers/bookmarks_provider.dart @@ -46,8 +46,7 @@ class BookmarksProvider extends ChangeNotifier { String get searchQuery => _searchQuery; /// Whether there are any bookmarks at all (unfiltered). - bool get hasBookmarks => - _dataStore != null && _dataStore!.bookmarks.isNotEmpty; + bool get hasBookmarks => _dataStore != null && _dataStore!.bookmarks.isNotEmpty; /// Whether current filters yield results. bool get hasResults => bookmarks.isNotEmpty; @@ -155,9 +154,7 @@ class BookmarksProvider extends ChangeNotifier { var result = all; if (_selectedColors.isNotEmpty) { - result = result - .where((b) => _selectedColors.contains(b.colorHex)) - .toList(); + result = result.where((b) => _selectedColors.contains(b.colorHex)).toList(); } if (_searchQuery.isNotEmpty) { @@ -166,9 +163,7 @@ class BookmarksProvider extends ChangeNotifier { final bookTitle = getBookTitle(b.bookId).toLowerCase(); final note = b.note?.toLowerCase() ?? ''; final chapter = b.chapterTitle?.toLowerCase() ?? ''; - return bookTitle.contains(query) || - note.contains(query) || - chapter.contains(query); + return bookTitle.contains(query) || note.contains(query) || chapter.contains(query); }).toList(); } @@ -183,9 +178,7 @@ class BookmarksProvider extends ChangeNotifier { case BookmarkSortOption.dateOldest: return a.createdAt.compareTo(b.createdAt); case BookmarkSortOption.bookTitle: - return getBookTitle( - a.bookId, - ).toLowerCase().compareTo(getBookTitle(b.bookId).toLowerCase()); + return getBookTitle(a.bookId).toLowerCase().compareTo(getBookTitle(b.bookId).toLowerCase()); case BookmarkSortOption.position: return a.position.compareTo(b.position); } diff --git a/app/lib/providers/dashboard_provider.dart b/app/lib/providers/dashboard_provider.dart index 8e96f25..dc8dad5 100644 --- a/app/lib/providers/dashboard_provider.dart +++ b/app/lib/providers/dashboard_provider.dart @@ -55,11 +55,7 @@ class DashboardProvider extends ChangeNotifier { Book? get currentBook { if (_dataStore == null) return null; final readingBooks = _dataStore!.books.where((b) => b.isReading).toList() - ..sort( - (a, b) => (b.lastReadAt ?? DateTime(2000)).compareTo( - a.lastReadAt ?? DateTime(2000), - ), - ); + ..sort((a, b) => (b.lastReadAt ?? DateTime(2000)).compareTo(a.lastReadAt ?? DateTime(2000))); return readingBooks.isNotEmpty ? readingBooks.first : null; } @@ -74,8 +70,7 @@ class DashboardProvider extends ChangeNotifier { /// Get recently added books (last 5). List get recentlyAdded { if (_dataStore == null) return []; - final books = List.from(_dataStore!.books) - ..sort((a, b) => b.addedAt.compareTo(a.addedAt)); + final books = List.from(_dataStore!.books)..sort((a, b) => b.addedAt.compareTo(a.addedAt)); return books.take(5).toList(); } @@ -84,9 +79,7 @@ class DashboardProvider extends ChangeNotifier { if (_dataStore == null) return 0; final today = DateTime.now(); final todayStart = DateTime(today.year, today.month, today.day); - final todaySessions = _dataStore!.readingSessions.where( - (s) => s.startTime.isAfter(todayStart), - ); + final todaySessions = _dataStore!.readingSessions.where((s) => s.startTime.isAfter(todayStart)); return todaySessions.fold(0, (sum, s) => sum + s.durationMinutes); } @@ -105,10 +98,7 @@ class DashboardProvider extends ChangeNotifier { /// Total reading minutes from all sessions. int get totalReadingMinutes { if (_dataStore == null) return 0; - return _dataStore!.readingSessions.fold( - 0, - (sum, s) => sum + s.durationMinutes, - ); + return _dataStore!.readingSessions.fold(0, (sum, s) => sum + s.durationMinutes); } ActivityPeriod get activityPeriod => _activityPeriod; @@ -253,9 +243,7 @@ class DashboardProvider extends ChangeNotifier { return; } - final offset = _activityPeriod == ActivityPeriod.week - ? _weekOffset - : _monthOffset; + final offset = _activityPeriod == ActivityPeriod.week ? _weekOffset : _monthOffset; _weeklyActivity = _generateActivityFromSessions(offset); } @@ -263,9 +251,7 @@ class DashboardProvider extends ChangeNotifier { if (_dataStore == null) return []; final now = DateTime.now(); - final weekStart = now.subtract( - Duration(days: now.weekday - 1 + (-offset * 7)), - ); + final weekStart = now.subtract(Duration(days: now.weekday - 1 + (-offset * 7))); return List.generate(7, (i) { final date = weekStart.add(Duration(days: i)); @@ -274,46 +260,22 @@ class DashboardProvider extends ChangeNotifier { // Get sessions for this day final daySessions = _dataStore!.readingSessions.where( - (s) => - s.startTime.isAfter( - dayStart.subtract(const Duration(seconds: 1)), - ) && - s.startTime.isBefore(dayEnd), + (s) => s.startTime.isAfter(dayStart.subtract(const Duration(seconds: 1))) && s.startTime.isBefore(dayEnd), ); final minutes = daySessions.fold(0, (sum, s) => sum + s.durationMinutes); final pages = daySessions.fold(0, (sum, s) => sum + (s.pagesRead ?? 0)); - return DailyActivity( - date: date, - readingMinutes: minutes, - pagesRead: pages, - booksRead: [], - ); + return DailyActivity(date: date, readingMinutes: minutes, pagesRead: pages, booksRead: []); }); } String _getWeekRangeLabel(int offset) { final now = DateTime.now(); - final weekStart = now.subtract( - Duration(days: now.weekday - 1 + (-offset * 7)), - ); + final weekStart = now.subtract(Duration(days: now.weekday - 1 + (-offset * 7))); final weekEnd = weekStart.add(const Duration(days: 6)); - const months = [ - 'Jan', - 'Feb', - 'Mar', - 'Apr', - 'May', - 'Jun', - 'Jul', - 'Aug', - 'Sep', - 'Oct', - 'Nov', - 'Dec', - ]; + const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; if (weekStart.month == weekEnd.month) { return '${months[weekStart.month - 1]} ${weekStart.day}-${weekEnd.day}'; diff --git a/app/lib/providers/goals_provider.dart b/app/lib/providers/goals_provider.dart index 7d8a421..2bf3c4d 100644 --- a/app/lib/providers/goals_provider.dart +++ b/app/lib/providers/goals_provider.dart @@ -137,10 +137,7 @@ class GoalsProvider extends ChangeNotifier { // Check if goal is now completed if (updatedGoal.isCompleted && !goal.isArchived) { - updatedGoal = updatedGoal.copyWith( - completedAt: DateTime.now(), - isArchived: true, - ); + updatedGoal = updatedGoal.copyWith(completedAt: DateTime.now(), isArchived: true); } _dataStore!.updateReadingGoal(updatedGoal); @@ -148,23 +145,14 @@ class GoalsProvider extends ChangeNotifier { } /// Updates a goal's properties. Persists to DataStore. - Future updateGoal({ - required String goalId, - int? target, - GoalType? type, - }) async { + Future updateGoal({required String goalId, int? target, GoalType? type}) async { if (_dataStore == null) { throw Exception('DataStore not attached'); } final goal = _dataStore!.getReadingGoal(goalId); if (goal != null) { - _dataStore!.updateReadingGoal( - goal.copyWith( - targetValue: target ?? goal.targetValue, - type: type ?? goal.type, - ), - ); + _dataStore!.updateReadingGoal(goal.copyWith(targetValue: target ?? goal.targetValue, type: type ?? goal.type)); } } @@ -176,9 +164,7 @@ class GoalsProvider extends ChangeNotifier { final goal = _dataStore!.getReadingGoal(goalId); if (goal != null) { - _dataStore!.updateReadingGoal( - goal.copyWith(isArchived: true, completedAt: DateTime.now()), - ); + _dataStore!.updateReadingGoal(goal.copyWith(isArchived: true, completedAt: DateTime.now())); } } @@ -217,9 +203,7 @@ class GoalsProvider extends ChangeNotifier { case GoalPeriod.yearly: return DateTime(now.year, 12, 31); case GoalPeriod.custom: - return now.add( - const Duration(days: 30), - ); // Should be provided by caller + return now.add(const Duration(days: 30)); // Should be provided by caller } } } diff --git a/app/lib/providers/library_provider.dart b/app/lib/providers/library_provider.dart index 3184a0c..c9df5b5 100644 --- a/app/lib/providers/library_provider.dart +++ b/app/lib/providers/library_provider.dart @@ -5,15 +5,7 @@ import 'package:papyrus/models/book.dart'; enum LibraryViewMode { grid, list } /// Active filter type for library content. -enum LibraryFilterType { - all, - shelves, - topics, - favorites, - reading, - finished, - unread, -} +enum LibraryFilterType { all, shelves, topics, favorites, reading, finished, unread } /// Sort options for library books. Each value encodes its direction. enum LibrarySortOption { @@ -91,9 +83,7 @@ class LibraryProvider extends ChangeNotifier { /// Toggle between grid and list view. void toggleViewMode() { - _viewMode = _viewMode == LibraryViewMode.grid - ? LibraryViewMode.list - : LibraryViewMode.grid; + _viewMode = _viewMode == LibraryViewMode.grid ? LibraryViewMode.list : LibraryViewMode.grid; notifyListeners(); } diff --git a/app/lib/providers/notes_provider.dart b/app/lib/providers/notes_provider.dart index 5d27424..d2e7d09 100644 --- a/app/lib/providers/notes_provider.dart +++ b/app/lib/providers/notes_provider.dart @@ -162,9 +162,7 @@ class NotesProvider extends ChangeNotifier { var result = all; if (_selectedTags.isNotEmpty) { - result = result - .where((n) => n.tags.any((t) => _selectedTags.contains(t))) - .toList(); + result = result.where((n) => n.tags.any((t) => _selectedTags.contains(t))).toList(); } if (_searchQuery.isNotEmpty) { @@ -174,10 +172,7 @@ class NotesProvider extends ChangeNotifier { final title = n.title.toLowerCase(); final content = n.content.toLowerCase(); final tags = n.tags.join(' ').toLowerCase(); - return bookTitle.contains(query) || - title.contains(query) || - content.contains(query) || - tags.contains(query); + return bookTitle.contains(query) || title.contains(query) || content.contains(query) || tags.contains(query); }).toList(); } @@ -192,9 +187,7 @@ class NotesProvider extends ChangeNotifier { case NoteSortOption.dateOldest: return a.createdAt.compareTo(b.createdAt); case NoteSortOption.bookTitle: - return getBookTitle( - a.bookId, - ).toLowerCase().compareTo(getBookTitle(b.bookId).toLowerCase()); + return getBookTitle(a.bookId).toLowerCase().compareTo(getBookTitle(b.bookId).toLowerCase()); case NoteSortOption.pinnedFirst: if (a.isPinned != b.isPinned) { return a.isPinned ? -1 : 1; diff --git a/app/lib/providers/preferences_provider.dart b/app/lib/providers/preferences_provider.dart index c3c0381..c273b24 100644 --- a/app/lib/providers/preferences_provider.dart +++ b/app/lib/providers/preferences_provider.dart @@ -134,8 +134,7 @@ class PreferencesProvider extends ChangeNotifier { } /// Highlight color: 'yellow', 'green', 'blue', 'pink', or 'orange'. - String get defaultHighlightColor => - _prefs.getString(_keyDefaultHighlightColor) ?? 'yellow'; + String get defaultHighlightColor => _prefs.getString(_keyDefaultHighlightColor) ?? 'yellow'; set defaultHighlightColor(String value) { _prefs.setString(_keyDefaultHighlightColor, value); @@ -153,8 +152,7 @@ class PreferencesProvider extends ChangeNotifier { } /// Sort order: 'title', 'author', 'date_added', 'last_read', or 'rating'. - String get defaultSortOrder => - _prefs.getString(_keyDefaultSortOrder) ?? 'date_added'; + String get defaultSortOrder => _prefs.getString(_keyDefaultSortOrder) ?? 'date_added'; set defaultSortOrder(String value) { _prefs.setString(_keyDefaultSortOrder, value); @@ -162,8 +160,7 @@ class PreferencesProvider extends ChangeNotifier { } /// Metadata source: 'Open Library' or 'Google Books'. - String get metadataSource => - _prefs.getString(_keyMetadataSource) ?? 'Open Library'; + String get metadataSource => _prefs.getString(_keyMetadataSource) ?? 'Open Library'; set metadataSource(String value) { _prefs.setString(_keyMetadataSource, value); @@ -171,8 +168,7 @@ class PreferencesProvider extends ChangeNotifier { } /// Annotation export format: 'Markdown', 'PDF', 'TXT', or 'HTML'. - String get annotationExportFormat => - _prefs.getString(_keyAnnotationExportFormat) ?? 'Markdown'; + String get annotationExportFormat => _prefs.getString(_keyAnnotationExportFormat) ?? 'Markdown'; set annotationExportFormat(String value) { _prefs.setString(_keyAnnotationExportFormat, value); @@ -195,8 +191,7 @@ class PreferencesProvider extends ChangeNotifier { notifyListeners(); } - bool get syncStatusNotifications => - _prefs.getBool(_keySyncStatusNotifications) ?? false; + bool get syncStatusNotifications => _prefs.getBool(_keySyncStatusNotifications) ?? false; set syncStatusNotifications(bool value) { _prefs.setBool(_keySyncStatusNotifications, value); @@ -243,8 +238,7 @@ class PreferencesProvider extends ChangeNotifier { } /// Conflict resolution: 'server', 'client', or 'ask'. - String get conflictResolution => - _prefs.getString(_keyConflictResolution) ?? 'server'; + String get conflictResolution => _prefs.getString(_keyConflictResolution) ?? 'server'; set conflictResolution(String value) { _prefs.setString(_keyConflictResolution, value); diff --git a/app/lib/providers/shelves_provider.dart b/app/lib/providers/shelves_provider.dart index b760938..d618078 100644 --- a/app/lib/providers/shelves_provider.dart +++ b/app/lib/providers/shelves_provider.dart @@ -88,8 +88,7 @@ class ShelvesProvider extends ChangeNotifier { if (_searchQuery.isNotEmpty) { final query = _searchQuery.toLowerCase(); list = list.where((shelf) { - return shelf.name.toLowerCase().contains(query) || - (shelf.description?.toLowerCase().contains(query) ?? false); + return shelf.name.toLowerCase().contains(query) || (shelf.description?.toLowerCase().contains(query) ?? false); }).toList(); } _applySorting(list); @@ -109,20 +108,15 @@ class ShelvesProvider extends ChangeNotifier { bool get bookSortAscending => _bookSortAscending; String get bookSearchQuery => _bookSearchQuery; - Set get activeBookFilters => - Set.unmodifiable(_activeBookFilters); + Set get activeBookFilters => Set.unmodifiable(_activeBookFilters); /// Whether a specific book filter is active. - bool isBookFilterActive(BookFilterType filter) => - _activeBookFilters.contains(filter); + bool isBookFilterActive(BookFilterType filter) => _activeBookFilters.contains(filter); /// Get total book count across all shelves. int get totalBookCount { if (_dataStore == null) return 0; - return _dataStore!.shelves.fold( - 0, - (sum, shelf) => sum + _dataStore!.getBookCountForShelf(shelf.id), - ); + return _dataStore!.shelves.fold(0, (sum, shelf) => sum + _dataStore!.getBookCountForShelf(shelf.id)); } // ============================================================================ @@ -174,9 +168,7 @@ class ShelvesProvider extends ChangeNotifier { /// Toggles between grid and list view. void toggleViewMode() { - _viewMode = _viewMode == ShelvesViewMode.grid - ? ShelvesViewMode.list - : ShelvesViewMode.grid; + _viewMode = _viewMode == ShelvesViewMode.grid ? ShelvesViewMode.list : ShelvesViewMode.grid; notifyListeners(); } @@ -310,10 +302,7 @@ class ShelvesProvider extends ChangeNotifier { } /// Gets filtered and sorted books for a shelf, applying search and filters. - List getFilteredBooksForShelf( - String shelfId, { - bool Function(String bookId)? isFavorite, - }) { + List getFilteredBooksForShelf(String shelfId, {bool Function(String bookId)? isFavorite}) { if (_dataStore == null) return []; var books = _dataStore!.getBooksInShelf(shelfId); @@ -322,9 +311,7 @@ class ShelvesProvider extends ChangeNotifier { if (_bookSearchQuery.isNotEmpty) { final searchQuery = SearchQueryParser.parse(_bookSearchQuery); if (searchQuery.isNotEmpty) { - books = books - .where((book) => searchQuery.matches(book, dataStore: _dataStore)) - .toList(); + books = books.where((book) => searchQuery.matches(book, dataStore: _dataStore)).toList(); } } @@ -342,9 +329,7 @@ class ShelvesProvider extends ChangeNotifier { books = books.where((book) => book.isFinished).toList(); } if (_activeBookFilters.contains(BookFilterType.unread)) { - books = books - .where((book) => book.readingStatus == ReadingStatus.notStarted) - .toList(); + books = books.where((book) => book.readingStatus == ReadingStatus.notStarted).toList(); } } @@ -358,12 +343,7 @@ class ShelvesProvider extends ChangeNotifier { } /// Creates a new shelf. - Future createShelf({ - required String name, - String? description, - String? colorHex, - IconData? icon, - }) async { + Future createShelf({required String name, String? description, String? colorHex, IconData? icon}) async { if (_dataStore == null) { throw Exception('DataStore not attached'); } @@ -437,10 +417,7 @@ class ShelvesProvider extends ChangeNotifier { } /// Adds a book to a shelf. - Future addBookToShelf({ - required String shelfId, - required String bookId, - }) async { + Future addBookToShelf({required String shelfId, required String bookId}) async { if (_dataStore == null) { throw Exception('DataStore not attached'); } @@ -455,10 +432,7 @@ class ShelvesProvider extends ChangeNotifier { } /// Removes a book from a shelf. - Future removeBookFromShelf({ - required String shelfId, - required String bookId, - }) async { + Future removeBookFromShelf({required String shelfId, required String bookId}) async { if (_dataStore == null) { throw Exception('DataStore not attached'); } diff --git a/app/lib/providers/statistics_provider.dart b/app/lib/providers/statistics_provider.dart index 53584ad..b609db5 100644 --- a/app/lib/providers/statistics_provider.dart +++ b/app/lib/providers/statistics_provider.dart @@ -24,20 +24,7 @@ class MonthlyStats { }); String get monthLabel { - const months = [ - 'Jan', - 'Feb', - 'Mar', - 'Apr', - 'May', - 'Jun', - 'Jul', - 'Aug', - 'Sep', - 'Oct', - 'Nov', - 'Dec', - ]; + const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; return months[month - 1]; } @@ -66,19 +53,13 @@ class SessionStats { final int totalMinutes; final int totalPages; - const SessionStats({ - required this.totalSessions, - required this.totalMinutes, - required this.totalPages, - }); + const SessionStats({required this.totalSessions, required this.totalMinutes, required this.totalPages}); /// Average session duration in minutes. - double get averageSessionDuration => - totalSessions > 0 ? totalMinutes / totalSessions : 0; + double get averageSessionDuration => totalSessions > 0 ? totalMinutes / totalSessions : 0; /// Reading velocity (pages per hour). - double get pagesPerHour => - totalMinutes > 0 ? (totalPages / totalMinutes) * 60 : 0; + double get pagesPerHour => totalMinutes > 0 ? (totalPages / totalMinutes) * 60 : 0; /// Formatted average session duration. String get averageSessionLabel { @@ -156,10 +137,7 @@ class StatisticsProvider extends ChangeNotifier { final range = _getDateRangeForPeriod(); return _dataStore!.books .where( - (b) => - b.completedAt != null && - b.completedAt!.isAfter(range.start) && - b.completedAt!.isBefore(range.end), + (b) => b.completedAt != null && b.completedAt!.isAfter(range.start) && b.completedAt!.isBefore(range.end), ) .length; } @@ -170,10 +148,7 @@ class StatisticsProvider extends ChangeNotifier { final range = _getDateRangeForPeriod(); return _dataStore!.readingGoals .where( - (g) => - g.completedAt != null && - g.completedAt!.isAfter(range.start) && - g.completedAt!.isBefore(range.end), + (g) => g.completedAt != null && g.completedAt!.isAfter(range.start) && g.completedAt!.isBefore(range.end), ) .length; } @@ -183,11 +158,7 @@ class StatisticsProvider extends ChangeNotifier { if (_dataStore == null) return 0; final range = _getDateRangeForPeriod(); return _dataStore!.readingSessions - .where( - (s) => - s.startTime.isAfter(range.start) && - s.startTime.isBefore(range.end), - ) + .where((s) => s.startTime.isAfter(range.start) && s.startTime.isBefore(range.end)) .fold(0, (sum, s) => sum + s.durationMinutes); } @@ -196,30 +167,18 @@ class StatisticsProvider extends ChangeNotifier { if (_dataStore == null) return 0; final range = _getDateRangeForPeriod(); return _dataStore!.readingSessions - .where( - (s) => - s.startTime.isAfter(range.start) && - s.startTime.isBefore(range.end), - ) + .where((s) => s.startTime.isAfter(range.start) && s.startTime.isBefore(range.end)) .fold(0, (sum, s) => sum + (s.pagesRead ?? 0)); } /// Session statistics for the selected period. SessionStats get sessionStats { if (_dataStore == null) { - return const SessionStats( - totalSessions: 0, - totalMinutes: 0, - totalPages: 0, - ); + return const SessionStats(totalSessions: 0, totalMinutes: 0, totalPages: 0); } final range = _getDateRangeForPeriod(); final sessions = _dataStore!.readingSessions - .where( - (s) => - s.startTime.isAfter(range.start) && - s.startTime.isBefore(range.end), - ) + .where((s) => s.startTime.isAfter(range.start) && s.startTime.isBefore(range.end)) .toList(); return SessionStats( @@ -299,9 +258,7 @@ class StatisticsProvider extends ChangeNotifier { /// Whether custom date range is active. bool get hasCustomRange => - _selectedPeriod == StatsPeriod.custom && - _customStartDate != null && - _customEndDate != null; + _selectedPeriod == StatsPeriod.custom && _customStartDate != null && _customEndDate != null; // ============================================================================ // METHODS @@ -364,33 +321,18 @@ class StatisticsProvider extends ChangeNotifier { switch (_selectedPeriod) { case StatsPeriod.week: final weekStart = now.subtract(Duration(days: now.weekday - 1)); - return ( - start: DateTime(weekStart.year, weekStart.month, weekStart.day), - end: now.add(const Duration(days: 1)), - ); + return (start: DateTime(weekStart.year, weekStart.month, weekStart.day), end: now.add(const Duration(days: 1))); case StatsPeriod.month: - return ( - start: DateTime(now.year, now.month, 1), - end: now.add(const Duration(days: 1)), - ); + return (start: DateTime(now.year, now.month, 1), end: now.add(const Duration(days: 1))); case StatsPeriod.year: - return ( - start: DateTime(now.year, 1, 1), - end: now.add(const Duration(days: 1)), - ); + return (start: DateTime(now.year, 1, 1), end: now.add(const Duration(days: 1))); case StatsPeriod.allTime: return (start: DateTime(2000), end: now.add(const Duration(days: 1))); case StatsPeriod.custom: if (_customStartDate != null && _customEndDate != null) { - return ( - start: _customStartDate!, - end: _customEndDate!.add(const Duration(days: 1)), - ); + return (start: _customStartDate!, end: _customEndDate!.add(const Duration(days: 1))); } - return ( - start: now.subtract(const Duration(days: 7)), - end: now.add(const Duration(days: 1)), - ); + return (start: now.subtract(const Duration(days: 7)), end: now.add(const Duration(days: 1))); } } @@ -424,19 +366,12 @@ class StatisticsProvider extends ChangeNotifier { final dayEnd = dayStart.add(const Duration(days: 1)); final daySessions = _dataStore!.readingSessions.where( - (s) => - s.startTime.isAfter( - dayStart.subtract(const Duration(seconds: 1)), - ) && - s.startTime.isBefore(dayEnd), + (s) => s.startTime.isAfter(dayStart.subtract(const Duration(seconds: 1))) && s.startTime.isBefore(dayEnd), ); return DailyActivity( date: date, - readingMinutes: daySessions.fold( - 0, - (sum, s) => sum + s.durationMinutes, - ), + readingMinutes: daySessions.fold(0, (sum, s) => sum + s.durationMinutes), pagesRead: daySessions.fold(0, (sum, s) => sum + (s.pagesRead ?? 0)), booksRead: [], ); @@ -464,19 +399,12 @@ class StatisticsProvider extends ChangeNotifier { final monthEnd = DateTime(year, month + 1, 1); final monthSessions = _dataStore!.readingSessions.where( - (s) => - s.startTime.isAfter( - monthStart.subtract(const Duration(seconds: 1)), - ) && - s.startTime.isBefore(monthEnd), + (s) => s.startTime.isAfter(monthStart.subtract(const Duration(seconds: 1))) && s.startTime.isBefore(monthEnd), ); final booksCompleted = _dataStore!.books .where( - (b) => - b.completedAt != null && - b.completedAt!.isAfter(monthStart) && - b.completedAt!.isBefore(monthEnd), + (b) => b.completedAt != null && b.completedAt!.isAfter(monthStart) && b.completedAt!.isBefore(monthEnd), ) .length; @@ -485,10 +413,7 @@ class StatisticsProvider extends ChangeNotifier { year: year, booksRead: booksCompleted, pagesRead: monthSessions.fold(0, (sum, s) => sum + (s.pagesRead ?? 0)), - readingMinutes: monthSessions.fold( - 0, - (sum, s) => sum + s.durationMinutes, - ), + readingMinutes: monthSessions.fold(0, (sum, s) => sum + s.durationMinutes), ); }).reversed.toList(); } diff --git a/app/lib/services/book_import_service.dart b/app/lib/services/book_import_service.dart index 6c650f4..b2611a0 100644 --- a/app/lib/services/book_import_service.dart +++ b/app/lib/services/book_import_service.dart @@ -64,8 +64,7 @@ class BookImportService { final type = _jsToNullableString(obj['type']); if (type == 'error') { - final message = - _jsToNullableString(obj['message']) ?? 'Unknown error'; + final message = _jsToNullableString(obj['message']) ?? 'Unknown error'; final error = Exception(message); final action = _jsToNullableString(obj['action']); final bookId = _jsToNullableString(obj['bookId']); @@ -131,9 +130,7 @@ class BookImportService { // Transfer bytes as ArrayBuffer for zero-copy transfer. // Ensure we only send the actual byte range, not the whole backing buffer. - final actualBytes = - bytes.offsetInBytes == 0 && - bytes.lengthInBytes == bytes.buffer.lengthInBytes + final actualBytes = bytes.offsetInBytes == 0 && bytes.lengthInBytes == bytes.buffer.lengthInBytes ? bytes : Uint8List.fromList(bytes); final jsBuffer = actualBytes.buffer.toJS; @@ -149,10 +146,7 @@ class BookImportService { _timeout, onTimeout: () { _pending.remove('process:$bookId'); - throw TimeoutException( - 'Book import timed out after ${_timeout.inSeconds}s', - _timeout, - ); + throw TimeoutException('Book import timed out after ${_timeout.inSeconds}s', _timeout); }, ); return _parseImportResult(obj, bookId, ext); @@ -181,10 +175,7 @@ class BookImportService { _timeout, onTimeout: () { _pending.remove('delete:$bookId'); - throw TimeoutException( - 'Delete timed out after ${_timeout.inSeconds}s', - _timeout, - ); + throw TimeoutException('Delete timed out after ${_timeout.inSeconds}s', _timeout); }, ); } @@ -213,10 +204,7 @@ class BookImportService { _timeout, onTimeout: () { _pending.remove('getFile:$bookId'); - throw TimeoutException( - 'Get file timed out after ${_timeout.inSeconds}s', - _timeout, - ); + throw TimeoutException('Get file timed out after ${_timeout.inSeconds}s', _timeout); }, ); final fileDataJs = obj['fileData']; @@ -241,16 +229,10 @@ class BookImportService { // Private helpers // --------------------------------------------------------------------------- - BookImportResult _parseImportResult( - JSObject data, - String bookId, - String fileExtension, - ) { + BookImportResult _parseImportResult(JSObject data, String bookId, String fileExtension) { final metadataRaw = data['metadata']; if (metadataRaw == null || metadataRaw.isNull || metadataRaw.isUndefined) { - throw StateError( - 'Worker response is missing required "metadata" field for book $bookId.', - ); + throw StateError('Worker response is missing required "metadata" field for book $bookId.'); } final metadataJs = metadataRaw as JSObject; @@ -266,9 +248,7 @@ class BookImportService { // co-authors array final coAuthorsJs = metadataJs['coAuthors']; final coAuthors = []; - if (coAuthorsJs != null && - !coAuthorsJs.isNull && - !coAuthorsJs.isUndefined) { + if (coAuthorsJs != null && !coAuthorsJs.isNull && !coAuthorsJs.isUndefined) { final arr = coAuthorsJs as JSArray; for (var i = 0; i < arr.length; i++) { final item = _jsToNullableString(arr[i]); @@ -279,9 +259,7 @@ class BookImportService { // Cover image Uint8List? coverImage; final coverDataJs = data['coverData']; - if (coverDataJs != null && - !coverDataJs.isNull && - !coverDataJs.isUndefined) { + if (coverDataJs != null && !coverDataJs.isNull && !coverDataJs.isUndefined) { coverImage = (coverDataJs as JSArrayBuffer).toDart.asUint8List(); } final coverMimeType = _jsToNullableString(data['coverMimeType']); @@ -294,9 +272,7 @@ class BookImportService { final fileHashRaw = _jsToNullableString(data['fileHash']); if (fileHashRaw == null) { - throw StateError( - 'Worker response is missing required "fileHash" field for book $bookId.', - ); + throw StateError('Worker response is missing required "fileHash" field for book $bookId.'); } final fileHash = fileHashRaw; diff --git a/app/lib/services/file_metadata_service.dart b/app/lib/services/file_metadata_service.dart index e2672ff..2f5cb81 100644 --- a/app/lib/services/file_metadata_service.dart +++ b/app/lib/services/file_metadata_service.dart @@ -54,8 +54,7 @@ class FileMetadataResult { String get primaryAuthor => authors?.isNotEmpty == true ? authors!.first : ''; /// Get co-authors (all authors except the first). - List get coAuthors => - authors != null && authors!.length > 1 ? authors!.sublist(1) : []; + List get coAuthors => authors != null && authors!.length > 1 ? authors!.sublist(1) : []; } /// Parsed ComicInfo.xml fields shared between CBZ and CBR extractors. @@ -67,14 +66,7 @@ class _ComicInfoData { final String? language; final int? pageCount; - const _ComicInfoData({ - this.title, - this.authors, - this.publisher, - this.description, - this.language, - this.pageCount, - }); + const _ComicInfoData({this.title, this.authors, this.publisher, this.description, this.language, this.pageCount}); } /// Service for extracting metadata from book files. @@ -84,23 +76,13 @@ class _ComicInfoData { class FileMetadataService { static const _charsPerPage = 1500; - static const _imageExtensions = { - '.jpg', - '.jpeg', - '.png', - '.gif', - '.bmp', - '.webp', - }; + static const _imageExtensions = {'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp'}; /// Extract metadata from file bytes. /// /// Format is detected from the [filename] extension. Returns partial results /// with warnings if extraction encounters issues. - Future extractMetadata( - Uint8List bytes, - String filename, - ) async { + Future extractMetadata(Uint8List bytes, String filename) async { final ext = p.extension(filename).toLowerCase(); try { @@ -118,9 +100,7 @@ class FileMetadataService { case '.txt': return _extractTxt(bytes, filename); default: - return FileMetadataResult( - warnings: ['Unsupported file format: $ext'], - ); + return FileMetadataResult(warnings: ['Unsupported file format: $ext']); } } catch (e) { return FileMetadataResult(warnings: ['Failed to extract metadata: $e']); @@ -140,47 +120,20 @@ class FileMetadataService { final title = _tryRead('EPUB title', warnings, () => book.title); final authors = _tryRead('EPUB authors', warnings, () { - final raw = book.authors - .whereType() - .where((a) => a.isNotEmpty) - .toList(); + final raw = book.authors.whereType().where((a) => a.isNotEmpty).toList(); return raw.isNotEmpty ? raw : null; }); - final publisher = _tryRead( - 'EPUB publisher', - warnings, - () => metadata?.publishers.firstOrNull, - ); - final description = _tryRead( - 'EPUB description', - warnings, - () => metadata?.description, - ); - final language = _tryRead( - 'EPUB language', - warnings, - () => metadata?.languages.firstOrNull, - ); - final publishedDate = _tryRead( - 'EPUB date', - warnings, - () => _findEpubDate(metadata?.dates), - ); + final publisher = _tryRead('EPUB publisher', warnings, () => metadata?.publishers.firstOrNull); + final description = _tryRead('EPUB description', warnings, () => metadata?.description); + final language = _tryRead('EPUB language', warnings, () => metadata?.languages.firstOrNull); + final publishedDate = _tryRead('EPUB date', warnings, () => _findEpubDate(metadata?.dates)); - final isbns = _tryRead( - 'EPUB identifiers', - warnings, - () => _findEpubIsbns(metadata?.identifiers), - ); + final isbns = _tryRead('EPUB identifiers', warnings, () => _findEpubIsbns(metadata?.identifiers)); Uint8List? coverImageBytes; String? coverImageMimeType; - final coverImage = _tryRead( - 'EPUB cover image', - warnings, - () => book.coverImage, - ); + final coverImage = _tryRead('EPUB cover image', warnings, () => book.coverImage); if (coverImage != null) { coverImageBytes = img.encodePng(coverImage); coverImageMimeType = 'image/png'; @@ -205,9 +158,7 @@ class FileMetadataService { if (dates == null || dates.isEmpty) return null; // Prefer publication date, fall back to first date. - final pubDate = dates - .where((d) => d.event?.toLowerCase() == 'publication') - .firstOrNull; + final pubDate = dates.where((d) => d.event?.toLowerCase() == 'publication').firstOrNull; return (pubDate ?? dates.first).date; } @@ -228,9 +179,7 @@ class FileMetadataService { isbn = clean; } else if (clean.length == 13 && isbn13 == null) { // ISBN-13 can appear without scheme in some EPUBs. - if (isIsbnScheme || - clean.startsWith('978') || - clean.startsWith('979')) { + if (isIsbnScheme || clean.startsWith('978') || clean.startsWith('979')) { isbn13 = clean; } } @@ -340,11 +289,7 @@ class FileMetadataService { List? authors; if (authorStr != null && authorStr.isNotEmpty) { // Authors can be separated by '&', ';', or ',' - authors = authorStr - .split(RegExp(r'[;&,]')) - .map((a) => a.trim()) - .where((a) => a.isNotEmpty) - .toList(); + authors = authorStr.split(RegExp(r'[;&,]')).map((a) => a.trim()).where((a) => a.isNotEmpty).toList(); } final publisher = getExthString(MobiExthTag.publisher); @@ -357,15 +302,9 @@ class FileMetadataService { Uint8List? coverImageBytes; String? coverImageMimeType; try { - final coverRecord = DartMobiReader.getExthRecordByTag( - mobiData, - MobiExthTag.coverOffset, - ); + final coverRecord = DartMobiReader.getExthRecordByTag(mobiData, MobiExthTag.coverOffset); if (coverRecord?.data != null) { - final offset = DartMobiReader.decodeExthValue( - coverRecord!.data!, - coverRecord.size!, - ); + final offset = DartMobiReader.decodeExthValue(coverRecord!.data!, coverRecord.size!); final imageIndex = mobiData.mobiHeader?.imageIndex ?? 0; final coverRecordIndex = imageIndex + offset; @@ -424,10 +363,7 @@ class FileMetadataService { final warnings = []; final archive = ZipDecoder().decodeBytes(bytes); - final comicInfoBytes = archive.files - .where((f) => f.name.toLowerCase() == 'comicinfo.xml') - .firstOrNull - ?.content; + final comicInfoBytes = archive.files.where((f) => f.name.toLowerCase() == 'comicinfo.xml').firstOrNull?.content; final comicInfo = _parseComicInfo(comicInfoBytes, 'CBZ', warnings); @@ -435,9 +371,8 @@ class FileMetadataService { Uint8List? coverImageBytes; String? coverImageMimeType; try { - final imageFiles = - archive.files.where((f) => f.isFile && _isImageFile(f.name)).toList() - ..sort((a, b) => a.name.compareTo(b.name)); + final imageFiles = archive.files.where((f) => f.isFile && _isImageFile(f.name)).toList() + ..sort((a, b) => a.name.compareTo(b.name)); if (imageFiles.isNotEmpty) { coverImageBytes = imageFiles.first.content; @@ -464,10 +399,7 @@ class FileMetadataService { // CBR (RAR-based comic archive) // ============================================================================ - Future _extractCbr( - Uint8List bytes, - String filename, - ) async { + Future _extractCbr(Uint8List bytes, String filename) async { final warnings = []; // unrar_file requires file paths. Write bytes to a temp file, extract, @@ -481,10 +413,7 @@ class FileMetadataService { final List files = rar.files; final comicInfoBytes = files - .where( - (f) => - f.name?.toLowerCase() == 'comicinfo.xml' && f.content != null, - ) + .where((f) => f.name?.toLowerCase() == 'comicinfo.xml' && f.content != null) .firstOrNull ?.content; @@ -494,16 +423,8 @@ class FileMetadataService { Uint8List? coverImageBytes; String? coverImageMimeType; try { - final imageFiles = - files - .where( - (f) => - f.content != null && - f.name != null && - _isImageFile(f.name!), - ) - .toList() - ..sort((a, b) => a.name!.compareTo(b.name!)); + final imageFiles = files.where((f) => f.content != null && f.name != null && _isImageFile(f.name!)).toList() + ..sort((a, b) => a.name!.compareTo(b.name!)); if (imageFiles.isNotEmpty) { coverImageBytes = imageFiles.first.content; @@ -564,12 +485,7 @@ class FileMetadataService { warnings.add('Could not estimate page count: $e'); } - return FileMetadataResult( - title: title, - authors: authors, - pageCount: pageCount, - warnings: warnings, - ); + return FileMetadataResult(title: title, authors: authors, pageCount: pageCount, warnings: warnings); } // ============================================================================ @@ -589,11 +505,7 @@ class FileMetadataService { /// Parse ComicInfo.xml bytes into metadata fields. /// /// If [xmlBytes] is null, adds a "not found" warning for [archiveType]. - _ComicInfoData _parseComicInfo( - Uint8List? xmlBytes, - String archiveType, - List warnings, - ) { + _ComicInfoData _parseComicInfo(Uint8List? xmlBytes, String archiveType, List warnings) { if (xmlBytes == null) { warnings.add('No ComicInfo.xml found in $archiveType archive'); return const _ComicInfoData(); @@ -657,10 +569,7 @@ class FileMetadataService { return 'image/jpeg'; } // PNG: 89 50 4E 47 - if (bytes[0] == 0x89 && - bytes[1] == 0x50 && - bytes[2] == 0x4E && - bytes[3] == 0x47) { + if (bytes[0] == 0x89 && bytes[1] == 0x50 && bytes[2] == 0x4E && bytes[3] == 0x47) { return 'image/png'; } // GIF: 47 49 46 diff --git a/app/lib/services/metadata_service.dart b/app/lib/services/metadata_service.dart index c0478a1..57a81f4 100644 --- a/app/lib/services/metadata_service.dart +++ b/app/lib/services/metadata_service.dart @@ -38,8 +38,7 @@ class BookMetadataResult { String get primaryAuthor => authors?.isNotEmpty == true ? authors!.first : ''; /// Get co-authors (all authors except the first). - List get coAuthors => - authors != null && authors!.length > 1 ? authors!.sublist(1) : []; + List get coAuthors => authors != null && authors!.length > 1 ? authors!.sublist(1) : []; /// Source display name. String get sourceLabel { @@ -59,10 +58,7 @@ class MetadataService { MetadataService({http.Client? client}) : _client = client ?? http.Client(); /// Search for books by query (title, author, or general search). - Future> search( - String query, - MetadataSource source, - ) async { + Future> search(String query, MetadataSource source) async { if (query.trim().isEmpty) return []; switch (source) { @@ -74,10 +70,7 @@ class MetadataService { } /// Search for a book by ISBN. - Future> searchByIsbn( - String isbn, - MetadataSource source, - ) async { + Future> searchByIsbn(String isbn, MetadataSource source) async { final cleanIsbn = isbn.replaceAll(RegExp(r'[-\s]'), ''); if (cleanIsbn.isEmpty) return []; @@ -95,9 +88,7 @@ class MetadataService { Future> _searchOpenLibrary(String query) async { try { - final uri = Uri.parse( - 'https://openlibrary.org/search.json?q=${Uri.encodeComponent(query)}&limit=10', - ); + final uri = Uri.parse('https://openlibrary.org/search.json?q=${Uri.encodeComponent(query)}&limit=10'); final response = await _client.get(uri); if (response.statusCode != 200) return []; @@ -113,9 +104,7 @@ class MetadataService { Future> _searchOpenLibraryByIsbn(String isbn) async { try { - final uri = Uri.parse( - 'https://openlibrary.org/search.json?isbn=$isbn&limit=5', - ); + final uri = Uri.parse('https://openlibrary.org/search.json?isbn=$isbn&limit=5'); final response = await _client.get(uri); if (response.statusCode != 200) return []; @@ -192,9 +181,7 @@ class MetadataService { Future> _searchGoogleBooksByIsbn(String isbn) async { try { - final uri = Uri.parse( - 'https://www.googleapis.com/books/v1/volumes?q=isbn:$isbn&maxResults=5', - ); + final uri = Uri.parse('https://www.googleapis.com/books/v1/volumes?q=isbn:$isbn&maxResults=5'); final response = await _client.get(uri); if (response.statusCode != 200) return []; @@ -216,9 +203,7 @@ class MetadataService { final imageLinks = volumeInfo['imageLinks'] as Map?; if (imageLinks != null) { coverUrl = - imageLinks['large'] as String? ?? - imageLinks['medium'] as String? ?? - imageLinks['thumbnail'] as String?; + imageLinks['large'] as String? ?? imageLinks['medium'] as String? ?? imageLinks['thumbnail'] as String?; // Convert HTTP to HTTPS coverUrl = coverUrl?.replaceFirst('http://', 'https://'); } @@ -226,8 +211,7 @@ class MetadataService { // Get ISBNs from industry identifiers String? isbn; String? isbn13; - final identifiers = - volumeInfo['industryIdentifiers'] as List? ?? []; + final identifiers = volumeInfo['industryIdentifiers'] as List? ?? []; for (final id in identifiers) { final type = id['type'] as String?; final identifier = id['identifier'] as String?; diff --git a/app/lib/themes/app_theme.dart b/app/lib/themes/app_theme.dart index 92d42ce..ef7528b 100644 --- a/app/lib/themes/app_theme.dart +++ b/app/lib/themes/app_theme.dart @@ -81,160 +81,40 @@ class AppTheme { // =========================================================================== static const TextTheme _textTheme = TextTheme( - displayLarge: TextStyle( - fontSize: 57, - fontWeight: FontWeight.w400, - letterSpacing: -0.25, - ), - displayMedium: TextStyle( - fontSize: 45, - fontWeight: FontWeight.w400, - letterSpacing: 0, - ), - displaySmall: TextStyle( - fontSize: 36, - fontWeight: FontWeight.w400, - letterSpacing: 0, - ), - headlineLarge: TextStyle( - fontSize: 32, - fontWeight: FontWeight.w400, - letterSpacing: 0, - ), - headlineMedium: TextStyle( - fontSize: 28, - fontWeight: FontWeight.w400, - letterSpacing: 0, - ), - headlineSmall: TextStyle( - fontSize: 24, - fontWeight: FontWeight.w400, - letterSpacing: 0, - ), - titleLarge: TextStyle( - fontSize: 22, - fontWeight: FontWeight.w400, - letterSpacing: 0, - ), - titleMedium: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - letterSpacing: 0.15, - ), - titleSmall: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - letterSpacing: 0.1, - ), - bodyLarge: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w400, - letterSpacing: 0.5, - ), - bodyMedium: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w400, - letterSpacing: 0.25, - ), - bodySmall: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w400, - letterSpacing: 0.4, - ), - labelLarge: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - letterSpacing: 0.1, - ), - labelMedium: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w500, - letterSpacing: 0.5, - ), - labelSmall: TextStyle( - fontSize: 11, - fontWeight: FontWeight.w500, - letterSpacing: 0.5, - ), + displayLarge: TextStyle(fontSize: 57, fontWeight: FontWeight.w400, letterSpacing: -0.25), + displayMedium: TextStyle(fontSize: 45, fontWeight: FontWeight.w400, letterSpacing: 0), + displaySmall: TextStyle(fontSize: 36, fontWeight: FontWeight.w400, letterSpacing: 0), + headlineLarge: TextStyle(fontSize: 32, fontWeight: FontWeight.w400, letterSpacing: 0), + headlineMedium: TextStyle(fontSize: 28, fontWeight: FontWeight.w400, letterSpacing: 0), + headlineSmall: TextStyle(fontSize: 24, fontWeight: FontWeight.w400, letterSpacing: 0), + titleLarge: TextStyle(fontSize: 22, fontWeight: FontWeight.w400, letterSpacing: 0), + titleMedium: TextStyle(fontSize: 16, fontWeight: FontWeight.w500, letterSpacing: 0.15), + titleSmall: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, letterSpacing: 0.1), + bodyLarge: TextStyle(fontSize: 16, fontWeight: FontWeight.w400, letterSpacing: 0.5), + bodyMedium: TextStyle(fontSize: 14, fontWeight: FontWeight.w400, letterSpacing: 0.25), + bodySmall: TextStyle(fontSize: 12, fontWeight: FontWeight.w400, letterSpacing: 0.4), + labelLarge: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, letterSpacing: 0.1), + labelMedium: TextStyle(fontSize: 12, fontWeight: FontWeight.w500, letterSpacing: 0.5), + labelSmall: TextStyle(fontSize: 11, fontWeight: FontWeight.w500, letterSpacing: 0.5), ); // E-ink typography: larger minimum size, bolder weights static const TextTheme _einkTextTheme = TextTheme( - displayLarge: TextStyle( - fontSize: 57, - fontWeight: FontWeight.w500, - letterSpacing: -0.25, - ), - displayMedium: TextStyle( - fontSize: 45, - fontWeight: FontWeight.w500, - letterSpacing: 0, - ), - displaySmall: TextStyle( - fontSize: 36, - fontWeight: FontWeight.w500, - letterSpacing: 0, - ), - headlineLarge: TextStyle( - fontSize: 32, - fontWeight: FontWeight.w600, - letterSpacing: 0, - ), - headlineMedium: TextStyle( - fontSize: 28, - fontWeight: FontWeight.w600, - letterSpacing: 0, - ), - headlineSmall: TextStyle( - fontSize: 24, - fontWeight: FontWeight.w600, - letterSpacing: 0, - ), - titleLarge: TextStyle( - fontSize: 22, - fontWeight: FontWeight.w500, - letterSpacing: 0, - ), - titleMedium: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - letterSpacing: 0.15, - ), - titleSmall: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - letterSpacing: 0.1, - ), - bodyLarge: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w400, - letterSpacing: 0.5, - ), - bodyMedium: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w400, - letterSpacing: 0.25, - ), - bodySmall: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w400, - letterSpacing: 0.4, - ), - labelLarge: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - letterSpacing: 0.1, - ), - labelMedium: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w600, - letterSpacing: 0.5, - ), - labelSmall: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w600, - letterSpacing: 0.5, - ), + displayLarge: TextStyle(fontSize: 57, fontWeight: FontWeight.w500, letterSpacing: -0.25), + displayMedium: TextStyle(fontSize: 45, fontWeight: FontWeight.w500, letterSpacing: 0), + displaySmall: TextStyle(fontSize: 36, fontWeight: FontWeight.w500, letterSpacing: 0), + headlineLarge: TextStyle(fontSize: 32, fontWeight: FontWeight.w600, letterSpacing: 0), + headlineMedium: TextStyle(fontSize: 28, fontWeight: FontWeight.w600, letterSpacing: 0), + headlineSmall: TextStyle(fontSize: 24, fontWeight: FontWeight.w600, letterSpacing: 0), + titleLarge: TextStyle(fontSize: 22, fontWeight: FontWeight.w500, letterSpacing: 0), + titleMedium: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, letterSpacing: 0.15), + titleSmall: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, letterSpacing: 0.1), + bodyLarge: TextStyle(fontSize: 18, fontWeight: FontWeight.w400, letterSpacing: 0.5), + bodyMedium: TextStyle(fontSize: 16, fontWeight: FontWeight.w400, letterSpacing: 0.25), + bodySmall: TextStyle(fontSize: 14, fontWeight: FontWeight.w400, letterSpacing: 0.4), + labelLarge: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, letterSpacing: 0.1), + labelMedium: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, letterSpacing: 0.5), + labelSmall: TextStyle(fontSize: 12, fontWeight: FontWeight.w600, letterSpacing: 0.5), ); // =========================================================================== @@ -249,9 +129,7 @@ class AppTheme { horizontal: Spacing.buttonPaddingHorizontal, vertical: Spacing.buttonPaddingVertical, ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(AppRadius.button), - ), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(AppRadius.button)), elevation: AppElevation.level1, textStyle: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500), ), @@ -266,9 +144,7 @@ class AppTheme { horizontal: Spacing.buttonPaddingHorizontal, vertical: Spacing.buttonPaddingVertical, ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(AppRadius.button), - ), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(AppRadius.button)), side: BorderSide(color: colors.outline, width: BorderWidths.thin), textStyle: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500), ), @@ -289,38 +165,23 @@ class AppTheme { filled: false, border: OutlineInputBorder( borderRadius: BorderRadius.circular(AppRadius.input), - borderSide: BorderSide( - color: colors.outline, - width: BorderWidths.inputDefault, - ), + borderSide: BorderSide(color: colors.outline, width: BorderWidths.inputDefault), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(AppRadius.input), - borderSide: BorderSide( - color: colors.outline, - width: BorderWidths.inputDefault, - ), + borderSide: BorderSide(color: colors.outline, width: BorderWidths.inputDefault), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(AppRadius.input), - borderSide: BorderSide( - color: colors.primary, - width: BorderWidths.inputFocused, - ), + borderSide: BorderSide(color: colors.primary, width: BorderWidths.inputFocused), ), errorBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(AppRadius.input), - borderSide: BorderSide( - color: colors.error, - width: BorderWidths.inputError, - ), + borderSide: BorderSide(color: colors.error, width: BorderWidths.inputError), ), focusedErrorBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(AppRadius.input), - borderSide: BorderSide( - color: colors.error, - width: BorderWidths.inputError, - ), + borderSide: BorderSide(color: colors.error, width: BorderWidths.inputError), ), contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), labelStyle: TextStyle(color: colors.onSurfaceVariant), @@ -331,9 +192,7 @@ class AppTheme { static CardThemeData _cardTheme(ColorScheme colors) { return CardThemeData( elevation: AppElevation.level1, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(AppRadius.card), - ), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(AppRadius.card)), margin: const EdgeInsets.all(Spacing.sm), ); } @@ -344,11 +203,7 @@ class AppTheme { scrolledUnderElevation: AppElevation.level1, backgroundColor: colors.surface, foregroundColor: colors.onSurface, - titleTextStyle: TextStyle( - color: colors.onSurface, - fontSize: 22, - fontWeight: FontWeight.w400, - ), + titleTextStyle: TextStyle(color: colors.onSurface, fontSize: 22, fontWeight: FontWeight.w400), ); } @@ -363,11 +218,7 @@ class AppTheme { } static DividerThemeData _dividerTheme(ColorScheme colors) { - return DividerThemeData( - color: colors.outlineVariant, - thickness: 1, - space: Spacing.md, - ); + return DividerThemeData(color: colors.outlineVariant, thickness: 1, space: Spacing.md); } static SnackBarThemeData _snackBarTheme(ColorScheme colors) { @@ -375,9 +226,7 @@ class AppTheme { backgroundColor: colors.inverseSurface, contentTextStyle: TextStyle(color: colors.onInverseSurface), actionTextColor: colors.inversePrimary, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(AppRadius.sm), - ), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(AppRadius.sm)), behavior: SnackBarBehavior.floating, ); } @@ -388,10 +237,7 @@ class AppTheme { color: colors.surfaceContainerHigh, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(AppRadius.md), - side: BorderSide( - color: colors.outlineVariant, - width: BorderWidths.thin, - ), + side: BorderSide(color: colors.outlineVariant, width: BorderWidths.thin), ), textStyle: TextStyle(color: colors.onSurface), mouseCursor: WidgetStateMouseCursor.clickable, @@ -414,11 +260,7 @@ class AppTheme { elevation: 0, backgroundColor: EinkColors.black, foregroundColor: EinkColors.white, - textStyle: const TextStyle( - fontSize: 20, - fontWeight: FontWeight.w600, - letterSpacing: 0.5, - ), + textStyle: const TextStyle(fontSize: 20, fontWeight: FontWeight.w600, letterSpacing: 0.5), ), ); } @@ -432,17 +274,10 @@ class AppTheme { vertical: Spacing.buttonPaddingVertical, ), shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero), - side: const BorderSide( - color: EinkColors.black, - width: BorderWidths.einkDefault, - ), + side: const BorderSide(color: EinkColors.black, width: BorderWidths.einkDefault), backgroundColor: EinkColors.white, foregroundColor: EinkColors.black, - textStyle: const TextStyle( - fontSize: 20, - fontWeight: FontWeight.w600, - letterSpacing: 0.5, - ), + textStyle: const TextStyle(fontSize: 20, fontWeight: FontWeight.w600, letterSpacing: 0.5), ), ); } @@ -452,11 +287,7 @@ class AppTheme { style: TextButton.styleFrom( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), foregroundColor: EinkColors.black, - textStyle: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - decoration: TextDecoration.underline, - ), + textStyle: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600, decoration: TextDecoration.underline), ), ); } @@ -467,45 +298,26 @@ class AppTheme { fillColor: EinkColors.container, border: OutlineInputBorder( borderRadius: BorderRadius.zero, - borderSide: BorderSide( - color: EinkColors.black, - width: BorderWidths.einkDefault, - ), + borderSide: BorderSide(color: EinkColors.black, width: BorderWidths.einkDefault), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.zero, - borderSide: BorderSide( - color: EinkColors.black, - width: BorderWidths.einkDefault, - ), + borderSide: BorderSide(color: EinkColors.black, width: BorderWidths.einkDefault), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.zero, - borderSide: BorderSide( - color: EinkColors.black, - width: BorderWidths.einkFocused, - ), + borderSide: BorderSide(color: EinkColors.black, width: BorderWidths.einkFocused), ), errorBorder: OutlineInputBorder( borderRadius: BorderRadius.zero, - borderSide: BorderSide( - color: EinkColors.black, - width: BorderWidths.einkError, - ), + borderSide: BorderSide(color: EinkColors.black, width: BorderWidths.einkError), ), focusedErrorBorder: OutlineInputBorder( borderRadius: BorderRadius.zero, - borderSide: BorderSide( - color: EinkColors.black, - width: BorderWidths.einkError, - ), + borderSide: BorderSide(color: EinkColors.black, width: BorderWidths.einkError), ), contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 20), - labelStyle: TextStyle( - color: EinkColors.black, - fontSize: 16, - fontWeight: FontWeight.w700, - ), + labelStyle: TextStyle(color: EinkColors.black, fontSize: 16, fontWeight: FontWeight.w700), hintStyle: TextStyle(color: EinkColors.mediumGray, fontSize: 20), floatingLabelBehavior: FloatingLabelBehavior.always, ); @@ -516,10 +328,7 @@ class AppTheme { elevation: 0, shape: RoundedRectangleBorder( borderRadius: BorderRadius.zero, - side: BorderSide( - color: EinkColors.black, - width: BorderWidths.einkDefault, - ), + side: BorderSide(color: EinkColors.black, width: BorderWidths.einkDefault), ), margin: EdgeInsets.all(Spacing.md), ); @@ -531,16 +340,9 @@ class AppTheme { scrolledUnderElevation: 0, backgroundColor: EinkColors.white, foregroundColor: EinkColors.black, - titleTextStyle: TextStyle( - color: EinkColors.black, - fontSize: 24, - fontWeight: FontWeight.w700, - ), + titleTextStyle: TextStyle(color: EinkColors.black, fontSize: 24, fontWeight: FontWeight.w700), shape: Border( - bottom: BorderSide( - color: EinkColors.black, - width: BorderWidths.einkDefault, - ), + bottom: BorderSide(color: EinkColors.black, width: BorderWidths.einkDefault), ), ); } @@ -553,19 +355,12 @@ class AppTheme { type: BottomNavigationBarType.fixed, elevation: 0, selectedLabelStyle: TextStyle(fontSize: 14, fontWeight: FontWeight.w700), - unselectedLabelStyle: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - ), + unselectedLabelStyle: TextStyle(fontSize: 14, fontWeight: FontWeight.w500), ); } static DividerThemeData _einkDividerTheme() { - return const DividerThemeData( - color: EinkColors.lightGray, - thickness: 2, - space: Spacing.lg, - ); + return const DividerThemeData(color: EinkColors.lightGray, thickness: 2, space: Spacing.lg); } static SnackBarThemeData _einkSnackBarTheme() { diff --git a/app/lib/utils/book_actions.dart b/app/lib/utils/book_actions.dart index 6695a71..ac048a0 100644 --- a/app/lib/utils/book_actions.dart +++ b/app/lib/utils/book_actions.dart @@ -13,11 +13,7 @@ import 'package:provider/provider.dart'; /// This helper centralizes the context menu logic used by book cards and list items. /// The [position] parameter determines where the menu appears; if null, it will /// be positioned at the center of the screen. -void showBookContextMenu({ - required BuildContext context, - required Book book, - Offset? position, -}) { +void showBookContextMenu({required BuildContext context, required Book book, Offset? position}) { final libraryProvider = context.read(); final isFavorite = libraryProvider.isBookFavorite(book.id, book.isFavorite); diff --git a/app/lib/utils/bulk_book_actions.dart b/app/lib/utils/bulk_book_actions.dart index ce24109..15cdc3a 100644 --- a/app/lib/utils/bulk_book_actions.dart +++ b/app/lib/utils/bulk_book_actions.dart @@ -14,11 +14,7 @@ import 'package:provider/provider.dart'; // ============================================================================= /// Add all selected books to the given shelves. -void bulkAddToShelves( - DataStore dataStore, - Set bookIds, - List shelfIds, -) { +void bulkAddToShelves(DataStore dataStore, Set bookIds, List shelfIds) { for (final bookId in bookIds) { for (final shelfId in shelfIds) { dataStore.addBookToShelf(bookId, shelfId); @@ -27,11 +23,7 @@ void bulkAddToShelves( } /// Set topics for all selected books (additive — does not remove existing). -void bulkAddTopics( - DataStore dataStore, - Set bookIds, - List tagIds, -) { +void bulkAddTopics(DataStore dataStore, Set bookIds, List tagIds) { for (final bookId in bookIds) { for (final tagId in tagIds) { dataStore.addTagToBook(bookId, tagId); @@ -40,11 +32,7 @@ void bulkAddTopics( } /// Change reading status for all selected books. -void bulkChangeStatus( - DataStore dataStore, - Set bookIds, - ReadingStatus status, -) { +void bulkChangeStatus(DataStore dataStore, Set bookIds, ReadingStatus status) { for (final bookId in bookIds) { final book = dataStore.getBook(bookId); if (book != null) { @@ -55,11 +43,7 @@ void bulkChangeStatus( /// Toggle favorite for all selected books. /// If any are not favorited, sets all to favorite; otherwise un-favorites all. -void bulkToggleFavorite( - LibraryProvider libraryProvider, - DataStore dataStore, - Set bookIds, -) { +void bulkToggleFavorite(LibraryProvider libraryProvider, DataStore dataStore, Set bookIds) { final allFavorite = bookIds.every((id) { final book = dataStore.getBook(id); return book != null && libraryProvider.isBookFavorite(id, book.isFavorite); @@ -91,10 +75,7 @@ void bulkDelete(DataStore dataStore, Set bookIds) { // ============================================================================= /// Show the move-to-shelf sheet for selected books. -void handleBulkAddToShelf( - BuildContext context, - LibraryProvider libraryProvider, -) { +void handleBulkAddToShelf(BuildContext context, LibraryProvider libraryProvider) { final dataStore = context.read(); final selectedIds = libraryProvider.selectedBookIds.toList(); @@ -109,10 +90,7 @@ void handleBulkAddToShelf( } /// Show the manage-topics sheet for selected books. -void handleBulkManageTopics( - BuildContext context, - LibraryProvider libraryProvider, -) { +void handleBulkManageTopics(BuildContext context, LibraryProvider libraryProvider) { final dataStore = context.read(); final selectedIds = libraryProvider.selectedBookIds.toList(); @@ -127,10 +105,7 @@ void handleBulkManageTopics( } /// Show the status change sheet for selected books. -void handleBulkChangeStatus( - BuildContext context, - LibraryProvider libraryProvider, -) { +void handleBulkChangeStatus(BuildContext context, LibraryProvider libraryProvider) { final dataStore = context.read(); BulkStatusSheet.show( @@ -144,16 +119,9 @@ void handleBulkChangeStatus( } /// Toggle favorite status for all selected books. -void handleBulkToggleFavorite( - BuildContext context, - LibraryProvider libraryProvider, -) { +void handleBulkToggleFavorite(BuildContext context, LibraryProvider libraryProvider) { final dataStore = context.read(); - bulkToggleFavorite( - libraryProvider, - dataStore, - libraryProvider.selectedBookIds, - ); + bulkToggleFavorite(libraryProvider, dataStore, libraryProvider.selectedBookIds); libraryProvider.exitSelectionMode(); } @@ -171,19 +139,14 @@ void handleBulkDelete(BuildContext context, LibraryProvider libraryProvider) { 'This action cannot be undone.', ), actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: const Text('Cancel'), - ), + TextButton(onPressed: () => Navigator.pop(context), child: const Text('Cancel')), FilledButton( onPressed: () { Navigator.pop(context); bulkDelete(dataStore, libraryProvider.selectedBookIds); libraryProvider.exitSelectionMode(); }, - style: FilledButton.styleFrom( - backgroundColor: Theme.of(context).colorScheme.error, - ), + style: FilledButton.styleFrom(backgroundColor: Theme.of(context).colorScheme.error), child: const Text('Delete'), ), ], @@ -192,10 +155,7 @@ void handleBulkDelete(BuildContext context, LibraryProvider libraryProvider) { } /// Build a [BulkActionBar] wired to all bulk action handlers. -BulkActionBar buildBulkActionBar( - BuildContext context, - LibraryProvider libraryProvider, -) { +BulkActionBar buildBulkActionBar(BuildContext context, LibraryProvider libraryProvider) { return BulkActionBar( onAddToShelf: () => handleBulkAddToShelf(context, libraryProvider), onManageTopics: () => handleBulkManageTopics(context, libraryProvider), @@ -206,10 +166,7 @@ BulkActionBar buildBulkActionBar( } /// Build the mobile bottom action bar container with bulk actions. -Widget buildMobileBottomActionBar( - BuildContext context, - LibraryProvider libraryProvider, -) { +Widget buildMobileBottomActionBar(BuildContext context, LibraryProvider libraryProvider) { final colorScheme = Theme.of(context).colorScheme; return Container( @@ -219,10 +176,7 @@ Widget buildMobileBottomActionBar( ), child: SafeArea( child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: Spacing.md, - vertical: Spacing.sm, - ), + padding: const EdgeInsets.symmetric(horizontal: Spacing.md, vertical: Spacing.sm), child: buildBulkActionBar(context, libraryProvider), ), ), diff --git a/app/lib/utils/image_utils.dart b/app/lib/utils/image_utils.dart index 977baed..ce3d114 100644 --- a/app/lib/utils/image_utils.dart +++ b/app/lib/utils/image_utils.dart @@ -5,17 +5,11 @@ import 'dart:typed_data'; String bytesToDataUri(Uint8List bytes) { String mimeType = 'image/jpeg'; if (bytes.length >= 8) { - if (bytes[0] == 0x89 && - bytes[1] == 0x50 && - bytes[2] == 0x4E && - bytes[3] == 0x47) { + if (bytes[0] == 0x89 && bytes[1] == 0x50 && bytes[2] == 0x4E && bytes[3] == 0x47) { mimeType = 'image/png'; } else if (bytes[0] == 0x47 && bytes[1] == 0x49 && bytes[2] == 0x46) { mimeType = 'image/gif'; - } else if (bytes[0] == 0x52 && - bytes[1] == 0x49 && - bytes[2] == 0x46 && - bytes[3] == 0x46) { + } else if (bytes[0] == 0x52 && bytes[1] == 0x49 && bytes[2] == 0x46 && bytes[3] == 0x46) { mimeType = 'image/webp'; } } diff --git a/app/lib/utils/responsive.dart b/app/lib/utils/responsive.dart index edb74dd..faac8e6 100644 --- a/app/lib/utils/responsive.dart +++ b/app/lib/utils/responsive.dart @@ -112,9 +112,7 @@ class Responsive { static double getPageMargin(BuildContext context) { switch (getDeviceType(context)) { case DeviceType.desktop: - return isLargeDesktop(context) - ? Breakpoints.desktopLargeMargin - : Breakpoints.desktopSmallMargin; + return isLargeDesktop(context) ? Breakpoints.desktopLargeMargin : Breakpoints.desktopSmallMargin; case DeviceType.tablet: return Breakpoints.tabletMargin; case DeviceType.mobile: @@ -148,16 +146,12 @@ class Responsive { /// Get button height based on device type. static double getButtonHeight(BuildContext context) { - return isDesktop(context) - ? ComponentSizes.buttonHeightDesktop - : ComponentSizes.buttonHeightMobile; + return isDesktop(context) ? ComponentSizes.buttonHeightDesktop : ComponentSizes.buttonHeightMobile; } /// Get touch target size based on device type. static double getTouchTarget(BuildContext context) { - return isDesktop(context) - ? TouchTargets.desktopRecommended - : TouchTargets.mobileRecommended; + return isDesktop(context) ? TouchTargets.desktopRecommended : TouchTargets.mobileRecommended; } } @@ -172,12 +166,7 @@ class ResponsiveBuilder extends StatelessWidget { /// Builder for desktop layout (optional, defaults to tablet or mobile) final Widget Function(BuildContext context)? desktop; - const ResponsiveBuilder({ - super.key, - required this.mobile, - this.tablet, - this.desktop, - }); + const ResponsiveBuilder({super.key, required this.mobile, this.tablet, this.desktop}); @override Widget build(BuildContext context) { diff --git a/app/lib/utils/search_query_parser.dart b/app/lib/utils/search_query_parser.dart index 10cd9c3..f8e10fb 100644 --- a/app/lib/utils/search_query_parser.dart +++ b/app/lib/utils/search_query_parser.dart @@ -89,11 +89,7 @@ class SearchQueryParser { pendingOperator = null; } - return SearchQuery( - filters: filters, - operators: operators, - notFilters: notFilters, - ); + return SearchQuery(filters: filters, operators: operators, notFilters: notFilters); } /// Parse a single filter token. @@ -101,11 +97,7 @@ class SearchQueryParser { // Check for negation prefix if (token.startsWith('-') && token.length > 1) { final inner = _parseFilter(token.substring(1)); - return SearchFilter( - field: inner.field, - operator: SearchOperator.notEquals, - value: inner.value, - ); + return SearchFilter(field: inner.field, operator: SearchOperator.notEquals, value: inner.value); } // Check for field:value pattern @@ -140,11 +132,7 @@ class SearchQueryParser { value = value.substring(1, value.length - 1); } - return SearchFilter( - field: SearchField.any, - operator: SearchOperator.contains, - value: value, - ); + return SearchFilter(field: SearchField.any, operator: SearchOperator.contains, value: value); } /// Parse field name string to SearchField enum. @@ -181,11 +169,7 @@ class SearchQueryParser { ]; /// Get suggestions for status values. - static List get statusSuggestions => [ - 'status:reading', - 'status:finished', - 'status:unread', - ]; + static List get statusSuggestions => ['status:reading', 'status:finished', 'status:unread']; /// Get suggestions for format values. static List get formatSuggestions => [ diff --git a/app/lib/widgets/add_book/add_book_choice_sheet.dart b/app/lib/widgets/add_book/add_book_choice_sheet.dart index 77e50d6..51515c5 100644 --- a/app/lib/widgets/add_book/add_book_choice_sheet.dart +++ b/app/lib/widgets/add_book/add_book_choice_sheet.dart @@ -24,9 +24,7 @@ class AddBookChoiceSheet extends StatelessWidget { return showDialog( context: context, builder: (_) => Dialog( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(AppRadius.dialog), - ), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(AppRadius.dialog)), child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 480), child: Padding( @@ -40,16 +38,9 @@ class AddBookChoiceSheet extends StatelessWidget { return showModalBottomSheet( context: context, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(AppRadius.xl)), - ), + shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(AppRadius.xl))), builder: (_) => Padding( - padding: const EdgeInsets.only( - left: Spacing.lg, - right: Spacing.lg, - top: Spacing.md, - bottom: Spacing.lg, - ), + padding: const EdgeInsets.only(left: Spacing.lg, right: Spacing.lg, top: Spacing.md, bottom: Spacing.lg), child: AddBookChoiceSheet(callerContext: context), ), ); @@ -58,17 +49,13 @@ class AddBookChoiceSheet extends StatelessWidget { @override Widget build(BuildContext context) { final textTheme = Theme.of(context).textTheme; - final isDesktop = - MediaQuery.of(context).size.width >= Breakpoints.desktopSmall; + final isDesktop = MediaQuery.of(context).size.width >= Breakpoints.desktopSmall; return Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (!isDesktop) ...[ - const BottomSheetHandle(), - const SizedBox(height: Spacing.lg), - ], + if (!isDesktop) ...[const BottomSheetHandle(), const SizedBox(height: Spacing.lg)], Text('Add book', style: textTheme.headlineSmall), const SizedBox(height: Spacing.lg), _ChoiceOption( @@ -101,12 +88,7 @@ class _ChoiceOption extends StatelessWidget { final String subtitle; final VoidCallback onTap; - const _ChoiceOption({ - required this.icon, - required this.title, - required this.subtitle, - required this.onTap, - }); + const _ChoiceOption({required this.icon, required this.title, required this.subtitle, required this.onTap}); @override Widget build(BuildContext context) { @@ -139,12 +121,7 @@ class _ChoiceOption extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(title, style: textTheme.titleMedium), - Text( - subtitle, - style: textTheme.bodySmall?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), + Text(subtitle, style: textTheme.bodySmall?.copyWith(color: colorScheme.onSurfaceVariant)), ], ), ), diff --git a/app/lib/widgets/add_book/add_physical_book_sheet.dart b/app/lib/widgets/add_book/add_physical_book_sheet.dart index 7b93114..874e873 100644 --- a/app/lib/widgets/add_book/add_physical_book_sheet.dart +++ b/app/lib/widgets/add_book/add_physical_book_sheet.dart @@ -29,16 +29,13 @@ class AddPhysicalBookSheet extends StatelessWidget { context: context, isScrollControlled: true, useRootNavigator: true, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(AppRadius.xl)), - ), + shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(AppRadius.xl))), builder: (context) => DraggableScrollableSheet( initialChildSize: 0.9, minChildSize: 0.5, maxChildSize: 0.95, expand: false, - builder: (context, scrollController) => - _PhysicalBookContent(scrollController: scrollController), + builder: (context, scrollController) => _PhysicalBookContent(scrollController: scrollController), ), ); } @@ -109,9 +106,7 @@ class _PhysicalBookContentState extends State<_PhysicalBookContent> { super.dispose(); } - bool get _canSave => - _titleController.text.trim().isNotEmpty && - _authorController.text.trim().isNotEmpty; + bool get _canSave => _titleController.text.trim().isNotEmpty && _authorController.text.trim().isNotEmpty; Future _onScanBarcode() async { final isbn = await IsbnScannerDialog.show(context); @@ -131,17 +126,11 @@ class _PhysicalBookContentState extends State<_PhysicalBookContent> { try { // Try Open Library first - var results = await _metadataService.searchByIsbn( - isbn.trim(), - MetadataSource.openLibrary, - ); + var results = await _metadataService.searchByIsbn(isbn.trim(), MetadataSource.openLibrary); // Fall back to Google Books if (results.isEmpty) { - results = await _metadataService.searchByIsbn( - isbn.trim(), - MetadataSource.googleBooks, - ); + results = await _metadataService.searchByIsbn(isbn.trim(), MetadataSource.googleBooks); } if (!mounted) return; @@ -181,9 +170,7 @@ class _PhysicalBookContentState extends State<_PhysicalBookContent> { if (result.publishedDate != null) { _publicationDate = _parsePublishedDate(result.publishedDate!); if (_publicationDate != null) { - _publicationDateController.text = DateFormat.yMMMMd().format( - _publicationDate!, - ); + _publicationDateController.text = DateFormat.yMMMMd().format(_publicationDate!); } } @@ -259,9 +246,7 @@ class _PhysicalBookContentState extends State<_PhysicalBookContent> { final messenger = ScaffoldMessenger.of(context); Navigator.of(context).pop(); - messenger.showSnackBar( - SnackBar(content: Text('Added "${book.title}" to library')), - ); + messenger.showSnackBar(SnackBar(content: Text('Added "${book.title}" to library'))); } // ============================================================================ @@ -280,21 +265,14 @@ class _PhysicalBookContentState extends State<_PhysicalBookContent> { @override Widget build(BuildContext context) { return Padding( - padding: EdgeInsets.only( - bottom: MediaQuery.of(context).viewInsets.bottom, - ), + padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), child: Form( key: _formKey, child: Column( children: [ // Fixed header Padding( - padding: const EdgeInsets.fromLTRB( - Spacing.md, - Spacing.md, - Spacing.md, - 0, - ), + padding: const EdgeInsets.fromLTRB(Spacing.md, Spacing.md, Spacing.md, 0), child: Column( children: [ const BottomSheetHandle(), @@ -316,10 +294,7 @@ class _PhysicalBookContentState extends State<_PhysicalBookContent> { Expanded( child: ListView( controller: widget.scrollController, - padding: const EdgeInsets.symmetric( - horizontal: Spacing.lg, - vertical: Spacing.md, - ), + padding: const EdgeInsets.symmetric(horizontal: Spacing.lg, vertical: Spacing.md), children: [ Column( crossAxisAlignment: CrossAxisAlignment.stretch, @@ -331,8 +306,7 @@ class _PhysicalBookContentState extends State<_PhysicalBookContent> { child: CoverImagePicker( initialUrl: _coverUrl, initialBytes: _coverImageBytes, - onUrlChanged: (url) => - setState(() => _coverUrl = url), + onUrlChanged: (url) => setState(() => _coverUrl = url), onFileChanged: (bytes) => setState(() { _coverImageBytes = bytes; if (bytes != null) _coverUrl = null; @@ -363,10 +337,7 @@ class _PhysicalBookContentState extends State<_PhysicalBookContent> { // SECTION CARD // ============================================================================ - Widget _buildSectionCard({ - required String? title, - required List children, - }) { + Widget _buildSectionCard({required String? title, required List children}) { return Card( margin: const EdgeInsets.only(bottom: Spacing.xs), child: Padding( @@ -375,12 +346,7 @@ class _PhysicalBookContentState extends State<_PhysicalBookContent> { crossAxisAlignment: CrossAxisAlignment.start, children: [ if (title != null) ...[ - Text( - title, - style: Theme.of( - context, - ).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600), - ), + Text(title, style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600)), const SizedBox(height: Spacing.md), ], ...children, @@ -398,12 +364,7 @@ class _PhysicalBookContentState extends State<_PhysicalBookContent> { return _buildSectionCard( title: 'Basic information', children: [ - BookTextField( - controller: _titleController, - label: 'Title', - required: true, - onChanged: (_) => setState(() {}), - ), + BookTextField(controller: _titleController, label: 'Title', required: true, onChanged: (_) => setState(() {})), const SizedBox(height: Spacing.md), BookTextField(controller: _subtitleController, label: 'Subtitle'), const SizedBox(height: Spacing.md), @@ -414,16 +375,9 @@ class _PhysicalBookContentState extends State<_PhysicalBookContent> { onChanged: (_) => setState(() {}), ), const SizedBox(height: Spacing.md), - CoAuthorEditor( - coAuthors: _coAuthors, - onChanged: (updated) => setState(() => _coAuthors = updated), - ), + CoAuthorEditor(coAuthors: _coAuthors, onChanged: (updated) => setState(() => _coAuthors = updated)), const SizedBox(height: Spacing.md), - BookTextField( - controller: _descriptionController, - label: 'Description', - maxLines: 5, - ), + BookTextField(controller: _descriptionController, label: 'Description', maxLines: 5), ], ); } @@ -443,11 +397,7 @@ class _PhysicalBookContentState extends State<_PhysicalBookContent> { onChanged: (date) => setState(() => _publicationDate = date), ), const SizedBox(height: Spacing.md), - BookTextField( - controller: _pageCountController, - label: 'Page count', - keyboardType: TextInputType.number, - ), + BookTextField(controller: _pageCountController, label: 'Page count', keyboardType: TextInputType.number), ], ); } @@ -518,26 +468,19 @@ class _PhysicalBookContentState extends State<_PhysicalBookContent> { keyboardType: TextInputType.number, decoration: InputDecoration( labelText: 'ISBN', - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(AppRadius.md), - ), + border: OutlineInputBorder(borderRadius: BorderRadius.circular(AppRadius.md)), ), onFieldSubmitted: (_) => _lookupIsbn(_isbnController.text), ), ), const SizedBox(width: Spacing.sm), IconButton( - onPressed: isFetching - ? null - : () => _lookupIsbn(_isbnController.text), + onPressed: isFetching ? null : () => _lookupIsbn(_isbnController.text), icon: isFetching ? const SizedBox( width: 20, height: 20, - child: CircularProgressIndicator( - strokeWidth: 2, - color: Colors.white, - ), + child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white), ) : const Icon(Icons.search), ), @@ -558,24 +501,16 @@ class _PhysicalBookContentState extends State<_PhysicalBookContent> { children: [ Icon(Icons.check_circle, size: 16, color: colorScheme.primary), const SizedBox(width: Spacing.xs), - Text( - 'Book details found', - style: textTheme.bodySmall?.copyWith( - color: colorScheme.primary, - ), - ), + Text('Book details found', style: textTheme.bodySmall?.copyWith(color: colorScheme.primary)), ], ), ], - if (_lookupState == _IsbnLookupState.notFound || - _lookupState == _IsbnLookupState.error) ...[ + if (_lookupState == _IsbnLookupState.notFound || _lookupState == _IsbnLookupState.error) ...[ const SizedBox(height: Spacing.md), Row( children: [ Icon( - _lookupState == _IsbnLookupState.notFound - ? Icons.info_outline - : Icons.error_outline, + _lookupState == _IsbnLookupState.notFound ? Icons.info_outline : Icons.error_outline, size: 16, color: colorScheme.onSurfaceVariant, ), @@ -583,9 +518,7 @@ class _PhysicalBookContentState extends State<_PhysicalBookContent> { Expanded( child: Text( _lookupMessage ?? '', - style: textTheme.bodySmall?.copyWith( - color: colorScheme.onSurfaceVariant, - ), + style: textTheme.bodySmall?.copyWith(color: colorScheme.onSurfaceVariant), ), ), ], diff --git a/app/lib/widgets/add_book/import_book_sheet.dart b/app/lib/widgets/add_book/import_book_sheet.dart index 0aa4bee..7693e5b 100644 --- a/app/lib/widgets/add_book/import_book_sheet.dart +++ b/app/lib/widgets/add_book/import_book_sheet.dart @@ -30,15 +30,10 @@ class ImportBookSheet extends StatelessWidget { return showDialog( context: context, builder: (_) => Dialog( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(AppRadius.dialog), - ), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(AppRadius.dialog)), child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 520), - child: const Padding( - padding: EdgeInsets.all(Spacing.lg), - child: _ImportContent(), - ), + child: const Padding(padding: EdgeInsets.all(Spacing.lg), child: _ImportContent()), ), ), ); @@ -48,9 +43,7 @@ class ImportBookSheet extends StatelessWidget { context: context, isScrollControlled: true, useRootNavigator: true, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(AppRadius.xl)), - ), + shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(AppRadius.xl))), builder: (_) => DraggableScrollableSheet( initialChildSize: 0.6, minChildSize: 0.4, @@ -59,12 +52,7 @@ class ImportBookSheet extends StatelessWidget { builder: (context, scrollController) => SingleChildScrollView( controller: scrollController, child: const Padding( - padding: EdgeInsets.only( - left: Spacing.lg, - right: Spacing.lg, - top: Spacing.md, - bottom: Spacing.lg, - ), + padding: EdgeInsets.only(left: Spacing.lg, right: Spacing.lg, top: Spacing.md, bottom: Spacing.lg), child: _ImportContent(), ), ), @@ -98,15 +86,7 @@ class _ImportContentState extends State<_ImportContent> { /// Allowed file extensions per platform. static const _webExtensions = ['epub']; - static const _nativeExtensions = [ - 'epub', - 'pdf', - 'mobi', - 'azw3', - 'txt', - 'cbr', - 'cbz', - ]; + static const _nativeExtensions = ['epub', 'pdf', 'mobi', 'azw3', 'txt', 'cbr', 'cbz']; /// Schedule a setState that is guaranteed to trigger a frame. /// @@ -213,16 +193,13 @@ class _ImportContentState extends State<_ImportContent> { final messenger = ScaffoldMessenger.of(context); Navigator.of(context).pop(); - messenger.showSnackBar( - SnackBar(content: Text('Added "${book.title}" to library')), - ); + messenger.showSnackBar(SnackBar(content: Text('Added "${book.title}" to library'))); } @override Widget build(BuildContext context) { final textTheme = Theme.of(context).textTheme; - final isDesktop = - MediaQuery.of(context).size.width >= Breakpoints.desktopSmall; + final isDesktop = MediaQuery.of(context).size.width >= Breakpoints.desktopSmall; return Column( mainAxisSize: MainAxisSize.min, @@ -232,13 +209,8 @@ class _ImportContentState extends State<_ImportContent> { if (!isDesktop) const SizedBox(height: Spacing.lg), Row( children: [ - Expanded( - child: Text('Import book', style: textTheme.headlineSmall), - ), - IconButton( - onPressed: () => Navigator.of(context).pop(), - icon: const Icon(Icons.close), - ), + Expanded(child: Text('Import book', style: textTheme.headlineSmall)), + IconButton(onPressed: () => Navigator.of(context).pop(), icon: const Icon(Icons.close)), ], ), const SizedBox(height: Spacing.lg), @@ -269,16 +241,11 @@ class _ImportContentState extends State<_ImportContent> { children: [ Icon(Icons.upload_file, size: 48, color: colorScheme.primary), const SizedBox(height: Spacing.md), - Text( - kIsWeb ? 'Select an EPUB file' : 'Select a book file', - style: textTheme.titleMedium, - ), + Text(kIsWeb ? 'Select an EPUB file' : 'Select a book file', style: textTheme.titleMedium), const SizedBox(height: Spacing.xs), Text( 'The file will be stored offline on this device', - style: textTheme.bodySmall?.copyWith( - color: colorScheme.onSurfaceVariant, - ), + style: textTheme.bodySmall?.copyWith(color: colorScheme.onSurfaceVariant), ), const SizedBox(height: Spacing.lg), FilledButton.icon( @@ -306,21 +273,12 @@ class _ImportContentState extends State<_ImportContent> { ), child: Column( children: [ - const SizedBox( - width: 48, - height: 48, - child: CircularProgressIndicator(), - ), + const SizedBox(width: 48, height: 48, child: CircularProgressIndicator()), const SizedBox(height: Spacing.lg), Text('Processing...', style: textTheme.titleMedium), if (_filename != null) ...[ const SizedBox(height: Spacing.xs), - Text( - _filename!, - style: textTheme.bodySmall?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), + Text(_filename!, style: textTheme.bodySmall?.copyWith(color: colorScheme.onSurfaceVariant)), ], ], ), @@ -349,13 +307,7 @@ class _ImportContentState extends State<_ImportContent> { clipBehavior: Clip.antiAlias, child: result.coverImage != null ? Image.memory(result.coverImage!, fit: BoxFit.cover) - : Center( - child: Icon( - Icons.menu_book, - size: 32, - color: colorScheme.onSurfaceVariant, - ), - ), + : Center(child: Icon(Icons.menu_book, size: 32, color: colorScheme.onSurfaceVariant)), ), const SizedBox(width: Spacing.md), Expanded( @@ -364,19 +316,12 @@ class _ImportContentState extends State<_ImportContent> { children: [ Text(result.title, style: textTheme.titleMedium), const SizedBox(height: Spacing.xs), - Text( - result.author, - style: textTheme.bodyMedium?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), + Text(result.author, style: textTheme.bodyMedium?.copyWith(color: colorScheme.onSurfaceVariant)), if (result.pageCount != null) ...[ const SizedBox(height: Spacing.xs), Text( '~${result.pageCount} pages', - style: textTheme.bodySmall?.copyWith( - color: colorScheme.onSurfaceVariant, - ), + style: textTheme.bodySmall?.copyWith(color: colorScheme.onSurfaceVariant), ), ], ], @@ -401,10 +346,7 @@ class _ImportContentState extends State<_ImportContent> { ), const SizedBox(width: Spacing.md), Expanded( - child: FilledButton( - onPressed: _addToLibrary, - child: const Text('Add to library'), - ), + child: FilledButton(onPressed: _addToLibrary, child: const Text('Add to library')), ), ], ), @@ -433,10 +375,7 @@ class _ImportContentState extends State<_ImportContent> { textAlign: TextAlign.center, ), const SizedBox(height: Spacing.lg), - FilledButton( - onPressed: _pickAndProcess, - child: const Text('Try again'), - ), + FilledButton(onPressed: _pickAndProcess, child: const Text('Try again')), ], ), ); diff --git a/app/lib/widgets/add_book/isbn_scanner_dialog.dart b/app/lib/widgets/add_book/isbn_scanner_dialog.dart index c785066..90406ab 100644 --- a/app/lib/widgets/add_book/isbn_scanner_dialog.dart +++ b/app/lib/widgets/add_book/isbn_scanner_dialog.dart @@ -8,12 +8,9 @@ class IsbnScannerDialog extends StatefulWidget { /// Show the scanner and return the scanned ISBN, or null if cancelled. static Future show(BuildContext context) { - return Navigator.of(context).push( - MaterialPageRoute( - fullscreenDialog: true, - builder: (context) => const IsbnScannerDialog(), - ), - ); + return Navigator.of( + context, + ).push(MaterialPageRoute(fullscreenDialog: true, builder: (context) => const IsbnScannerDialog())); } @override @@ -48,11 +45,7 @@ class _IsbnScannerDialogState extends State { return Scaffold( backgroundColor: Colors.black, - appBar: AppBar( - backgroundColor: Colors.black, - foregroundColor: Colors.white, - title: const Text('Scan ISBN'), - ), + appBar: AppBar(backgroundColor: Colors.black, foregroundColor: Colors.white, title: const Text('Scan ISBN')), body: Column( children: [ Expanded( @@ -66,31 +59,17 @@ class _IsbnScannerDialogState extends State { child: Column( mainAxisSize: MainAxisSize.min, children: [ - Icon( - Icons.no_photography, - size: IconSizes.display, - color: colorScheme.error, - ), + Icon(Icons.no_photography, size: IconSizes.display, color: colorScheme.error), const SizedBox(height: Spacing.md), - Text( - 'Camera unavailable', - style: textTheme.titleMedium?.copyWith( - color: Colors.white, - ), - ), + Text('Camera unavailable', style: textTheme.titleMedium?.copyWith(color: Colors.white)), const SizedBox(height: Spacing.sm), Text( 'Please check camera permissions and try again.', - style: textTheme.bodyMedium?.copyWith( - color: Colors.white70, - ), + style: textTheme.bodyMedium?.copyWith(color: Colors.white70), textAlign: TextAlign.center, ), const SizedBox(height: Spacing.lg), - FilledButton( - onPressed: () => Navigator.of(context).pop(), - child: const Text('Go back'), - ), + FilledButton(onPressed: () => Navigator.of(context).pop(), child: const Text('Go back')), ], ), ), @@ -103,10 +82,7 @@ class _IsbnScannerDialogState extends State { padding: const EdgeInsets.all(Spacing.lg), child: Column( children: [ - Text( - 'Point camera at ISBN barcode', - style: textTheme.bodyLarge?.copyWith(color: Colors.white), - ), + Text('Point camera at ISBN barcode', style: textTheme.bodyLarge?.copyWith(color: Colors.white)), const SizedBox(height: Spacing.md), ValueListenableBuilder( valueListenable: _controller, @@ -114,9 +90,7 @@ class _IsbnScannerDialogState extends State { return IconButton( onPressed: () => _controller.toggleTorch(), icon: Icon( - state.torchState == TorchState.on - ? Icons.flash_on - : Icons.flash_off, + state.torchState == TorchState.on ? Icons.flash_on : Icons.flash_off, color: Colors.white, size: IconSizes.medium, ), diff --git a/app/lib/widgets/annotations/annotation_action_sheet.dart b/app/lib/widgets/annotations/annotation_action_sheet.dart index a0ba94f..c5fc345 100644 --- a/app/lib/widgets/annotations/annotation_action_sheet.dart +++ b/app/lib/widgets/annotations/annotation_action_sheet.dart @@ -15,17 +15,12 @@ class AnnotationNoteSheet extends StatefulWidget { const AnnotationNoteSheet({super.key, required this.annotation}); /// Show the note editing sheet. Returns the new note text, or null if cancelled. - static Future show( - BuildContext context, { - required Annotation annotation, - }) { + static Future show(BuildContext context, {required Annotation annotation}) { return showModalBottomSheet( context: context, isScrollControlled: true, shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical( - top: Radius.circular(AppRadius.bottomSheet), - ), + borderRadius: BorderRadius.vertical(top: Radius.circular(AppRadius.bottomSheet)), ), builder: (context) => AnnotationNoteSheet(annotation: annotation), ); @@ -105,28 +100,17 @@ class _AnnotationNoteSheetState extends State { /// Confirmation dialog for deleting an annotation. class DeleteAnnotationDialog { /// Show the delete confirmation dialog. Returns true if confirmed. - static Future show( - BuildContext context, { - required Annotation annotation, - required String bookTitle, - }) async { + static Future show(BuildContext context, {required Annotation annotation, required String bookTitle}) async { final result = await showDialog( context: context, builder: (context) => AlertDialog( title: const Text('Delete annotation'), - content: Text( - 'Delete annotation at ${annotation.location.shortLocation} in "$bookTitle"?', - ), + content: Text('Delete annotation at ${annotation.location.shortLocation} in "$bookTitle"?'), actions: [ - TextButton( - onPressed: () => Navigator.pop(context, false), - child: const Text('Cancel'), - ), + TextButton(onPressed: () => Navigator.pop(context, false), child: const Text('Cancel')), FilledButton( onPressed: () => Navigator.pop(context, true), - style: FilledButton.styleFrom( - backgroundColor: Theme.of(context).colorScheme.error, - ), + style: FilledButton.styleFrom(backgroundColor: Theme.of(context).colorScheme.error), child: const Text('Delete'), ), ], diff --git a/app/lib/widgets/auth/auth_branding.dart b/app/lib/widgets/auth/auth_branding.dart index 3474f20..ad26937 100644 --- a/app/lib/widgets/auth/auth_branding.dart +++ b/app/lib/widgets/auth/auth_branding.dart @@ -11,11 +11,7 @@ class AuthBranding extends StatelessWidget { return Row( mainAxisSize: MainAxisSize.min, children: [ - SvgPicture.asset( - 'assets/images/logo-icon-light.svg', - width: 56, - height: 56, - ), + SvgPicture.asset('assets/images/logo-icon-light.svg', width: 56, height: 56), const SizedBox(width: 16), Text( 'Papyrus', diff --git a/app/lib/widgets/auth/auth_continue_button.dart b/app/lib/widgets/auth/auth_continue_button.dart index 11b6da0..9a81c7b 100644 --- a/app/lib/widgets/auth/auth_continue_button.dart +++ b/app/lib/widgets/auth/auth_continue_button.dart @@ -7,20 +7,13 @@ class AuthContinueButton extends StatelessWidget { final VoidCallback onPressed; final bool isDesktop; - const AuthContinueButton({ - super.key, - required this.isLoading, - required this.onPressed, - required this.isDesktop, - }); + const AuthContinueButton({super.key, required this.isLoading, required this.onPressed, required this.isDesktop}); @override Widget build(BuildContext context) { final theme = Theme.of(context); - final buttonHeight = isDesktop - ? ComponentSizes.buttonHeightDesktop - : ComponentSizes.buttonHeightMobile; + final buttonHeight = isDesktop ? ComponentSizes.buttonHeightDesktop : ComponentSizes.buttonHeightMobile; return ElevatedButton( onPressed: isLoading ? null : onPressed, @@ -29,9 +22,7 @@ class AuthContinueButton extends StatelessWidget { backgroundColor: theme.colorScheme.primary, foregroundColor: theme.colorScheme.onPrimary, elevation: AppElevation.level2, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(AppRadius.button), - ), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(AppRadius.button)), padding: const EdgeInsets.symmetric( horizontal: Spacing.buttonPaddingHorizontal, vertical: Spacing.buttonPaddingVertical, @@ -43,24 +34,15 @@ class AuthContinueButton extends StatelessWidget { height: 24, child: CircularProgressIndicator( strokeWidth: 2, - valueColor: AlwaysStoppedAnimation( - theme.colorScheme.onPrimary, - ), + valueColor: AlwaysStoppedAnimation(theme.colorScheme.onPrimary), ), ) : Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - const Text( - 'Continue', - style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500), - ), + const Text('Continue', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500)), const SizedBox(width: Spacing.sm), - Icon( - Icons.arrow_forward, - size: IconSizes.medium, - color: theme.colorScheme.onPrimary, - ), + Icon(Icons.arrow_forward, size: IconSizes.medium, color: theme.colorScheme.onPrimary), ], ), ); diff --git a/app/lib/widgets/auth/auth_hero_panel.dart b/app/lib/widgets/auth/auth_hero_panel.dart index d72c9ee..16c5da0 100644 --- a/app/lib/widgets/auth/auth_hero_panel.dart +++ b/app/lib/widgets/auth/auth_hero_panel.dart @@ -23,12 +23,8 @@ class AuthHeroPanel extends StatelessWidget { @override Widget build(BuildContext context) { - final gradientStart = isDark - ? AuthColors.gradientStartDark - : AuthColors.gradientStartLight; - final gradientEnd = isDark - ? AuthColors.gradientEndDark - : AuthColors.gradientEndLight; + final gradientStart = isDark ? AuthColors.gradientStartDark : AuthColors.gradientStartLight; + final gradientEnd = isDark ? AuthColors.gradientEndDark : AuthColors.gradientEndLight; return Stack( fit: StackFit.expand, @@ -45,11 +41,7 @@ class AuthHeroPanel extends StatelessWidget { ), // Illustration filling the panel - clean, no overlays Positioned.fill( - child: Image.asset( - 'assets/images/auth-illustration.png', - fit: BoxFit.cover, - alignment: Alignment.center, - ), + child: Image.asset('assets/images/auth-illustration.png', fit: BoxFit.cover, alignment: Alignment.center), ), ], ); @@ -66,12 +58,8 @@ class CompactAuthHeader extends StatelessWidget { @override Widget build(BuildContext context) { - final gradientStart = isDark - ? AuthColors.gradientStartDark - : AuthColors.gradientStartLight; - final gradientEnd = isDark - ? AuthColors.gradientEndDark - : AuthColors.gradientEndLight; + final gradientStart = isDark ? AuthColors.gradientStartDark : AuthColors.gradientStartLight; + final gradientEnd = isDark ? AuthColors.gradientEndDark : AuthColors.gradientEndLight; return ClipPath( clipper: CurvedBottomClipper(curveHeight: 30), diff --git a/app/lib/widgets/auth/auth_page_layouts.dart b/app/lib/widgets/auth/auth_page_layouts.dart index 6899a66..62d422d 100644 --- a/app/lib/widgets/auth/auth_page_layouts.dart +++ b/app/lib/widgets/auth/auth_page_layouts.dart @@ -35,10 +35,7 @@ class MobileAuthLayout extends StatelessWidget { backgroundColor: theme.colorScheme.surface, body: Column( children: [ - CompactAuthHeader( - isDark: isDark, - height: ComponentSizes.mobileHeroHeight, - ), + CompactAuthHeader(isDark: isDark, height: ComponentSizes.mobileHeroHeight), Expanded( child: CustomScrollView( slivers: [ @@ -62,9 +59,7 @@ class MobileAuthLayout extends StatelessWidget { ), Text( subtitle, - style: theme.textTheme.bodyMedium?.copyWith( - color: theme.colorScheme.onSurfaceVariant, - ), + style: theme.textTheme.bodyMedium?.copyWith(color: theme.colorScheme.onSurfaceVariant), ), const SizedBox(height: Spacing.lg), ], @@ -130,20 +125,14 @@ class _DesktopAuthLayoutState extends State { decoration: BoxDecoration( color: theme.colorScheme.surface, boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.08), - blurRadius: 24, - offset: Offset(isSwapped ? 4 : -4, 0), - ), + BoxShadow(color: Colors.black.withValues(alpha: 0.08), blurRadius: 24, offset: Offset(isSwapped ? 4 : -4, 0)), ], ), child: Center( child: SingleChildScrollView( padding: const EdgeInsets.all(ComponentSizes.authFormPanelPadding), child: ConstrainedBox( - constraints: const BoxConstraints( - maxWidth: ComponentSizes.authFormPanelMaxWidth, - ), + constraints: const BoxConstraints(maxWidth: ComponentSizes.authFormPanelMaxWidth), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, @@ -160,9 +149,7 @@ class _DesktopAuthLayoutState extends State { ), Text( widget.subtitle, - style: theme.textTheme.bodyMedium?.copyWith( - color: theme.colorScheme.onSurfaceVariant, - ), + style: theme.textTheme.bodyMedium?.copyWith(color: theme.colorScheme.onSurfaceVariant), ), const SizedBox(height: Spacing.xl), ], @@ -196,10 +183,7 @@ class _DesktopAuthLayoutState extends State { right: isSwapped ? 0 : null, top: 0, bottom: 0, - child: _SwapPanelsButton( - isSwapped: isSwapped, - onPressed: _toggleSwap, - ), + child: _SwapPanelsButton(isSwapped: isSwapped, onPressed: _toggleSwap), ), ], ), @@ -229,12 +213,7 @@ class _SwapPanelsButton extends StatelessWidget { child: Align( alignment: Alignment.centerLeft, child: Transform.translate( - offset: Offset( - isSwapped - ? totalWidth - boundaryOffset - 20 - : boundaryOffset - 20, - 0, - ), + offset: Offset(isSwapped ? totalWidth - boundaryOffset - 20 : boundaryOffset - 20, 0), child: SizedBox( width: 40, height: 40, @@ -245,11 +224,7 @@ class _SwapPanelsButton extends StatelessWidget { clipBehavior: Clip.antiAlias, child: InkWell( onTap: onPressed, - child: Icon( - Icons.swap_horiz, - size: 20, - color: theme.colorScheme.onSurfaceVariant, - ), + child: Icon(Icons.swap_horiz, size: 20, color: theme.colorScheme.onSurfaceVariant), ), ), ), diff --git a/app/lib/widgets/auth/auth_switch_link.dart b/app/lib/widgets/auth/auth_switch_link.dart index cfa9c5f..80fba06 100644 --- a/app/lib/widgets/auth/auth_switch_link.dart +++ b/app/lib/widgets/auth/auth_switch_link.dart @@ -11,12 +11,7 @@ class AuthSwitchLink extends StatelessWidget { final VoidCallback onPressed; - const AuthSwitchLink({ - super.key, - required this.promptText, - required this.actionText, - required this.onPressed, - }); + const AuthSwitchLink({super.key, required this.promptText, required this.actionText, required this.onPressed}); @override Widget build(BuildContext context) { @@ -25,19 +20,12 @@ class AuthSwitchLink extends StatelessWidget { return Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - Text( - promptText, - style: theme.textTheme.bodyMedium?.copyWith( - color: theme.colorScheme.onSurfaceVariant, - ), - ), + Text(promptText, style: theme.textTheme.bodyMedium?.copyWith(color: theme.colorScheme.onSurfaceVariant)), TextButton( onPressed: onPressed, style: TextButton.styleFrom( foregroundColor: theme.colorScheme.primary, - textStyle: theme.textTheme.bodyMedium?.copyWith( - fontWeight: FontWeight.w600, - ), + textStyle: theme.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w600), ), child: Text(actionText), ), diff --git a/app/lib/widgets/auth/curved_bottom_clipper.dart b/app/lib/widgets/auth/curved_bottom_clipper.dart index 19e4e23..0b9aef6 100644 --- a/app/lib/widgets/auth/curved_bottom_clipper.dart +++ b/app/lib/widgets/auth/curved_bottom_clipper.dart @@ -54,20 +54,10 @@ class WaveBottomClipper extends CustomClipper { path.lineTo(0, size.height - waveHeight); // First curve (left to center) - path.quadraticBezierTo( - size.width * 0.25, - size.height, - size.width * 0.5, - size.height - waveHeight, - ); + path.quadraticBezierTo(size.width * 0.25, size.height, size.width * 0.5, size.height - waveHeight); // Second curve (center to right) - path.quadraticBezierTo( - size.width * 0.75, - size.height - waveHeight * 2, - size.width, - size.height - waveHeight, - ); + path.quadraticBezierTo(size.width * 0.75, size.height - waveHeight * 2, size.width, size.height - waveHeight); path.lineTo(size.width, 0); path.close(); diff --git a/app/lib/widgets/book/book.dart b/app/lib/widgets/book/book.dart index 143b273..b4a7925 100644 --- a/app/lib/widgets/book/book.dart +++ b/app/lib/widgets/book/book.dart @@ -22,20 +22,13 @@ class _BookState extends State with SingleTickerProviderStateMixin { void initState() { super.initState(); isFinished = widget.data.isFinished; - animationController = AnimationController( - duration: const Duration(milliseconds: 250), - vsync: this, - ); + animationController = AnimationController(duration: const Duration(milliseconds: 250), vsync: this); - animation = ColorTween( - begin: Colors.transparent, - end: Colors.green[500], - ).animate(animationController)..addListener(() => setState(() {})); + animation = ColorTween(begin: Colors.transparent, end: Colors.green[500]).animate(animationController) + ..addListener(() => setState(() {})); - backgroundAnimation = ColorTween( - begin: Colors.transparent, - end: Colors.green[100], - ).animate(animationController)..addListener(() => setState(() {})); + backgroundAnimation = ColorTween(begin: Colors.transparent, end: Colors.green[100]).animate(animationController) + ..addListener(() => setState(() {})); } @override @@ -50,17 +43,12 @@ class _BookState extends State with SingleTickerProviderStateMixin { child: InkWell( borderRadius: BorderRadius.circular(8.0), onTap: () { - context.pushNamed( - 'BOOK_DETAILS', - pathParameters: {"bookId": widget.id}, - ); + context.pushNamed('BOOK_DETAILS', pathParameters: {"bookId": widget.id}); }, onLongPress: () { showModalBottomSheet( isScrollControlled: true, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(18.0)), - ), + shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(18.0))), context: context, builder: (context) { return Container( @@ -71,13 +59,7 @@ class _BookState extends State with SingleTickerProviderStateMixin { children: [ TextButton( onPressed: () {}, - child: const Row( - children: [ - Icon(Icons.info_outline), - SizedBox(width: 8), - Text("View details"), - ], - ), + child: const Row(children: [Icon(Icons.info_outline), SizedBox(width: 8), Text("View details")]), ), TextButton( onPressed: () { @@ -88,48 +70,26 @@ class _BookState extends State with SingleTickerProviderStateMixin { }, child: Row( children: [ - Icon( - !isFinished - ? Icons.check_box_outline_blank_rounded - : Icons.check_box_rounded, - ), + Icon(!isFinished ? Icons.check_box_outline_blank_rounded : Icons.check_box_rounded), const SizedBox(width: 8), - Text( - isFinished - ? "Mark as unfinished" - : "Mark as finished", - ), + Text(isFinished ? "Mark as unfinished" : "Mark as finished"), ], ), ), TextButton( onPressed: () {}, child: const Row( - children: [ - Icon(Icons.add_to_photos_rounded), - SizedBox(width: 8), - Text("Add to shelf"), - ], + children: [Icon(Icons.add_to_photos_rounded), SizedBox(width: 8), Text("Add to shelf")], ), ), TextButton( onPressed: () {}, - child: const Row( - children: [ - Icon(Icons.download_rounded), - SizedBox(width: 8), - Text("Export"), - ], - ), + child: const Row(children: [Icon(Icons.download_rounded), SizedBox(width: 8), Text("Export")]), ), TextButton( onPressed: () {}, child: const Row( - children: [ - Icon(Icons.delete_outline), - SizedBox(width: 8), - Text("Delete from library"), - ], + children: [Icon(Icons.delete_outline), SizedBox(width: 8), Text("Delete from library")], ), ), ], @@ -152,14 +112,8 @@ class _BookState extends State with SingleTickerProviderStateMixin { right: 0, bottom: 0, child: widget.data.coverURL != null - ? Image( - image: AssetImage(widget.data.coverURL!), - fit: BoxFit.cover, - ) - : Container( - color: Colors.grey[300], - child: const Icon(Icons.menu_book, size: 48), - ), + ? Image(image: AssetImage(widget.data.coverURL!), fit: BoxFit.cover) + : Container(color: Colors.grey[300], child: const Icon(Icons.menu_book, size: 48)), ), Positioned( right: 6, @@ -185,11 +139,7 @@ class _BookState extends State with SingleTickerProviderStateMixin { ), const SizedBox(height: 2), Text(widget.data.title, overflow: TextOverflow.ellipsis), - Text( - widget.data.author, - overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.labelSmall, - ), + Text(widget.data.author, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.labelSmall), ], ), ), diff --git a/app/lib/widgets/book/book_annotations.dart b/app/lib/widgets/book/book_annotations.dart index e16fb28..8af1069 100644 --- a/app/lib/widgets/book/book_annotations.dart +++ b/app/lib/widgets/book/book_annotations.dart @@ -121,10 +121,7 @@ class _BookAnnotationsState extends State { if (widget.annotations.isEmpty) { return SingleChildScrollView( - child: EmptyAnnotationsState( - isPhysical: widget.isPhysical, - onAddAnnotation: widget.onAddAnnotation, - ), + child: EmptyAnnotationsState(isPhysical: widget.isPhysical, onAddAnnotation: widget.onAddAnnotation), ); } @@ -144,12 +141,7 @@ class _BookAnnotationsState extends State { ? _buildNoResultsState(context, colorScheme) : _buildAnnotationsList( filtered, - padding: const EdgeInsets.fromLTRB( - Spacing.md, - Spacing.sm, - Spacing.md, - Spacing.md, - ), + padding: const EdgeInsets.fromLTRB(Spacing.md, Spacing.sm, Spacing.md, Spacing.md), separatorHeight: Spacing.md, showActionMenu: true, ), @@ -170,12 +162,7 @@ class _BookAnnotationsState extends State { ? _buildNoResultsState(context, colorScheme) : _buildAnnotationsList( filtered, - padding: const EdgeInsets.fromLTRB( - Spacing.md, - 0, - Spacing.md, - Spacing.md, - ), + padding: const EdgeInsets.fromLTRB(Spacing.md, 0, Spacing.md, Spacing.md), separatorHeight: Spacing.sm, showActionMenu: false, ), @@ -226,10 +213,7 @@ class _BookAnnotationsState extends State { ); } - PopupMenuItem<_AnnotationSort> _buildSortMenuItem( - _AnnotationSort option, - String label, - ) { + PopupMenuItem<_AnnotationSort> _buildSortMenuItem(_AnnotationSort option, String label) { return PopupMenuItem( value: option, child: Row( @@ -238,9 +222,7 @@ class _BookAnnotationsState extends State { Icon( Icons.check, size: IconSizes.small, - color: option == _sortOption - ? Theme.of(context).colorScheme.primary - : Colors.transparent, + color: option == _sortOption ? Theme.of(context).colorScheme.primary : Colors.transparent, ), ], ), @@ -274,32 +256,21 @@ class _BookAnnotationsState extends State { Widget _buildNoResultsState(BuildContext context, ColorScheme colorScheme) { return SingleChildScrollView( child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: Spacing.xl, - vertical: Spacing.xxl, - ), + padding: const EdgeInsets.symmetric(horizontal: Spacing.xl, vertical: Spacing.xxl), child: Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ - Icon( - Icons.search_off, - size: 48, - color: colorScheme.onSurfaceVariant.withValues(alpha: 0.5), - ), + Icon(Icons.search_off, size: 48, color: colorScheme.onSurfaceVariant.withValues(alpha: 0.5)), const SizedBox(height: Spacing.md), Text( 'No annotations found', - style: Theme.of(context).textTheme.titleMedium?.copyWith( - color: colorScheme.onSurfaceVariant, - ), + style: Theme.of(context).textTheme.titleMedium?.copyWith(color: colorScheme.onSurfaceVariant), ), const SizedBox(height: Spacing.xs), Text( 'Try a different search term', - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: colorScheme.onSurfaceVariant, - ), + style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: colorScheme.onSurfaceVariant), textAlign: TextAlign.center, ), ], diff --git a/app/lib/widgets/book/book_bookmarks.dart b/app/lib/widgets/book/book_bookmarks.dart index db998a8..618a9c8 100644 --- a/app/lib/widgets/book/book_bookmarks.dart +++ b/app/lib/widgets/book/book_bookmarks.dart @@ -89,10 +89,7 @@ class _BookBookmarksState extends State { if (widget.bookmarks.isEmpty) { return SingleChildScrollView( - child: EmptyBookmarksState( - isPhysical: widget.isPhysical, - onAddBookmark: widget.onAddBookmark, - ), + child: EmptyBookmarksState(isPhysical: widget.isPhysical, onAddBookmark: widget.onAddBookmark), ); } @@ -112,12 +109,7 @@ class _BookBookmarksState extends State { ? _buildNoResultsState(context, colorScheme) : _buildBookmarksList( filtered, - padding: const EdgeInsets.fromLTRB( - Spacing.md, - Spacing.sm, - Spacing.md, - Spacing.md, - ), + padding: const EdgeInsets.fromLTRB(Spacing.md, Spacing.sm, Spacing.md, Spacing.md), separatorHeight: Spacing.md, showActionMenu: true, ), @@ -166,12 +158,7 @@ class _BookBookmarksState extends State { ? _buildNoResultsState(context, colorScheme) : _buildBookmarksList( filtered, - padding: const EdgeInsets.fromLTRB( - Spacing.md, - 0, - Spacing.md, - Spacing.md, - ), + padding: const EdgeInsets.fromLTRB(Spacing.md, 0, Spacing.md, Spacing.md), separatorHeight: Spacing.sm, showActionMenu: false, ), @@ -213,10 +200,7 @@ class _BookBookmarksState extends State { ); } - PopupMenuItem<_BookmarkSort> _buildSortMenuItem( - _BookmarkSort option, - String label, - ) { + PopupMenuItem<_BookmarkSort> _buildSortMenuItem(_BookmarkSort option, String label) { return PopupMenuItem( value: option, child: Row( @@ -225,9 +209,7 @@ class _BookBookmarksState extends State { Icon( Icons.check, size: IconSizes.small, - color: option == _sortOption - ? Theme.of(context).colorScheme.primary - : Colors.transparent, + color: option == _sortOption ? Theme.of(context).colorScheme.primary : Colors.transparent, ), ], ), @@ -259,32 +241,21 @@ class _BookBookmarksState extends State { Widget _buildNoResultsState(BuildContext context, ColorScheme colorScheme) { return SingleChildScrollView( child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: Spacing.xl, - vertical: Spacing.xxl, - ), + padding: const EdgeInsets.symmetric(horizontal: Spacing.xl, vertical: Spacing.xxl), child: Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ - Icon( - Icons.search_off, - size: 48, - color: colorScheme.onSurfaceVariant.withValues(alpha: 0.5), - ), + Icon(Icons.search_off, size: 48, color: colorScheme.onSurfaceVariant.withValues(alpha: 0.5)), const SizedBox(height: Spacing.md), Text( 'No bookmarks found', - style: Theme.of(context).textTheme.titleMedium?.copyWith( - color: colorScheme.onSurfaceVariant, - ), + style: Theme.of(context).textTheme.titleMedium?.copyWith(color: colorScheme.onSurfaceVariant), ), const SizedBox(height: Spacing.xs), Text( 'Try a different search term', - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: colorScheme.onSurfaceVariant, - ), + style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: colorScheme.onSurfaceVariant), textAlign: TextAlign.center, ), ], diff --git a/app/lib/widgets/book/book_details.dart b/app/lib/widgets/book/book_details.dart index f4c6d6a..16e97c5 100644 --- a/app/lib/widgets/book/book_details.dart +++ b/app/lib/widgets/book/book_details.dart @@ -15,12 +15,7 @@ class BookDetails extends StatefulWidget { final bool isDescriptionExpanded; final VoidCallback? onToggleDescription; - const BookDetails({ - super.key, - required this.book, - this.isDescriptionExpanded = false, - this.onToggleDescription, - }); + const BookDetails({super.key, required this.book, this.isDescriptionExpanded = false, this.onToggleDescription}); @override State createState() => _BookDetailsState(); @@ -114,12 +109,7 @@ class _BookDetailsState extends State { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - title, - style: Theme.of( - context, - ).textTheme.titleSmall?.copyWith(fontWeight: FontWeight.bold), - ), + Text(title, style: Theme.of(context).textTheme.titleSmall?.copyWith(fontWeight: FontWeight.bold)), const SizedBox(height: 4), Divider(height: 1, thickness: 1, color: colorScheme.outlineVariant), ], @@ -133,10 +123,9 @@ class _BookDetailsState extends State { if (description.isEmpty) { return Text( 'No description available.', - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - fontStyle: FontStyle.italic, - color: colorScheme.onSurfaceVariant, - ), + style: Theme.of( + context, + ).textTheme.bodyMedium?.copyWith(fontStyle: FontStyle.italic, color: colorScheme.onSurfaceVariant), ); } @@ -158,10 +147,9 @@ class _BookDetailsState extends State { onTap: widget.onToggleDescription, child: Text( widget.isDescriptionExpanded ? 'Show less' : 'Read more', - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: colorScheme.primary, - fontWeight: FontWeight.w600, - ), + style: Theme.of( + context, + ).textTheme.bodyMedium?.copyWith(color: colorScheme.primary, fontWeight: FontWeight.w600), ), ), ], @@ -210,10 +198,7 @@ class _BookDetailsState extends State { avatar: Container( width: 8, height: 8, - decoration: BoxDecoration( - color: tag.color, - shape: BoxShape.circle, - ), + decoration: BoxDecoration(color: tag.color, shape: BoxShape.circle), ), label: Text(tag.name), visualDensity: VisualDensity.compact, @@ -234,9 +219,7 @@ class _BookDetailsState extends State { void _showMoveToShelfSheet(BuildContext context) { final dataStore = context.read(); - final currentShelfIds = dataStore - .getShelfIdsForBook(widget.book.id) - .toSet(); + final currentShelfIds = dataStore.getShelfIdsForBook(widget.book.id).toSet(); MoveToShelfSheet.show( context, diff --git a/app/lib/widgets/book/book_notes.dart b/app/lib/widgets/book/book_notes.dart index db4bd4b..7498b78 100644 --- a/app/lib/widgets/book/book_notes.dart +++ b/app/lib/widgets/book/book_notes.dart @@ -49,13 +49,7 @@ class BookNotes extends StatefulWidget { final Function(Note)? onNoteActions; /// Creates a notes tab widget. - const BookNotes({ - super.key, - required this.notes, - this.onAddNote, - this.onNoteTap, - this.onNoteActions, - }); + const BookNotes({super.key, required this.notes, this.onAddNote, this.onNoteTap, this.onNoteActions}); @override State createState() => _BookNotesState(); @@ -115,9 +109,7 @@ class _BookNotesState extends State { final isDesktop = screenWidth >= Breakpoints.desktopSmall; if (widget.notes.isEmpty) { - return SingleChildScrollView( - child: EmptyNotesState(onAddNote: widget.onAddNote), - ); + return SingleChildScrollView(child: EmptyNotesState(onAddNote: widget.onAddNote)); } if (isDesktop) return _buildDesktopLayout(context); @@ -136,12 +128,7 @@ class _BookNotesState extends State { ? _buildNoResultsState(context, colorScheme) : _buildNotesList( filtered, - padding: const EdgeInsets.fromLTRB( - Spacing.md, - Spacing.sm, - Spacing.md, - Spacing.md, - ), + padding: const EdgeInsets.fromLTRB(Spacing.md, Spacing.sm, Spacing.md, Spacing.md), separatorHeight: Spacing.md, showActionMenu: true, ), @@ -166,11 +153,7 @@ class _BookNotesState extends State { const SizedBox(width: Spacing.sm), _buildSortButton(), const SizedBox(width: Spacing.md), - FilledButton.icon( - onPressed: widget.onAddNote, - icon: const Icon(Icons.add), - label: const Text('Add note'), - ), + FilledButton.icon(onPressed: widget.onAddNote, icon: const Icon(Icons.add), label: const Text('Add note')), ], ), ); @@ -188,12 +171,7 @@ class _BookNotesState extends State { ? _buildNoResultsState(context, colorScheme) : _buildNotesList( filtered, - padding: const EdgeInsets.fromLTRB( - Spacing.md, - 0, - Spacing.md, - Spacing.md, - ), + padding: const EdgeInsets.fromLTRB(Spacing.md, 0, Spacing.md, Spacing.md), separatorHeight: Spacing.sm, showActionMenu: false, ), @@ -244,9 +222,7 @@ class _BookNotesState extends State { Icon( Icons.check, size: IconSizes.small, - color: option == _sortOption - ? Theme.of(context).colorScheme.primary - : Colors.transparent, + color: option == _sortOption ? Theme.of(context).colorScheme.primary : Colors.transparent, ), ], ), @@ -280,32 +256,21 @@ class _BookNotesState extends State { Widget _buildNoResultsState(BuildContext context, ColorScheme colorScheme) { return SingleChildScrollView( child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: Spacing.xl, - vertical: Spacing.xxl, - ), + padding: const EdgeInsets.symmetric(horizontal: Spacing.xl, vertical: Spacing.xxl), child: Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ - Icon( - Icons.search_off, - size: 48, - color: colorScheme.onSurfaceVariant.withValues(alpha: 0.5), - ), + Icon(Icons.search_off, size: 48, color: colorScheme.onSurfaceVariant.withValues(alpha: 0.5)), const SizedBox(height: Spacing.md), Text( 'No notes found', - style: Theme.of(context).textTheme.titleMedium?.copyWith( - color: colorScheme.onSurfaceVariant, - ), + style: Theme.of(context).textTheme.titleMedium?.copyWith(color: colorScheme.onSurfaceVariant), ), const SizedBox(height: Spacing.xs), Text( 'Try a different search term', - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: colorScheme.onSurfaceVariant, - ), + style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: colorScheme.onSurfaceVariant), textAlign: TextAlign.center, ), ], diff --git a/app/lib/widgets/book_details/annotation_action_sheet.dart b/app/lib/widgets/book_details/annotation_action_sheet.dart index 4efd7a5..053c611 100644 --- a/app/lib/widgets/book_details/annotation_action_sheet.dart +++ b/app/lib/widgets/book_details/annotation_action_sheet.dart @@ -12,10 +12,7 @@ class AnnotationActionSheet extends StatelessWidget { const AnnotationActionSheet({super.key, required this.annotation}); /// Shows the action sheet and returns the selected action. - static Future show( - BuildContext context, { - required Annotation annotation, - }) async { + static Future show(BuildContext context, {required Annotation annotation}) async { return showModalBottomSheet( context: context, builder: (context) => AnnotationActionSheet(annotation: annotation), @@ -36,10 +33,7 @@ class AnnotationActionSheet extends StatelessWidget { Container( width: 40, height: 4, - decoration: BoxDecoration( - color: colorScheme.outlineVariant, - borderRadius: BorderRadius.circular(2), - ), + decoration: BoxDecoration(color: colorScheme.outlineVariant, borderRadius: BorderRadius.circular(2)), ), const SizedBox(height: Spacing.md), @@ -48,9 +42,7 @@ class AnnotationActionSheet extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: Spacing.lg), child: Text( annotation.location.shortLocation, - style: Theme.of( - context, - ).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600), + style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600), maxLines: 1, overflow: TextOverflow.ellipsis, ), @@ -68,10 +60,7 @@ class AnnotationActionSheet extends StatelessWidget { // Delete action ListTile( leading: Icon(Icons.delete_outline, color: colorScheme.error), - title: Text( - 'Delete annotation', - style: TextStyle(color: colorScheme.error), - ), + title: Text('Delete annotation', style: TextStyle(color: colorScheme.error)), onTap: () => Navigator.of(context).pop(AnnotationAction.delete), ), diff --git a/app/lib/widgets/book_details/annotation_card.dart b/app/lib/widgets/book_details/annotation_card.dart index f5fc30d..a0835c0 100644 --- a/app/lib/widgets/book_details/annotation_card.dart +++ b/app/lib/widgets/book_details/annotation_card.dart @@ -49,13 +49,7 @@ class AnnotationCard extends StatefulWidget { final bool showActionMenu; /// Creates an annotation card widget. - const AnnotationCard({ - super.key, - required this.annotation, - this.onTap, - this.onLongPress, - this.showActionMenu = true, - }); + const AnnotationCard({super.key, required this.annotation, this.onTap, this.onLongPress, this.showActionMenu = true}); @override State createState() => _AnnotationCardState(); @@ -96,8 +90,7 @@ class _AnnotationCardState extends State { _buildHeader(context, colorScheme, textTheme, accentColor), const SizedBox(height: Spacing.sm), _buildHighlightText(textTheme), - if (widget.annotation.hasNote) - _buildNote(colorScheme, textTheme), + if (widget.annotation.hasNote) _buildNote(colorScheme, textTheme), const SizedBox(height: Spacing.sm), _buildDate(colorScheme, textTheme), ], @@ -110,21 +103,14 @@ class _AnnotationCardState extends State { } /// Header row with color indicator, location, and action menu. - Widget _buildHeader( - BuildContext context, - ColorScheme colorScheme, - TextTheme textTheme, - Color accentColor, - ) { + Widget _buildHeader(BuildContext context, ColorScheme colorScheme, TextTheme textTheme, Color accentColor) { return Row( children: [ _buildColorDot(accentColor), const SizedBox(width: Spacing.sm), Text( widget.annotation.location.shortLocation, - style: textTheme.labelSmall?.copyWith( - color: colorScheme.onSurfaceVariant, - ), + style: textTheme.labelSmall?.copyWith(color: colorScheme.onSurfaceVariant), ), const Spacer(), if (widget.showActionMenu) _buildActionMenu(colorScheme), @@ -184,28 +170,13 @@ class _AnnotationCardState extends State { Widget _buildDate(ColorScheme colorScheme, TextTheme textTheme) { return Text( _formatDate(widget.annotation.createdAt), - style: textTheme.labelSmall?.copyWith( - color: colorScheme.onSurfaceVariant, - ), + style: textTheme.labelSmall?.copyWith(color: colorScheme.onSurfaceVariant), ); } /// Formats a date as "Mon DD, YYYY". String _formatDate(DateTime date) { - const months = [ - 'Jan', - 'Feb', - 'Mar', - 'Apr', - 'May', - 'Jun', - 'Jul', - 'Aug', - 'Sep', - 'Oct', - 'Nov', - 'Dec', - ]; + const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; return '${months[date.month - 1]} ${date.day}, ${date.year}'; } } diff --git a/app/lib/widgets/book_details/annotation_dialog.dart b/app/lib/widgets/book_details/annotation_dialog.dart index 8abd089..de2a967 100644 --- a/app/lib/widgets/book_details/annotation_dialog.dart +++ b/app/lib/widgets/book_details/annotation_dialog.dart @@ -10,26 +10,15 @@ class AnnotationDialog extends StatefulWidget { final String bookId; final Annotation? existingAnnotation; - const AnnotationDialog({ - super.key, - required this.bookId, - this.existingAnnotation, - }); + const AnnotationDialog({super.key, required this.bookId, this.existingAnnotation}); /// Shows the dialog and returns the created/updated annotation, or null if cancelled. - static Future show( - BuildContext context, { - required String bookId, - Annotation? existingAnnotation, - }) { + static Future show(BuildContext context, {required String bookId, Annotation? existingAnnotation}) { return showModalBottomSheet( context: context, isScrollControlled: true, useSafeArea: true, - builder: (context) => AnnotationDialog( - bookId: bookId, - existingAnnotation: existingAnnotation, - ), + builder: (context) => AnnotationDialog(bookId: bookId, existingAnnotation: existingAnnotation), ); } @@ -50,18 +39,10 @@ class _AnnotationDialogState extends State { @override void initState() { super.initState(); - _textController = TextEditingController( - text: widget.existingAnnotation?.selectedText ?? '', - ); - _pageController = TextEditingController( - text: widget.existingAnnotation?.location.pageNumber.toString() ?? '', - ); - _chapterController = TextEditingController( - text: widget.existingAnnotation?.location.chapterTitle ?? '', - ); - _noteController = TextEditingController( - text: widget.existingAnnotation?.note ?? '', - ); + _textController = TextEditingController(text: widget.existingAnnotation?.selectedText ?? ''); + _pageController = TextEditingController(text: widget.existingAnnotation?.location.pageNumber.toString() ?? ''); + _chapterController = TextEditingController(text: widget.existingAnnotation?.location.chapterTitle ?? ''); + _noteController = TextEditingController(text: widget.existingAnnotation?.note ?? ''); _selectedColor = widget.existingAnnotation?.color ?? HighlightColor.yellow; } @@ -82,16 +63,11 @@ class _AnnotationDialogState extends State { final note = _noteController.text.trim(); final annotation = Annotation( - id: - widget.existingAnnotation?.id ?? - DateTime.now().millisecondsSinceEpoch.toString(), + id: widget.existingAnnotation?.id ?? DateTime.now().millisecondsSinceEpoch.toString(), bookId: widget.bookId, selectedText: _textController.text.trim(), color: _selectedColor, - location: BookLocation( - pageNumber: page, - chapterTitle: chapter.isNotEmpty ? chapter : null, - ), + location: BookLocation(pageNumber: page, chapterTitle: chapter.isNotEmpty ? chapter : null), note: note.isNotEmpty ? note : null, createdAt: widget.existingAnnotation?.createdAt ?? DateTime.now(), updatedAt: _isEditing ? DateTime.now() : null, @@ -115,27 +91,18 @@ class _AnnotationDialogState extends State { return Container( decoration: BoxDecoration( color: colorScheme.surface, - borderRadius: const BorderRadius.vertical( - top: Radius.circular(AppRadius.lg), - ), + borderRadius: const BorderRadius.vertical(top: Radius.circular(AppRadius.lg)), ), child: Column( children: [ Padding( - padding: const EdgeInsets.fromLTRB( - Spacing.md, - Spacing.md, - Spacing.md, - 0, - ), + padding: const EdgeInsets.fromLTRB(Spacing.md, Spacing.md, Spacing.md, 0), child: Column( children: [ const BottomSheetHandle(), const SizedBox(height: Spacing.md), BottomSheetHeader( - title: _isEditing - ? 'Edit annotation' - : 'New annotation', + title: _isEditing ? 'Edit annotation' : 'New annotation', onCancel: () => Navigator.of(context).pop(), onSave: _save, ), @@ -178,13 +145,8 @@ class _AnnotationDialogState extends State { TextFormField( controller: _pageController, keyboardType: TextInputType.number, - inputFormatters: [ - FilteringTextInputFormatter.digitsOnly, - ], - decoration: const InputDecoration( - labelText: 'Page number', - border: OutlineInputBorder(), - ), + inputFormatters: [FilteringTextInputFormatter.digitsOnly], + decoration: const InputDecoration(labelText: 'Page number', border: OutlineInputBorder()), validator: (value) { if (value == null || value.trim().isEmpty) { return 'Please enter a page number'; @@ -225,37 +187,24 @@ class _AnnotationDialogState extends State { const SizedBox(height: Spacing.md), // Highlight color - Text( - 'Highlight color', - style: Theme.of(context).textTheme.titleSmall, - ), + Text('Highlight color', style: Theme.of(context).textTheme.titleSmall), const SizedBox(height: Spacing.sm), Wrap( spacing: Spacing.sm, children: HighlightColor.values.map((color) { final isSelected = color == _selectedColor; return GestureDetector( - onTap: () => - setState(() => _selectedColor = color), + onTap: () => setState(() => _selectedColor = color), child: Container( width: 36, height: 36, decoration: BoxDecoration( color: color.color, shape: BoxShape.circle, - border: isSelected - ? Border.all( - color: color.accentColor, - width: 2, - ) - : null, + border: isSelected ? Border.all(color: color.accentColor, width: 2) : null, ), child: isSelected - ? Icon( - Icons.check, - color: color.accentColor, - size: IconSizes.small, - ) + ? Icon(Icons.check, color: color.accentColor, size: IconSizes.small) : null, ), ); diff --git a/app/lib/widgets/book_details/book_action_buttons.dart b/app/lib/widgets/book_details/book_action_buttons.dart index 7a2ebc1..d351505 100644 --- a/app/lib/widgets/book_details/book_action_buttons.dart +++ b/app/lib/widgets/book_details/book_action_buttons.dart @@ -25,9 +25,7 @@ class BookActionButtons extends StatelessWidget { @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; - final buttonHeight = isDesktop - ? ComponentSizes.buttonHeightDesktop - : ComponentSizes.buttonHeightMobile; + final buttonHeight = isDesktop ? ComponentSizes.buttonHeightDesktop : ComponentSizes.buttonHeightMobile; return Row( mainAxisSize: isDesktop ? MainAxisSize.min : MainAxisSize.max, @@ -45,12 +43,8 @@ class BookActionButtons extends StatelessWidget { ) : FilledButton.icon( onPressed: onContinueReading, - icon: Icon( - book.progress > 0 ? Icons.play_arrow : Icons.menu_book, - ), - label: Text( - book.progress > 0 ? 'Continue' : 'Start reading', - ), + icon: Icon(book.progress > 0 ? Icons.play_arrow : Icons.menu_book), + label: Text(book.progress > 0 ? 'Continue' : 'Start reading'), ), ) else @@ -66,9 +60,7 @@ class BookActionButtons extends StatelessWidget { ) : FilledButton.icon( onPressed: onContinueReading, - icon: Icon( - book.progress > 0 ? Icons.play_arrow : Icons.menu_book, - ), + icon: Icon(book.progress > 0 ? Icons.play_arrow : Icons.menu_book), label: Text(book.progress > 0 ? 'Continue' : 'Read'), ), ), @@ -83,9 +75,7 @@ class BookActionButtons extends StatelessWidget { onPressed: onToggleFavorite, style: OutlinedButton.styleFrom( padding: EdgeInsets.zero, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(AppRadius.button), - ), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(AppRadius.button)), ), child: Icon( book.isFavorite ? Icons.favorite : Icons.favorite_border, @@ -103,9 +93,7 @@ class BookActionButtons extends StatelessWidget { onPressed: onEdit, style: OutlinedButton.styleFrom( padding: EdgeInsets.zero, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(AppRadius.button), - ), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(AppRadius.button)), ), child: Icon(Icons.edit_outlined, color: colorScheme.primary), ), diff --git a/app/lib/widgets/book_details/book_cover_image.dart b/app/lib/widgets/book_details/book_cover_image.dart index c2ad5a3..54aa71f 100644 --- a/app/lib/widgets/book_details/book_cover_image.dart +++ b/app/lib/widgets/book_details/book_cover_image.dart @@ -23,12 +23,7 @@ class BookCoverImage extends StatelessWidget { final String? bookTitle; final BookCoverSize size; - const BookCoverImage({ - super.key, - this.imageUrl, - this.bookTitle, - this.size = BookCoverSize.medium, - }); + const BookCoverImage({super.key, this.imageUrl, this.bookTitle, this.size = BookCoverSize.medium}); @override Widget build(BuildContext context) { @@ -41,17 +36,10 @@ class BookCoverImage extends StatelessWidget { decoration: BoxDecoration( borderRadius: BorderRadius.circular(AppRadius.lg), boxShadow: [ - BoxShadow( - color: colorScheme.shadow.withValues(alpha: 0.15), - blurRadius: 8, - offset: const Offset(0, 4), - ), + BoxShadow(color: colorScheme.shadow.withValues(alpha: 0.15), blurRadius: 8, offset: const Offset(0, 4)), ], ), - child: ClipRRect( - borderRadius: BorderRadius.circular(AppRadius.lg), - child: _buildImage(context, colorScheme), - ), + child: ClipRRect(borderRadius: BorderRadius.circular(AppRadius.lg), child: _buildImage(context, colorScheme)), ); } @@ -60,10 +48,8 @@ class BookCoverImage extends StatelessWidget { return CachedNetworkImage( imageUrl: imageUrl!, fit: BoxFit.cover, - errorWidget: (context, url, error) => - _buildPlaceholder(context, colorScheme), - progressIndicatorBuilder: (context, url, progress) => - _buildLoadingIndicator(context, colorScheme, progress), + errorWidget: (context, url, error) => _buildPlaceholder(context, colorScheme), + progressIndicatorBuilder: (context, url, progress) => _buildLoadingIndicator(context, colorScheme, progress), ); } return _buildPlaceholder(context, colorScheme); @@ -89,9 +75,9 @@ class BookCoverImage extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: Spacing.sm), child: Text( bookTitle!, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: colorScheme.onSurfaceVariant.withValues(alpha: 0.7), - ), + style: Theme.of( + context, + ).textTheme.bodySmall?.copyWith(color: colorScheme.onSurfaceVariant.withValues(alpha: 0.7)), textAlign: TextAlign.center, maxLines: 2, overflow: TextOverflow.ellipsis, @@ -103,19 +89,10 @@ class BookCoverImage extends StatelessWidget { ); } - Widget _buildLoadingIndicator( - BuildContext context, - ColorScheme colorScheme, - DownloadProgress progress, - ) { + Widget _buildLoadingIndicator(BuildContext context, ColorScheme colorScheme, DownloadProgress progress) { return Container( color: colorScheme.surfaceContainerHighest, - child: Center( - child: CircularProgressIndicator( - value: progress.progress, - strokeWidth: 2, - ), - ), + child: Center(child: CircularProgressIndicator(value: progress.progress, strokeWidth: 2)), ); } diff --git a/app/lib/widgets/book_details/book_header.dart b/app/lib/widgets/book_details/book_header.dart index b427096..e94b8a3 100644 --- a/app/lib/widgets/book_details/book_header.dart +++ b/app/lib/widgets/book_details/book_header.dart @@ -40,11 +40,7 @@ class BookHeader extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ // Cover image - BookCoverImage( - imageUrl: book.coverURL, - bookTitle: book.title, - size: BookCoverSize.large, - ), + BookCoverImage(imageUrl: book.coverURL, bookTitle: book.title, size: BookCoverSize.large), const SizedBox(width: Spacing.xl), // Book info @@ -53,12 +49,7 @@ class BookHeader extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ // Title - Text( - book.title, - style: Theme.of( - context, - ).textTheme.displaySmall?.copyWith(fontWeight: FontWeight.bold), - ), + Text(book.title, style: Theme.of(context).textTheme.displaySmall?.copyWith(fontWeight: FontWeight.bold)), if (book.subtitle != null && book.subtitle!.isNotEmpty) ...[ const SizedBox(height: Spacing.xs), Text( @@ -75,9 +66,7 @@ class BookHeader extends StatelessWidget { // Author Text( book.allAuthors, - style: Theme.of( - context, - ).textTheme.titleMedium?.copyWith(color: colorScheme.onSurface), + style: Theme.of(context).textTheme.titleMedium?.copyWith(color: colorScheme.onSurface), ), const SizedBox(height: Spacing.md), @@ -122,29 +111,22 @@ class BookHeader extends StatelessWidget { const SizedBox(height: Spacing.lg), // Cover image (centered) - BookCoverImage( - imageUrl: book.coverURL, - bookTitle: book.title, - size: BookCoverSize.medium, - ), + BookCoverImage(imageUrl: book.coverURL, bookTitle: book.title, size: BookCoverSize.medium), const SizedBox(height: Spacing.md), // Title (centered) Text( book.title, - style: Theme.of( - context, - ).textTheme.headlineMedium?.copyWith(fontWeight: FontWeight.bold), + style: Theme.of(context).textTheme.headlineMedium?.copyWith(fontWeight: FontWeight.bold), textAlign: TextAlign.center, ), if (book.subtitle != null && book.subtitle!.isNotEmpty) ...[ const SizedBox(height: Spacing.xs), Text( book.subtitle!, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: colorScheme.onSurfaceVariant, - fontStyle: FontStyle.italic, - ), + style: Theme.of( + context, + ).textTheme.bodyMedium?.copyWith(color: colorScheme.onSurfaceVariant, fontStyle: FontStyle.italic), textAlign: TextAlign.center, ), ], @@ -153,9 +135,7 @@ class BookHeader extends StatelessWidget { // Author (centered) Text( book.allAuthors, - style: Theme.of( - context, - ).textTheme.titleSmall?.copyWith(color: colorScheme.onSurface), + style: Theme.of(context).textTheme.titleSmall?.copyWith(color: colorScheme.onSurface), textAlign: TextAlign.center, ), const SizedBox(height: Spacing.md), @@ -199,19 +179,14 @@ class BookHeader extends StatelessWidget { children: [ // Format badge Container( - padding: const EdgeInsets.symmetric( - horizontal: Spacing.sm, - vertical: 2, - ), + padding: const EdgeInsets.symmetric(horizontal: Spacing.sm, vertical: 2), decoration: BoxDecoration( color: colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(AppRadius.sm), ), child: Text( book.formatLabel, - style: Theme.of( - context, - ).textTheme.labelSmall?.copyWith(fontWeight: FontWeight.w600), + style: Theme.of(context).textTheme.labelSmall?.copyWith(fontWeight: FontWeight.w600), ), ), // Topics (first 2) @@ -222,9 +197,7 @@ class BookHeader extends StatelessWidget { label: Text(topic), visualDensity: VisualDensity.compact, padding: EdgeInsets.zero, - labelPadding: const EdgeInsets.symmetric( - horizontal: Spacing.sm, - ), + labelPadding: const EdgeInsets.symmetric(horizontal: Spacing.sm), ), ), ], @@ -238,10 +211,7 @@ class BookHeader extends StatelessWidget { Text(book.formatLabel, style: Theme.of(context).textTheme.bodyMedium), if (book.totalPages != null) ...[ Text(' • ', style: TextStyle(color: colorScheme.onSurfaceVariant)), - Text( - '${book.totalPages} pages', - style: Theme.of(context).textTheme.bodyMedium, - ), + Text('${book.totalPages} pages', style: Theme.of(context).textTheme.bodyMedium), ], ], ); diff --git a/app/lib/widgets/book_details/book_info_grid.dart b/app/lib/widgets/book_details/book_info_grid.dart index 48648f3..65b2819 100644 --- a/app/lib/widgets/book_details/book_info_grid.dart +++ b/app/lib/widgets/book_details/book_info_grid.dart @@ -27,17 +27,10 @@ class BookInfoGrid extends StatelessWidget { width: 100, child: Text( entry.label, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), - ), - Expanded( - child: Text( - entry.value, - style: Theme.of(context).textTheme.bodyMedium, + style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: colorScheme.onSurfaceVariant), ), ), + Expanded(child: Text(entry.value, style: Theme.of(context).textTheme.bodyMedium)), ], ), ); @@ -59,12 +52,7 @@ class BookInfoGrid extends StatelessWidget { } if (book.publicationDate != null) { - entries.add( - _InfoEntry( - 'Published', - DateFormat.yMMMMd().format(book.publicationDate!), - ), - ); + entries.add(_InfoEntry('Published', DateFormat.yMMMMd().format(book.publicationDate!))); } if (book.isbn13 != null && book.isbn13!.isNotEmpty) { @@ -84,19 +72,12 @@ class BookInfoGrid extends StatelessWidget { : book.seriesName!; entries.add(_InfoEntry('Series', seriesValue)); } else if (book.seriesNumber != null) { - entries.add( - _InfoEntry('Series', '#${_formatSeriesNumber(book.seriesNumber!)}'), - ); + entries.add(_InfoEntry('Series', '#${_formatSeriesNumber(book.seriesNumber!)}')); } // Rating if (book.rating != null) { - entries.add( - _InfoEntry( - 'Rating', - '${'★' * book.rating!}${'☆' * (5 - book.rating!)}', - ), - ); + entries.add(_InfoEntry('Rating', '${'★' * book.rating!}${'☆' * (5 - book.rating!)}')); } // Reading status @@ -105,9 +86,7 @@ class BookInfoGrid extends StatelessWidget { } // Physical location - if (book.isPhysical && - book.physicalLocation != null && - book.physicalLocation!.isNotEmpty) { + if (book.isPhysical && book.physicalLocation != null && book.physicalLocation!.isNotEmpty) { entries.add(_InfoEntry('Location', book.physicalLocation!)); } @@ -124,9 +103,7 @@ class BookInfoGrid extends StatelessWidget { /// Format series number: show as integer if whole, otherwise as decimal. static String _formatSeriesNumber(double number) { - return number == number.roundToDouble() - ? number.toInt().toString() - : number.toString(); + return number == number.roundToDouble() ? number.toInt().toString() : number.toString(); } } diff --git a/app/lib/widgets/book_details/book_progress_bar.dart b/app/lib/widgets/book_details/book_progress_bar.dart index 64f4197..63a111d 100644 --- a/app/lib/widgets/book_details/book_progress_bar.dart +++ b/app/lib/widgets/book_details/book_progress_bar.dart @@ -38,9 +38,7 @@ class BookProgressBar extends StatelessWidget { child: LinearProgressIndicator( value: progress, backgroundColor: colorScheme.surfaceContainerHighest, - color: progress >= 1.0 - ? colorScheme.tertiary - : colorScheme.primary, + color: progress >= 1.0 ? colorScheme.tertiary : colorScheme.primary, minHeight: barHeight, ), ), @@ -49,9 +47,7 @@ class BookProgressBar extends StatelessWidget { const SizedBox(width: Spacing.sm), Text( _getProgressLabel(), - style: Theme.of(context).textTheme.labelSmall?.copyWith( - color: colorScheme.onSurfaceVariant, - ), + style: Theme.of(context).textTheme.labelSmall?.copyWith(color: colorScheme.onSurfaceVariant), ), ], ], diff --git a/app/lib/widgets/book_details/bookmark_dialog.dart b/app/lib/widgets/book_details/bookmark_dialog.dart index a604b48..82910f2 100644 --- a/app/lib/widgets/book_details/bookmark_dialog.dart +++ b/app/lib/widgets/book_details/bookmark_dialog.dart @@ -11,12 +11,7 @@ class BookmarkDialog extends StatefulWidget { final int? pageCount; final Bookmark? existingBookmark; - const BookmarkDialog({ - super.key, - required this.bookId, - this.pageCount, - this.existingBookmark, - }); + const BookmarkDialog({super.key, required this.bookId, this.pageCount, this.existingBookmark}); /// Shows the dialog and returns the created/updated bookmark, or null if cancelled. static Future show( @@ -29,15 +24,9 @@ class BookmarkDialog extends StatefulWidget { context: context, isScrollControlled: true, shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical( - top: Radius.circular(AppRadius.bottomSheet), - ), - ), - builder: (context) => BookmarkDialog( - bookId: bookId, - pageCount: pageCount, - existingBookmark: existingBookmark, + borderRadius: BorderRadius.vertical(top: Radius.circular(AppRadius.bottomSheet)), ), + builder: (context) => BookmarkDialog(bookId: bookId, pageCount: pageCount, existingBookmark: existingBookmark), ); } @@ -57,17 +46,10 @@ class _BookmarkDialogState extends State { @override void initState() { super.initState(); - _pageController = TextEditingController( - text: widget.existingBookmark?.pageNumber?.toString() ?? '', - ); - _chapterController = TextEditingController( - text: widget.existingBookmark?.chapterTitle ?? '', - ); - _noteController = TextEditingController( - text: widget.existingBookmark?.note ?? '', - ); - _selectedColor = - widget.existingBookmark?.colorHex ?? Bookmark.availableColors.first; + _pageController = TextEditingController(text: widget.existingBookmark?.pageNumber?.toString() ?? ''); + _chapterController = TextEditingController(text: widget.existingBookmark?.chapterTitle ?? ''); + _noteController = TextEditingController(text: widget.existingBookmark?.note ?? ''); + _selectedColor = widget.existingBookmark?.colorHex ?? Bookmark.availableColors.first; } @override @@ -82,16 +64,12 @@ class _BookmarkDialogState extends State { if (!(_formKey.currentState?.validate() ?? false)) return; final page = int.parse(_pageController.text); - final position = widget.pageCount != null - ? (page / widget.pageCount!).clamp(0.0, 1.0) - : 0.0; + final position = widget.pageCount != null ? (page / widget.pageCount!).clamp(0.0, 1.0) : 0.0; final chapter = _chapterController.text.trim(); final note = _noteController.text.trim(); final bookmark = Bookmark( - id: - widget.existingBookmark?.id ?? - DateTime.now().millisecondsSinceEpoch.toString(), + id: widget.existingBookmark?.id ?? DateTime.now().millisecondsSinceEpoch.toString(), bookId: widget.bookId, position: position, pageNumber: page, @@ -139,9 +117,7 @@ class _BookmarkDialogState extends State { decoration: InputDecoration( labelText: 'Page number', border: const OutlineInputBorder(), - suffixText: widget.pageCount != null - ? 'of ${widget.pageCount}' - : null, + suffixText: widget.pageCount != null ? 'of ${widget.pageCount}' : null, ), validator: (value) { if (value == null || value.trim().isEmpty) { @@ -173,10 +149,7 @@ class _BookmarkDialogState extends State { // Note (optional) TextFormField( controller: _noteController, - decoration: const InputDecoration( - labelText: 'Note (optional)', - border: OutlineInputBorder(), - ), + decoration: const InputDecoration(labelText: 'Note (optional)', border: OutlineInputBorder()), textCapitalization: TextCapitalization.sentences, maxLines: 2, ), @@ -189,9 +162,7 @@ class _BookmarkDialogState extends State { spacing: Spacing.sm, children: Bookmark.availableColors.map((hex) { final isSelected = hex == _selectedColor; - final color = Color( - int.parse('FF${hex.replaceFirst('#', '')}', radix: 16), - ); + final color = Color(int.parse('FF${hex.replaceFirst('#', '')}', radix: 16)); return GestureDetector( onTap: () => setState(() => _selectedColor = hex), child: Container( @@ -200,20 +171,9 @@ class _BookmarkDialogState extends State { decoration: BoxDecoration( color: color, shape: BoxShape.circle, - border: isSelected - ? Border.all( - color: colorScheme.onSurface, - width: 2, - ) - : null, + border: isSelected ? Border.all(color: colorScheme.onSurface, width: 2) : null, ), - child: isSelected - ? Icon( - Icons.check, - color: colorScheme.surface, - size: IconSizes.small, - ) - : null, + child: isSelected ? Icon(Icons.check, color: colorScheme.surface, size: IconSizes.small) : null, ), ); }).toList(), diff --git a/app/lib/widgets/book_details/eink_book_details_tab_bar.dart b/app/lib/widgets/book_details/eink_book_details_tab_bar.dart index 927694e..b04f463 100644 --- a/app/lib/widgets/book_details/eink_book_details_tab_bar.dart +++ b/app/lib/widgets/book_details/eink_book_details_tab_bar.dart @@ -27,26 +27,12 @@ class EinkBookDetailsTabBar extends StatelessWidget { return Container( height: TouchTargets.einkMin, decoration: BoxDecoration( - border: Border.all( - color: colorScheme.outline, - width: BorderWidths.einkDefault, - ), + border: Border.all(color: colorScheme.outline, width: BorderWidths.einkDefault), ), child: Row( children: [ - _buildTab( - context, - tab: BookDetailsTab.details, - label: 'Details', - isLast: false, - ), - _buildTab( - context, - tab: BookDetailsTab.bookmarks, - label: 'Bookmarks', - count: bookmarkCount, - isLast: false, - ), + _buildTab(context, tab: BookDetailsTab.details, label: 'Details', isLast: false), + _buildTab(context, tab: BookDetailsTab.bookmarks, label: 'Bookmarks', count: bookmarkCount, isLast: false), _buildTab( context, tab: BookDetailsTab.annotations, @@ -54,13 +40,7 @@ class EinkBookDetailsTabBar extends StatelessWidget { count: annotationCount, isLast: false, ), - _buildTab( - context, - tab: BookDetailsTab.notes, - label: 'Notes', - count: noteCount, - isLast: true, - ), + _buildTab(context, tab: BookDetailsTab.notes, label: 'Notes', count: noteCount, isLast: true), ], ), ); @@ -86,10 +66,7 @@ class EinkBookDetailsTabBar extends StatelessWidget { border: isLast ? null : Border( - right: BorderSide( - color: colorScheme.outline, - width: BorderWidths.einkDefault, - ), + right: BorderSide(color: colorScheme.outline, width: BorderWidths.einkDefault), ), ), alignment: Alignment.center, diff --git a/app/lib/widgets/book_details/empty_annotations_state.dart b/app/lib/widgets/book_details/empty_annotations_state.dart index a9c8f81..f0d5a10 100644 --- a/app/lib/widgets/book_details/empty_annotations_state.dart +++ b/app/lib/widgets/book_details/empty_annotations_state.dart @@ -6,45 +6,30 @@ class EmptyAnnotationsState extends StatelessWidget { final bool isPhysical; final VoidCallback? onAddAnnotation; - const EmptyAnnotationsState({ - super.key, - this.isPhysical = false, - this.onAddAnnotation, - }); + const EmptyAnnotationsState({super.key, this.isPhysical = false, this.onAddAnnotation}); @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; return Padding( - padding: const EdgeInsets.symmetric( - horizontal: Spacing.xl, - vertical: Spacing.xxl, - ), + padding: const EdgeInsets.symmetric(horizontal: Spacing.xl, vertical: Spacing.xxl), child: Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ - Icon( - Icons.highlight_outlined, - size: 64, - color: colorScheme.onSurfaceVariant.withValues(alpha: 0.5), - ), + Icon(Icons.highlight_outlined, size: 64, color: colorScheme.onSurfaceVariant.withValues(alpha: 0.5)), const SizedBox(height: Spacing.md), Text( 'No annotations yet', - style: Theme.of(context).textTheme.titleLarge?.copyWith( - color: colorScheme.onSurfaceVariant, - ), + style: Theme.of(context).textTheme.titleLarge?.copyWith(color: colorScheme.onSurfaceVariant), ), const SizedBox(height: Spacing.sm), Text( isPhysical ? 'Add passages you\'ve highlighted or underlined in your book.' : 'Highlight text while reading to create annotations.', - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: colorScheme.onSurfaceVariant, - ), + style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: colorScheme.onSurfaceVariant), textAlign: TextAlign.center, ), if (isPhysical) ...[ diff --git a/app/lib/widgets/book_details/empty_bookmarks_state.dart b/app/lib/widgets/book_details/empty_bookmarks_state.dart index c8cc5e0..18f8be1 100644 --- a/app/lib/widgets/book_details/empty_bookmarks_state.dart +++ b/app/lib/widgets/book_details/empty_bookmarks_state.dart @@ -6,45 +6,30 @@ class EmptyBookmarksState extends StatelessWidget { final bool isPhysical; final VoidCallback? onAddBookmark; - const EmptyBookmarksState({ - super.key, - this.isPhysical = false, - this.onAddBookmark, - }); + const EmptyBookmarksState({super.key, this.isPhysical = false, this.onAddBookmark}); @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; return Padding( - padding: const EdgeInsets.symmetric( - horizontal: Spacing.xl, - vertical: Spacing.xxl, - ), + padding: const EdgeInsets.symmetric(horizontal: Spacing.xl, vertical: Spacing.xxl), child: Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ - Icon( - Icons.bookmark_outline, - size: 64, - color: colorScheme.onSurfaceVariant.withValues(alpha: 0.5), - ), + Icon(Icons.bookmark_outline, size: 64, color: colorScheme.onSurfaceVariant.withValues(alpha: 0.5)), const SizedBox(height: Spacing.md), Text( 'No bookmarks yet', - style: Theme.of(context).textTheme.titleLarge?.copyWith( - color: colorScheme.onSurfaceVariant, - ), + style: Theme.of(context).textTheme.titleLarge?.copyWith(color: colorScheme.onSurfaceVariant), ), const SizedBox(height: Spacing.sm), Text( isPhysical ? 'Save pages you want to return to later.' : 'Bookmarks you create while reading will appear here.', - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: colorScheme.onSurfaceVariant, - ), + style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: colorScheme.onSurfaceVariant), textAlign: TextAlign.center, ), if (isPhysical) ...[ diff --git a/app/lib/widgets/book_details/empty_notes_state.dart b/app/lib/widgets/book_details/empty_notes_state.dart index 4c84fbf..c8edae5 100644 --- a/app/lib/widgets/book_details/empty_notes_state.dart +++ b/app/lib/widgets/book_details/empty_notes_state.dart @@ -12,40 +12,25 @@ class EmptyNotesState extends StatelessWidget { final colorScheme = Theme.of(context).colorScheme; return Padding( - padding: const EdgeInsets.symmetric( - horizontal: Spacing.xl, - vertical: Spacing.xxl, - ), + padding: const EdgeInsets.symmetric(horizontal: Spacing.xl, vertical: Spacing.xxl), child: Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ - Icon( - Icons.note_outlined, - size: 64, - color: colorScheme.onSurfaceVariant.withValues(alpha: 0.5), - ), + Icon(Icons.note_outlined, size: 64, color: colorScheme.onSurfaceVariant.withValues(alpha: 0.5)), const SizedBox(height: Spacing.md), Text( 'No notes yet', - style: Theme.of(context).textTheme.titleLarge?.copyWith( - color: colorScheme.onSurfaceVariant, - ), + style: Theme.of(context).textTheme.titleLarge?.copyWith(color: colorScheme.onSurfaceVariant), ), const SizedBox(height: Spacing.sm), Text( 'Create notes to capture your thoughts about this book.', - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: colorScheme.onSurfaceVariant, - ), + style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: colorScheme.onSurfaceVariant), textAlign: TextAlign.center, ), const SizedBox(height: Spacing.lg), - FilledButton.icon( - onPressed: onAddNote, - icon: const Icon(Icons.add), - label: const Text('Add note'), - ), + FilledButton.icon(onPressed: onAddNote, icon: const Icon(Icons.add), label: const Text('Add note')), ], ), ), diff --git a/app/lib/widgets/book_details/note_action_sheet.dart b/app/lib/widgets/book_details/note_action_sheet.dart index 6f35fd8..7a2c90a 100644 --- a/app/lib/widgets/book_details/note_action_sheet.dart +++ b/app/lib/widgets/book_details/note_action_sheet.dart @@ -13,10 +13,7 @@ class NoteActionSheet extends StatelessWidget { const NoteActionSheet({super.key, required this.note}); /// Shows the action sheet and returns the selected action. - static Future show( - BuildContext context, { - required Note note, - }) async { + static Future show(BuildContext context, {required Note note}) async { return showModalBottomSheet( context: context, builder: (context) => NoteActionSheet(note: note), @@ -41,9 +38,7 @@ class NoteActionSheet extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: Spacing.lg), child: Text( note.title, - style: Theme.of( - context, - ).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600), + style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600), maxLines: 1, overflow: TextOverflow.ellipsis, ), @@ -61,10 +56,7 @@ class NoteActionSheet extends StatelessWidget { // Delete action ListTile( leading: Icon(Icons.delete_outline, color: colorScheme.error), - title: Text( - 'Delete note', - style: TextStyle(color: colorScheme.error), - ), + title: Text('Delete note', style: TextStyle(color: colorScheme.error)), onTap: () => Navigator.of(context).pop(NoteAction.delete), ), @@ -97,14 +89,9 @@ class DeleteNoteDialog extends StatelessWidget { return AlertDialog( title: const Text('Delete note'), - content: Text( - 'Are you sure you want to delete "${note.title}"? This action cannot be undone.', - ), + content: Text('Are you sure you want to delete "${note.title}"? This action cannot be undone.'), actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(false), - child: const Text('Cancel'), - ), + TextButton(onPressed: () => Navigator.of(context).pop(false), child: const Text('Cancel')), FilledButton( onPressed: () => Navigator.of(context).pop(true), style: FilledButton.styleFrom(backgroundColor: colorScheme.error), diff --git a/app/lib/widgets/book_details/note_card.dart b/app/lib/widgets/book_details/note_card.dart index 9ffe2ed..31be18c 100644 --- a/app/lib/widgets/book_details/note_card.dart +++ b/app/lib/widgets/book_details/note_card.dart @@ -109,11 +109,7 @@ class _NoteCardState extends State { } /// Title row with action menu button. - Widget _buildTitleRow( - BuildContext context, - ColorScheme colorScheme, - TextTheme textTheme, - ) { + Widget _buildTitleRow(BuildContext context, ColorScheme colorScheme, TextTheme textTheme) { return Row( children: [ Expanded( @@ -171,32 +167,18 @@ class _NoteCardState extends State { } /// Location and date metadata row. - Widget _buildMetadata( - BuildContext context, - ColorScheme colorScheme, - TextTheme textTheme, - ) { - final metaStyle = textTheme.labelSmall?.copyWith( - color: colorScheme.onSurfaceVariant, - ); + Widget _buildMetadata(BuildContext context, ColorScheme colorScheme, TextTheme textTheme) { + final metaStyle = textTheme.labelSmall?.copyWith(color: colorScheme.onSurfaceVariant); return Row( children: [ if (widget.note.hasLocation) ...[ - Icon( - Icons.location_on_outlined, - size: IconSizes.small, - color: colorScheme.onSurfaceVariant, - ), + Icon(Icons.location_on_outlined, size: IconSizes.small, color: colorScheme.onSurfaceVariant), const SizedBox(width: 4), Text(widget.note.location!.shortLocation, style: metaStyle), const SizedBox(width: Spacing.md), ], - Icon( - Icons.access_time, - size: IconSizes.small, - color: colorScheme.onSurfaceVariant, - ), + Icon(Icons.access_time, size: IconSizes.small, color: colorScheme.onSurfaceVariant), const SizedBox(width: 4), Text(widget.note.dateLabel, style: metaStyle), ], diff --git a/app/lib/widgets/book_details/note_dialog.dart b/app/lib/widgets/book_details/note_dialog.dart index 1253b92..002a783 100644 --- a/app/lib/widgets/book_details/note_dialog.dart +++ b/app/lib/widgets/book_details/note_dialog.dart @@ -14,17 +14,12 @@ class NoteDialog extends StatelessWidget { bool get isEditing => existingNote != null; /// Shows the dialog and returns the created/updated note, or null if cancelled. - static Future show( - BuildContext context, { - required String bookId, - Note? existingNote, - }) async { + static Future show(BuildContext context, {required String bookId, Note? existingNote}) async { return showModalBottomSheet( context: context, isScrollControlled: true, useSafeArea: true, - builder: (context) => - _BottomSheetNote(bookId: bookId, existingNote: existingNote), + builder: (context) => _BottomSheetNote(bookId: bookId, existingNote: existingNote), ); } @@ -58,12 +53,8 @@ class _BottomSheetNoteState extends State<_BottomSheetNote> { @override void initState() { super.initState(); - _titleController = TextEditingController( - text: widget.existingNote?.title ?? '', - ); - _contentController = TextEditingController( - text: widget.existingNote?.content ?? '', - ); + _titleController = TextEditingController(text: widget.existingNote?.title ?? ''); + _contentController = TextEditingController(text: widget.existingNote?.content ?? ''); _tagController = TextEditingController(); _tags = List.from(widget.existingNote?.tags ?? []); @@ -103,9 +94,7 @@ class _BottomSheetNoteState extends State<_BottomSheetNote> { void _save() { if (_formKey.currentState?.validate() ?? false) { final note = Note( - id: - widget.existingNote?.id ?? - DateTime.now().millisecondsSinceEpoch.toString(), + id: widget.existingNote?.id ?? DateTime.now().millisecondsSinceEpoch.toString(), bookId: widget.bookId, title: _titleController.text.trim(), content: _contentController.text.trim(), @@ -134,19 +123,12 @@ class _BottomSheetNoteState extends State<_BottomSheetNote> { return Container( decoration: BoxDecoration( color: colorScheme.surface, - borderRadius: const BorderRadius.vertical( - top: Radius.circular(AppRadius.lg), - ), + borderRadius: const BorderRadius.vertical(top: Radius.circular(AppRadius.lg)), ), child: Column( children: [ Padding( - padding: const EdgeInsets.fromLTRB( - Spacing.md, - Spacing.md, - Spacing.md, - 0, - ), + padding: const EdgeInsets.fromLTRB(Spacing.md, Spacing.md, Spacing.md, 0), child: Column( children: [ const BottomSheetHandle(), @@ -184,8 +166,7 @@ class _BottomSheetNoteState extends State<_BottomSheetNote> { hintText: 'Enter note title', border: OutlineInputBorder(), ), - textCapitalization: - TextCapitalization.sentences, + textCapitalization: TextCapitalization.sentences, validator: (value) { if (value == null || value.trim().isEmpty) { return 'Please enter a title'; @@ -202,12 +183,7 @@ class _BottomSheetNoteState extends State<_BottomSheetNote> { SliverFillRemaining( hasScrollBody: false, child: Padding( - padding: const EdgeInsets.fromLTRB( - Spacing.md, - 0, - Spacing.md, - Spacing.md, - ), + padding: const EdgeInsets.fromLTRB(Spacing.md, 0, Spacing.md, Spacing.md), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -220,14 +196,12 @@ class _BottomSheetNoteState extends State<_BottomSheetNote> { border: OutlineInputBorder(), alignLabelWithHint: true, ), - textCapitalization: - TextCapitalization.sentences, + textCapitalization: TextCapitalization.sentences, maxLines: null, expands: true, textAlignVertical: TextAlignVertical.top, validator: (value) { - if (value == null || - value.trim().isEmpty) { + if (value == null || value.trim().isEmpty) { return 'Please enter some content'; } return null; @@ -253,10 +227,7 @@ class _BottomSheetNoteState extends State<_BottomSheetNote> { ), ), const SizedBox(width: Spacing.sm), - IconButton.filled( - onPressed: _addTag, - icon: const Icon(Icons.add), - ), + IconButton.filled(onPressed: _addTag, icon: const Icon(Icons.add)), ], ), @@ -269,10 +240,7 @@ class _BottomSheetNoteState extends State<_BottomSheetNote> { children: _tags.map((tag) { return Chip( label: Text(tag), - deleteIcon: const Icon( - Icons.close, - size: 18, - ), + deleteIcon: const Icon(Icons.close, size: 18), onDeleted: () => _removeTag(tag), visualDensity: VisualDensity.compact, ); @@ -281,10 +249,9 @@ class _BottomSheetNoteState extends State<_BottomSheetNote> { else Text( 'Tags will appear here', - style: Theme.of(context).textTheme.bodySmall - ?.copyWith( - color: colorScheme.onSurfaceVariant, - ), + style: Theme.of( + context, + ).textTheme.bodySmall?.copyWith(color: colorScheme.onSurfaceVariant), ), ], ), diff --git a/app/lib/widgets/book_details/update_progress_sheet.dart b/app/lib/widgets/book_details/update_progress_sheet.dart index bbe1341..76c5398 100644 --- a/app/lib/widgets/book_details/update_progress_sheet.dart +++ b/app/lib/widgets/book_details/update_progress_sheet.dart @@ -10,11 +10,7 @@ class UpdateProgressSheet extends StatefulWidget { final Book book; final void Function(int page, double position) onSave; - const UpdateProgressSheet({ - super.key, - required this.book, - required this.onSave, - }); + const UpdateProgressSheet({super.key, required this.book, required this.onSave}); /// Shows the bottom sheet and calls [onSave] when the user saves. static Future show( @@ -26,9 +22,7 @@ class UpdateProgressSheet extends StatefulWidget { context: context, isScrollControlled: true, shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical( - top: Radius.circular(AppRadius.bottomSheet), - ), + borderRadius: BorderRadius.vertical(top: Radius.circular(AppRadius.bottomSheet)), ), builder: (context) => UpdateProgressSheet(book: book, onSave: onSave), ); @@ -47,9 +41,7 @@ class _UpdateProgressSheetState extends State { void initState() { super.initState(); final currentPage = widget.book.currentPage; - _pageController = TextEditingController( - text: currentPage != null && currentPage > 0 ? '$currentPage' : '', - ); + _pageController = TextEditingController(text: currentPage != null && currentPage > 0 ? '$currentPage' : ''); _sliderValue = widget.book.currentPosition; } @@ -70,10 +62,7 @@ class _UpdateProgressSheetState extends State { int _calculatePage() { if (_hasPageCount) { - return (int.tryParse(_pageController.text) ?? 0).clamp( - 0, - widget.book.pageCount!, - ); + return (int.tryParse(_pageController.text) ?? 0).clamp(0, widget.book.pageCount!); } return (_sliderValue * (widget.book.pageCount ?? 100)).round(); } @@ -100,11 +89,7 @@ class _UpdateProgressSheetState extends State { children: [ const BottomSheetHandle(), const SizedBox(height: Spacing.md), - BottomSheetHeader( - title: 'Update progress', - onCancel: () => Navigator.of(context).pop(), - onSave: _save, - ), + BottomSheetHeader(title: 'Update progress', onCancel: () => Navigator.of(context).pop(), onSave: _save), const SizedBox(height: Spacing.md), const Divider(height: 1), const SizedBox(height: Spacing.md), @@ -140,10 +125,7 @@ class _UpdateProgressSheetState extends State { decoration: const InputDecoration( border: OutlineInputBorder(), isDense: true, - contentPadding: EdgeInsets.symmetric( - horizontal: Spacing.sm, - vertical: Spacing.sm, - ), + contentPadding: EdgeInsets.symmetric(horizontal: Spacing.sm, vertical: Spacing.sm), ), autofocus: true, onChanged: (_) => setState(() {}), @@ -158,9 +140,7 @@ class _UpdateProgressSheetState extends State { // Live percentage preview Text( '${(_calculatePosition() * 100).round()}% complete', - style: Theme.of( - context, - ).textTheme.bodyMedium?.copyWith(color: colorScheme.onSurfaceVariant), + style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: colorScheme.onSurfaceVariant), ), ], ); @@ -169,15 +149,9 @@ class _UpdateProgressSheetState extends State { Widget _buildSlider() { return Column( children: [ - Text( - '${(_sliderValue * 100).round()}% complete', - style: Theme.of(context).textTheme.titleMedium, - ), + Text('${(_sliderValue * 100).round()}% complete', style: Theme.of(context).textTheme.titleMedium), const SizedBox(height: Spacing.md), - Slider( - value: _sliderValue, - onChanged: (value) => setState(() => _sliderValue = value), - ), + Slider(value: _sliderValue, onChanged: (value) => setState(() => _sliderValue = value)), ], ); } diff --git a/app/lib/widgets/book_edit/cover_image_picker.dart b/app/lib/widgets/book_edit/cover_image_picker.dart index 56a857c..a818651 100644 --- a/app/lib/widgets/book_edit/cover_image_picker.dart +++ b/app/lib/widgets/book_edit/cover_image_picker.dart @@ -57,8 +57,7 @@ class _CoverImagePickerState extends State { if (widget.initialUrl != oldWidget.initialUrl) { setState(() { _imageUrl = widget.initialUrl; - if (widget.initialUrl != null && - !widget.initialUrl!.startsWith('data:')) { + if (widget.initialUrl != null && !widget.initialUrl!.startsWith('data:')) { _urlController.text = widget.initialUrl!; } else { _urlController.clear(); @@ -106,9 +105,7 @@ class _CoverImagePickerState extends State { onPressed: _pickImage, icon: const Icon(Icons.upload, size: 18), label: const Text('Upload'), - style: OutlinedButton.styleFrom( - padding: const EdgeInsets.symmetric(vertical: 12), - ), + style: OutlinedButton.styleFrom(padding: const EdgeInsets.symmetric(vertical: 12)), ), ), const SizedBox(width: Spacing.md), @@ -119,9 +116,7 @@ class _CoverImagePickerState extends State { label: const Text('URL'), style: OutlinedButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 12), - backgroundColor: _showUrlInput - ? colorScheme.secondaryContainer - : null, + backgroundColor: _showUrlInput ? colorScheme.secondaryContainer : null, ), ), ), @@ -137,9 +132,7 @@ class _CoverImagePickerState extends State { labelText: 'Image URL', hintText: 'https://...', isDense: true, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(AppRadius.md), - ), + border: OutlineInputBorder(borderRadius: BorderRadius.circular(AppRadius.md)), suffixIcon: _urlController.text.isNotEmpty ? IconButton( icon: const Icon(Icons.clear, size: 20), @@ -181,18 +174,9 @@ class _CoverImagePickerState extends State { color: colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(AppRadius.lg), border: Border.all(color: colorScheme.outlineVariant), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.1), - blurRadius: 8, - offset: const Offset(0, 4), - ), - ], - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(AppRadius.lg), - child: _buildCoverImage(context), + boxShadow: [BoxShadow(color: Colors.black.withValues(alpha: 0.1), blurRadius: 8, offset: const Offset(0, 4))], ), + child: ClipRRect(borderRadius: BorderRadius.circular(AppRadius.lg), child: _buildCoverImage(context)), ), ); @@ -217,11 +201,7 @@ class _CoverImagePickerState extends State { return Stack( fit: StackFit.expand, children: [ - Image.memory( - _imageBytes!, - fit: BoxFit.cover, - errorBuilder: (_, e, s) => _buildPlaceholder(context), - ), + Image.memory(_imageBytes!, fit: BoxFit.cover, errorBuilder: (_, e, s) => _buildPlaceholder(context)), Positioned(top: 8, right: 8, child: _buildRemoveButton(context)), ], ); @@ -277,10 +257,7 @@ class _CoverImagePickerState extends State { const SizedBox(height: Spacing.sm), Text( 'Tap to add cover', - style: TextStyle( - color: colorScheme.onSurfaceVariant, - fontSize: widget.isDesktop ? 14 : 12, - ), + style: TextStyle(color: colorScheme.onSurfaceVariant, fontSize: widget.isDesktop ? 14 : 12), ), ], ), @@ -294,11 +271,7 @@ class _CoverImagePickerState extends State { }); try { - final result = await FilePicker.platform.pickFiles( - type: FileType.image, - allowMultiple: false, - withData: true, - ); + final result = await FilePicker.platform.pickFiles(type: FileType.image, allowMultiple: false, withData: true); if (result != null && result.files.isNotEmpty) { final file = result.files.first; diff --git a/app/lib/widgets/book_form/book_date_field.dart b/app/lib/widgets/book_form/book_date_field.dart index 7290da4..12fea7b 100644 --- a/app/lib/widgets/book_form/book_date_field.dart +++ b/app/lib/widgets/book_form/book_date_field.dart @@ -24,9 +24,7 @@ class BookDateField extends StatelessWidget { readOnly: true, decoration: InputDecoration( labelText: label, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(AppRadius.md), - ), + border: OutlineInputBorder(borderRadius: BorderRadius.circular(AppRadius.md)), suffixIcon: Row( mainAxisSize: MainAxisSize.min, children: [ @@ -38,10 +36,7 @@ class BookDateField extends StatelessWidget { onChanged(null); }, ), - IconButton( - icon: const Icon(Icons.calendar_today, size: 20), - onPressed: () => _pickDate(context), - ), + IconButton(icon: const Icon(Icons.calendar_today, size: 20), onPressed: () => _pickDate(context)), ], ), ), diff --git a/app/lib/widgets/book_form/book_text_field.dart b/app/lib/widgets/book_form/book_text_field.dart index f80c58d..a1dbf43 100644 --- a/app/lib/widgets/book_form/book_text_field.dart +++ b/app/lib/widgets/book_form/book_text_field.dart @@ -29,14 +29,9 @@ class BookTextField extends StatelessWidget { decoration: InputDecoration( labelText: required ? '$label*' : label, alignLabelWithHint: maxLines > 1, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(AppRadius.md), - ), + border: OutlineInputBorder(borderRadius: BorderRadius.circular(AppRadius.md)), ), - validator: required - ? (value) => - value?.trim().isEmpty == true ? '$label is required' : null - : null, + validator: required ? (value) => value?.trim().isEmpty == true ? '$label is required' : null : null, onChanged: onChanged, ); } diff --git a/app/lib/widgets/book_form/co_author_editor.dart b/app/lib/widgets/book_form/co_author_editor.dart index f177840..80ba1e3 100644 --- a/app/lib/widgets/book_form/co_author_editor.dart +++ b/app/lib/widgets/book_form/co_author_editor.dart @@ -6,11 +6,7 @@ class CoAuthorEditor extends StatelessWidget { final List coAuthors; final void Function(List) onChanged; - const CoAuthorEditor({ - super.key, - required this.coAuthors, - required this.onChanged, - }); + const CoAuthorEditor({super.key, required this.coAuthors, required this.onChanged}); @override Widget build(BuildContext context) { @@ -19,12 +15,7 @@ class CoAuthorEditor extends StatelessWidget { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - 'Co-authors', - style: Theme.of( - context, - ).textTheme.bodySmall?.copyWith(color: colorScheme.onSurfaceVariant), - ), + Text('Co-authors', style: Theme.of(context).textTheme.bodySmall?.copyWith(color: colorScheme.onSurfaceVariant)), const SizedBox(height: Spacing.xs), Wrap( spacing: Spacing.xs, @@ -60,10 +51,7 @@ class CoAuthorEditor extends StatelessWidget { content: TextField( controller: controller, autofocus: true, - decoration: const InputDecoration( - labelText: 'Name', - hintText: 'Enter co-author name', - ), + decoration: const InputDecoration(labelText: 'Name', hintText: 'Enter co-author name'), onSubmitted: (value) { if (value.trim().isNotEmpty) { Navigator.pop(ctx, value.trim()); @@ -71,10 +59,7 @@ class CoAuthorEditor extends StatelessWidget { }, ), actions: [ - TextButton( - onPressed: () => Navigator.pop(ctx), - child: const Text('Cancel'), - ), + TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('Cancel')), FilledButton( onPressed: () { if (controller.text.trim().isNotEmpty) { diff --git a/app/lib/widgets/book_form/responsive_form_row.dart b/app/lib/widgets/book_form/responsive_form_row.dart index 57346f3..d49be95 100644 --- a/app/lib/widgets/book_form/responsive_form_row.dart +++ b/app/lib/widgets/book_form/responsive_form_row.dart @@ -7,11 +7,7 @@ class ResponsiveFormRow extends StatelessWidget { final bool isDesktop; final List children; - const ResponsiveFormRow({ - super.key, - required this.isDesktop, - required this.children, - }); + const ResponsiveFormRow({super.key, required this.isDesktop, required this.children}); @override Widget build(BuildContext context) { diff --git a/app/lib/widgets/bookmarks/bookmark_action_sheet.dart b/app/lib/widgets/bookmarks/bookmark_action_sheet.dart index 034c1d0..4b01261 100644 --- a/app/lib/widgets/bookmarks/bookmark_action_sheet.dart +++ b/app/lib/widgets/bookmarks/bookmark_action_sheet.dart @@ -18,10 +18,7 @@ class BookmarkActionSheet extends StatelessWidget { const BookmarkActionSheet({super.key, required this.bookmark}); /// Shows the action sheet and returns the selected action. - static Future show( - BuildContext context, { - required Bookmark bookmark, - }) async { + static Future show(BuildContext context, {required Bookmark bookmark}) async { return showModalBottomSheet( context: context, builder: (context) => BookmarkActionSheet(bookmark: bookmark), @@ -46,9 +43,7 @@ class BookmarkActionSheet extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: Spacing.lg), child: Text( bookmark.displayLocation, - style: Theme.of( - context, - ).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600), + style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600), maxLines: 1, overflow: TextOverflow.ellipsis, ), @@ -65,22 +60,15 @@ class BookmarkActionSheet extends StatelessWidget { // Change color action ListTile( - leading: Icon( - Icons.palette_outlined, - color: colorScheme.onSurface, - ), + leading: Icon(Icons.palette_outlined, color: colorScheme.onSurface), title: const Text('Change color'), - onTap: () => - Navigator.of(context).pop(BookmarkAction.changeColor), + onTap: () => Navigator.of(context).pop(BookmarkAction.changeColor), ), // Delete action ListTile( leading: Icon(Icons.delete_outline, color: colorScheme.error), - title: Text( - 'Delete bookmark', - style: TextStyle(color: colorScheme.error), - ), + title: Text('Delete bookmark', style: TextStyle(color: colorScheme.error)), onTap: () => Navigator.of(context).pop(BookmarkAction.delete), ), @@ -118,17 +106,12 @@ class BookmarkNoteSheet extends StatefulWidget { const BookmarkNoteSheet({super.key, required this.bookmark}); /// Show the note editing sheet. Returns the new note text, or null if cancelled. - static Future show( - BuildContext context, { - required Bookmark bookmark, - }) { + static Future show(BuildContext context, {required Bookmark bookmark}) { return showModalBottomSheet( context: context, isScrollControlled: true, shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical( - top: Radius.circular(AppRadius.bottomSheet), - ), + borderRadius: BorderRadius.vertical(top: Radius.circular(AppRadius.bottomSheet)), ), builder: (context) => BookmarkNoteSheet(bookmark: bookmark), ); @@ -212,16 +195,11 @@ class BookmarkColorSheet extends StatelessWidget { const BookmarkColorSheet({super.key, required this.bookmark}); /// Show the color picker sheet. Returns the selected color hex, or null. - static Future show( - BuildContext context, { - required Bookmark bookmark, - }) { + static Future show(BuildContext context, {required Bookmark bookmark}) { return showModalBottomSheet( context: context, shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical( - top: Radius.circular(AppRadius.bottomSheet), - ), + borderRadius: BorderRadius.vertical(top: Radius.circular(AppRadius.bottomSheet)), ), builder: (context) => BookmarkColorSheet(bookmark: bookmark), ); @@ -241,10 +219,7 @@ class BookmarkColorSheet extends StatelessWidget { const SizedBox(height: Spacing.md), // Title - Text( - 'Change color', - style: Theme.of(context).textTheme.titleMedium, - ), + Text('Change color', style: Theme.of(context).textTheme.titleMedium), const SizedBox(height: Spacing.lg), // Color grid @@ -253,9 +228,7 @@ class BookmarkColorSheet extends StatelessWidget { runSpacing: Spacing.md, children: Bookmark.availableColors.map((hex) { final isSelected = hex == bookmark.colorHex; - final color = Color( - int.parse('FF${hex.replaceFirst('#', '')}', radix: 16), - ); + final color = Color(int.parse('FF${hex.replaceFirst('#', '')}', radix: 16)); return GestureDetector( onTap: () => Navigator.pop(context, hex), @@ -265,17 +238,9 @@ class BookmarkColorSheet extends StatelessWidget { decoration: BoxDecoration( color: color, shape: BoxShape.circle, - border: isSelected - ? Border.all(color: colorScheme.onSurface, width: 2) - : null, + border: isSelected ? Border.all(color: colorScheme.onSurface, width: 2) : null, ), - child: isSelected - ? Icon( - Icons.check, - color: colorScheme.surface, - size: IconSizes.action, - ) - : null, + child: isSelected ? Icon(Icons.check, color: colorScheme.surface, size: IconSizes.action) : null, ), ); }).toList(), @@ -288,9 +253,7 @@ class BookmarkColorSheet extends StatelessWidget { runSpacing: Spacing.xs, children: Bookmark.availableColors.map((hex) { final name = _colorNames[hex] ?? 'Unknown'; - final color = Color( - int.parse('FF${hex.replaceFirst('#', '')}', radix: 16), - ); + final color = Color(int.parse('FF${hex.replaceFirst('#', '')}', radix: 16)); return Row( mainAxisSize: MainAxisSize.min, @@ -298,17 +261,12 @@ class BookmarkColorSheet extends StatelessWidget { Container( width: 8, height: 8, - decoration: BoxDecoration( - color: color, - shape: BoxShape.circle, - ), + decoration: BoxDecoration(color: color, shape: BoxShape.circle), ), const SizedBox(width: Spacing.xs), Text( name, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: colorScheme.onSurfaceVariant, - ), + style: Theme.of(context).textTheme.bodySmall?.copyWith(color: colorScheme.onSurfaceVariant), ), ], ); @@ -329,28 +287,17 @@ class BookmarkColorSheet extends StatelessWidget { /// Confirmation dialog for deleting a bookmark. class DeleteBookmarkDialog { /// Show the delete confirmation dialog. Returns true if confirmed. - static Future show( - BuildContext context, { - required Bookmark bookmark, - required String bookTitle, - }) async { + static Future show(BuildContext context, {required Bookmark bookmark, required String bookTitle}) async { final result = await showDialog( context: context, builder: (context) => AlertDialog( title: const Text('Delete bookmark'), - content: Text( - 'Delete bookmark at ${bookmark.displayLocation} in "$bookTitle"?', - ), + content: Text('Delete bookmark at ${bookmark.displayLocation} in "$bookTitle"?'), actions: [ - TextButton( - onPressed: () => Navigator.pop(context, false), - child: const Text('Cancel'), - ), + TextButton(onPressed: () => Navigator.pop(context, false), child: const Text('Cancel')), FilledButton( onPressed: () => Navigator.pop(context, true), - style: FilledButton.styleFrom( - backgroundColor: Theme.of(context).colorScheme.error, - ), + style: FilledButton.styleFrom(backgroundColor: Theme.of(context).colorScheme.error), child: const Text('Delete'), ), ], diff --git a/app/lib/widgets/bookmarks/bookmark_list_item.dart b/app/lib/widgets/bookmarks/bookmark_list_item.dart index 251095a..47a40d7 100644 --- a/app/lib/widgets/bookmarks/bookmark_list_item.dart +++ b/app/lib/widgets/bookmarks/bookmark_list_item.dart @@ -60,9 +60,7 @@ class _BookmarkListItemState extends State { onLongPress: widget.onLongPress, child: Container( decoration: BoxDecoration( - border: Border( - left: BorderSide(color: widget.bookmark.color, width: 4), - ), + border: Border(left: BorderSide(color: widget.bookmark.color, width: 4)), ), child: Padding( padding: const EdgeInsets.all(Spacing.md), @@ -91,10 +89,7 @@ class _BookmarkListItemState extends State { Container( width: 8, height: 8, - decoration: BoxDecoration( - color: widget.bookmark.color, - shape: BoxShape.circle, - ), + decoration: BoxDecoration(color: widget.bookmark.color, shape: BoxShape.circle), ), const SizedBox(width: Spacing.sm), Expanded( @@ -136,11 +131,7 @@ class _BookmarkListItemState extends State { child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Icon( - Icons.note_outlined, - size: IconSizes.small, - color: colorScheme.onSurfaceVariant, - ), + Icon(Icons.note_outlined, size: IconSizes.small, color: colorScheme.onSurfaceVariant), const SizedBox(width: Spacing.sm), Expanded( child: Text( @@ -158,9 +149,7 @@ class _BookmarkListItemState extends State { Widget _buildDate(ColorScheme colorScheme, TextTheme textTheme) { return Text( formatRelativeDate(widget.bookmark.createdAt), - style: textTheme.labelSmall?.copyWith( - color: colorScheme.onSurfaceVariant, - ), + style: textTheme.labelSmall?.copyWith(color: colorScheme.onSurfaceVariant), ); } diff --git a/app/lib/widgets/buttons/google_sign_in.dart b/app/lib/widgets/buttons/google_sign_in.dart index 1b6649f..dc2a875 100644 --- a/app/lib/widgets/buttons/google_sign_in.dart +++ b/app/lib/widgets/buttons/google_sign_in.dart @@ -15,12 +15,7 @@ class GoogleSignInButton extends StatefulWidget { /// Optional callback when sign-in fails final VoidCallback? onError; - const GoogleSignInButton({ - super.key, - required this.title, - this.onSuccess, - this.onError, - }); + const GoogleSignInButton({super.key, required this.title, this.onSuccess, this.onError}); @override State createState() => _GoogleSignInButtonState(); @@ -41,9 +36,18 @@ class _GoogleSignInButtonState extends State { if (!mounted) return; - if (result != null) { + if (result) { widget.onSuccess?.call(); context.goNamed('LIBRARY'); + } else if (provider.error != null) { + widget.onError?.call(); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + duration: const Duration(seconds: 5), + content: Text(provider.error!), + backgroundColor: Theme.of(context).colorScheme.error, + ), + ); } } catch (error) { if (!mounted) return; @@ -76,9 +80,7 @@ class _GoogleSignInButtonState extends State { height: 24, child: CircularProgressIndicator( strokeWidth: 2, - valueColor: AlwaysStoppedAnimation( - theme.colorScheme.primary, - ), + valueColor: AlwaysStoppedAnimation(theme.colorScheme.primary), ), ), ), @@ -88,34 +90,18 @@ class _GoogleSignInButtonState extends State { return OutlinedButton( style: OutlinedButton.styleFrom( minimumSize: const Size.fromHeight(ComponentSizes.buttonHeightMobile), - side: BorderSide( - width: BorderWidths.thin, - color: theme.colorScheme.outline, - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(AppRadius.googleButton), - ), - padding: const EdgeInsets.symmetric( - horizontal: Spacing.lg, - vertical: Spacing.buttonPaddingVertical, - ), + side: BorderSide(width: BorderWidths.thin, color: theme.colorScheme.outline), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(AppRadius.googleButton)), + padding: const EdgeInsets.symmetric(horizontal: Spacing.lg, vertical: Spacing.buttonPaddingVertical), ), onPressed: _handleSignIn, child: Row( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, children: [ - const Image( - image: AssetImage('assets/images/google_logo.png'), - height: 24.0, - ), + const Image(image: AssetImage('assets/images/google_logo.png'), height: 24.0), const SizedBox(width: Spacing.sm), - Text( - widget.title, - style: theme.textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.w500, - ), - ), + Text(widget.title, style: theme.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w500)), ], ), ); diff --git a/app/lib/widgets/context_menu/book_context_menu.dart b/app/lib/widgets/context_menu/book_context_menu.dart index 5379344..7f903f5 100644 --- a/app/lib/widgets/context_menu/book_context_menu.dart +++ b/app/lib/widgets/context_menu/book_context_menu.dart @@ -70,14 +70,9 @@ class BookContextMenu { showMenu( context: context, - position: RelativeRect.fromRect( - Rect.fromLTWH(position.dx, position.dy, 0, 0), - Offset.zero & overlay.size, - ), + position: RelativeRect.fromRect(Rect.fromLTWH(position.dx, position.dy, 0, 0), Offset.zero & overlay.size), elevation: AppElevation.level2, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(AppRadius.md), - ), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(AppRadius.md)), items: [ const PopupMenuItem( value: 'select', @@ -103,47 +98,29 @@ class BookContextMenu { const PopupMenuItem( value: 'shelf', height: 40, - child: _MenuItemRow( - icon: Icons.folder_outlined, - label: 'Move to shelf', - ), + child: _MenuItemRow(icon: Icons.folder_outlined, label: 'Move to shelf'), ), const PopupMenuItem( value: 'topics', height: 40, - child: _MenuItemRow( - icon: Icons.label_outline, - label: 'Manage topics', - ), + child: _MenuItemRow(icon: Icons.label_outline, label: 'Manage topics'), ), const PopupMenuDivider(), PopupMenuItem( value: 'reading', height: 40, - child: _MenuItemRow( - icon: Icons.auto_stories, - label: 'Mark as reading', - isSelected: book.isReading, - ), + child: _MenuItemRow(icon: Icons.auto_stories, label: 'Mark as reading', isSelected: book.isReading), ), PopupMenuItem( value: 'finished', height: 40, - child: _MenuItemRow( - icon: Icons.check_circle_outline, - label: 'Mark as finished', - isSelected: book.isFinished, - ), + child: _MenuItemRow(icon: Icons.check_circle_outline, label: 'Mark as finished', isSelected: book.isFinished), ), const PopupMenuDivider(), const PopupMenuItem( value: 'delete', height: 40, - child: _MenuItemRow( - icon: Icons.delete_outline, - label: 'Delete book', - isDestructive: true, - ), + child: _MenuItemRow(icon: Icons.delete_outline, label: 'Delete book', isDestructive: true), ), ], ).then((value) { @@ -187,9 +164,7 @@ class BookContextMenu { context: context, isScrollControlled: true, shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical( - top: Radius.circular(AppRadius.bottomSheet), - ), + borderRadius: BorderRadius.vertical(top: Radius.circular(AppRadius.bottomSheet)), ), builder: (context) => _BookContextBottomSheet( book: book, @@ -205,11 +180,7 @@ class BookContextMenu { ); } - static void _confirmDelete( - BuildContext context, - Book book, - VoidCallback? onDelete, - ) { + static void _confirmDelete(BuildContext context, Book book, VoidCallback? onDelete) { showDialog( context: context, builder: (context) => AlertDialog( @@ -219,18 +190,13 @@ class BookContextMenu { 'This action cannot be undone.', ), actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: const Text('Cancel'), - ), + TextButton(onPressed: () => Navigator.pop(context), child: const Text('Cancel')), FilledButton( onPressed: () { Navigator.pop(context); onDelete?.call(); }, - style: FilledButton.styleFrom( - backgroundColor: Theme.of(context).colorScheme.error, - ), + style: FilledButton.styleFrom(backgroundColor: Theme.of(context).colorScheme.error), child: const Text('Delete'), ), ], @@ -268,8 +234,7 @@ class _MenuItemRow extends StatelessWidget { Expanded( child: Text(label, style: TextStyle(color: color)), ), - if (isSelected) - Icon(Icons.check, size: IconSizes.small, color: colorScheme.primary), + if (isSelected) Icon(Icons.check, size: IconSizes.small, color: colorScheme.primary), ], ); } @@ -315,10 +280,7 @@ class _BookContextBottomSheet extends StatelessWidget { child: Container( width: 32, height: 4, - decoration: BoxDecoration( - color: colorScheme.outline, - borderRadius: BorderRadius.circular(2), - ), + decoration: BoxDecoration(color: colorScheme.outline, borderRadius: BorderRadius.circular(2)), ), ), const SizedBox(height: Spacing.md), @@ -328,11 +290,7 @@ class _BookContextBottomSheet extends StatelessWidget { children: [ ClipRRect( borderRadius: BorderRadius.circular(AppRadius.sm), - child: SizedBox( - width: 48, - height: 72, - child: _buildCover(context), - ), + child: SizedBox(width: 48, height: 72, child: _buildCover(context)), ), const SizedBox(width: Spacing.md), Expanded( @@ -347,9 +305,7 @@ class _BookContextBottomSheet extends StatelessWidget { ), Text( book.author, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: colorScheme.onSurfaceVariant, - ), + style: Theme.of(context).textTheme.bodySmall?.copyWith(color: colorScheme.onSurfaceVariant), ), ], ), @@ -410,15 +366,10 @@ class _BookContextBottomSheet extends StatelessWidget { // Reading status section Padding( - padding: const EdgeInsets.symmetric( - horizontal: Spacing.md, - vertical: Spacing.sm, - ), + padding: const EdgeInsets.symmetric(horizontal: Spacing.md, vertical: Spacing.sm), child: Text( 'Reading status', - style: Theme.of(context).textTheme.labelMedium?.copyWith( - color: colorScheme.onSurfaceVariant, - ), + style: Theme.of(context).textTheme.labelMedium?.copyWith(color: colorScheme.onSurfaceVariant), ), ), _BottomSheetItem( @@ -520,9 +471,7 @@ class _BottomSheetItem extends StatelessWidget { return ListTile( leading: Icon(icon, color: effectiveIconColor), title: Text(label, style: TextStyle(color: color)), - trailing: isSelected - ? Icon(Icons.check, color: colorScheme.primary, size: IconSizes.small) - : null, + trailing: isSelected ? Icon(Icons.check, color: colorScheme.primary, size: IconSizes.small) : null, onTap: onTap, contentPadding: const EdgeInsets.symmetric(horizontal: Spacing.md), visualDensity: VisualDensity.compact, diff --git a/app/lib/widgets/dashboard/continue_reading_card.dart b/app/lib/widgets/dashboard/continue_reading_card.dart index 265e354..83005b1 100644 --- a/app/lib/widgets/dashboard/continue_reading_card.dart +++ b/app/lib/widgets/dashboard/continue_reading_card.dart @@ -15,12 +15,7 @@ class ContinueReadingCard extends StatelessWidget { /// Whether to use desktop styling (larger cover, different layout). final bool isDesktop; - const ContinueReadingCard({ - super.key, - this.book, - this.onContinue, - this.isDesktop = false, - }); + const ContinueReadingCard({super.key, this.book, this.onContinue, this.isDesktop = false}); @override Widget build(BuildContext context) { @@ -43,10 +38,7 @@ class ContinueReadingCard extends StatelessWidget { decoration: BoxDecoration( color: colorScheme.surfaceContainerLow, borderRadius: BorderRadius.circular(AppRadius.lg), - border: Border.all( - color: colorScheme.outlineVariant, - width: BorderWidths.thin, - ), + border: Border.all(color: colorScheme.outlineVariant, width: BorderWidths.thin), ), child: Row( children: [ @@ -56,18 +48,11 @@ class ContinueReadingCard extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - book!.title, - style: textTheme.titleMedium, - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), + Text(book!.title, style: textTheme.titleMedium, maxLines: 2, overflow: TextOverflow.ellipsis), const SizedBox(height: Spacing.xs), Text( book!.author, - style: textTheme.bodyMedium?.copyWith( - color: colorScheme.onSurfaceVariant, - ), + style: textTheme.bodyMedium?.copyWith(color: colorScheme.onSurfaceVariant), maxLines: 1, overflow: TextOverflow.ellipsis, ), @@ -103,10 +88,7 @@ class ContinueReadingCard extends StatelessWidget { decoration: BoxDecoration( color: colorScheme.surfaceContainerLow, borderRadius: BorderRadius.circular(AppRadius.lg), - border: Border.all( - color: colorScheme.outlineVariant, - width: BorderWidths.thin, - ), + border: Border.all(color: colorScheme.outlineVariant, width: BorderWidths.thin), ), child: Row( children: [ @@ -117,18 +99,11 @@ class ContinueReadingCard extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, children: [ - Text( - book!.title, - style: textTheme.headlineSmall, - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), + Text(book!.title, style: textTheme.headlineSmall, maxLines: 2, overflow: TextOverflow.ellipsis), const SizedBox(height: Spacing.xs), Text( book!.author, - style: textTheme.titleMedium?.copyWith( - color: colorScheme.onSurfaceVariant, - ), + style: textTheme.titleMedium?.copyWith(color: colorScheme.onSurfaceVariant), maxLines: 1, overflow: TextOverflow.ellipsis, ), @@ -168,38 +143,22 @@ class ContinueReadingCard extends StatelessWidget { decoration: BoxDecoration( color: colorScheme.surfaceContainerLow, borderRadius: BorderRadius.circular(AppRadius.lg), - border: Border.all( - color: colorScheme.outlineVariant, - width: BorderWidths.thin, - ), + border: Border.all(color: colorScheme.outlineVariant, width: BorderWidths.thin), ), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon( - Icons.menu_book_outlined, - size: 48, - color: colorScheme.onSurfaceVariant, - ), + Icon(Icons.menu_book_outlined, size: 48, color: colorScheme.onSurfaceVariant), const SizedBox(height: Spacing.md), - Text( - 'No book in progress', - style: textTheme.titleMedium, - textAlign: TextAlign.center, - ), + Text('No book in progress', style: textTheme.titleMedium, textAlign: TextAlign.center), const SizedBox(height: Spacing.xs), Text( 'Start reading from your library', - style: textTheme.bodyMedium?.copyWith( - color: colorScheme.onSurfaceVariant, - ), + style: textTheme.bodyMedium?.copyWith(color: colorScheme.onSurfaceVariant), textAlign: TextAlign.center, ), const SizedBox(height: Spacing.md), - TextButton( - onPressed: () => context.go('/library'), - child: const Text('Browse library'), - ), + TextButton(onPressed: () => context.go('/library'), child: const Text('Browse library')), ], ), ); @@ -210,11 +169,7 @@ class ContinueReadingCard extends StatelessWidget { // ============================================================================ /// Builds the book cover image with rounded corners. - Widget _buildCover( - BuildContext context, { - required double width, - required double height, - }) { + Widget _buildCover(BuildContext context, {required double width, required double height}) { final colorScheme = Theme.of(context).colorScheme; return Container( @@ -239,13 +194,7 @@ class ContinueReadingCard extends StatelessWidget { Widget _buildCoverPlaceholder(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; - return Center( - child: Icon( - Icons.menu_book, - size: 32, - color: colorScheme.onSurfaceVariant, - ), - ); + return Center(child: Icon(Icons.menu_book, size: 32, color: colorScheme.onSurfaceVariant)); } /// Builds the circular play button for mobile layout. @@ -259,9 +208,7 @@ class ContinueReadingCard extends StatelessWidget { onPressed: onContinue ?? () => _navigateToBook(context), style: FilledButton.styleFrom( padding: EdgeInsets.zero, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(AppRadius.full), - ), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(AppRadius.full)), ), child: Icon(Icons.play_arrow, color: colorScheme.onPrimary), ), diff --git a/app/lib/widgets/dashboard/dashboard_greeting.dart b/app/lib/widgets/dashboard/dashboard_greeting.dart index 0ceff82..52221a1 100644 --- a/app/lib/widgets/dashboard/dashboard_greeting.dart +++ b/app/lib/widgets/dashboard/dashboard_greeting.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; -import 'package:supabase_flutter/supabase_flutter.dart'; +import 'package:papyrus/providers/auth_provider.dart'; import 'package:papyrus/themes/design_tokens.dart'; +import 'package:provider/provider.dart'; /// Greeting section for the dashboard displaying time-based greeting. class DashboardGreeting extends StatelessWidget { @@ -13,18 +14,11 @@ class DashboardGreeting extends StatelessWidget { /// Whether to use desktop styling. final bool isDesktop; - const DashboardGreeting({ - super.key, - required this.greeting, - this.onNotificationsTap, - this.isDesktop = false, - }); + const DashboardGreeting({super.key, required this.greeting, this.onNotificationsTap, this.isDesktop = false}); /// Returns the current user's first name, or "reader" as fallback. - String get _userName { - final user = Supabase.instance.client.auth.currentUser; - final displayName = - (user?.userMetadata?['full_name'] as String?) ?? 'reader'; + String _userName(BuildContext context) { + final displayName = context.watch().user?.displayName ?? 'reader'; return displayName.split(' ').first; } @@ -32,21 +26,14 @@ class DashboardGreeting extends StatelessWidget { Widget build(BuildContext context) { final textTheme = Theme.of(context).textTheme; - final greetingStyle = isDesktop - ? textTheme.headlineMedium - : textTheme.titleLarge; + final greetingStyle = isDesktop ? textTheme.headlineMedium : textTheme.titleLarge; return Padding( - padding: isDesktop - ? EdgeInsets.zero - : const EdgeInsets.symmetric(horizontal: Spacing.md), + padding: isDesktop ? EdgeInsets.zero : const EdgeInsets.symmetric(horizontal: Spacing.md), child: Row( children: [ - Expanded(child: Text('$greeting, $_userName!', style: greetingStyle)), - IconButton( - icon: const Icon(Icons.notifications_outlined), - onPressed: onNotificationsTap, - ), + Expanded(child: Text('$greeting, ${_userName(context)}!', style: greetingStyle)), + IconButton(icon: const Icon(Icons.notifications_outlined), onPressed: onNotificationsTap), ], ), ); diff --git a/app/lib/widgets/dashboard/quick_stats_widget.dart b/app/lib/widgets/dashboard/quick_stats_widget.dart index 8e8f503..0cce326 100644 --- a/app/lib/widgets/dashboard/quick_stats_widget.dart +++ b/app/lib/widgets/dashboard/quick_stats_widget.dart @@ -34,10 +34,7 @@ class QuickStatsWidget extends StatelessWidget { decoration: BoxDecoration( color: colorScheme.surfaceContainerLow, borderRadius: BorderRadius.circular(AppRadius.lg), - border: Border.all( - color: colorScheme.outlineVariant, - width: BorderWidths.thin, - ), + border: Border.all(color: colorScheme.outlineVariant, width: BorderWidths.thin), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -59,12 +56,7 @@ class QuickStatsWidget extends StatelessWidget { ], ), const SizedBox(height: Spacing.md), - _buildStatRow( - context, - icon: Icons.menu_book_outlined, - label: 'Books', - value: totalBooks.toString(), - ), + _buildStatRow(context, icon: Icons.menu_book_outlined, label: 'Books', value: totalBooks.toString()), const SizedBox(height: Spacing.sm), _buildStatRow( context, @@ -73,31 +65,16 @@ class QuickStatsWidget extends StatelessWidget { value: totalShelves.toString(), ), const SizedBox(height: Spacing.sm), - _buildStatRow( - context, - icon: Icons.label_outline, - label: 'Topics', - value: totalTopics.toString(), - ), + _buildStatRow(context, icon: Icons.label_outline, label: 'Topics', value: totalTopics.toString()), const SizedBox(height: Spacing.sm), - _buildStatRow( - context, - icon: Icons.schedule_outlined, - label: 'Reading time', - value: totalReadingLabel, - ), + _buildStatRow(context, icon: Icons.schedule_outlined, label: 'Reading time', value: totalReadingLabel), ], ), ); } /// Builds a single stat row with icon, label, and value. - Widget _buildStatRow( - BuildContext context, { - required IconData icon, - required String label, - required String value, - }) { + Widget _buildStatRow(BuildContext context, {required IconData icon, required String label, required String value}) { final colorScheme = Theme.of(context).colorScheme; final textTheme = Theme.of(context).textTheme; @@ -106,17 +83,9 @@ class QuickStatsWidget extends StatelessWidget { Icon(icon, size: 18, color: colorScheme.onSurfaceVariant), const SizedBox(width: Spacing.sm), Expanded( - child: Text( - label, - style: textTheme.bodyMedium?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), - ), - Text( - value, - style: textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w600), + child: Text(label, style: textTheme.bodyMedium?.copyWith(color: colorScheme.onSurfaceVariant)), ), + Text(value, style: textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w600)), ], ); } diff --git a/app/lib/widgets/dashboard/reading_goal_card.dart b/app/lib/widgets/dashboard/reading_goal_card.dart index 5e577a1..37538b3 100644 --- a/app/lib/widgets/dashboard/reading_goal_card.dart +++ b/app/lib/widgets/dashboard/reading_goal_card.dart @@ -15,12 +15,7 @@ class ReadingGoalCard extends StatelessWidget { /// Whether to use desktop styling. final bool isDesktop; - const ReadingGoalCard({ - super.key, - required this.goals, - this.onTap, - this.isDesktop = false, - }); + const ReadingGoalCard({super.key, required this.goals, this.onTap, this.isDesktop = false}); @override Widget build(BuildContext context) { @@ -42,10 +37,7 @@ class ReadingGoalCard extends StatelessWidget { decoration: BoxDecoration( color: colorScheme.surfaceContainerLow, borderRadius: BorderRadius.circular(AppRadius.lg), - border: Border.all( - color: colorScheme.outlineVariant, - width: BorderWidths.thin, - ), + border: Border.all(color: colorScheme.outlineVariant, width: BorderWidths.thin), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -98,9 +90,7 @@ class ReadingGoalCard extends StatelessWidget { goal.type == GoalType.minutes ? '${formatDuration(goal.current)}/${formatDuration(goal.target)}' : '${goal.current}/${goal.target}', - style: textTheme.bodySmall?.copyWith( - color: colorScheme.onSurfaceVariant, - ), + style: textTheme.bodySmall?.copyWith(color: colorScheme.onSurfaceVariant), ), ], ), @@ -110,9 +100,7 @@ class ReadingGoalCard extends StatelessWidget { child: LinearProgressIndicator( value: goal.progress, backgroundColor: colorScheme.surfaceContainerHighest, - color: goal.isCompleted - ? colorScheme.tertiary - : colorScheme.primary, + color: goal.isCompleted ? colorScheme.tertiary : colorScheme.primary, minHeight: 4, ), ), @@ -135,38 +123,22 @@ class ReadingGoalCard extends StatelessWidget { decoration: BoxDecoration( color: colorScheme.surfaceContainerLow, borderRadius: BorderRadius.circular(AppRadius.lg), - border: Border.all( - color: colorScheme.outlineVariant, - width: BorderWidths.thin, - ), + border: Border.all(color: colorScheme.outlineVariant, width: BorderWidths.thin), ), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon( - Icons.flag_outlined, - size: 40, - color: colorScheme.onSurfaceVariant, - ), + Icon(Icons.flag_outlined, size: 40, color: colorScheme.onSurfaceVariant), const SizedBox(height: Spacing.md), - Text( - 'No reading goals set', - style: textTheme.titleMedium, - textAlign: TextAlign.center, - ), + Text('No reading goals set', style: textTheme.titleMedium, textAlign: TextAlign.center), const SizedBox(height: Spacing.xs), Text( 'Set a goal to track your progress', - style: textTheme.bodyMedium?.copyWith( - color: colorScheme.onSurfaceVariant, - ), + style: textTheme.bodyMedium?.copyWith(color: colorScheme.onSurfaceVariant), textAlign: TextAlign.center, ), const SizedBox(height: Spacing.md), - TextButton( - onPressed: () => context.go('/goals'), - child: const Text('Set a goal'), - ), + TextButton(onPressed: () => context.go('/goals'), child: const Text('Set a goal')), ], ), ); diff --git a/app/lib/widgets/dashboard/recently_added_section.dart b/app/lib/widgets/dashboard/recently_added_section.dart index 2f72693..4dc4e58 100644 --- a/app/lib/widgets/dashboard/recently_added_section.dart +++ b/app/lib/widgets/dashboard/recently_added_section.dart @@ -17,13 +17,7 @@ class RecentlyAddedSection extends StatelessWidget { /// Whether to use desktop styling (larger covers). final bool isDesktop; - const RecentlyAddedSection({ - super.key, - required this.books, - this.onBookTap, - this.onSeeAll, - this.isDesktop = false, - }); + const RecentlyAddedSection({super.key, required this.books, this.onBookTap, this.onSeeAll, this.isDesktop = false}); @override Widget build(BuildContext context) { @@ -51,9 +45,7 @@ class RecentlyAddedSection extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text('Recently added', style: textTheme.titleMedium), - _buildViewAllButton( - onPressed: onSeeAll ?? () => context.go('/library'), - ), + _buildViewAllButton(onPressed: onSeeAll ?? () => context.go('/library')), ], ), ), @@ -64,16 +56,10 @@ class RecentlyAddedSection extends StatelessWidget { scrollDirection: Axis.horizontal, padding: EdgeInsets.symmetric(horizontal: horizontalPadding), itemCount: books.length, - separatorBuilder: (_, _) => - const SizedBox(width: Spacing.sm + Spacing.xs), + separatorBuilder: (_, _) => const SizedBox(width: Spacing.sm + Spacing.xs), itemBuilder: (context, index) { final book = books[index]; - return _buildBookCover( - context, - book: book, - width: coverWidth, - height: coverHeight, - ); + return _buildBookCover(context, book: book, width: coverWidth, height: coverHeight); }, ), ), @@ -82,12 +68,7 @@ class RecentlyAddedSection extends StatelessWidget { } /// Builds a single book cover with tap handling and hover cursor. - Widget _buildBookCover( - BuildContext context, { - required Book book, - required double width, - required double height, - }) { + Widget _buildBookCover(BuildContext context, {required Book book, required double width, required double height}) { final colorScheme = Theme.of(context).colorScheme; return MouseRegion( @@ -101,11 +82,7 @@ class RecentlyAddedSection extends StatelessWidget { color: colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(AppRadius.md), boxShadow: [ - BoxShadow( - color: colorScheme.shadow.withValues(alpha: 0.1), - blurRadius: 4, - offset: const Offset(0, 2), - ), + BoxShadow(color: colorScheme.shadow.withValues(alpha: 0.1), blurRadius: 4, offset: const Offset(0, 2)), ], ), clipBehavior: Clip.antiAlias, @@ -113,8 +90,7 @@ class RecentlyAddedSection extends StatelessWidget { ? Image.network( book.coverURL!, fit: BoxFit.cover, - errorBuilder: (_, _, _) => - _buildCoverPlaceholder(context, book), + errorBuilder: (_, _, _) => _buildCoverPlaceholder(context, book), ) : _buildCoverPlaceholder(context, book), ), @@ -133,17 +109,11 @@ class RecentlyAddedSection extends StatelessWidget { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon( - Icons.menu_book, - size: 24, - color: colorScheme.onPrimaryContainer, - ), + Icon(Icons.menu_book, size: 24, color: colorScheme.onPrimaryContainer), const SizedBox(height: Spacing.xs), Text( book.title, - style: textTheme.labelSmall?.copyWith( - color: colorScheme.onPrimaryContainer, - ), + style: textTheme.labelSmall?.copyWith(color: colorScheme.onPrimaryContainer), maxLines: 2, overflow: TextOverflow.ellipsis, textAlign: TextAlign.center, @@ -180,18 +150,9 @@ class RecentlyAddedSection extends StatelessWidget { padding: const EdgeInsets.all(Spacing.lg), child: Column( children: [ - Icon( - Icons.library_books_outlined, - size: 40, - color: colorScheme.onSurfaceVariant, - ), + Icon(Icons.library_books_outlined, size: 40, color: colorScheme.onSurfaceVariant), const SizedBox(height: Spacing.sm), - Text( - 'No books added recently', - style: textTheme.bodyMedium?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), + Text('No books added recently', style: textTheme.bodyMedium?.copyWith(color: colorScheme.onSurfaceVariant)), ], ), ); diff --git a/app/lib/widgets/dashboard/weekly_activity_chart.dart b/app/lib/widgets/dashboard/weekly_activity_chart.dart index a57c471..507db30 100644 --- a/app/lib/widgets/dashboard/weekly_activity_chart.dart +++ b/app/lib/widgets/dashboard/weekly_activity_chart.dart @@ -61,10 +61,7 @@ class WeeklyActivityChart extends StatelessWidget { decoration: BoxDecoration( color: colorScheme.surfaceContainerLow, borderRadius: BorderRadius.circular(AppRadius.lg), - border: Border.all( - color: colorScheme.outlineVariant, - width: BorderWidths.thin, - ), + border: Border.all(color: colorScheme.outlineVariant, width: BorderWidths.thin), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -76,9 +73,7 @@ class WeeklyActivityChart extends StatelessWidget { if (showPeriodToggle) Text( 'Total: ${activities.totalTimeLabel} • Avg: ${activities.averageTimeLabel}/day', - style: textTheme.bodySmall?.copyWith( - color: colorScheme.onSurfaceVariant, - ), + style: textTheme.bodySmall?.copyWith(color: colorScheme.onSurfaceVariant), ), ], ), @@ -86,11 +81,7 @@ class WeeklyActivityChart extends StatelessWidget { } /// Builds the header row with period navigation and optional toggle. - Widget _buildHeader( - BuildContext context, - TextTheme textTheme, - ColorScheme colorScheme, - ) { + Widget _buildHeader(BuildContext context, TextTheme textTheme, ColorScheme colorScheme) { return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -109,9 +100,7 @@ class WeeklyActivityChart extends StatelessWidget { icon: Icon( Icons.chevron_right, size: 20, - color: canGoToNextPeriod - ? null - : colorScheme.onSurfaceVariant.withValues(alpha: 0.3), + color: canGoToNextPeriod ? null : colorScheme.onSurfaceVariant.withValues(alpha: 0.3), ), onPressed: canGoToNextPeriod ? onNextPeriod : null, visualDensity: VisualDensity.compact, @@ -126,23 +115,14 @@ class WeeklyActivityChart extends StatelessWidget { } /// Builds the bar chart showing activity per day. - Widget _buildBarChart( - BuildContext context, - TextTheme textTheme, - ColorScheme colorScheme, - int maxMinutes, - ) { + Widget _buildBarChart(BuildContext context, TextTheme textTheme, ColorScheme colorScheme, int maxMinutes) { return SizedBox( height: 120, child: Row( crossAxisAlignment: CrossAxisAlignment.end, children: activities.map((activity) { return Expanded( - child: _buildBar( - context, - activity: activity, - maxMinutes: maxMinutes, - ), + child: _buildBar(context, activity: activity, maxMinutes: maxMinutes), ); }).toList(), ), @@ -150,21 +130,12 @@ class WeeklyActivityChart extends StatelessWidget { } /// Builds a single bar with time label and day label. - Widget _buildBar( - BuildContext context, { - required DailyActivity activity, - required int maxMinutes, - }) { + Widget _buildBar(BuildContext context, {required DailyActivity activity, required int maxMinutes}) { final colorScheme = Theme.of(context).colorScheme; final textTheme = Theme.of(context).textTheme; final maxHeight = 80.0; - final barHeight = maxMinutes > 0 - ? (activity.readingMinutes / maxMinutes * maxHeight).clamp( - 2.0, - maxHeight, - ) - : 2.0; + final barHeight = maxMinutes > 0 ? (activity.readingMinutes / maxMinutes * maxHeight).clamp(2.0, maxHeight) : 2.0; return Padding( padding: const EdgeInsets.symmetric(horizontal: 4), @@ -176,18 +147,13 @@ class WeeklyActivityChart extends StatelessWidget { padding: const EdgeInsets.only(bottom: 2), child: Text( activity.readingTimeLabel, - style: textTheme.labelSmall?.copyWith( - fontSize: 10, - color: colorScheme.onSurfaceVariant, - ), + style: textTheme.labelSmall?.copyWith(fontSize: 10, color: colorScheme.onSurfaceVariant), ), ), Container( height: activity.hasActivity ? barHeight : 2, decoration: BoxDecoration( - color: activity.isToday - ? colorScheme.primary - : colorScheme.primary.withValues(alpha: 0.6), + color: activity.isToday ? colorScheme.primary : colorScheme.primary.withValues(alpha: 0.6), borderRadius: BorderRadius.circular(2), ), ), @@ -195,12 +161,8 @@ class WeeklyActivityChart extends StatelessWidget { Text( activity.dayLabel, style: textTheme.labelSmall?.copyWith( - fontWeight: activity.isToday - ? FontWeight.bold - : FontWeight.normal, - color: activity.isToday - ? colorScheme.primary - : colorScheme.onSurfaceVariant, + fontWeight: activity.isToday ? FontWeight.bold : FontWeight.normal, + color: activity.isToday ? colorScheme.primary : colorScheme.onSurfaceVariant, ), ), ], diff --git a/app/lib/widgets/filter/active_filter_bar.dart b/app/lib/widgets/filter/active_filter_bar.dart index c89e950..df73687 100644 --- a/app/lib/widgets/filter/active_filter_bar.dart +++ b/app/lib/widgets/filter/active_filter_bar.dart @@ -14,12 +14,7 @@ class ActiveFilterBar extends StatelessWidget { /// Callback when "Clear All" is tapped. final VoidCallback? onClearAll; - const ActiveFilterBar({ - super.key, - required this.filters, - this.onFilterRemoved, - this.onClearAll, - }); + const ActiveFilterBar({super.key, required this.filters, this.onFilterRemoved, this.onClearAll}); @override Widget build(BuildContext context) { @@ -48,19 +43,13 @@ class ActiveFilterBar extends StatelessWidget { ), child: Text( 'Clear all', - style: TextStyle( - color: colorScheme.primary, - fontWeight: FontWeight.w500, - ), + style: TextStyle(color: colorScheme.primary, fontWeight: FontWeight.w500), ), ); } final filter = filters[index]; - return _ActiveFilterChip( - filter: filter, - onRemoved: () => onFilterRemoved?.call(filter), - ); + return _ActiveFilterChip(filter: filter, onRemoved: () => onFilterRemoved?.call(filter)); }, ), ); @@ -82,24 +71,16 @@ class _ActiveFilterChip extends StatelessWidget { visualDensity: VisualDensity.compact, backgroundColor: colorScheme.secondaryContainer, side: BorderSide.none, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(AppRadius.sm), - ), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(AppRadius.sm)), label: _buildLabel(context), - deleteIcon: Icon( - Icons.close, - size: 18, - color: colorScheme.onSecondaryContainer, - ), + deleteIcon: Icon(Icons.close, size: 18, color: colorScheme.onSecondaryContainer), onDeleted: onRemoved, ); } Widget _buildLabel(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; - final textStyle = Theme.of( - context, - ).textTheme.labelLarge?.copyWith(color: colorScheme.onSecondaryContainer); + final textStyle = Theme.of(context).textTheme.labelLarge?.copyWith(color: colorScheme.onSecondaryContainer); if (filter.type == ActiveFilterType.quick) { return Text(filter.label, style: textStyle); @@ -111,10 +92,7 @@ class _ActiveFilterChip extends StatelessWidget { children: [ TextSpan( text: '${filter.label}: ', - style: textStyle?.copyWith( - color: colorScheme.primary, - fontWeight: FontWeight.w600, - ), + style: textStyle?.copyWith(color: colorScheme.primary, fontWeight: FontWeight.w600), ), TextSpan(text: filter.value, style: textStyle), ], diff --git a/app/lib/widgets/filter/filter_bottom_sheet.dart b/app/lib/widgets/filter/filter_bottom_sheet.dart index cd762e8..42a8878 100644 --- a/app/lib/widgets/filter/filter_bottom_sheet.dart +++ b/app/lib/widgets/filter/filter_bottom_sheet.dart @@ -22,8 +22,7 @@ class FilterOptions { List topicNames = const [], }) { return FilterOptions( - formats: books.map((b) => b.formatLabel.toLowerCase()).toSet().toList() - ..sort(), + formats: books.map((b) => b.formatLabel.toLowerCase()).toSet().toList()..sort(), shelves: shelfNames.toList()..sort(), topics: topicNames.toList()..sort(), ); @@ -208,13 +207,7 @@ class FilterBottomSheet extends StatefulWidget { /// Initial filter values. final AppliedFilters? initialFilters; - const FilterBottomSheet({ - super.key, - required this.filterOptions, - this.onApply, - this.onReset, - this.initialFilters, - }); + const FilterBottomSheet({super.key, required this.filterOptions, this.onApply, this.onReset, this.initialFilters}); @override State createState() => _FilterBottomSheetState(); @@ -237,9 +230,7 @@ class FilterBottomSheet extends StatefulWidget { builder: (context, scrollController) => Container( decoration: BoxDecoration( color: Theme.of(context).colorScheme.surface, - borderRadius: const BorderRadius.vertical( - top: Radius.circular(AppRadius.xl), - ), + borderRadius: const BorderRadius.vertical(top: Radius.circular(AppRadius.xl)), ), child: FilterBottomSheet( filterOptions: filterOptions, @@ -376,24 +367,15 @@ class _FilterBottomSheetState extends State { margin: const EdgeInsets.only(top: Spacing.sm), width: 32, height: 4, - decoration: BoxDecoration( - color: colorScheme.outlineVariant, - borderRadius: BorderRadius.circular(2), - ), + decoration: BoxDecoration(color: colorScheme.outlineVariant, borderRadius: BorderRadius.circular(2)), ), Padding( - padding: const EdgeInsets.symmetric( - horizontal: Spacing.md, - vertical: Spacing.xs, - ), + padding: const EdgeInsets.symmetric(horizontal: Spacing.md, vertical: Spacing.xs), child: Row( children: [ Text('Filters', style: Theme.of(context).textTheme.titleLarge), const Spacer(), - IconButton( - icon: const Icon(Icons.close), - onPressed: () => Navigator.of(context).pop(), - ), + IconButton(icon: const Icon(Icons.close), onPressed: () => Navigator.of(context).pop()), ], ), ), @@ -410,10 +392,7 @@ class _FilterBottomSheetState extends State { icon: Icons.person_outline, child: TextField( controller: _authorController, - decoration: const InputDecoration( - hintText: 'Enter author name...', - border: OutlineInputBorder(), - ), + decoration: const InputDecoration(hintText: 'Enter author name...', border: OutlineInputBorder()), onChanged: (_) => setState(() {}), ), ), @@ -428,12 +407,9 @@ class _FilterBottomSheetState extends State { hintText: 'Any format', dropdownMenuEntries: [ const DropdownMenuEntry(value: '', label: 'Any format'), - ...widget.filterOptions.formats.map( - (f) => DropdownMenuEntry(value: f, label: f.toUpperCase()), - ), + ...widget.filterOptions.formats.map((f) => DropdownMenuEntry(value: f, label: f.toUpperCase())), ], - onSelected: (v) => - setState(() => _selectedFormat = v?.isEmpty == true ? null : v), + onSelected: (v) => setState(() => _selectedFormat = v?.isEmpty == true ? null : v), ), ), @@ -447,12 +423,9 @@ class _FilterBottomSheetState extends State { hintText: 'Any shelf', dropdownMenuEntries: [ const DropdownMenuEntry(value: '', label: 'Any shelf'), - ...widget.filterOptions.shelves.map( - (s) => DropdownMenuEntry(value: s, label: s), - ), + ...widget.filterOptions.shelves.map((s) => DropdownMenuEntry(value: s, label: s)), ], - onSelected: (v) => - setState(() => _selectedShelf = v?.isEmpty == true ? null : v), + onSelected: (v) => setState(() => _selectedShelf = v?.isEmpty == true ? null : v), ), ), @@ -466,12 +439,9 @@ class _FilterBottomSheetState extends State { hintText: 'Any topic', dropdownMenuEntries: [ const DropdownMenuEntry(value: '', label: 'Any topic'), - ...widget.filterOptions.topics.map( - (t) => DropdownMenuEntry(value: t, label: t), - ), + ...widget.filterOptions.topics.map((t) => DropdownMenuEntry(value: t, label: t)), ], - onSelected: (v) => - setState(() => _selectedTopic = v?.isEmpty == true ? null : v), + onSelected: (v) => setState(() => _selectedTopic = v?.isEmpty == true ? null : v), ), ), @@ -489,8 +459,7 @@ class _FilterBottomSheetState extends State { DropdownMenuEntry(value: 'finished', label: 'Finished'), DropdownMenuEntry(value: 'unread', label: 'Unread'), ], - onSelected: (v) => - setState(() => _selectedStatus = v?.isEmpty == true ? null : v), + onSelected: (v) => setState(() => _selectedStatus = v?.isEmpty == true ? null : v), ), ), ], @@ -508,16 +477,11 @@ class _FilterBottomSheetState extends State { children: [ SizedBox( width: 36, - child: Text( - '${_progressRange.start.toInt()}%', - style: Theme.of(context).textTheme.bodySmall, - ), + child: Text('${_progressRange.start.toInt()}%', style: Theme.of(context).textTheme.bodySmall), ), Expanded( child: SliderTheme( - data: SliderTheme.of( - context, - ).copyWith(overlayShape: SliderComponentShape.noOverlay), + data: SliderTheme.of(context).copyWith(overlayShape: SliderComponentShape.noOverlay), child: RangeSlider( values: _progressRange, min: 0, @@ -561,9 +525,7 @@ class _FilterBottomSheetState extends State { child: OutlinedButton( onPressed: _resetFilters, style: OutlinedButton.styleFrom( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(AppRadius.md), - ), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(AppRadius.md)), ), child: const Text('Reset'), ), @@ -577,9 +539,7 @@ class _FilterBottomSheetState extends State { child: FilledButton( onPressed: _applyFilters, style: FilledButton.styleFrom( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(AppRadius.md), - ), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(AppRadius.md)), ), child: const Text('Apply filters'), ), @@ -637,11 +597,7 @@ class _FilterBottomSheetState extends State { ); } - Widget _buildFilterField({ - required String label, - required IconData icon, - required Widget child, - }) { + Widget _buildFilterField({required String label, required IconData icon, required Widget child}) { final colorScheme = Theme.of(context).colorScheme; return Padding( @@ -651,17 +607,11 @@ class _FilterBottomSheetState extends State { children: [ Row( children: [ - Icon( - icon, - size: IconSizes.small, - color: colorScheme.onSurfaceVariant, - ), + Icon(icon, size: IconSizes.small, color: colorScheme.onSurfaceVariant), const SizedBox(width: Spacing.sm), Text( label, - style: Theme.of(context).textTheme.labelMedium?.copyWith( - color: colorScheme.onSurfaceVariant, - ), + style: Theme.of(context).textTheme.labelMedium?.copyWith(color: colorScheme.onSurfaceVariant), ), ], ), @@ -679,12 +629,7 @@ class _QuickFilterCheckbox extends StatelessWidget { final bool value; final ValueChanged? onChanged; - const _QuickFilterCheckbox({ - required this.label, - required this.icon, - required this.value, - this.onChanged, - }); + const _QuickFilterCheckbox({required this.label, required this.icon, required this.value, this.onChanged}); @override Widget build(BuildContext context) { @@ -698,33 +643,23 @@ class _QuickFilterCheckbox extends StatelessWidget { height: 44, padding: const EdgeInsets.symmetric(horizontal: Spacing.sm), decoration: BoxDecoration( - color: value - ? colorScheme.secondaryContainer - : colorScheme.surfaceContainerHighest, + color: value ? colorScheme.secondaryContainer : colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(AppRadius.md), border: value ? null : Border.all(color: colorScheme.outlineVariant), ), child: Row( children: [ - Checkbox( - value: value, - onChanged: onChanged, - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - ), + Checkbox(value: value, onChanged: onChanged, materialTapTargetSize: MaterialTapTargetSize.shrinkWrap), Icon( icon, size: IconSizes.small, - color: value - ? colorScheme.onSecondaryContainer - : colorScheme.onSurfaceVariant, + color: value ? colorScheme.onSecondaryContainer : colorScheme.onSurfaceVariant, ), const SizedBox(width: Spacing.sm), Text( label, style: TextStyle( - color: value - ? colorScheme.onSecondaryContainer - : colorScheme.onSurfaceVariant, + color: value ? colorScheme.onSecondaryContainer : colorScheme.onSurfaceVariant, fontWeight: value ? FontWeight.w600 : FontWeight.normal, ), ), diff --git a/app/lib/widgets/filter/filter_dialog.dart b/app/lib/widgets/filter/filter_dialog.dart index f962055..bb44b99 100644 --- a/app/lib/widgets/filter/filter_dialog.dart +++ b/app/lib/widgets/filter/filter_dialog.dart @@ -11,11 +11,7 @@ class FilterDialog extends StatefulWidget { /// Initial filter values to populate the dialog. final AppliedFilters? initialFilters; - const FilterDialog({ - super.key, - required this.filterOptions, - this.initialFilters, - }); + const FilterDialog({super.key, required this.filterOptions, this.initialFilters}); /// Show the filter dialog and return the applied filters. static Future show( @@ -25,10 +21,7 @@ class FilterDialog extends StatefulWidget { }) { return showDialog( context: context, - builder: (context) => FilterDialog( - filterOptions: filterOptions, - initialFilters: initialFilters, - ), + builder: (context) => FilterDialog(filterOptions: filterOptions, initialFilters: initialFilters), ); } @@ -132,10 +125,9 @@ class _FilterDialogState extends State { // Quick filters section Text( 'Quick filters', - style: Theme.of(context).textTheme.labelSmall?.copyWith( - color: colorScheme.onSurfaceVariant, - fontWeight: FontWeight.bold, - ), + style: Theme.of( + context, + ).textTheme.labelSmall?.copyWith(color: colorScheme.onSurfaceVariant, fontWeight: FontWeight.bold), ), const SizedBox(height: Spacing.sm), Wrap( @@ -172,10 +164,9 @@ class _FilterDialogState extends State { // Advanced filters section Text( 'Advanced filters', - style: Theme.of(context).textTheme.labelSmall?.copyWith( - color: colorScheme.onSurfaceVariant, - fontWeight: FontWeight.bold, - ), + style: Theme.of( + context, + ).textTheme.labelSmall?.copyWith(color: colorScheme.onSurfaceVariant, fontWeight: FontWeight.bold), ), const SizedBox(height: Spacing.md), @@ -194,21 +185,10 @@ class _FilterDialogState extends State { // Format dropdown DropdownButtonFormField( initialValue: _selectedFormat, - decoration: const InputDecoration( - labelText: 'Format', - prefixIcon: Icon(Icons.book_outlined), - ), + decoration: const InputDecoration(labelText: 'Format', prefixIcon: Icon(Icons.book_outlined)), items: [ - const DropdownMenuItem( - value: null, - child: Text('Any format'), - ), - ...formats.map( - (f) => DropdownMenuItem( - value: f.toLowerCase(), - child: Text(f.toUpperCase()), - ), - ), + const DropdownMenuItem(value: null, child: Text('Any format')), + ...formats.map((f) => DropdownMenuItem(value: f.toLowerCase(), child: Text(f.toUpperCase()))), ], onChanged: (v) => setState(() => _selectedFormat = v), ), @@ -217,15 +197,10 @@ class _FilterDialogState extends State { // Shelf dropdown DropdownButtonFormField( initialValue: _selectedShelf, - decoration: const InputDecoration( - labelText: 'Shelf', - prefixIcon: Icon(Icons.folder_outlined), - ), + decoration: const InputDecoration(labelText: 'Shelf', prefixIcon: Icon(Icons.folder_outlined)), items: [ const DropdownMenuItem(value: null, child: Text('Any shelf')), - ...shelves.map( - (s) => DropdownMenuItem(value: s, child: Text(s)), - ), + ...shelves.map((s) => DropdownMenuItem(value: s, child: Text(s))), ], onChanged: (v) => setState(() => _selectedShelf = v), ), @@ -234,15 +209,10 @@ class _FilterDialogState extends State { // Topic dropdown DropdownButtonFormField( initialValue: _selectedTopic, - decoration: const InputDecoration( - labelText: 'Topic', - prefixIcon: Icon(Icons.label_outline), - ), + decoration: const InputDecoration(labelText: 'Topic', prefixIcon: Icon(Icons.label_outline)), items: [ const DropdownMenuItem(value: null, child: Text('Any topic')), - ...topics.map( - (t) => DropdownMenuItem(value: t, child: Text(t)), - ), + ...topics.map((t) => DropdownMenuItem(value: t, child: Text(t))), ], onChanged: (v) => setState(() => _selectedTopic = v), ), @@ -251,16 +221,10 @@ class _FilterDialogState extends State { // Status dropdown DropdownButtonFormField( initialValue: _selectedStatus, - decoration: const InputDecoration( - labelText: 'Status', - prefixIcon: Icon(Icons.schedule), - ), + decoration: const InputDecoration(labelText: 'Status', prefixIcon: Icon(Icons.schedule)), items: const [ DropdownMenuItem(value: null, child: Text('Any status')), - DropdownMenuItem( - value: 'reading', - child: Text('Currently reading'), - ), + DropdownMenuItem(value: 'reading', child: Text('Currently reading')), DropdownMenuItem(value: 'finished', child: Text('Finished')), DropdownMenuItem(value: 'unread', child: Text('Unread')), ], @@ -271,17 +235,11 @@ class _FilterDialogState extends State { // Progress range slider Row( children: [ - Icon( - Icons.show_chart, - size: IconSizes.small, - color: colorScheme.onSurfaceVariant, - ), + Icon(Icons.show_chart, size: IconSizes.small, color: colorScheme.onSurfaceVariant), const SizedBox(width: Spacing.sm), Text( 'Progress', - style: Theme.of(context).textTheme.labelMedium?.copyWith( - color: colorScheme.onSurfaceVariant, - ), + style: Theme.of(context).textTheme.labelMedium?.copyWith(color: colorScheme.onSurfaceVariant), ), ], ), @@ -290,16 +248,11 @@ class _FilterDialogState extends State { children: [ SizedBox( width: 36, - child: Text( - '${_progressRange.start.toInt()}%', - style: Theme.of(context).textTheme.bodySmall, - ), + child: Text('${_progressRange.start.toInt()}%', style: Theme.of(context).textTheme.bodySmall), ), Expanded( child: SliderTheme( - data: SliderTheme.of( - context, - ).copyWith(overlayShape: SliderComponentShape.noOverlay), + data: SliderTheme.of(context).copyWith(overlayShape: SliderComponentShape.noOverlay), child: RangeSlider( values: _progressRange, min: 0, @@ -329,20 +282,11 @@ class _FilterDialogState extends State { actions: [ Row( children: [ - TextButton( - onPressed: _clearFilters, - child: const Text('Clear all'), - ), + TextButton(onPressed: _clearFilters, child: const Text('Clear all')), const Spacer(), - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: const Text('Cancel'), - ), + TextButton(onPressed: () => Navigator.of(context).pop(), child: const Text('Cancel')), const SizedBox(width: Spacing.sm), - FilledButton( - onPressed: _applyFilters, - child: const Text('Apply filters'), - ), + FilledButton(onPressed: _applyFilters, child: const Text('Apply filters')), ], ), ], diff --git a/app/lib/widgets/goals/active_goal_details_sheet.dart b/app/lib/widgets/goals/active_goal_details_sheet.dart index d76d0d1..e14e6e3 100644 --- a/app/lib/widgets/goals/active_goal_details_sheet.dart +++ b/app/lib/widgets/goals/active_goal_details_sheet.dart @@ -17,13 +17,7 @@ class ActiveGoalDetailsSheet extends StatefulWidget { /// Called when goal is deleted. final VoidCallback? onDelete; - const ActiveGoalDetailsSheet({ - super.key, - required this.goal, - this.onUpdateProgress, - this.onEdit, - this.onDelete, - }); + const ActiveGoalDetailsSheet({super.key, required this.goal, this.onUpdateProgress, this.onEdit, this.onDelete}); /// Shows the active goal details sheet. static Future show( @@ -36,15 +30,9 @@ class ActiveGoalDetailsSheet extends StatefulWidget { return showModalBottomSheet( context: context, isScrollControlled: true, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(AppRadius.xl)), - ), - builder: (context) => ActiveGoalDetailsSheet( - goal: goal, - onUpdateProgress: onUpdateProgress, - onEdit: onEdit, - onDelete: onDelete, - ), + shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(AppRadius.xl))), + builder: (context) => + ActiveGoalDetailsSheet(goal: goal, onUpdateProgress: onUpdateProgress, onEdit: onEdit, onDelete: onDelete), ); } @@ -65,9 +53,7 @@ class _ActiveGoalDetailsSheetState extends State { super.initState(); _currentProgress = widget.goal.current; _currentTarget = widget.goal.target; - _progressController = TextEditingController( - text: _currentProgress.toString(), - ); + _progressController = TextEditingController(text: _currentProgress.toString()); _targetController = TextEditingController(text: _currentTarget.toString()); } @@ -108,28 +94,17 @@ class _ActiveGoalDetailsSheetState extends State { color: colorScheme.primaryContainer, borderRadius: BorderRadius.circular(AppRadius.md), ), - child: Icon( - _getIconForType(widget.goal.type), - size: 24, - color: colorScheme.onPrimaryContainer, - ), + child: Icon(_getIconForType(widget.goal.type), size: 24, color: colorScheme.onPrimaryContainer), ), const SizedBox(width: Spacing.md), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - widget.goal.description, - style: textTheme.titleLarge?.copyWith( - fontWeight: FontWeight.bold, - ), - ), + Text(widget.goal.description, style: textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold)), Text( _getGoalTypeLabel(), - style: textTheme.bodyMedium?.copyWith( - color: colorScheme.onSurfaceVariant, - ), + style: textTheme.bodyMedium?.copyWith(color: colorScheme.onSurfaceVariant), ), ], ), @@ -155,22 +130,10 @@ class _ActiveGoalDetailsSheetState extends State { ), child: Column( children: [ - _buildInfoRow( - context, - icon: _getRecurrenceIcon(), - label: 'Type', - value: widget.goal.recurrenceLabel, - ), + _buildInfoRow(context, icon: _getRecurrenceIcon(), label: 'Type', value: widget.goal.recurrenceLabel), const Divider(height: Spacing.lg), - _buildInfoRow( - context, - icon: Icons.date_range, - label: 'Period', - value: _getPeriodDescription(), - ), - if (widget.goal.isDaily && - widget.goal.isRecurring && - widget.goal.streak > 0) ...[ + _buildInfoRow(context, icon: Icons.date_range, label: 'Period', value: _getPeriodDescription()), + if (widget.goal.isDaily && widget.goal.isRecurring && widget.goal.streak > 0) ...[ const Divider(height: Spacing.lg), _buildInfoRow( context, @@ -200,9 +163,7 @@ class _ActiveGoalDetailsSheetState extends State { foregroundColor: colorScheme.error, side: BorderSide(color: colorScheme.error), padding: const EdgeInsets.symmetric(vertical: Spacing.md), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(AppRadius.full), - ), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(AppRadius.full)), ), ), ), @@ -214,9 +175,7 @@ class _ActiveGoalDetailsSheetState extends State { label: const Text('Done'), style: FilledButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: Spacing.md), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(AppRadius.full), - ), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(AppRadius.full)), ), ), ), @@ -228,11 +187,7 @@ class _ActiveGoalDetailsSheetState extends State { ); } - Widget _buildProgressSection( - BuildContext context, - ColorScheme colorScheme, - TextTheme textTheme, - ) { + Widget _buildProgressSection(BuildContext context, ColorScheme colorScheme, TextTheme textTheme) { return Container( padding: const EdgeInsets.all(Spacing.md), decoration: BoxDecoration( @@ -246,20 +201,13 @@ class _ActiveGoalDetailsSheetState extends State { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text( - 'Progress', - style: textTheme.titleSmall?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), + Text('Progress', style: textTheme.titleSmall?.copyWith(color: colorScheme.onSurfaceVariant)), if (!_isEditingProgress) TextButton.icon( onPressed: () => setState(() => _isEditingProgress = true), icon: const Icon(Icons.edit, size: 16), label: const Text('Update'), - style: TextButton.styleFrom( - visualDensity: VisualDensity.compact, - ), + style: TextButton.styleFrom(visualDensity: VisualDensity.compact), ), ], ), @@ -290,22 +238,14 @@ class _ActiveGoalDetailsSheetState extends State { autofocus: true, decoration: InputDecoration( isDense: true, - contentPadding: const EdgeInsets.symmetric( - horizontal: Spacing.sm, - vertical: Spacing.sm, - ), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(AppRadius.sm), - ), + contentPadding: const EdgeInsets.symmetric(horizontal: Spacing.sm, vertical: Spacing.sm), + border: OutlineInputBorder(borderRadius: BorderRadius.circular(AppRadius.sm)), suffixText: 'of $_currentTarget', ), ), ), const SizedBox(width: Spacing.sm), - IconButton.filled( - onPressed: _saveProgress, - icon: const Icon(Icons.check, size: 20), - ), + IconButton.filled(onPressed: _saveProgress, icon: const Icon(Icons.check, size: 20)), const SizedBox(width: Spacing.xs), IconButton.outlined( onPressed: () { @@ -321,10 +261,7 @@ class _ActiveGoalDetailsSheetState extends State { ] else ...[ Builder( builder: (context) { - final progress = (_currentProgress / _currentTarget).clamp( - 0.0, - 1.0, - ); + final progress = (_currentProgress / _currentTarget).clamp(0.0, 1.0); final isCompleted = _currentProgress >= _currentTarget; final progressLabel = '${(progress * 100).round()}%'; @@ -338,9 +275,7 @@ class _ActiveGoalDetailsSheetState extends State { widget.goal.type == GoalType.minutes ? '${formatDuration(_currentProgress)} of ${formatDuration(_currentTarget)}' : '$_currentProgress of $_currentTarget ${widget.goal.typeLabel}', - style: textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.bold, - ), + style: textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold), ), const SizedBox(height: Spacing.xs), ClipRRect( @@ -348,11 +283,8 @@ class _ActiveGoalDetailsSheetState extends State { child: LinearProgressIndicator( value: progress, minHeight: 8, - backgroundColor: - colorScheme.surfaceContainerHighest, - color: isCompleted - ? colorScheme.tertiary - : colorScheme.primary, + backgroundColor: colorScheme.surfaceContainerHighest, + color: isCompleted ? colorScheme.tertiary : colorScheme.primary, ), ), ], @@ -363,9 +295,7 @@ class _ActiveGoalDetailsSheetState extends State { progressLabel, style: textTheme.headlineSmall?.copyWith( fontWeight: FontWeight.bold, - color: isCompleted - ? colorScheme.tertiary - : colorScheme.primary, + color: isCompleted ? colorScheme.tertiary : colorScheme.primary, ), ), ], @@ -378,11 +308,7 @@ class _ActiveGoalDetailsSheetState extends State { ); } - Widget _buildTargetSection( - BuildContext context, - ColorScheme colorScheme, - TextTheme textTheme, - ) { + Widget _buildTargetSection(BuildContext context, ColorScheme colorScheme, TextTheme textTheme) { return Container( padding: const EdgeInsets.all(Spacing.md), decoration: BoxDecoration( @@ -396,20 +322,13 @@ class _ActiveGoalDetailsSheetState extends State { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text( - 'Target', - style: textTheme.titleSmall?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), + Text('Target', style: textTheme.titleSmall?.copyWith(color: colorScheme.onSurfaceVariant)), if (!_isEditingTarget) TextButton.icon( onPressed: () => setState(() => _isEditingTarget = true), icon: const Icon(Icons.edit, size: 16), label: const Text('Edit'), - style: TextButton.styleFrom( - visualDensity: VisualDensity.compact, - ), + style: TextButton.styleFrom(visualDensity: VisualDensity.compact), ), ], ), @@ -442,31 +361,20 @@ class _ActiveGoalDetailsSheetState extends State { autofocus: true, decoration: InputDecoration( isDense: true, - contentPadding: const EdgeInsets.symmetric( - horizontal: Spacing.sm, - vertical: Spacing.sm, - ), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular( - AppRadius.sm, - ), - ), + contentPadding: const EdgeInsets.symmetric(horizontal: Spacing.sm, vertical: Spacing.sm), + border: OutlineInputBorder(borderRadius: BorderRadius.circular(AppRadius.sm)), suffixText: widget.goal.typeLabel, ), ), ), const SizedBox(width: Spacing.sm), - IconButton.filled( - onPressed: _saveTarget, - icon: const Icon(Icons.check, size: 20), - ), + IconButton.filled(onPressed: _saveTarget, icon: const Icon(Icons.check, size: 20)), const SizedBox(width: Spacing.xs), IconButton.outlined( onPressed: () { setState(() { _isEditingTarget = false; - _targetController.text = _currentTarget - .toString(); + _targetController.text = _currentTarget.toString(); }); }, icon: const Icon(Icons.close, size: 20), @@ -483,9 +391,7 @@ class _ActiveGoalDetailsSheetState extends State { widget.goal.type == GoalType.minutes ? formatDuration(_currentTarget) : '$_currentTarget ${widget.goal.typeLabel}', - style: textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.bold, - ), + style: textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold), ), ), ), @@ -508,19 +414,11 @@ class _ActiveGoalDetailsSheetState extends State { children: [ Icon(icon, size: 20, color: colorScheme.onSurfaceVariant), const SizedBox(width: Spacing.sm), - Text( - label, - style: textTheme.bodyMedium?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), + Text(label, style: textTheme.bodyMedium?.copyWith(color: colorScheme.onSurfaceVariant)), const Spacer(), Text( value, - style: textTheme.bodyMedium?.copyWith( - fontWeight: FontWeight.w600, - color: valueColor, - ), + style: textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w600, color: valueColor), ), ], ); @@ -540,10 +438,7 @@ class _ActiveGoalDetailsSheetState extends State { children: [ Expanded( child: Container( - padding: const EdgeInsets.symmetric( - horizontal: Spacing.sm, - vertical: Spacing.xs, - ), + padding: const EdgeInsets.symmetric(horizontal: Spacing.sm, vertical: Spacing.xs), decoration: BoxDecoration( border: Border.all(color: colorScheme.outline), borderRadius: BorderRadius.circular(AppRadius.sm), @@ -561,12 +456,7 @@ class _ActiveGoalDetailsSheetState extends State { icon: const Icon(Icons.remove, size: 20), visualDensity: VisualDensity.compact, ), - Text( - formatDuration(value), - style: textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.bold, - ), - ), + Text(formatDuration(value), style: textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold)), IconButton( onPressed: () { final step = value >= 60 ? 15 : 5; @@ -580,15 +470,9 @@ class _ActiveGoalDetailsSheetState extends State { ), ), const SizedBox(width: Spacing.sm), - IconButton.filled( - onPressed: onSave, - icon: const Icon(Icons.check, size: 20), - ), + IconButton.filled(onPressed: onSave, icon: const Icon(Icons.check, size: 20)), const SizedBox(width: Spacing.xs), - IconButton.outlined( - onPressed: onCancel, - icon: const Icon(Icons.close, size: 20), - ), + IconButton.outlined(onPressed: onCancel, icon: const Icon(Icons.close, size: 20)), ], ); } @@ -622,14 +506,9 @@ class _ActiveGoalDetailsSheetState extends State { context: context, builder: (context) => AlertDialog( title: const Text('Delete goal?'), - content: Text( - 'This will permanently remove "${widget.goal.description}" from your goals.', - ), + content: Text('This will permanently remove "${widget.goal.description}" from your goals.'), actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: const Text('Cancel'), - ), + TextButton(onPressed: () => Navigator.of(context).pop(), child: const Text('Cancel')), FilledButton( onPressed: () { Navigator.of(context).pop(); @@ -688,20 +567,7 @@ class _ActiveGoalDetailsSheetState extends State { } String _formatDate(DateTime date) { - const months = [ - 'Jan', - 'Feb', - 'Mar', - 'Apr', - 'May', - 'Jun', - 'Jul', - 'Aug', - 'Sep', - 'Oct', - 'Nov', - 'Dec', - ]; + const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; return '${months[date.month - 1]} ${date.day}, ${date.year}'; } diff --git a/app/lib/widgets/goals/add_goal_card.dart b/app/lib/widgets/goals/add_goal_card.dart index 54f1a46..f8ec5ff 100644 --- a/app/lib/widgets/goals/add_goal_card.dart +++ b/app/lib/widgets/goals/add_goal_card.dart @@ -23,10 +23,7 @@ class AddGoalCard extends StatelessWidget { padding: EdgeInsets.all(isDesktop ? Spacing.lg : Spacing.md), decoration: BoxDecoration( color: colorScheme.surfaceContainerLowest, - border: Border.all( - color: colorScheme.outline.withValues(alpha: 0.4), - width: 1.5, - ), + border: Border.all(color: colorScheme.outline.withValues(alpha: 0.4), width: 1.5), borderRadius: BorderRadius.circular(AppRadius.xl), ), child: Column( @@ -35,29 +32,15 @@ class AddGoalCard extends StatelessWidget { Container( width: 56, height: 56, - decoration: BoxDecoration( - color: colorScheme.primaryContainer, - shape: BoxShape.circle, - ), - child: Icon( - Icons.add, - size: 28, - color: colorScheme.onPrimaryContainer, - ), + decoration: BoxDecoration(color: colorScheme.primaryContainer, shape: BoxShape.circle), + child: Icon(Icons.add, size: 28, color: colorScheme.onPrimaryContainer), ), const SizedBox(height: Spacing.md), - Text( - 'Add new goal', - style: textTheme.titleMedium?.copyWith( - color: colorScheme.primary, - ), - ), + Text('Add new goal', style: textTheme.titleMedium?.copyWith(color: colorScheme.primary)), const SizedBox(height: Spacing.xs), Text( 'Create custom goals\nto track progress', - style: textTheme.bodySmall?.copyWith( - color: colorScheme.onSurfaceVariant, - ), + style: textTheme.bodySmall?.copyWith(color: colorScheme.onSurfaceVariant), textAlign: TextAlign.center, ), ], diff --git a/app/lib/widgets/goals/add_goal_sheet.dart b/app/lib/widgets/goals/add_goal_sheet.dart index 03fd858..80384bd 100644 --- a/app/lib/widgets/goals/add_goal_sheet.dart +++ b/app/lib/widgets/goals/add_goal_sheet.dart @@ -41,9 +41,7 @@ class AddGoalSheet extends StatefulWidget { return showModalBottomSheet( context: context, isScrollControlled: true, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(AppRadius.xl)), - ), + shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(AppRadius.xl))), builder: (context) => AddGoalSheet(onCreate: onCreate), ); } @@ -94,12 +92,7 @@ class _AddGoalSheetState extends State { const SizedBox(height: Spacing.lg), // Schedule type selection - Text( - 'Goal type', - style: textTheme.titleSmall?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), + Text('Goal type', style: textTheme.titleSmall?.copyWith(color: colorScheme.onSurfaceVariant)), const SizedBox(height: Spacing.sm), SegmentedButton( segments: const [ @@ -136,18 +129,12 @@ class _AddGoalSheetState extends State { ), child: Row( children: [ - Icon( - Icons.info_outline, - size: 16, - color: colorScheme.onSurfaceVariant, - ), + Icon(Icons.info_outline, size: 16, color: colorScheme.onSurfaceVariant), const SizedBox(width: Spacing.sm), Expanded( child: Text( _getScheduleDescription(), - style: textTheme.bodySmall?.copyWith( - color: colorScheme.onSurfaceVariant, - ), + style: textTheme.bodySmall?.copyWith(color: colorScheme.onSurfaceVariant), ), ), ], @@ -156,29 +143,16 @@ class _AddGoalSheetState extends State { const SizedBox(height: Spacing.lg), // What to track - Text( - 'What to track', - style: textTheme.titleSmall?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), + Text('What to track', style: textTheme.titleSmall?.copyWith(color: colorScheme.onSurfaceVariant)), const SizedBox(height: Spacing.sm), DropdownButtonFormField( initialValue: _selectedType, decoration: InputDecoration( - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(AppRadius.md), - ), - contentPadding: const EdgeInsets.symmetric( - horizontal: Spacing.md, - vertical: Spacing.sm, - ), + border: OutlineInputBorder(borderRadius: BorderRadius.circular(AppRadius.md)), + contentPadding: const EdgeInsets.symmetric(horizontal: Spacing.md, vertical: Spacing.sm), ), items: GoalType.values.map((type) { - return DropdownMenuItem( - value: type, - child: Text(_getTypeLabel(type)), - ); + return DropdownMenuItem(value: type, child: Text(_getTypeLabel(type))); }).toList(), onChanged: (value) { if (value != null) { @@ -193,12 +167,7 @@ class _AddGoalSheetState extends State { const SizedBox(height: Spacing.lg), // Target - Text( - 'Target', - style: textTheme.titleSmall?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), + Text('Target', style: textTheme.titleSmall?.copyWith(color: colorScheme.onSurfaceVariant)), const SizedBox(height: Spacing.sm), if (_selectedType == GoalType.minutes) _buildDurationPicker(colorScheme, textTheme) @@ -207,13 +176,8 @@ class _AddGoalSheetState extends State { controller: _targetController, keyboardType: TextInputType.number, decoration: InputDecoration( - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(AppRadius.md), - ), - contentPadding: const EdgeInsets.symmetric( - horizontal: Spacing.md, - vertical: Spacing.sm, - ), + border: OutlineInputBorder(borderRadius: BorderRadius.circular(AppRadius.md)), + contentPadding: const EdgeInsets.symmetric(horizontal: Spacing.md, vertical: Spacing.sm), suffixText: _getTypeSuffix(_selectedType), ), ), @@ -221,26 +185,12 @@ class _AddGoalSheetState extends State { // Period selection (for recurring and one-off) if (_scheduleType != GoalScheduleType.custom) ...[ - Text( - 'Time period', - style: textTheme.titleSmall?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), + Text('Time period', style: textTheme.titleSmall?.copyWith(color: colorScheme.onSurfaceVariant)), const SizedBox(height: Spacing.sm), SegmentedButton( - segments: - [ - GoalPeriod.daily, - GoalPeriod.weekly, - GoalPeriod.monthly, - GoalPeriod.yearly, - ].map((period) { - return ButtonSegment( - value: period, - label: Text(_getPeriodLabel(period)), - ); - }).toList(), + segments: [GoalPeriod.daily, GoalPeriod.weekly, GoalPeriod.monthly, GoalPeriod.yearly].map((period) { + return ButtonSegment(value: period, label: Text(_getPeriodLabel(period))); + }).toList(), selected: {_selectedPeriod}, onSelectionChanged: (selected) { setState(() => _selectedPeriod = selected.first); @@ -252,12 +202,7 @@ class _AddGoalSheetState extends State { // Custom date range picker if (_scheduleType == GoalScheduleType.custom) ...[ - Text( - 'Date range', - style: textTheme.titleSmall?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), + Text('Date range', style: textTheme.titleSmall?.copyWith(color: colorScheme.onSurfaceVariant)), const SizedBox(height: Spacing.sm), Row( children: [ @@ -271,21 +216,14 @@ class _AddGoalSheetState extends State { ), const SizedBox(width: Spacing.md), Expanded( - child: _buildDateButton( - context, - label: 'End', - date: _endDate, - onTap: () => _pickEndDate(context), - ), + child: _buildDateButton(context, label: 'End', date: _endDate, onTap: () => _pickEndDate(context)), ), ], ), const SizedBox(height: Spacing.sm), Text( '${_daysBetween()} days total', - style: textTheme.bodySmall?.copyWith( - color: colorScheme.onSurfaceVariant, - ), + style: textTheme.bodySmall?.copyWith(color: colorScheme.onSurfaceVariant), ), const SizedBox(height: Spacing.lg), ], @@ -330,10 +268,7 @@ class _AddGoalSheetState extends State { const SizedBox(height: Spacing.md), // Stepper row Container( - padding: const EdgeInsets.symmetric( - horizontal: Spacing.md, - vertical: Spacing.sm, - ), + padding: const EdgeInsets.symmetric(horizontal: Spacing.md, vertical: Spacing.sm), decoration: BoxDecoration( border: Border.all(color: colorScheme.outline), borderRadius: BorderRadius.circular(AppRadius.md), @@ -346,10 +281,7 @@ class _AddGoalSheetState extends State { ? () { setState(() { final step = _durationMinutes > 60 ? 15 : 5; - _durationMinutes = (_durationMinutes - step).clamp( - 5, - _durationMinutes, - ); + _durationMinutes = (_durationMinutes - step).clamp(5, _durationMinutes); }); } : null, @@ -358,9 +290,7 @@ class _AddGoalSheetState extends State { const SizedBox(width: Spacing.md), Text( formatDuration(_durationMinutes), - style: textTheme.titleLarge?.copyWith( - fontWeight: FontWeight.bold, - ), + style: textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold), ), const SizedBox(width: Spacing.md), IconButton( @@ -400,20 +330,11 @@ class _AddGoalSheetState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - label, - style: textTheme.labelSmall?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), + Text(label, style: textTheme.labelSmall?.copyWith(color: colorScheme.onSurfaceVariant)), const SizedBox(height: Spacing.xs), Row( children: [ - Icon( - Icons.calendar_today, - size: 16, - color: colorScheme.primary, - ), + Icon(Icons.calendar_today, size: 16, color: colorScheme.primary), const SizedBox(width: Spacing.sm), Text(_formatDate(date), style: textTheme.bodyMedium), ], @@ -455,9 +376,7 @@ class _AddGoalSheetState extends State { } void _onCreate() { - final target = _selectedType == GoalType.minutes - ? _durationMinutes - : (int.tryParse(_targetController.text) ?? 0); + final target = _selectedType == GoalType.minutes ? _durationMinutes : (int.tryParse(_targetController.text) ?? 0); if (target <= 0) return; final GoalPeriod period; @@ -482,14 +401,7 @@ class _AddGoalSheetState extends State { break; } - widget.onCreate?.call( - _selectedType, - target, - period, - isRecurring, - startDate, - endDate, - ); + widget.onCreate?.call(_selectedType, target, period, isRecurring, startDate, endDate); Navigator.of(context).pop(); } @@ -553,20 +465,7 @@ class _AddGoalSheetState extends State { } String _formatDate(DateTime date) { - const months = [ - 'Jan', - 'Feb', - 'Mar', - 'Apr', - 'May', - 'Jun', - 'Jul', - 'Aug', - 'Sep', - 'Oct', - 'Nov', - 'Dec', - ]; + const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; return '${months[date.month - 1]} ${date.day}, ${date.year}'; } diff --git a/app/lib/widgets/goals/completed_goal_chip.dart b/app/lib/widgets/goals/completed_goal_chip.dart index a84cc97..5701c70 100644 --- a/app/lib/widgets/goals/completed_goal_chip.dart +++ b/app/lib/widgets/goals/completed_goal_chip.dart @@ -17,13 +17,7 @@ class CompletedGoalChip extends StatelessWidget { /// Whether to use expanded card layout (for desktop). final bool isExpanded; - const CompletedGoalChip({ - super.key, - required this.goal, - this.onTap, - this.onDelete, - this.isExpanded = false, - }); + const CompletedGoalChip({super.key, required this.goal, this.onTap, this.onDelete, this.isExpanded = false}); @override Widget build(BuildContext context) { @@ -59,41 +53,24 @@ class CompletedGoalChip extends StatelessWidget { Container( width: 24, height: 24, - decoration: BoxDecoration( - color: colorScheme.tertiary, - shape: BoxShape.circle, - ), - child: Icon( - Icons.check, - size: 14, - color: colorScheme.onTertiary, - ), + decoration: BoxDecoration(color: colorScheme.tertiary, shape: BoxShape.circle), + child: Icon(Icons.check, size: 14, color: colorScheme.onTertiary), ), const SizedBox(width: Spacing.xs), Expanded( child: Text( _getGoalTypeLabel(), - style: textTheme.labelSmall?.copyWith( - color: colorScheme.onSurfaceVariant, - ), + style: textTheme.labelSmall?.copyWith(color: colorScheme.onSurfaceVariant), ), ), - Icon( - Icons.chevron_right, - size: 16, - color: colorScheme.onSurfaceVariant, - ), + Icon(Icons.chevron_right, size: 16, color: colorScheme.onSurfaceVariant), ], ), const SizedBox(height: Spacing.sm), // Target achieved Text( - goal.type == GoalType.minutes - ? formatDuration(goal.target) - : '${goal.target} ${goal.typeLabel}', - style: textTheme.titleSmall?.copyWith( - fontWeight: FontWeight.w600, - ), + goal.type == GoalType.minutes ? formatDuration(goal.target) : '${goal.target} ${goal.typeLabel}', + style: textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w600), maxLines: 1, overflow: TextOverflow.ellipsis, ), @@ -101,17 +78,11 @@ class CompletedGoalChip extends StatelessWidget { // Completion info Row( children: [ - Icon( - Icons.calendar_today, - size: 12, - color: colorScheme.onSurfaceVariant, - ), + Icon(Icons.calendar_today, size: 12, color: colorScheme.onSurfaceVariant), const SizedBox(width: 4), Text( _getCompletionDateLabel(), - style: textTheme.labelSmall?.copyWith( - color: colorScheme.onSurfaceVariant, - ), + style: textTheme.labelSmall?.copyWith(color: colorScheme.onSurfaceVariant), ), ], ), @@ -152,15 +123,8 @@ class CompletedGoalChip extends StatelessWidget { Container( width: 28, height: 28, - decoration: BoxDecoration( - color: colorScheme.tertiary, - shape: BoxShape.circle, - ), - child: Icon( - Icons.check, - size: 16, - color: colorScheme.onTertiary, - ), + decoration: BoxDecoration(color: colorScheme.tertiary, shape: BoxShape.circle), + child: Icon(Icons.check, size: 16, color: colorScheme.onTertiary), ), const SizedBox(width: Spacing.sm), Expanded( @@ -171,15 +135,11 @@ class CompletedGoalChip extends StatelessWidget { goal.type == GoalType.minutes ? formatDuration(goal.target) : '${goal.target} ${goal.typeLabel}', - style: textTheme.titleSmall?.copyWith( - fontWeight: FontWeight.w600, - ), + style: textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w600), ), Text( _getGoalTypeLabel(), - style: textTheme.labelSmall?.copyWith( - color: colorScheme.onSurfaceVariant, - ), + style: textTheme.labelSmall?.copyWith(color: colorScheme.onSurfaceVariant), ), ], ), @@ -190,17 +150,9 @@ class CompletedGoalChip extends StatelessWidget { // Details row Row( children: [ - _buildInfoChip( - context, - icon: Icons.calendar_today, - label: _getCompletionDateLabel(), - ), + _buildInfoChip(context, icon: Icons.calendar_today, label: _getCompletionDateLabel()), const SizedBox(width: Spacing.sm), - _buildInfoChip( - context, - icon: _getRecurrenceIcon(), - label: goal.recurrenceLabel, - ), + _buildInfoChip(context, icon: _getRecurrenceIcon(), label: goal.recurrenceLabel), ], ), const SizedBox(height: Spacing.sm), @@ -208,18 +160,9 @@ class CompletedGoalChip extends StatelessWidget { Row( mainAxisAlignment: MainAxisAlignment.end, children: [ - Text( - 'Tap for details', - style: textTheme.labelSmall?.copyWith( - color: colorScheme.primary, - ), - ), + Text('Tap for details', style: textTheme.labelSmall?.copyWith(color: colorScheme.primary)), const SizedBox(width: 4), - Icon( - Icons.arrow_forward, - size: 12, - color: colorScheme.primary, - ), + Icon(Icons.arrow_forward, size: 12, color: colorScheme.primary), ], ), ], @@ -229,11 +172,7 @@ class CompletedGoalChip extends StatelessWidget { ); } - Widget _buildInfoChip( - BuildContext context, { - required IconData icon, - required String label, - }) { + Widget _buildInfoChip(BuildContext context, {required IconData icon, required String label}) { final colorScheme = Theme.of(context).colorScheme; final textTheme = Theme.of(context).textTheme; @@ -248,13 +187,7 @@ class CompletedGoalChip extends StatelessWidget { children: [ Icon(icon, size: 10, color: colorScheme.onSurfaceVariant), const SizedBox(width: 4), - Text( - label, - style: textTheme.labelSmall?.copyWith( - color: colorScheme.onSurfaceVariant, - fontSize: 10, - ), - ), + Text(label, style: textTheme.labelSmall?.copyWith(color: colorScheme.onSurfaceVariant, fontSize: 10)), ], ), ); @@ -268,18 +201,11 @@ class CompletedGoalChip extends StatelessWidget { /// /// Can be called externally to show details for a completed goal /// without needing a [CompletedGoalChip] instance. - static void showDetailsSheet( - BuildContext context, { - required ReadingGoal goal, - VoidCallback? onDelete, - }) { + static void showDetailsSheet(BuildContext context, {required ReadingGoal goal, VoidCallback? onDelete}) { showModalBottomSheet( context: context, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(AppRadius.xl)), - ), - builder: (context) => - _CompletedGoalDetailsSheet(goal: goal, onDelete: onDelete), + shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(AppRadius.xl))), + builder: (context) => _CompletedGoalDetailsSheet(goal: goal, onDelete: onDelete), ); } @@ -324,20 +250,7 @@ class CompletedGoalChip extends StatelessWidget { } String _formatShortDate(DateTime date) { - const months = [ - 'Jan', - 'Feb', - 'Mar', - 'Apr', - 'May', - 'Jun', - 'Jul', - 'Aug', - 'Sep', - 'Oct', - 'Nov', - 'Dec', - ]; + const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; return '${months[date.month - 1]} ${date.year}'; } @@ -379,33 +292,16 @@ class _CompletedGoalDetailsSheet extends StatelessWidget { Container( width: 48, height: 48, - decoration: BoxDecoration( - color: colorScheme.tertiaryContainer, - shape: BoxShape.circle, - ), - child: Icon( - Icons.emoji_events, - size: 24, - color: colorScheme.onTertiaryContainer, - ), + decoration: BoxDecoration(color: colorScheme.tertiaryContainer, shape: BoxShape.circle), + child: Icon(Icons.emoji_events, size: 24, color: colorScheme.onTertiaryContainer), ), const SizedBox(width: Spacing.md), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - 'Goal completed!', - style: textTheme.titleLarge?.copyWith( - fontWeight: FontWeight.bold, - ), - ), - Text( - goal.description, - style: textTheme.bodyMedium?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), + Text('Goal completed!', style: textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold)), + Text(goal.description, style: textTheme.bodyMedium?.copyWith(color: colorScheme.onSurfaceVariant)), ], ), ), @@ -446,19 +342,9 @@ class _CompletedGoalDetailsSheet extends StatelessWidget { value: _getFullGoalTypeLabel(), ), const Divider(height: Spacing.lg), - _buildDetailRow( - context, - icon: Icons.date_range, - label: 'Period', - value: _getPeriodDescription(), - ), + _buildDetailRow(context, icon: Icons.date_range, label: 'Period', value: _getPeriodDescription()), const Divider(height: Spacing.lg), - _buildDetailRow( - context, - icon: Icons.check_circle, - label: 'Completed on', - value: _getCompletionDate(), - ), + _buildDetailRow(context, icon: Icons.check_circle, label: 'Completed on', value: _getCompletionDate()), ], ), ), @@ -501,19 +387,11 @@ class _CompletedGoalDetailsSheet extends StatelessWidget { children: [ Icon(icon, size: 20, color: colorScheme.onSurfaceVariant), const SizedBox(width: Spacing.sm), - Text( - label, - style: textTheme.bodyMedium?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), + Text(label, style: textTheme.bodyMedium?.copyWith(color: colorScheme.onSurfaceVariant)), const Spacer(), Text( value, - style: textTheme.bodyMedium?.copyWith( - fontWeight: FontWeight.w600, - color: valueColor, - ), + style: textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w600, color: valueColor), ), ], ); @@ -526,14 +404,9 @@ class _CompletedGoalDetailsSheet extends StatelessWidget { context: context, builder: (context) => AlertDialog( title: const Text('Delete goal?'), - content: Text( - 'This will permanently remove "${goal.description}" from your completed goals.', - ), + content: Text('This will permanently remove "${goal.description}" from your completed goals.'), actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: const Text('Cancel'), - ), + TextButton(onPressed: () => Navigator.of(context).pop(), child: const Text('Cancel')), FilledButton( onPressed: () { Navigator.of(context).pop(); @@ -597,20 +470,7 @@ class _CompletedGoalDetailsSheet extends StatelessWidget { } String _formatDate(DateTime date) { - const months = [ - 'Jan', - 'Feb', - 'Mar', - 'Apr', - 'May', - 'Jun', - 'Jul', - 'Aug', - 'Sep', - 'Oct', - 'Nov', - 'Dec', - ]; + const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; return '${months[date.month - 1]} ${date.day}, ${date.year}'; } diff --git a/app/lib/widgets/goals/goal_card.dart b/app/lib/widgets/goals/goal_card.dart index a968686..ae48849 100644 --- a/app/lib/widgets/goals/goal_card.dart +++ b/app/lib/widgets/goals/goal_card.dart @@ -16,12 +16,7 @@ class GoalCard extends StatelessWidget { /// Whether to use desktop styling. final bool isDesktop; - const GoalCard({ - super.key, - required this.goal, - this.onTap, - this.isDesktop = false, - }); + const GoalCard({super.key, required this.goal, this.onTap, this.isDesktop = false}); @override Widget build(BuildContext context) { @@ -62,11 +57,7 @@ class GoalCard extends StatelessWidget { // ============================================================================ /// Builds the header with icon, title, and optional badges. - Widget _buildHeader( - BuildContext context, - ColorScheme colorScheme, - TextTheme textTheme, - ) { + Widget _buildHeader(BuildContext context, ColorScheme colorScheme, TextTheme textTheme) { return Row( children: [ Container( @@ -76,11 +67,7 @@ class GoalCard extends StatelessWidget { color: colorScheme.primaryContainer, borderRadius: BorderRadius.circular(AppRadius.md), ), - child: Icon( - _getIconForType(goal.type), - size: 20, - color: colorScheme.onPrimaryContainer, - ), + child: Icon(_getIconForType(goal.type), size: 20, color: colorScheme.onPrimaryContainer), ), const SizedBox(width: Spacing.sm), Expanded( @@ -99,10 +86,7 @@ class GoalCard extends StatelessWidget { const SizedBox(width: Spacing.sm), _buildOneOffBadge(colorScheme, textTheme), ], - if (goal.isCustomPeriod) ...[ - const SizedBox(width: Spacing.sm), - _buildDateRangeBadge(colorScheme, textTheme), - ], + if (goal.isCustomPeriod) ...[const SizedBox(width: Spacing.sm), _buildDateRangeBadge(colorScheme, textTheme)], if (goal.isCompleted) ...[ const SizedBox(width: Spacing.sm), Icon(Icons.check_circle, size: 24, color: colorScheme.tertiary), @@ -122,18 +106,11 @@ class GoalCard extends StatelessWidget { child: Row( mainAxisSize: MainAxisSize.min, children: [ - Icon( - Icons.local_fire_department, - size: 14, - color: colorScheme.onTertiaryContainer, - ), + Icon(Icons.local_fire_department, size: 14, color: colorScheme.onTertiaryContainer), const SizedBox(width: 4), Text( '${goal.streak}', - style: textTheme.labelSmall?.copyWith( - color: colorScheme.onTertiaryContainer, - fontWeight: FontWeight.bold, - ), + style: textTheme.labelSmall?.copyWith(color: colorScheme.onTertiaryContainer, fontWeight: FontWeight.bold), ), ], ), @@ -151,18 +128,11 @@ class GoalCard extends StatelessWidget { child: Row( mainAxisSize: MainAxisSize.min, children: [ - Icon( - Icons.date_range, - size: 14, - color: colorScheme.onSecondaryContainer, - ), + Icon(Icons.date_range, size: 14, color: colorScheme.onSecondaryContainer), const SizedBox(width: 4), Text( _formatDateRange(), - style: textTheme.labelSmall?.copyWith( - color: colorScheme.onSecondaryContainer, - fontWeight: FontWeight.w500, - ), + style: textTheme.labelSmall?.copyWith(color: colorScheme.onSecondaryContainer, fontWeight: FontWeight.w500), ), ], ), @@ -170,20 +140,7 @@ class GoalCard extends StatelessWidget { } String _formatDateRange() { - const months = [ - 'Jan', - 'Feb', - 'Mar', - 'Apr', - 'May', - 'Jun', - 'Jul', - 'Aug', - 'Sep', - 'Oct', - 'Nov', - 'Dec', - ]; + const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; final start = '${months[goal.startDate.month - 1]} ${goal.startDate.day}'; final end = '${months[goal.endDate.month - 1]} ${goal.endDate.day}'; return '$start - $end'; @@ -199,20 +156,13 @@ class GoalCard extends StatelessWidget { ), child: Text( 'One-off', - style: textTheme.labelSmall?.copyWith( - color: colorScheme.onSecondaryContainer, - fontWeight: FontWeight.w500, - ), + style: textTheme.labelSmall?.copyWith(color: colorScheme.onSecondaryContainer, fontWeight: FontWeight.w500), ), ); } /// Builds the progress values row showing current/target and percentage. - Widget _buildProgressValues( - BuildContext context, - ColorScheme colorScheme, - TextTheme textTheme, - ) { + Widget _buildProgressValues(BuildContext context, ColorScheme colorScheme, TextTheme textTheme) { return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -220,17 +170,13 @@ class GoalCard extends StatelessWidget { goal.type == GoalType.minutes ? '${formatDuration(goal.current)} of ${formatDuration(goal.target)}' : '${goal.current} of ${goal.target} ${goal.typeLabel}', - style: textTheme.bodyMedium?.copyWith( - color: colorScheme.onSurfaceVariant, - ), + style: textTheme.bodyMedium?.copyWith(color: colorScheme.onSurfaceVariant), ), Text( goal.progressLabel, style: textTheme.titleMedium?.copyWith( fontWeight: FontWeight.bold, - color: (goal.isCompleted && goal.isArchived) - ? colorScheme.tertiary - : colorScheme.primary, + color: (goal.isCompleted && goal.isArchived) ? colorScheme.tertiary : colorScheme.primary, ), ), ], @@ -245,9 +191,7 @@ class GoalCard extends StatelessWidget { value: goal.progress.clamp(0.0, 1.0), minHeight: 8, backgroundColor: colorScheme.surfaceContainerHighest, - color: (goal.isCompleted && goal.isArchived) - ? colorScheme.tertiary - : colorScheme.primary, + color: (goal.isCompleted && goal.isArchived) ? colorScheme.tertiary : colorScheme.primary, ), ); } @@ -259,27 +203,18 @@ class GoalCard extends StatelessWidget { children: [ Text( goal.isCompleted - ? (goal.isRecurring && !goal.isArchived - ? "Today's goal met" - : 'Completed!') + ? (goal.isRecurring && !goal.isArchived ? "Today's goal met" : 'Completed!') : goal.type == GoalType.minutes ? '${formatDuration(goal.remaining)} to go' : '${goal.remaining} ${goal.typeLabel} to go', style: textTheme.bodySmall?.copyWith( color: goal.isCompleted - ? (goal.isRecurring && !goal.isArchived - ? colorScheme.primary - : colorScheme.tertiary) + ? (goal.isRecurring && !goal.isArchived ? colorScheme.primary : colorScheme.tertiary) : colorScheme.onSurfaceVariant, fontWeight: goal.isCompleted ? FontWeight.w500 : FontWeight.normal, ), ), - Text( - _getTimeContext(), - style: textTheme.bodySmall?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), + Text(_getTimeContext(), style: textTheme.bodySmall?.copyWith(color: colorScheme.onSurfaceVariant)), ], ); } @@ -308,38 +243,12 @@ class GoalCard extends StatelessWidget { } String _formatDate(DateTime date) { - const months = [ - 'Jan', - 'Feb', - 'Mar', - 'Apr', - 'May', - 'Jun', - 'Jul', - 'Aug', - 'Sep', - 'Oct', - 'Nov', - 'Dec', - ]; + const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; return '${months[date.month - 1]} ${date.day}, ${date.year}'; } String _formatShortDate(DateTime date) { - const months = [ - 'Jan', - 'Feb', - 'Mar', - 'Apr', - 'May', - 'Jun', - 'Jul', - 'Aug', - 'Sep', - 'Oct', - 'Nov', - 'Dec', - ]; + const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; return '${months[date.month - 1]} ${date.day}'; } diff --git a/app/lib/widgets/heading.dart b/app/lib/widgets/heading.dart index 239ccc7..ba12852 100644 --- a/app/lib/widgets/heading.dart +++ b/app/lib/widgets/heading.dart @@ -14,10 +14,7 @@ class Heading extends StatelessWidget { Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - const Image( - image: AssetImage("assets/images/logo.png"), - height: 64.0, - ), + const Image(image: AssetImage("assets/images/logo.png"), height: 64.0), const SizedBox(width: 12.0), Text("Papyrus", style: Theme.of(context).textTheme.displayMedium), ], diff --git a/app/lib/widgets/input/email_input.dart b/app/lib/widgets/input/email_input.dart index b297304..8e43902 100644 --- a/app/lib/widgets/input/email_input.dart +++ b/app/lib/widgets/input/email_input.dart @@ -34,10 +34,7 @@ class EmailInput extends StatelessWidget { decoration: InputDecoration( border: const OutlineInputBorder(), labelText: labelText, - suffixIcon: Icon( - Icons.email_outlined, - color: theme.colorScheme.onSurfaceVariant, - ), + suffixIcon: Icon(Icons.email_outlined, color: theme.colorScheme.onSurfaceVariant), ), controller: controller, focusNode: focusNode, @@ -56,9 +53,7 @@ class EmailInput extends StatelessWidget { return 'This field is required'; } - if (!RegExp( - r"^[a-zA-Z0-9.a-zA-Z0-9.!#$%&'*+-/=?^_`{|}~]+@[a-zA-Z0-9]+\.[a-zA-Z]+", - ).hasMatch(value)) { + if (!RegExp(r"^[a-zA-Z0-9.a-zA-Z0-9.!#$%&'*+-/=?^_`{|}~]+@[a-zA-Z0-9]+\.[a-zA-Z]+").hasMatch(value)) { return 'Not a valid email'; } diff --git a/app/lib/widgets/input/name_input.dart b/app/lib/widgets/input/name_input.dart index bd9e284..4b2e42a 100644 --- a/app/lib/widgets/input/name_input.dart +++ b/app/lib/widgets/input/name_input.dart @@ -34,10 +34,7 @@ class NameInput extends StatelessWidget { decoration: InputDecoration( border: const OutlineInputBorder(), labelText: labelText, - suffixIcon: Icon( - Icons.person_outline, - color: theme.colorScheme.onSurfaceVariant, - ), + suffixIcon: Icon(Icons.person_outline, color: theme.colorScheme.onSurfaceVariant), ), controller: controller, focusNode: focusNode, diff --git a/app/lib/widgets/input/password_input.dart b/app/lib/widgets/input/password_input.dart index 0c4e8f2..df083ae 100644 --- a/app/lib/widgets/input/password_input.dart +++ b/app/lib/widgets/input/password_input.dart @@ -56,9 +56,7 @@ class _PasswordInputState extends State { suffixIcon: IconButton( onPressed: _toggleVisibility, icon: Icon( - _isTextHidden - ? Icons.visibility_outlined - : Icons.visibility_off_outlined, + _isTextHidden ? Icons.visibility_outlined : Icons.visibility_off_outlined, color: theme.colorScheme.onSurfaceVariant, ), ), diff --git a/app/lib/widgets/input/search_field.dart b/app/lib/widgets/input/search_field.dart index 017f60d..a8e023d 100644 --- a/app/lib/widgets/input/search_field.dart +++ b/app/lib/widgets/input/search_field.dart @@ -65,15 +65,8 @@ class SearchField extends StatelessWidget { decoration: InputDecoration( hintText: hintText, prefixIcon: const Icon(Icons.search, size: 20), - suffixIcon: _hasContent - ? IconButton( - icon: const Icon(Icons.close, size: 20), - onPressed: _handleClear, - ) - : null, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(AppRadius.md), - ), + suffixIcon: _hasContent ? IconButton(icon: const Icon(Icons.close, size: 20), onPressed: _handleClear) : null, + border: OutlineInputBorder(borderRadius: BorderRadius.circular(AppRadius.md)), contentPadding: const EdgeInsets.symmetric(horizontal: Spacing.md), isDense: true, ), diff --git a/app/lib/widgets/input/text_input.dart b/app/lib/widgets/input/text_input.dart index b3bf071..9f439d7 100644 --- a/app/lib/widgets/input/text_input.dart +++ b/app/lib/widgets/input/text_input.dart @@ -6,22 +6,12 @@ class TextInput extends StatelessWidget { final String labelText; final TextEditingController? controller; - const TextInput({ - super.key, - this.controller, - this.isRequired = false, - this.isDense = false, - required this.labelText, - }); + const TextInput({super.key, this.controller, this.isRequired = false, this.isDense = false, required this.labelText}); @override Widget build(BuildContext context) { return TextFormField( - decoration: InputDecoration( - border: const OutlineInputBorder(), - labelText: labelText, - isDense: isDense, - ), + decoration: InputDecoration(border: const OutlineInputBorder(), labelText: labelText, isDense: isDense), onSaved: (value) => {}, controller: controller, ); diff --git a/app/lib/widgets/library/book_card.dart b/app/lib/widgets/library/book_card.dart index f30c43d..1d7166e 100644 --- a/app/lib/widgets/library/book_card.dart +++ b/app/lib/widgets/library/book_card.dart @@ -39,8 +39,7 @@ class BookCard extends StatefulWidget { class _BookCardState extends State { bool _isHovered = false; - bool get _isDesktop => - MediaQuery.of(context).size.width >= Breakpoints.desktopSmall; + bool get _isDesktop => MediaQuery.of(context).size.width >= Breakpoints.desktopSmall; @override Widget build(BuildContext context) { @@ -54,11 +53,7 @@ class _BookCardState extends State { onLongPressStart: _isDesktop ? null : (details) { - showBookContextMenu( - context: context, - book: widget.book, - position: details.globalPosition, - ); + showBookContextMenu(context: context, book: widget.book, position: details.globalPosition); }, child: Card( clipBehavior: Clip.antiAlias, @@ -76,25 +71,17 @@ class _BookCardState extends State { _buildCover(context), // Selection tint overlay if (inSelection && widget.isSelected) - Container( - color: colorScheme.primary.withValues(alpha: 0.15), - ), + Container(color: colorScheme.primary.withValues(alpha: 0.15)), // Favorite button - hidden in selection mode if (!inSelection) Positioned( top: Spacing.xs, left: Spacing.xs, child: _CardIconButton( - icon: widget.isFavorite - ? Icons.favorite - : Icons.favorite_border, - color: widget.isFavorite - ? colorScheme.error - : Colors.white, + icon: widget.isFavorite ? Icons.favorite : Icons.favorite_border, + color: widget.isFavorite ? colorScheme.error : Colors.white, onTap: widget.onToggleFavorite != null - ? () => widget.onToggleFavorite!( - widget.isFavorite, - ) + ? () => widget.onToggleFavorite!(widget.isFavorite) : null, ), ), @@ -104,12 +91,8 @@ class _BookCardState extends State { top: Spacing.xs, right: Spacing.xs, child: _CardIconButton( - icon: widget.isSelected - ? Icons.check_circle - : Icons.radio_button_unchecked, - color: widget.isSelected - ? colorScheme.primary - : Colors.white, + icon: widget.isSelected ? Icons.check_circle : Icons.radio_button_unchecked, + color: widget.isSelected ? colorScheme.primary : Colors.white, onTap: widget.onSelectToggle, ), ), @@ -124,17 +107,11 @@ class _BookCardState extends State { child: Row( mainAxisSize: MainAxisSize.min, children: [ - _CardIconButton( - icon: Icons.radio_button_unchecked, - onTap: widget.onEnterSelectionMode, - ), + _CardIconButton(icon: Icons.radio_button_unchecked, onTap: widget.onEnterSelectionMode), const SizedBox(width: Spacing.xs), _CardIconButton( icon: Icons.more_vert, - onTap: () => showBookContextMenu( - context: context, - book: widget.book, - ), + onTap: () => showBookContextMenu(context: context, book: widget.book), ), ], ), @@ -145,22 +122,17 @@ class _BookCardState extends State { bottom: Spacing.xs, left: Spacing.xs, child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 6, - vertical: 2, - ), + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( - color: colorScheme.surfaceContainerHighest - .withValues(alpha: 0.9), + color: colorScheme.surfaceContainerHighest.withValues(alpha: 0.9), borderRadius: BorderRadius.circular(AppRadius.sm), ), child: Text( widget.book.formatLabel, - style: Theme.of(context).textTheme.labelSmall - ?.copyWith( - color: colorScheme.onSurfaceVariant, - fontWeight: FontWeight.w600, - ), + style: Theme.of(context).textTheme.labelSmall?.copyWith( + color: colorScheme.onSurfaceVariant, + fontWeight: FontWeight.w600, + ), ), ), ), @@ -172,9 +144,7 @@ class _BookCardState extends State { LinearProgressIndicator( value: widget.book.progress, backgroundColor: colorScheme.surfaceContainerHighest, - color: widget.book.isFinished - ? colorScheme.tertiary - : colorScheme.primary, + color: widget.book.isFinished ? colorScheme.tertiary : colorScheme.primary, minHeight: 3, ), // Title and author @@ -192,9 +162,7 @@ class _BookCardState extends State { const SizedBox(height: 2), Text( widget.book.author, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: colorScheme.onSurfaceVariant, - ), + style: Theme.of(context).textTheme.bodySmall?.copyWith(color: colorScheme.onSurfaceVariant), maxLines: 1, overflow: TextOverflow.ellipsis, ), @@ -235,19 +203,15 @@ class _BookCardState extends State { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon( - Icons.menu_book, - size: IconSizes.display, - color: colorScheme.onSurfaceVariant.withValues(alpha: 0.5), - ), + Icon(Icons.menu_book, size: IconSizes.display, color: colorScheme.onSurfaceVariant.withValues(alpha: 0.5)), const SizedBox(height: Spacing.xs), Padding( padding: const EdgeInsets.symmetric(horizontal: Spacing.sm), child: Text( widget.book.title, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: colorScheme.onSurfaceVariant.withValues(alpha: 0.7), - ), + style: Theme.of( + context, + ).textTheme.bodySmall?.copyWith(color: colorScheme.onSurfaceVariant.withValues(alpha: 0.7)), textAlign: TextAlign.center, maxLines: 2, overflow: TextOverflow.ellipsis, @@ -277,11 +241,7 @@ class _CardIconButton extends StatelessWidget { onTap: onTap, child: Padding( padding: const EdgeInsets.all(6), - child: Icon( - icon, - size: IconSizes.small, - color: color ?? Colors.white, - ), + child: Icon(icon, size: IconSizes.small, color: color ?? Colors.white), ), ), ); diff --git a/app/lib/widgets/library/book_grid.dart b/app/lib/widgets/library/book_grid.dart index 8bc8880..0870f88 100644 --- a/app/lib/widgets/library/book_grid.dart +++ b/app/lib/widgets/library/book_grid.dart @@ -14,12 +14,7 @@ class BookGrid extends StatelessWidget { final void Function(Book book)? onBookTap; final EdgeInsets? padding; - const BookGrid({ - super.key, - required this.books, - this.onBookTap, - this.padding, - }); + const BookGrid({super.key, required this.books, this.onBookTap, this.padding}); @override Widget build(BuildContext context) { @@ -55,13 +50,7 @@ class BookGrid extends StatelessWidget { context: context, removeTop: true, child: GridView.builder( - padding: - padding ?? - const EdgeInsets.only( - left: Spacing.md, - right: Spacing.md, - bottom: Spacing.md, - ), + padding: padding ?? const EdgeInsets.only(left: Spacing.md, right: Spacing.md, bottom: Spacing.md), cacheExtent: 200, gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: crossAxisCount, @@ -72,21 +61,16 @@ class BookGrid extends StatelessWidget { itemCount: books.length, itemBuilder: (context, index) { final book = books[index]; - final isFavorite = libraryProvider.isBookFavorite( - book.id, - book.isFavorite, - ); + final isFavorite = libraryProvider.isBookFavorite(book.id, book.isFavorite); return BookCard( book: book, isFavorite: isFavorite, - onToggleFavorite: (current) => - libraryProvider.toggleFavorite(book.id, current), + onToggleFavorite: (current) => libraryProvider.toggleFavorite(book.id, current), onTap: onBookTap != null ? () => onBookTap!(book) : null, isSelectionMode: isSelectionMode, isSelected: libraryProvider.isBookSelected(book.id), onSelectToggle: () => libraryProvider.toggleBookSelection(book.id), - onEnterSelectionMode: () => - libraryProvider.enterSelectionMode(book.id), + onEnterSelectionMode: () => libraryProvider.enterSelectionMode(book.id), ); }, ), diff --git a/app/lib/widgets/library/book_list_item.dart b/app/lib/widgets/library/book_list_item.dart index c0641dd..024811d 100644 --- a/app/lib/widgets/library/book_list_item.dart +++ b/app/lib/widgets/library/book_list_item.dart @@ -34,8 +34,7 @@ class BookListItem extends StatefulWidget { class _BookListItemState extends State { bool _isHovered = false; - bool get _isDesktop => - MediaQuery.of(context).size.width >= Breakpoints.desktopSmall; + bool get _isDesktop => MediaQuery.of(context).size.width >= Breakpoints.desktopSmall; @override Widget build(BuildContext context) { @@ -49,46 +48,29 @@ class _BookListItemState extends State { onLongPressStart: _isDesktop ? null : (details) { - showBookContextMenu( - context: context, - book: widget.book, - position: details.globalPosition, - ); + showBookContextMenu(context: context, book: widget.book, position: details.globalPosition); }, child: Material( - color: inSelection && widget.isSelected - ? colorScheme.primary.withValues(alpha: 0.08) - : Colors.transparent, + color: inSelection && widget.isSelected ? colorScheme.primary.withValues(alpha: 0.08) : Colors.transparent, child: InkWell( onTap: inSelection ? widget.onSelectToggle : widget.onTap, child: Container( - padding: const EdgeInsets.symmetric( - horizontal: Spacing.md, - vertical: Spacing.sm, - ), + padding: const EdgeInsets.symmetric(horizontal: Spacing.md, vertical: Spacing.sm), decoration: BoxDecoration( - border: Border( - bottom: BorderSide(color: colorScheme.outlineVariant), - ), + border: Border(bottom: BorderSide(color: colorScheme.outlineVariant)), ), child: Row( children: [ // Selection checkbox (leading) if (inSelection) ...[ - Checkbox( - value: widget.isSelected, - onChanged: (_) => widget.onSelectToggle?.call(), - ), + Checkbox(value: widget.isSelected, onChanged: (_) => widget.onSelectToggle?.call()), const SizedBox(width: Spacing.sm), ], // Cover thumbnail SizedBox( width: ComponentSizes.bookCoverWidthList, height: ComponentSizes.bookCoverHeightList, - child: ClipRRect( - borderRadius: BorderRadius.circular(AppRadius.sm), - child: _buildCover(context), - ), + child: ClipRRect(borderRadius: BorderRadius.circular(AppRadius.sm), child: _buildCover(context)), ), const SizedBox(width: Spacing.md), @@ -100,42 +82,35 @@ class _BookListItemState extends State { children: [ Text( widget.book.title, - style: Theme.of(context).textTheme.titleMedium - ?.copyWith(fontWeight: FontWeight.w600), + style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600), maxLines: 1, overflow: TextOverflow.ellipsis, ), const SizedBox(height: Spacing.xs), Text( widget.book.author, - style: Theme.of(context).textTheme.bodyMedium - ?.copyWith(color: colorScheme.onSurfaceVariant), + style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: colorScheme.onSurfaceVariant), maxLines: 1, overflow: TextOverflow.ellipsis, ), - if (widget.showProgress && - widget.book.progress > 0) ...[ + if (widget.showProgress && widget.book.progress > 0) ...[ const SizedBox(height: Spacing.xs), Row( children: [ Expanded( child: LinearProgressIndicator( value: widget.book.progress, - backgroundColor: - colorScheme.surfaceContainerHighest, - color: widget.book.isFinished - ? colorScheme.tertiary - : colorScheme.primary, + backgroundColor: colorScheme.surfaceContainerHighest, + color: widget.book.isFinished ? colorScheme.tertiary : colorScheme.primary, minHeight: 3, ), ), const SizedBox(width: Spacing.sm), Text( widget.book.progressLabel, - style: Theme.of(context).textTheme.labelSmall - ?.copyWith( - color: colorScheme.onSurfaceVariant, - ), + style: Theme.of( + context, + ).textTheme.labelSmall?.copyWith(color: colorScheme.onSurfaceVariant), ), ], ), @@ -151,35 +126,27 @@ class _BookListItemState extends State { children: [ // Format badge Container( - padding: const EdgeInsets.symmetric( - horizontal: 6, - vertical: 2, - ), + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(AppRadius.sm), ), child: Text( widget.book.formatLabel, - style: Theme.of(context).textTheme.labelSmall - ?.copyWith( - color: colorScheme.onSurfaceVariant, - fontWeight: FontWeight.w600, - ), + style: Theme.of(context).textTheme.labelSmall?.copyWith( + color: colorScheme.onSurfaceVariant, + fontWeight: FontWeight.w600, + ), ), ), const SizedBox(width: Spacing.sm), // Favorite indicator Icon( - widget.isFavorite - ? Icons.favorite - : Icons.favorite_border, + widget.isFavorite ? Icons.favorite : Icons.favorite_border, size: IconSizes.indicator, color: widget.isFavorite ? colorScheme.error - : colorScheme.onSurfaceVariant.withValues( - alpha: 0.5, - ), + : colorScheme.onSurfaceVariant.withValues(alpha: 0.5), ), // Overflow menu - show on hover (desktop only) if (_isDesktop) @@ -189,10 +156,7 @@ class _BookListItemState extends State { child: IconButton( icon: const Icon(Icons.more_vert), iconSize: IconSizes.action, - onPressed: () => showBookContextMenu( - context: context, - book: widget.book, - ), + onPressed: () => showBookContextMenu(context: context, book: widget.book), tooltip: 'More options', visualDensity: VisualDensity.compact, ), @@ -226,11 +190,7 @@ class _BookListItemState extends State { return Container( color: cs.surfaceContainerHighest, - child: Icon( - Icons.menu_book, - size: IconSizes.medium, - color: cs.onSurfaceVariant.withValues(alpha: 0.5), - ), + child: Icon(Icons.menu_book, size: IconSizes.medium, color: cs.onSurfaceVariant.withValues(alpha: 0.5)), ); } } diff --git a/app/lib/widgets/library/bulk_action_bar.dart b/app/lib/widgets/library/bulk_action_bar.dart index 33c0168..a783dae 100644 --- a/app/lib/widgets/library/bulk_action_bar.dart +++ b/app/lib/widgets/library/bulk_action_bar.dart @@ -24,35 +24,16 @@ class BulkActionBar extends StatelessWidget { return Row( mainAxisSize: MainAxisSize.min, children: [ - IconButton( - icon: const Icon(Icons.folder_outlined), - tooltip: 'Add to shelf', - onPressed: onAddToShelf, - ), + IconButton(icon: const Icon(Icons.folder_outlined), tooltip: 'Add to shelf', onPressed: onAddToShelf), const SizedBox(width: Spacing.xs), - IconButton( - icon: const Icon(Icons.label_outline), - tooltip: 'Manage topics', - onPressed: onManageTopics, - ), + IconButton(icon: const Icon(Icons.label_outline), tooltip: 'Manage topics', onPressed: onManageTopics), const SizedBox(width: Spacing.xs), - IconButton( - icon: const Icon(Icons.auto_stories), - tooltip: 'Change status', - onPressed: onChangeStatus, - ), + IconButton(icon: const Icon(Icons.auto_stories), tooltip: 'Change status', onPressed: onChangeStatus), const SizedBox(width: Spacing.xs), - IconButton( - icon: const Icon(Icons.favorite_border), - tooltip: 'Toggle favorite', - onPressed: onToggleFavorite, - ), + IconButton(icon: const Icon(Icons.favorite_border), tooltip: 'Toggle favorite', onPressed: onToggleFavorite), const SizedBox(width: Spacing.xs), IconButton( - icon: Icon( - Icons.delete_outline, - color: Theme.of(context).colorScheme.error, - ), + icon: Icon(Icons.delete_outline, color: Theme.of(context).colorScheme.error), tooltip: 'Delete', onPressed: onDelete, ), diff --git a/app/lib/widgets/library/bulk_status_sheet.dart b/app/lib/widgets/library/bulk_status_sheet.dart index 2432845..0e0ce8f 100644 --- a/app/lib/widgets/library/bulk_status_sheet.dart +++ b/app/lib/widgets/library/bulk_status_sheet.dart @@ -5,21 +5,9 @@ import 'package:papyrus/utils/text_utils.dart'; import 'package:papyrus/widgets/shared/bottom_sheet_handle.dart'; final statusTiles = [ - ( - icon: Icons.auto_stories, - status: ReadingStatus.inProgress, - title: "in progress", - ), - ( - icon: Icons.check_circle_outline, - status: ReadingStatus.completed, - title: "finished", - ), - ( - icon: Icons.bookmark_add_outlined, - status: ReadingStatus.notStarted, - title: "unread", - ), + (icon: Icons.auto_stories, status: ReadingStatus.inProgress, title: "in progress"), + (icon: Icons.check_circle_outline, status: ReadingStatus.completed, title: "finished"), + (icon: Icons.bookmark_add_outlined, status: ReadingStatus.notStarted, title: "unread"), ]; /// Bottom sheet for changing reading status of multiple books. @@ -27,11 +15,7 @@ class BulkStatusSheet extends StatelessWidget { final int bookCount; final void Function(ReadingStatus status) onStatusSelected; - const BulkStatusSheet({ - super.key, - required this.bookCount, - required this.onStatusSelected, - }); + const BulkStatusSheet({super.key, required this.bookCount, required this.onStatusSelected}); /// Show as a bottom sheet on mobile. static Future show( @@ -42,14 +26,9 @@ class BulkStatusSheet extends StatelessWidget { return showModalBottomSheet( context: context, shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical( - top: Radius.circular(AppRadius.bottomSheet), - ), - ), - builder: (context) => BulkStatusSheet( - bookCount: bookCount, - onStatusSelected: onStatusSelected, + borderRadius: BorderRadius.vertical(top: Radius.circular(AppRadius.bottomSheet)), ), + builder: (context) => BulkStatusSheet(bookCount: bookCount, onStatusSelected: onStatusSelected), ); } @@ -66,38 +45,26 @@ class BulkStatusSheet extends StatelessWidget { const SizedBox(height: Spacing.md), const BottomSheetHandle(), Padding( - padding: const EdgeInsets.fromLTRB( - Spacing.lg, - Spacing.lg, - Spacing.lg, - 0, - ), + padding: const EdgeInsets.fromLTRB(Spacing.lg, Spacing.lg, Spacing.lg, 0), child: Text( 'Change status for $bookCount ${maybePluralize(bookCount, "book")}', style: textTheme.titleLarge, ), ), Padding( - padding: const EdgeInsets.symmetric( - horizontal: Spacing.sm, - vertical: Spacing.md, - ), + padding: const EdgeInsets.symmetric(horizontal: Spacing.sm, vertical: Spacing.md), child: Column( children: [ for (final tile in statusTiles) ListTile( leading: Icon(tile.icon), title: Text('Mark as ${tile.title}'), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(24)), - ), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(24))), onTap: () { Navigator.pop(context); onStatusSelected(tile.status); }, - contentPadding: const EdgeInsets.symmetric( - horizontal: Spacing.md, - ), + contentPadding: const EdgeInsets.symmetric(horizontal: Spacing.md), ), ], ), diff --git a/app/lib/widgets/library/eink_tab_filter.dart b/app/lib/widgets/library/eink_tab_filter.dart index 479a0ad..d3af6a7 100644 --- a/app/lib/widgets/library/eink_tab_filter.dart +++ b/app/lib/widgets/library/eink_tab_filter.dart @@ -24,10 +24,7 @@ class EinkTabFilter extends StatelessWidget { height: TouchTargets.einkMin, decoration: BoxDecoration( border: Border( - bottom: BorderSide( - color: colorScheme.outline, - width: BorderWidths.einkDefault, - ), + bottom: BorderSide(color: colorScheme.outline, width: BorderWidths.einkDefault), ), ), child: Row( @@ -41,20 +38,14 @@ class EinkTabFilter extends StatelessWidget { onTap: () => libraryProvider.setFilter(tab.type), child: Container( decoration: BoxDecoration( - color: isSelected - ? colorScheme.primary - : Colors.transparent, - border: Border( - right: BorderSide(color: colorScheme.outline, width: 1), - ), + color: isSelected ? colorScheme.primary : Colors.transparent, + border: Border(right: BorderSide(color: colorScheme.outline, width: 1)), ), child: Center( child: Text( tab.label, style: Theme.of(context).textTheme.labelLarge?.copyWith( - color: isSelected - ? colorScheme.onPrimary - : colorScheme.onSurface, + color: isSelected ? colorScheme.onPrimary : colorScheme.onSurface, fontWeight: FontWeight.bold, letterSpacing: 1.0, ), diff --git a/app/lib/widgets/library/library_drawer.dart b/app/lib/widgets/library/library_drawer.dart index 883d60c..fdfe59e 100644 --- a/app/lib/widgets/library/library_drawer.dart +++ b/app/lib/widgets/library/library_drawer.dart @@ -25,12 +25,7 @@ class LibraryDrawer extends StatelessWidget { // Drawer header Padding( padding: const EdgeInsets.all(Spacing.lg), - child: Text( - 'Library', - style: textTheme.headlineSmall?.copyWith( - fontWeight: FontWeight.bold, - ), - ), + child: Text('Library', style: textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.bold)), ), Divider(height: 1, color: colorScheme.outlineVariant), const SizedBox(height: Spacing.sm), @@ -42,9 +37,7 @@ class LibraryDrawer extends StatelessWidget { _DrawerNavItem( icon: Icons.book, label: 'Books', - isSelected: - currentPath == '/library' || - currentPath == '/library/books', + isSelected: currentPath == '/library' || currentPath == '/library/books', onTap: () { Navigator.of(context).pop(); context.go('/library'); @@ -103,12 +96,7 @@ class _DrawerNavItem extends StatelessWidget { final bool isSelected; final VoidCallback onTap; - const _DrawerNavItem({ - required this.icon, - required this.label, - this.isSelected = false, - required this.onTap, - }); + const _DrawerNavItem({required this.icon, required this.label, this.isSelected = false, required this.onTap}); @override Widget build(BuildContext context) { @@ -116,10 +104,7 @@ class _DrawerNavItem extends StatelessWidget { final textTheme = Theme.of(context).textTheme; return ListTile( - leading: Icon( - icon, - color: isSelected ? colorScheme.primary : colorScheme.onSurfaceVariant, - ), + leading: Icon(icon, color: isSelected ? colorScheme.primary : colorScheme.onSurfaceVariant), title: Text( label, style: textTheme.bodyLarge?.copyWith( @@ -129,9 +114,7 @@ class _DrawerNavItem extends StatelessWidget { ), selected: isSelected, selectedTileColor: colorScheme.primaryContainer.withValues(alpha: 0.3), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(AppRadius.full), - ), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(AppRadius.full)), contentPadding: const EdgeInsets.symmetric(horizontal: Spacing.md), visualDensity: VisualDensity.compact, onTap: onTap, diff --git a/app/lib/widgets/library/library_filter_chips.dart b/app/lib/widgets/library/library_filter_chips.dart index 2a8e7f2..3409508 100644 --- a/app/lib/widgets/library/library_filter_chips.dart +++ b/app/lib/widgets/library/library_filter_chips.dart @@ -12,21 +12,9 @@ class LibraryFilterChips extends StatelessWidget { static const _filters = [ (type: LibraryFilterType.all, label: 'All', icon: Icons.apps), - ( - type: LibraryFilterType.reading, - label: 'Reading', - icon: Icons.auto_stories, - ), - ( - type: LibraryFilterType.favorites, - label: 'Favorites', - icon: Icons.favorite, - ), - ( - type: LibraryFilterType.finished, - label: 'Finished', - icon: Icons.check_circle, - ), + (type: LibraryFilterType.reading, label: 'Reading', icon: Icons.auto_stories), + (type: LibraryFilterType.favorites, label: 'Favorites', icon: Icons.favorite), + (type: LibraryFilterType.finished, label: 'Finished', icon: Icons.check_circle), (type: LibraryFilterType.unread, label: 'Unread', icon: Icons.book), ]; @@ -38,11 +26,8 @@ class LibraryFilterChips extends StatelessWidget { horizontalPadding: horizontalPadding, filters: _filters .map( - (f) => QuickFilterChipData( - label: f.label, - icon: f.icon, - isSelected: libraryProvider.isFilterActive(f.type), - ), + (f) => + QuickFilterChipData(label: f.label, icon: f.icon, isSelected: libraryProvider.isFilterActive(f.type)), ) .toList(), onFilterTapped: (index) { diff --git a/app/lib/widgets/library/selection_header.dart b/app/lib/widgets/library/selection_header.dart index acf76bb..8e630f2 100644 --- a/app/lib/widgets/library/selection_header.dart +++ b/app/lib/widgets/library/selection_header.dart @@ -30,18 +30,11 @@ class SelectionHeader extends StatelessWidget { return Row( children: [ - IconButton( - icon: const Icon(Icons.close), - tooltip: 'Exit selection', - onPressed: onClose, - ), + IconButton(icon: const Icon(Icons.close), tooltip: 'Exit selection', onPressed: onClose), const SizedBox(width: Spacing.sm), Text( '$selectedCount selected', - style: textTheme.titleMedium?.copyWith( - color: colorScheme.onSurface, - fontWeight: FontWeight.w600, - ), + style: textTheme.titleMedium?.copyWith(color: colorScheme.onSurface, fontWeight: FontWeight.w600), ), const SizedBox(width: Spacing.md), TextButton( diff --git a/app/lib/widgets/profile/profile_header.dart b/app/lib/widgets/profile/profile_header.dart index 10f5076..8779005 100644 --- a/app/lib/widgets/profile/profile_header.dart +++ b/app/lib/widgets/profile/profile_header.dart @@ -74,27 +74,18 @@ class ProfileHeader extends StatelessWidget { children: [ _buildAvatar(context, size: 128, borderRadius: 64), const SizedBox(height: Spacing.md), - Text( - displayName, - style: textTheme.headlineSmall, - textAlign: TextAlign.center, - ), + Text(displayName, style: textTheme.headlineSmall, textAlign: TextAlign.center), const SizedBox(height: Spacing.xs), Text( email, - style: textTheme.bodyMedium?.copyWith( - color: colorScheme.onSurfaceVariant, - ), + style: textTheme.bodyMedium?.copyWith(color: colorScheme.onSurfaceVariant), textAlign: TextAlign.center, ), const SizedBox(height: Spacing.md), SizedBox( width: 200, height: 40, - child: OutlinedButton( - onPressed: onEditProfile, - child: const Text('Edit profile'), - ), + child: OutlinedButton(onPressed: onEditProfile, child: const Text('Edit profile')), ), ], ); @@ -122,12 +113,7 @@ class ProfileHeader extends StatelessWidget { children: [ Text(displayName, style: textTheme.headlineMedium), const SizedBox(height: Spacing.xs), - Text( - email, - style: textTheme.bodyLarge?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), + Text(email, style: textTheme.bodyLarge?.copyWith(color: colorScheme.onSurfaceVariant)), ], ), ), @@ -144,47 +130,31 @@ class ProfileHeader extends StatelessWidget { } /// Builds circular avatar for standard modes. - Widget _buildAvatar( - BuildContext context, { - required double size, - required double borderRadius, - }) { + Widget _buildAvatar(BuildContext context, {required double size, required double borderRadius}) { final colorScheme = Theme.of(context).colorScheme; final textTheme = Theme.of(context).textTheme; return Container( width: size, height: size, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(borderRadius), - color: colorScheme.primaryContainer, - ), + decoration: BoxDecoration(borderRadius: BorderRadius.circular(borderRadius), color: colorScheme.primaryContainer), clipBehavior: Clip.antiAlias, child: avatarUrl != null && avatarUrl!.isNotEmpty ? Image.network( avatarUrl!, fit: BoxFit.cover, - errorBuilder: (_, _, _) => - _buildInitialsAvatar(context, colorScheme, textTheme, size), + errorBuilder: (_, _, _) => _buildInitialsAvatar(context, colorScheme, textTheme, size), ) : _buildInitialsAvatar(context, colorScheme, textTheme, size), ); } /// Builds initials fallback for avatar. - Widget _buildInitialsAvatar( - BuildContext context, - ColorScheme colorScheme, - TextTheme textTheme, - double size, - ) { + Widget _buildInitialsAvatar(BuildContext context, ColorScheme colorScheme, TextTheme textTheme, double size) { return Center( child: Text( _initials, - style: textTheme.headlineMedium?.copyWith( - color: colorScheme.onPrimaryContainer, - fontSize: size * 0.35, - ), + style: textTheme.headlineMedium?.copyWith(color: colorScheme.onPrimaryContainer, fontSize: size * 0.35), ), ); } diff --git a/app/lib/widgets/profile/profile_menu_item.dart b/app/lib/widgets/profile/profile_menu_item.dart index 0120216..6c268be 100644 --- a/app/lib/widgets/profile/profile_menu_item.dart +++ b/app/lib/widgets/profile/profile_menu_item.dart @@ -66,12 +66,8 @@ class ProfileMenuItem extends StatelessWidget { final colorScheme = Theme.of(context).colorScheme; final textTheme = Theme.of(context).textTheme; - final contentColor = isDestructive - ? colorScheme.error - : colorScheme.onSurface; - final iconContainerColor = isDestructive - ? colorScheme.errorContainer - : colorScheme.surfaceContainerHighest; + final contentColor = isDestructive ? colorScheme.error : colorScheme.onSurface; + final iconContainerColor = isDestructive ? colorScheme.errorContainer : colorScheme.surfaceContainerHighest; return Material( color: Colors.transparent, @@ -79,10 +75,7 @@ class ProfileMenuItem extends StatelessWidget { onTap: onTap, borderRadius: BorderRadius.circular(AppRadius.md), child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: Spacing.md, - vertical: Spacing.sm, - ), + padding: const EdgeInsets.symmetric(horizontal: Spacing.md, vertical: Spacing.sm), child: Row( children: [ _buildIconContainer(iconContainerColor, contentColor), @@ -90,19 +83,9 @@ class ProfileMenuItem extends StatelessWidget { Expanded( child: subtitle != null ? _buildTwoLineContent(textTheme, contentColor, colorScheme) - : Text( - label, - style: textTheme.bodyLarge?.copyWith( - color: contentColor, - ), - ), + : Text(label, style: textTheme.bodyLarge?.copyWith(color: contentColor)), ), - if (showChevron) - Icon( - Icons.chevron_right, - color: colorScheme.onSurfaceVariant, - size: IconSizes.medium, - ), + if (showChevron) Icon(Icons.chevron_right, color: colorScheme.onSurfaceVariant, size: IconSizes.medium), ], ), ), @@ -121,22 +104,13 @@ class ProfileMenuItem extends StatelessWidget { } /// Two-line content with label and subtitle. - Widget _buildTwoLineContent( - TextTheme textTheme, - Color labelColor, - ColorScheme colorScheme, - ) { + Widget _buildTwoLineContent(TextTheme textTheme, Color labelColor, ColorScheme colorScheme) { return Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text(label, style: textTheme.bodyLarge?.copyWith(color: labelColor)), - Text( - subtitle!, - style: textTheme.bodyMedium?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), + Text(subtitle!, style: textTheme.bodyMedium?.copyWith(color: colorScheme.onSurfaceVariant)), ], ); } @@ -171,18 +145,8 @@ class ProfileMenuCard extends StatelessWidget { children: [ if (title != null) Padding( - padding: const EdgeInsets.fromLTRB( - Spacing.md, - Spacing.md, - Spacing.md, - Spacing.sm, - ), - child: Text( - title!, - style: textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.w600, - ), - ), + padding: const EdgeInsets.fromLTRB(Spacing.md, Spacing.md, Spacing.md, Spacing.sm), + child: Text(title!, style: textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600)), ), ...children, ], diff --git a/app/lib/widgets/profile/profile_stats_card.dart b/app/lib/widgets/profile/profile_stats_card.dart index bff5e2f..cb8178d 100644 --- a/app/lib/widgets/profile/profile_stats_card.dart +++ b/app/lib/widgets/profile/profile_stats_card.dart @@ -51,12 +51,7 @@ class ProfileStatsCard extends StatelessWidget { final String title; /// Creates a profile stats card widget. - const ProfileStatsCard({ - super.key, - required this.stats, - this.onViewAllStats, - this.title = 'Reading statistics', - }); + const ProfileStatsCard({super.key, required this.stats, this.onViewAllStats, this.title = 'Reading statistics'}); @override Widget build(BuildContext context) { @@ -72,10 +67,7 @@ class ProfileStatsCard extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - title, - style: textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600), - ), + Text(title, style: textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600)), const SizedBox(height: Spacing.md), ...stats.map((stat) => _buildStatRow(context, stat)), if (onViewAllStats != null) ...[ @@ -112,25 +104,13 @@ class ProfileStatsCard extends StatelessWidget { Row( children: [ if (stat.icon != null) ...[ - Icon( - stat.icon, - size: IconSizes.small, - color: colorScheme.onSurfaceVariant, - ), + Icon(stat.icon, size: IconSizes.small, color: colorScheme.onSurfaceVariant), const SizedBox(width: Spacing.sm), ], - Text( - stat.label, - style: textTheme.bodyMedium?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), + Text(stat.label, style: textTheme.bodyMedium?.copyWith(color: colorScheme.onSurfaceVariant)), ], ), - Text( - stat.value, - style: textTheme.bodyLarge?.copyWith(fontWeight: FontWeight.w600), - ), + Text(stat.value, style: textTheme.bodyLarge?.copyWith(fontWeight: FontWeight.w600)), ], ), ); diff --git a/app/lib/widgets/profile_button.dart b/app/lib/widgets/profile_button.dart index fd39d73..65f1a0f 100644 --- a/app/lib/widgets/profile_button.dart +++ b/app/lib/widgets/profile_button.dart @@ -5,12 +5,7 @@ class ProfileButton extends StatelessWidget { final IconData icon; final Function? onPressed; - const ProfileButton({ - super.key, - required this.title, - required this.icon, - required this.onPressed, - }); + const ProfileButton({super.key, required this.title, required this.icon, required this.onPressed}); @override Widget build(BuildContext context) { diff --git a/app/lib/widgets/search/library_search_bar.dart b/app/lib/widgets/search/library_search_bar.dart index f676db9..b59ee59 100644 --- a/app/lib/widgets/search/library_search_bar.dart +++ b/app/lib/widgets/search/library_search_bar.dart @@ -93,23 +93,17 @@ class _LibrarySearchBarState extends State { // Check if typing a field name if (!lastWord.contains(':')) { - newSuggestions = SearchQueryParser.fieldSuggestions - .where((s) => s.toLowerCase().startsWith(lastWord)) - .toList(); + newSuggestions = SearchQueryParser.fieldSuggestions.where((s) => s.toLowerCase().startsWith(lastWord)).toList(); } // Check if typing status value else if (lastWord.startsWith('status:')) { final value = lastWord.substring(7); - newSuggestions = SearchQueryParser.statusSuggestions - .where((s) => s.toLowerCase().contains(value)) - .toList(); + newSuggestions = SearchQueryParser.statusSuggestions.where((s) => s.toLowerCase().contains(value)).toList(); } // Check if typing format value else if (lastWord.startsWith('format:')) { final value = lastWord.substring(7); - newSuggestions = SearchQueryParser.formatSuggestions - .where((s) => s.toLowerCase().contains(value)) - .toList(); + newSuggestions = SearchQueryParser.formatSuggestions.where((s) => s.toLowerCase().contains(value)).toList(); } _suggestions = newSuggestions; @@ -122,9 +116,7 @@ class _LibrarySearchBarState extends State { final lastSpace = text.lastIndexOf(' '); final prefix = lastSpace >= 0 ? text.substring(0, lastSpace + 1) : ''; _controller.text = '$prefix$suggestion'; - _controller.selection = TextSelection.fromPosition( - TextPosition(offset: _controller.text.length), - ); + _controller.selection = TextSelection.fromPosition(TextPosition(offset: _controller.text.length)); _suggestions = []; _selectedIndex = -1; _removeOverlay(); @@ -233,8 +225,7 @@ class _LibrarySearchBarState extends State { return KeyEventResult.handled; } if (key == LogicalKeyboardKey.arrowUp) { - _selectedIndex = - (_selectedIndex - 1 + _suggestions.length) % _suggestions.length; + _selectedIndex = (_selectedIndex - 1 + _suggestions.length) % _suggestions.length; _showOrUpdateOverlay(); return KeyEventResult.handled; } @@ -264,9 +255,7 @@ class _LibrarySearchBarState extends State { IconButton( icon: Icon( Icons.tune, - color: widget.activeFilterCount > 0 - ? colorScheme.primary - : colorScheme.onSurfaceVariant, + color: widget.activeFilterCount > 0 ? colorScheme.primary : colorScheme.onSurfaceVariant, ), onPressed: widget.onFilterTap, tooltip: 'Filters', @@ -277,18 +266,11 @@ class _LibrarySearchBarState extends State { top: 4, child: Container( padding: const EdgeInsets.all(4), - decoration: BoxDecoration( - color: colorScheme.primary, - shape: BoxShape.circle, - ), + decoration: BoxDecoration(color: colorScheme.primary, shape: BoxShape.circle), constraints: const BoxConstraints(minWidth: 18, minHeight: 18), child: Text( '${widget.activeFilterCount}', - style: TextStyle( - color: colorScheme.onPrimary, - fontSize: 10, - fontWeight: FontWeight.bold, - ), + style: TextStyle(color: colorScheme.onPrimary, fontSize: 10, fontWeight: FontWeight.bold), textAlign: TextAlign.center, ), ), @@ -315,19 +297,12 @@ class _LibrarySearchBarState extends State { mainAxisSize: MainAxisSize.min, children: [ if (_controller.text.isNotEmpty) - IconButton( - icon: const Icon(Icons.clear), - onPressed: _clearSearch, - tooltip: 'Clear', - ), + IconButton(icon: const Icon(Icons.clear), onPressed: _clearSearch, tooltip: 'Clear'), _buildFilterButton(colorScheme), ], ), isDense: true, - contentPadding: const EdgeInsets.symmetric( - horizontal: Spacing.md, - vertical: Spacing.sm, - ), + contentPadding: const EdgeInsets.symmetric(horizontal: Spacing.md, vertical: Spacing.sm), ), onChanged: _onQueryChanged, ), @@ -341,11 +316,7 @@ class _SuggestionTile extends StatelessWidget { final bool isHighlighted; final VoidCallback onTap; - const _SuggestionTile({ - required this.suggestion, - required this.isHighlighted, - required this.onTap, - }); + const _SuggestionTile({required this.suggestion, required this.isHighlighted, required this.onTap}); IconData _getIconForSuggestion(String suggestion) { final lower = suggestion.toLowerCase(); @@ -363,47 +334,29 @@ class _SuggestionTile extends StatelessWidget { final colorScheme = Theme.of(context).colorScheme; final platform = Theme.of(context).platform; final isDesktopPlatform = - platform == TargetPlatform.macOS || - platform == TargetPlatform.windows || - platform == TargetPlatform.linux; + platform == TargetPlatform.macOS || platform == TargetPlatform.windows || platform == TargetPlatform.linux; return InkWell( onTap: onTap, child: Container( - color: isHighlighted - ? colorScheme.primary.withValues(alpha: 0.12) - : null, - padding: const EdgeInsets.symmetric( - horizontal: Spacing.md, - vertical: Spacing.sm, - ), + color: isHighlighted ? colorScheme.primary.withValues(alpha: 0.12) : null, + padding: const EdgeInsets.symmetric(horizontal: Spacing.md, vertical: Spacing.sm), child: Row( children: [ Icon( _getIconForSuggestion(suggestion), - color: isHighlighted - ? colorScheme.primary - : colorScheme.onSurfaceVariant, + color: isHighlighted ? colorScheme.primary : colorScheme.onSurfaceVariant, size: IconSizes.small, ), const SizedBox(width: Spacing.sm), Expanded( child: Text( suggestion, - style: TextStyle( - color: isHighlighted - ? colorScheme.primary - : colorScheme.onSurface, - ), + style: TextStyle(color: isHighlighted ? colorScheme.primary : colorScheme.onSurface), ), ), if (isHighlighted && isDesktopPlatform) - Text( - 'Tab', - style: Theme.of(context).textTheme.labelSmall?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), + Text('Tab', style: Theme.of(context).textTheme.labelSmall?.copyWith(color: colorScheme.onSurfaceVariant)), ], ), ), diff --git a/app/lib/widgets/search_settings.dart b/app/lib/widgets/search_settings.dart index 96b8a65..1e0839a 100644 --- a/app/lib/widgets/search_settings.dart +++ b/app/lib/widgets/search_settings.dart @@ -5,9 +5,6 @@ class SearchSettings extends StatelessWidget { @override Widget build(BuildContext context) { - return Container( - padding: const EdgeInsets.fromLTRB(8, 8, 8, 0), - child: const Text("Search options..."), - ); + return Container(padding: const EdgeInsets.fromLTRB(8, 8, 8, 0), child: const Text("Search options...")); } } diff --git a/app/lib/widgets/settings/settings_row.dart b/app/lib/widgets/settings/settings_row.dart index deaef99..fce2327 100644 --- a/app/lib/widgets/settings/settings_row.dart +++ b/app/lib/widgets/settings/settings_row.dart @@ -46,13 +46,7 @@ class SettingsRow extends StatelessWidget { final bool showChevron; /// Creates a settings row widget. - const SettingsRow({ - super.key, - required this.label, - this.value, - this.onTap, - this.showChevron = true, - }); + const SettingsRow({super.key, required this.label, this.value, this.onTap, this.showChevron = true}); @override Widget build(BuildContext context) { @@ -65,10 +59,7 @@ class SettingsRow extends StatelessWidget { onTap: onTap, borderRadius: BorderRadius.circular(AppRadius.sm), child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: Spacing.sm, - vertical: Spacing.sm, - ), + padding: const EdgeInsets.symmetric(horizontal: Spacing.sm, vertical: Spacing.sm), child: Row( children: [ Expanded( @@ -78,29 +69,15 @@ class SettingsRow extends StatelessWidget { children: [ Text(label, style: textTheme.bodyLarge), const SizedBox(height: 2), - Text( - value!, - style: textTheme.bodyMedium?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), + Text(value!, style: textTheme.bodyMedium?.copyWith(color: colorScheme.onSurfaceVariant)), ], ) : Text(label, style: textTheme.bodyLarge), ), if (showChevron && onTap != null) - Icon( - Icons.chevron_right, - color: colorScheme.onSurfaceVariant, - size: IconSizes.medium, - ) + Icon(Icons.chevron_right, color: colorScheme.onSurfaceVariant, size: IconSizes.medium) else if (value != null && !showChevron) - Text( - value!, - style: textTheme.bodyMedium?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), + Text(value!, style: textTheme.bodyMedium?.copyWith(color: colorScheme.onSurfaceVariant)), ], ), ), @@ -121,22 +98,14 @@ class SettingsToggleRow extends StatelessWidget { final ValueChanged? onChanged; /// Creates a settings toggle row widget. - const SettingsToggleRow({ - super.key, - required this.label, - required this.value, - this.onChanged, - }); + const SettingsToggleRow({super.key, required this.label, required this.value, this.onChanged}); @override Widget build(BuildContext context) { final textTheme = Theme.of(context).textTheme; return Padding( - padding: const EdgeInsets.symmetric( - horizontal: Spacing.sm, - vertical: Spacing.xs, - ), + padding: const EdgeInsets.symmetric(horizontal: Spacing.sm, vertical: Spacing.xs), child: Row( children: [ Expanded(child: Text(label, style: textTheme.bodyLarge)), diff --git a/app/lib/widgets/settings/settings_section.dart b/app/lib/widgets/settings/settings_section.dart index b980e58..8d8394f 100644 --- a/app/lib/widgets/settings/settings_section.dart +++ b/app/lib/widgets/settings/settings_section.dart @@ -57,12 +57,7 @@ class SettingsCard extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ if (title != null) ...[ - Text( - title!, - style: textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.w600, - ), - ), + Text(title!, style: textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600)), const SizedBox(height: Spacing.md), ], ...children, diff --git a/app/lib/widgets/shared/book_group_header.dart b/app/lib/widgets/shared/book_group_header.dart index 95696d4..27da6e2 100644 --- a/app/lib/widgets/shared/book_group_header.dart +++ b/app/lib/widgets/shared/book_group_header.dart @@ -58,23 +58,14 @@ class BookGroupHeader extends StatelessWidget { ? Image.network( coverUrl!, fit: BoxFit.cover, - errorBuilder: (context, error, stackTrace) => - Container( - color: colorScheme.surfaceContainerHighest, - child: Icon( - Icons.menu_book, - size: 16, - color: colorScheme.onSurfaceVariant, - ), - ), + errorBuilder: (context, error, stackTrace) => Container( + color: colorScheme.surfaceContainerHighest, + child: Icon(Icons.menu_book, size: 16, color: colorScheme.onSurfaceVariant), + ), ) : Container( color: colorScheme.surfaceContainerHighest, - child: Icon( - Icons.menu_book, - size: 16, - color: colorScheme.onSurfaceVariant, - ), + child: Icon(Icons.menu_book, size: 16, color: colorScheme.onSurfaceVariant), ), ), ), @@ -85,17 +76,13 @@ class BookGroupHeader extends StatelessWidget { children: [ Text( bookTitle, - style: textTheme.titleSmall?.copyWith( - fontWeight: FontWeight.w600, - ), + style: textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w600), maxLines: 1, overflow: TextOverflow.ellipsis, ), Text( '$count ${count == 1 ? itemLabel : '${itemLabel}s'}', - style: textTheme.bodySmall?.copyWith( - color: colorScheme.onSurfaceVariant, - ), + style: textTheme.bodySmall?.copyWith(color: colorScheme.onSurfaceVariant), ), ], ), diff --git a/app/lib/widgets/shared/bottom_sheet_handle.dart b/app/lib/widgets/shared/bottom_sheet_handle.dart index 80b1cd3..2efaf21 100644 --- a/app/lib/widgets/shared/bottom_sheet_handle.dart +++ b/app/lib/widgets/shared/bottom_sheet_handle.dart @@ -15,10 +15,7 @@ class BottomSheetHandle extends StatelessWidget { child: Container( width: 40, height: 4, - decoration: BoxDecoration( - color: colorScheme.outlineVariant, - borderRadius: BorderRadius.circular(2), - ), + decoration: BoxDecoration(color: colorScheme.outlineVariant, borderRadius: BorderRadius.circular(2)), ), ); } diff --git a/app/lib/widgets/shared/bottom_sheet_header.dart b/app/lib/widgets/shared/bottom_sheet_header.dart index 470ccef..6c5bccc 100644 --- a/app/lib/widgets/shared/bottom_sheet_header.dart +++ b/app/lib/widgets/shared/bottom_sheet_header.dart @@ -26,17 +26,9 @@ class BottomSheetHeader extends StatelessWidget { children: [ TextButton(onPressed: onCancel, child: const Text('Cancel')), const Spacer(), - Text( - title, - style: Theme.of( - context, - ).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold), - ), + Text(title, style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold)), const Spacer(), - FilledButton( - onPressed: canSave ? onSave : null, - child: Text(saveLabel), - ), + FilledButton(onPressed: canSave ? onSave : null, child: Text(saveLabel)), ], ); } diff --git a/app/lib/widgets/shared/eink_page_header.dart b/app/lib/widgets/shared/eink_page_header.dart index a2e7a5f..387fad5 100644 --- a/app/lib/widgets/shared/eink_page_header.dart +++ b/app/lib/widgets/shared/eink_page_header.dart @@ -23,20 +23,12 @@ class EinkPageHeader extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: Spacing.lg), decoration: BoxDecoration( border: Border( - bottom: BorderSide( - color: colorScheme.outline, - width: BorderWidths.einkDefault, - ), + bottom: BorderSide(color: colorScheme.outline, width: BorderWidths.einkDefault), ), ), child: Row( children: [ - Text( - title, - style: Theme.of( - context, - ).textTheme.headlineMedium?.copyWith(fontWeight: FontWeight.bold), - ), + Text(title, style: Theme.of(context).textTheme.headlineMedium?.copyWith(fontWeight: FontWeight.bold)), const Spacer(), if (trailing != null) trailing!, ], diff --git a/app/lib/widgets/shared/empty_state.dart b/app/lib/widgets/shared/empty_state.dart index ced185b..bf6989e 100644 --- a/app/lib/widgets/shared/empty_state.dart +++ b/app/lib/widgets/shared/empty_state.dart @@ -40,33 +40,24 @@ class EmptyState extends StatelessWidget { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon( - icon, - size: iconSize, - color: colorScheme.onSurfaceVariant.withValues(alpha: 0.5), - ), + Icon(icon, size: iconSize, color: colorScheme.onSurfaceVariant.withValues(alpha: 0.5)), const SizedBox(height: Spacing.md), Text( title, - style: Theme.of(context).textTheme.titleLarge?.copyWith( - color: colorScheme.onSurfaceVariant, - ), + style: Theme.of(context).textTheme.titleLarge?.copyWith(color: colorScheme.onSurfaceVariant), textAlign: TextAlign.center, ), if (subtitle != null) ...[ const SizedBox(height: Spacing.sm), Text( subtitle!, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: colorScheme.onSurfaceVariant.withValues(alpha: 0.7), - ), + style: Theme.of( + context, + ).textTheme.bodyMedium?.copyWith(color: colorScheme.onSurfaceVariant.withValues(alpha: 0.7)), textAlign: TextAlign.center, ), ], - if (action != null) ...[ - const SizedBox(height: Spacing.lg), - action!, - ], + if (action != null) ...[const SizedBox(height: Spacing.lg), action!], ], ), ), diff --git a/app/lib/widgets/shared/quick_filter_chips.dart b/app/lib/widgets/shared/quick_filter_chips.dart index 235dca9..fa1a0b8 100644 --- a/app/lib/widgets/shared/quick_filter_chips.dart +++ b/app/lib/widgets/shared/quick_filter_chips.dart @@ -7,11 +7,7 @@ class QuickFilterChipData { final IconData icon; final bool isSelected; - const QuickFilterChipData({ - required this.label, - required this.icon, - required this.isSelected, - }); + const QuickFilterChipData({required this.label, required this.icon, required this.isSelected}); } /// Horizontal scrollable filter chips, provider-agnostic. @@ -20,12 +16,7 @@ class QuickFilterChips extends StatelessWidget { final ValueChanged onFilterTapped; final double? horizontalPadding; - const QuickFilterChips({ - super.key, - required this.filters, - required this.onFilterTapped, - this.horizontalPadding, - }); + const QuickFilterChips({super.key, required this.filters, required this.onFilterTapped, this.horizontalPadding}); @override Widget build(BuildContext context) { @@ -37,12 +28,9 @@ class QuickFilterChips extends StatelessWidget { height: 48, child: ListView.separated( scrollDirection: Axis.horizontal, - padding: EdgeInsets.symmetric( - horizontal: horizontalPadding ?? Spacing.md, - ), + padding: EdgeInsets.symmetric(horizontal: horizontalPadding ?? Spacing.md), itemCount: filters.length, - separatorBuilder: (context, index) => - const SizedBox(width: Spacing.sm), + separatorBuilder: (context, index) => const SizedBox(width: Spacing.sm), itemBuilder: (context, index) { final filter = filters[index]; @@ -53,17 +41,13 @@ class QuickFilterChips extends StatelessWidget { Icon( filter.icon, size: IconSizes.small, - color: filter.isSelected - ? colorScheme.onSecondaryContainer - : colorScheme.onSurfaceVariant, + color: filter.isSelected ? colorScheme.onSecondaryContainer : colorScheme.onSurfaceVariant, ), const SizedBox(width: Spacing.xs), Text( filter.label, style: TextStyle( - color: filter.isSelected - ? colorScheme.onSecondaryContainer - : colorScheme.onSurfaceVariant, + color: filter.isSelected ? colorScheme.onSecondaryContainer : colorScheme.onSurfaceVariant, ), ), ], diff --git a/app/lib/widgets/shared/view_mode_toggle.dart b/app/lib/widgets/shared/view_mode_toggle.dart index 41c5cbe..ad3aba6 100644 --- a/app/lib/widgets/shared/view_mode_toggle.dart +++ b/app/lib/widgets/shared/view_mode_toggle.dart @@ -6,11 +6,7 @@ import 'package:papyrus/themes/design_tokens.dart'; /// Used across library, shelves, and other pages that offer /// grid vs list view switching. class ViewModeToggle extends StatelessWidget { - const ViewModeToggle({ - super.key, - required this.isGridView, - required this.onChanged, - }); + const ViewModeToggle({super.key, required this.isGridView, required this.onChanged}); final bool isGridView; final ValueChanged onChanged; @@ -19,21 +15,12 @@ class ViewModeToggle extends StatelessWidget { Widget build(BuildContext context) { return SegmentedButton( segments: const [ - ButtonSegment( - value: true, - icon: Icon(Icons.grid_view, size: IconSizes.small), - ), - ButtonSegment( - value: false, - icon: Icon(Icons.view_list, size: IconSizes.small), - ), + ButtonSegment(value: true, icon: Icon(Icons.grid_view, size: IconSizes.small)), + ButtonSegment(value: false, icon: Icon(Icons.view_list, size: IconSizes.small)), ], selected: {isGridView}, onSelectionChanged: (selection) => onChanged(selection.first), - style: ButtonStyle( - visualDensity: VisualDensity.compact, - tapTargetSize: MaterialTapTargetSize.shrinkWrap, - ), + style: ButtonStyle(visualDensity: VisualDensity.compact, tapTargetSize: MaterialTapTargetSize.shrinkWrap), ); } } diff --git a/app/lib/widgets/shell/adaptive_app_shell.dart b/app/lib/widgets/shell/adaptive_app_shell.dart index 81aacf6..ac7060c 100644 --- a/app/lib/widgets/shell/adaptive_app_shell.dart +++ b/app/lib/widgets/shell/adaptive_app_shell.dart @@ -99,12 +99,7 @@ class AdaptiveAppShell extends StatelessWidget { icon: Icons.bar_chart_outlined, selectedIcon: Icons.bar_chart, ), - const AppShellNavItem( - path: '/profile', - label: 'Profile', - icon: Icons.person_outline, - selectedIcon: Icons.person, - ), + const AppShellNavItem(path: '/profile', label: 'Profile', icon: Icons.person_outline, selectedIcon: Icons.person), ]; } @@ -129,10 +124,7 @@ class AdaptiveAppShell extends StatelessWidget { } } - Widget _buildDesktopShell( - BuildContext context, - List navItems, - ) { + Widget _buildDesktopShell(BuildContext context, List navItems) { return Scaffold( body: Row( children: [ @@ -147,10 +139,7 @@ class AdaptiveAppShell extends StatelessWidget { ); } - Widget _buildMobileShell( - BuildContext context, - List navItems, - ) { + Widget _buildMobileShell(BuildContext context, List navItems) { final currentPath = GoRouterState.of(context).uri.toString(); final isInLibrary = currentPath.startsWith('/library'); @@ -176,10 +165,7 @@ class AdaptiveAppShell extends StatelessWidget { ); } - Widget _buildLibraryDrawer( - BuildContext context, - List navItems, - ) { + Widget _buildLibraryDrawer(BuildContext context, List navItems) { final currentPath = GoRouterState.of(context).uri.toString(); final libraryItem = navItems.firstWhere((item) => item.path == '/library'); final children = libraryItem.children ?? []; @@ -191,10 +177,7 @@ class AdaptiveAppShell extends StatelessWidget { children: [ Padding( padding: const EdgeInsets.all(Spacing.md), - child: Text( - 'Library', - style: Theme.of(context).textTheme.headlineSmall, - ), + child: Text('Library', style: Theme.of(context).textTheme.headlineSmall), ), const Divider(height: 1), Expanded( @@ -205,9 +188,7 @@ class AdaptiveAppShell extends StatelessWidget { final isSelected = currentPath.startsWith(item.path); return ListTile( - leading: Icon( - isSelected ? item.selectedIcon ?? item.icon : item.icon, - ), + leading: Icon(isSelected ? item.selectedIcon ?? item.icon : item.icon), title: Text(item.label), selected: isSelected, onTap: () { diff --git a/app/lib/widgets/shell/desktop_sidebar.dart b/app/lib/widgets/shell/desktop_sidebar.dart index 90a4e8e..a4a0272 100644 --- a/app/lib/widgets/shell/desktop_sidebar.dart +++ b/app/lib/widgets/shell/desktop_sidebar.dart @@ -16,12 +16,7 @@ class DesktopSidebar extends StatelessWidget { final String currentPath; final void Function(String path) onNavigate; - const DesktopSidebar({ - super.key, - required this.items, - required this.currentPath, - required this.onNavigate, - }); + const DesktopSidebar({super.key, required this.items, required this.currentPath, required this.onNavigate}); @override Widget build(BuildContext context) { @@ -41,12 +36,7 @@ class DesktopSidebar extends StatelessWidget { child: DecoratedBox( decoration: BoxDecoration( color: colorScheme.surfaceContainerLow, - border: Border( - right: BorderSide( - color: colorScheme.outlineVariant, - width: 1, - ), - ), + border: Border(right: BorderSide(color: colorScheme.outlineVariant, width: 1)), ), child: Column( children: [ @@ -85,15 +75,11 @@ class DesktopSidebar extends StatelessWidget { child: Padding( padding: const EdgeInsets.symmetric(horizontal: Spacing.md), child: Row( - mainAxisAlignment: isCollapsed - ? MainAxisAlignment.center - : MainAxisAlignment.start, + mainAxisAlignment: isCollapsed ? MainAxisAlignment.center : MainAxisAlignment.start, children: [ // Logo icon from SVG SvgPicture.asset( - isDark - ? 'assets/images/logo-icon-dark.svg' - : 'assets/images/logo-icon-light.svg', + isDark ? 'assets/images/logo-icon-dark.svg' : 'assets/images/logo-icon-light.svg', width: 40, height: 40, ), @@ -118,11 +104,7 @@ class DesktopSidebar extends StatelessWidget { ); } - Widget _buildNavItem( - BuildContext context, - AppShellNavItem item, - bool isCollapsed, - ) { + Widget _buildNavItem(BuildContext context, AppShellNavItem item, bool isCollapsed) { final isSelected = isNavItemSelected(currentPath, item); final hasChildren = item.children != null && item.children!.isNotEmpty; final sidebarProvider = context.read(); @@ -149,19 +131,13 @@ class DesktopSidebar extends StatelessWidget { }, child: Container( height: 48, - padding: EdgeInsets.symmetric( - horizontal: isCollapsed ? Spacing.md : Spacing.md, - ), + padding: EdgeInsets.symmetric(horizontal: isCollapsed ? Spacing.md : Spacing.md), decoration: BoxDecoration( - color: isSelected - ? Theme.of(context).colorScheme.primaryContainer - : Colors.transparent, + color: isSelected ? Theme.of(context).colorScheme.primaryContainer : Colors.transparent, borderRadius: BorderRadius.circular(AppRadius.md), ), child: Row( - mainAxisAlignment: isCollapsed - ? MainAxisAlignment.center - : MainAxisAlignment.start, + mainAxisAlignment: isCollapsed ? MainAxisAlignment.center : MainAxisAlignment.start, children: [ Icon( isSelected ? item.selectedIcon ?? item.icon : item.icon, @@ -179,9 +155,7 @@ class DesktopSidebar extends StatelessWidget { color: isSelected ? Theme.of(context).colorScheme.onPrimaryContainer : Theme.of(context).colorScheme.onSurfaceVariant, - fontWeight: isSelected - ? FontWeight.w600 - : FontWeight.normal, + fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal, ), ), ), @@ -194,14 +168,9 @@ class DesktopSidebar extends StatelessWidget { ); } - Widget _buildExpandableNavItem( - BuildContext context, - AppShellNavItem item, - bool isCollapsed, - ) { + Widget _buildExpandableNavItem(BuildContext context, AppShellNavItem item, bool isCollapsed) { final sidebarProvider = context.watch(); - final isExpanded = - item.path == '/library' && sidebarProvider.isLibraryExpanded; + final isExpanded = item.path == '/library' && sidebarProvider.isLibraryExpanded; final isSelected = isNavItemSelected(currentPath, item); return Column( @@ -209,10 +178,7 @@ class DesktopSidebar extends StatelessWidget { children: [ // Parent item Padding( - padding: const EdgeInsets.symmetric( - horizontal: Spacing.sm, - vertical: 2, - ), + padding: const EdgeInsets.symmetric(horizontal: Spacing.sm, vertical: 2), child: Material( color: Colors.transparent, borderRadius: BorderRadius.circular(AppRadius.md), @@ -249,9 +215,7 @@ class DesktopSidebar extends StatelessWidget { color: isSelected ? Theme.of(context).colorScheme.onPrimaryContainer : Theme.of(context).colorScheme.onSurfaceVariant, - fontWeight: isSelected - ? FontWeight.w600 - : FontWeight.normal, + fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal, ), ), ), @@ -285,12 +249,7 @@ class DesktopSidebar extends StatelessWidget { final isSelected = currentPath.startsWith(item.path); return Padding( - padding: const EdgeInsets.only( - left: Spacing.xl + Spacing.sm, - right: Spacing.sm, - top: 2, - bottom: 2, - ), + padding: const EdgeInsets.only(left: Spacing.xl + Spacing.sm, right: Spacing.sm, top: 2, bottom: 2), child: Material( color: Colors.transparent, borderRadius: BorderRadius.circular(AppRadius.md), @@ -301,9 +260,7 @@ class DesktopSidebar extends StatelessWidget { height: 40, padding: const EdgeInsets.symmetric(horizontal: Spacing.md), decoration: BoxDecoration( - color: isSelected - ? Theme.of(context).colorScheme.primaryContainer - : Colors.transparent, + color: isSelected ? Theme.of(context).colorScheme.primaryContainer : Colors.transparent, borderRadius: BorderRadius.circular(AppRadius.md), ), child: Row( @@ -325,41 +282,27 @@ class DesktopSidebar extends StatelessWidget { color: isSelected ? Theme.of(context).colorScheme.onPrimaryContainer : Theme.of(context).colorScheme.onSurfaceVariant, - fontWeight: isSelected - ? FontWeight.w600 - : FontWeight.normal, + fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal, ), ), Spacer(), if (item.count != null) Container( - padding: const EdgeInsets.symmetric( - horizontal: Spacing.sm, - vertical: 2, - ), + padding: const EdgeInsets.symmetric(horizontal: Spacing.sm, vertical: 2), decoration: BoxDecoration( color: isSelected - ? Theme.of(context) - .colorScheme - .onPrimaryContainer - .withValues(alpha: 0.12) - : Theme.of(context).colorScheme.onSurfaceVariant - .withValues(alpha: 0.12), + ? Theme.of(context).colorScheme.onPrimaryContainer.withValues(alpha: 0.12) + : Theme.of(context).colorScheme.onSurfaceVariant.withValues(alpha: 0.12), borderRadius: BorderRadius.circular(AppRadius.full), ), child: Text( item.count.toString(), - style: Theme.of(context).textTheme.labelSmall - ?.copyWith( - color: isSelected - ? Theme.of( - context, - ).colorScheme.onPrimaryContainer - : Theme.of( - context, - ).colorScheme.onSurfaceVariant, - ), + style: Theme.of(context).textTheme.labelSmall?.copyWith( + color: isSelected + ? Theme.of(context).colorScheme.onPrimaryContainer + : Theme.of(context).colorScheme.onSurfaceVariant, + ), ), ), ], @@ -384,9 +327,7 @@ class DesktopSidebar extends StatelessWidget { height: 56, padding: const EdgeInsets.symmetric(horizontal: Spacing.md), child: Row( - mainAxisAlignment: isCollapsed - ? MainAxisAlignment.center - : MainAxisAlignment.start, + mainAxisAlignment: isCollapsed ? MainAxisAlignment.center : MainAxisAlignment.start, children: [ Icon( isCollapsed ? Icons.chevron_right : Icons.chevron_left, @@ -396,9 +337,9 @@ class DesktopSidebar extends StatelessWidget { const SizedBox(width: Spacing.md), Text( '', - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), + style: Theme.of( + context, + ).textTheme.bodyMedium?.copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant), ), ], ], diff --git a/app/lib/widgets/shell/eink_bottom_nav.dart b/app/lib/widgets/shell/eink_bottom_nav.dart index dacf4fe..efebcbc 100644 --- a/app/lib/widgets/shell/eink_bottom_nav.dart +++ b/app/lib/widgets/shell/eink_bottom_nav.dart @@ -10,12 +10,7 @@ class EinkBottomNav extends StatelessWidget { final String currentPath; final void Function(String path) onNavigate; - const EinkBottomNav({ - super.key, - required this.items, - required this.currentPath, - required this.onNavigate, - }); + const EinkBottomNav({super.key, required this.items, required this.currentPath, required this.onNavigate}); @override Widget build(BuildContext context) { @@ -28,10 +23,7 @@ class EinkBottomNav extends StatelessWidget { decoration: BoxDecoration( color: colorScheme.surface, border: Border( - top: BorderSide( - color: colorScheme.outline, - width: BorderWidths.einkDefault, - ), + top: BorderSide(color: colorScheme.outline, width: BorderWidths.einkDefault), ), ), child: Row( @@ -43,11 +35,7 @@ class EinkBottomNav extends StatelessWidget { ); } - Widget _buildNavItem( - BuildContext context, - AppShellNavItem item, - bool isSelected, - ) { + Widget _buildNavItem(BuildContext context, AppShellNavItem item, bool isSelected) { final colorScheme = Theme.of(context).colorScheme; return Material( @@ -64,17 +52,13 @@ class EinkBottomNav extends StatelessWidget { child: Container( decoration: BoxDecoration( color: isSelected ? colorScheme.primary : Colors.transparent, - border: Border( - left: BorderSide(color: colorScheme.outline, width: 1), - ), + border: Border(left: BorderSide(color: colorScheme.outline, width: 1)), ), child: Center( child: Text( item.label, style: Theme.of(context).textTheme.labelLarge?.copyWith( - color: isSelected - ? colorScheme.onPrimary - : colorScheme.onSurface, + color: isSelected ? colorScheme.onPrimary : colorScheme.onSurface, fontWeight: FontWeight.bold, letterSpacing: 1.2, ), diff --git a/app/lib/widgets/shell/mobile_bottom_nav.dart b/app/lib/widgets/shell/mobile_bottom_nav.dart index 5d3e757..1fe56ec 100644 --- a/app/lib/widgets/shell/mobile_bottom_nav.dart +++ b/app/lib/widgets/shell/mobile_bottom_nav.dart @@ -8,12 +8,7 @@ class MobileBottomNav extends StatelessWidget { final String currentPath; final void Function(String path) onNavigate; - const MobileBottomNav({ - super.key, - required this.items, - required this.currentPath, - required this.onNavigate, - }); + const MobileBottomNav({super.key, required this.items, required this.currentPath, required this.onNavigate}); @override Widget build(BuildContext context) { diff --git a/app/lib/widgets/shelves/add_shelf_sheet.dart b/app/lib/widgets/shelves/add_shelf_sheet.dart index b558f45..97107f7 100644 --- a/app/lib/widgets/shelves/add_shelf_sheet.dart +++ b/app/lib/widgets/shelves/add_shelf_sheet.dart @@ -10,13 +10,7 @@ class AddShelfSheet extends StatefulWidget { final ShelfData? shelf; /// Called when the shelf is saved. - final void Function( - String name, - String? description, - String? colorHex, - IconData? icon, - )? - onSave; + final void Function(String name, String? description, String? colorHex, IconData? icon)? onSave; const AddShelfSheet({super.key, this.shelf, this.onSave}); @@ -24,20 +18,12 @@ class AddShelfSheet extends StatefulWidget { static Future show( BuildContext context, { ShelfData? shelf, - void Function( - String name, - String? description, - String? colorHex, - IconData? icon, - )? - onSave, + void Function(String name, String? description, String? colorHex, IconData? icon)? onSave, }) { return showModalBottomSheet( context: context, isScrollControlled: true, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(AppRadius.xl)), - ), + shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(AppRadius.xl))), builder: (context) => AddShelfSheet(shelf: shelf, onSave: onSave), ); } @@ -58,9 +44,7 @@ class _AddShelfSheetState extends State { void initState() { super.initState(); _nameController = TextEditingController(text: widget.shelf?.name ?? ''); - _descriptionController = TextEditingController( - text: widget.shelf?.description ?? '', - ); + _descriptionController = TextEditingController(text: widget.shelf?.description ?? ''); _selectedColorHex = widget.shelf?.colorHex ?? ShelfData.availableColors[5]; _selectedIcon = widget.shelf?.icon ?? Icons.folder_outlined; } @@ -93,19 +77,11 @@ class _AddShelfSheetState extends State { const BottomSheetHandle(), const SizedBox(height: Spacing.lg), // Title - Text( - _isEditing ? 'Edit shelf' : 'Create new shelf', - style: textTheme.headlineSmall, - ), + Text(_isEditing ? 'Edit shelf' : 'Create new shelf', style: textTheme.headlineSmall), const SizedBox(height: Spacing.lg), // Name field - Text( - 'Name', - style: textTheme.titleSmall?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), + Text('Name', style: textTheme.titleSmall?.copyWith(color: colorScheme.onSurfaceVariant)), const SizedBox(height: Spacing.sm), TextFormField( controller: _nameController, @@ -114,24 +90,14 @@ class _AddShelfSheetState extends State { onChanged: (_) => setState(() {}), decoration: InputDecoration( hintText: 'Enter shelf name', - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(AppRadius.md), - ), - contentPadding: const EdgeInsets.symmetric( - horizontal: Spacing.md, - vertical: Spacing.sm, - ), + border: OutlineInputBorder(borderRadius: BorderRadius.circular(AppRadius.md)), + contentPadding: const EdgeInsets.symmetric(horizontal: Spacing.md, vertical: Spacing.sm), ), ), const SizedBox(height: Spacing.lg), // Description field - Text( - 'Description (optional)', - style: textTheme.titleSmall?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), + Text('Description (optional)', style: textTheme.titleSmall?.copyWith(color: colorScheme.onSurfaceVariant)), const SizedBox(height: Spacing.sm), TextFormField( controller: _descriptionController, @@ -140,32 +106,20 @@ class _AddShelfSheetState extends State { onChanged: (_) => setState(() {}), decoration: InputDecoration( hintText: 'Add a description', - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(AppRadius.md), - ), + border: OutlineInputBorder(borderRadius: BorderRadius.circular(AppRadius.md)), contentPadding: const EdgeInsets.all(Spacing.md), ), ), const SizedBox(height: Spacing.lg), // Color picker - Text( - 'Color', - style: textTheme.titleSmall?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), + Text('Color', style: textTheme.titleSmall?.copyWith(color: colorScheme.onSurfaceVariant)), const SizedBox(height: Spacing.sm), _buildColorPicker(context), const SizedBox(height: Spacing.lg), // Icon picker - Text( - 'Icon', - style: textTheme.titleSmall?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), + Text('Icon', style: textTheme.titleSmall?.copyWith(color: colorScheme.onSurfaceVariant)), const SizedBox(height: Spacing.sm), _buildIconPicker(context), const SizedBox(height: Spacing.xl), @@ -211,22 +165,12 @@ class _AddShelfSheetState extends State { decoration: BoxDecoration( color: color, shape: BoxShape.circle, - border: isSelected - ? Border.all(color: colorScheme.primary, width: 3) - : null, + border: isSelected ? Border.all(color: colorScheme.primary, width: 3) : null, boxShadow: isSelected - ? [ - BoxShadow( - color: color.withValues(alpha: 0.4), - blurRadius: 8, - spreadRadius: 1, - ), - ] + ? [BoxShadow(color: color.withValues(alpha: 0.4), blurRadius: 8, spreadRadius: 1)] : null, ), - child: isSelected - ? Icon(Icons.check, size: 18, color: getContrastColor(color)) - : null, + child: isSelected ? Icon(Icons.check, size: 18, color: getContrastColor(color)) : null, ), ); }).toList(), @@ -235,9 +179,7 @@ class _AddShelfSheetState extends State { Widget _buildIconPicker(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; - final selectedColor = _selectedColorHex != null - ? parseHexColor(_selectedColorHex!) - : colorScheme.primary; + final selectedColor = _selectedColorHex != null ? parseHexColor(_selectedColorHex!) : colorScheme.primary; return Wrap( spacing: Spacing.sm, @@ -251,19 +193,13 @@ class _AddShelfSheetState extends State { width: 44, height: 44, decoration: BoxDecoration( - color: isSelected - ? selectedColor.withValues(alpha: 0.15) - : colorScheme.surfaceContainerHighest, + color: isSelected ? selectedColor.withValues(alpha: 0.15) : colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(AppRadius.md), border: isSelected ? Border.all(color: selectedColor, width: 2) : Border.all(color: colorScheme.outlineVariant, width: 1), ), - child: Icon( - icon, - size: 24, - color: isSelected ? selectedColor : colorScheme.onSurfaceVariant, - ), + child: Icon(icon, size: 24, color: isSelected ? selectedColor : colorScheme.onSurfaceVariant), ), ); }).toList(), @@ -273,9 +209,7 @@ class _AddShelfSheetState extends State { Widget _buildPreview(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; final textTheme = Theme.of(context).textTheme; - final shelfColor = _selectedColorHex != null - ? parseHexColor(_selectedColorHex!) - : colorScheme.primary; + final shelfColor = _selectedColorHex != null ? parseHexColor(_selectedColorHex!) : colorScheme.primary; return Container( padding: const EdgeInsets.all(Spacing.md), @@ -295,10 +229,7 @@ class _AddShelfSheetState extends State { borderRadius: BorderRadius.circular(AppRadius.md), border: Border.all(color: shelfColor.withValues(alpha: 0.3)), ), - child: Icon( - _selectedIcon ?? Icons.folder_outlined, - color: shelfColor, - ), + child: Icon(_selectedIcon ?? Icons.folder_outlined, color: shelfColor), ), const SizedBox(width: Spacing.md), // Info @@ -307,14 +238,10 @@ class _AddShelfSheetState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - _nameController.text.isNotEmpty - ? _nameController.text - : 'Shelf name', + _nameController.text.isNotEmpty ? _nameController.text : 'Shelf name', style: textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w600, - color: _nameController.text.isEmpty - ? colorScheme.onSurfaceVariant - : null, + color: _nameController.text.isEmpty ? colorScheme.onSurfaceVariant : null, ), maxLines: 1, overflow: TextOverflow.ellipsis, @@ -323,9 +250,7 @@ class _AddShelfSheetState extends State { const SizedBox(height: 2), Text( _descriptionController.text, - style: textTheme.bodySmall?.copyWith( - color: colorScheme.onSurfaceVariant, - ), + style: textTheme.bodySmall?.copyWith(color: colorScheme.onSurfaceVariant), maxLines: 1, overflow: TextOverflow.ellipsis, ), @@ -340,12 +265,7 @@ class _AddShelfSheetState extends State { color: colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(AppRadius.full), ), - child: Text( - 'Preview', - style: textTheme.labelSmall?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), + child: Text('Preview', style: textTheme.labelSmall?.copyWith(color: colorScheme.onSurfaceVariant)), ), ], ), @@ -358,9 +278,7 @@ class _AddShelfSheetState extends State { widget.onSave?.call( name, - _descriptionController.text.trim().isEmpty - ? null - : _descriptionController.text.trim(), + _descriptionController.text.trim().isEmpty ? null : _descriptionController.text.trim(), _selectedColorHex, _selectedIcon, ); diff --git a/app/lib/widgets/shelves/move_to_shelf_sheet.dart b/app/lib/widgets/shelves/move_to_shelf_sheet.dart index bde61f2..a22a708 100644 --- a/app/lib/widgets/shelves/move_to_shelf_sheet.dart +++ b/app/lib/widgets/shelves/move_to_shelf_sheet.dart @@ -25,17 +25,11 @@ class MoveToShelfSheet extends StatefulWidget { bool get isBulkMode => bulkBookIds != null && bulkBookIds!.isNotEmpty; /// Shows the move to shelf sheet for a single book. - static Future show( - BuildContext context, { - required Book book, - void Function(List shelfIds)? onSave, - }) { + static Future show(BuildContext context, {required Book book, void Function(List shelfIds)? onSave}) { return showModalBottomSheet( context: context, isScrollControlled: true, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(AppRadius.xl)), - ), + shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(AppRadius.xl))), builder: (context) => MoveToShelfSheet(book: book, onSave: onSave), ); } @@ -49,11 +43,8 @@ class MoveToShelfSheet extends StatefulWidget { return showModalBottomSheet( context: context, isScrollControlled: true, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(AppRadius.xl)), - ), - builder: (context) => - MoveToShelfSheet(bulkBookIds: bookIds, onSave: onSave), + shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(AppRadius.xl))), + builder: (context) => MoveToShelfSheet(bulkBookIds: bookIds, onSave: onSave), ); } @@ -95,11 +86,7 @@ class _MoveToShelfSheetState extends State { maxChildSize: 0.9, expand: false, builder: (context, scrollController) => Padding( - padding: const EdgeInsets.only( - left: Spacing.lg, - right: Spacing.lg, - top: Spacing.md, - ), + padding: const EdgeInsets.only(left: Spacing.lg, right: Spacing.lg, top: Spacing.md), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, @@ -115,11 +102,7 @@ class _MoveToShelfSheetState extends State { if (!widget.isBulkMode) ...[ ClipRRect( borderRadius: BorderRadius.circular(AppRadius.sm), - child: SizedBox( - width: 40, - height: 60, - child: _buildCover(context), - ), + child: SizedBox(width: 40, height: 60, child: _buildCover(context)), ), SizedBox(width: Spacing.sm + Spacing.xs), @@ -140,9 +123,7 @@ class _MoveToShelfSheetState extends State { const SizedBox(height: 2), Text( widget.book!.title, - style: textTheme.bodySmall?.copyWith( - color: colorScheme.onSurfaceVariant, - ), + style: textTheme.bodySmall?.copyWith(color: colorScheme.onSurfaceVariant), maxLines: 1, overflow: TextOverflow.ellipsis, ), @@ -176,9 +157,7 @@ class _MoveToShelfSheetState extends State { ? shelves : shelves .where( - (searchString) => searchString.name - .toLowerCase() - .contains(_searchQuery.toLowerCase()), + (searchString) => searchString.name.toLowerCase().contains(_searchQuery.toLowerCase()), ) .toList(); @@ -186,9 +165,7 @@ class _MoveToShelfSheetState extends State { return Center( child: Text( 'No shelves found', - style: textTheme.bodyMedium?.copyWith( - color: colorScheme.onSurfaceVariant, - ), + style: textTheme.bodyMedium?.copyWith(color: colorScheme.onSurfaceVariant), ), ); } @@ -196,8 +173,7 @@ class _MoveToShelfSheetState extends State { return ListView.builder( controller: scrollController, itemCount: filteredShelves.length, - itemBuilder: (context, index) => - _buildShelfTile(context, filteredShelves[index]), + itemBuilder: (context, index) => _buildShelfTile(context, filteredShelves[index]), ); }, ), @@ -205,17 +181,11 @@ class _MoveToShelfSheetState extends State { // Action buttons Padding( - padding: EdgeInsets.only( - top: Spacing.md, - bottom: MediaQuery.of(context).viewInsets.bottom + Spacing.md, - ), + padding: EdgeInsets.only(top: Spacing.md, bottom: MediaQuery.of(context).viewInsets.bottom + Spacing.md), child: Row( mainAxisAlignment: MainAxisAlignment.end, children: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: const Text('Cancel'), - ), + TextButton(onPressed: () => Navigator.pop(context), child: const Text('Cancel')), const SizedBox(width: Spacing.md), FilledButton(onPressed: _onSave, child: const Text('Save')), ], @@ -237,22 +207,14 @@ class _MoveToShelfSheetState extends State { fit: BoxFit.cover, errorBuilder: (context, error, stackTrace) => Container( color: colorScheme.surfaceContainerHighest, - child: Icon( - Icons.menu_book, - color: colorScheme.onSurfaceVariant, - size: 20, - ), + child: Icon(Icons.menu_book, color: colorScheme.onSurfaceVariant, size: 20), ), ); } return Container( color: colorScheme.surfaceContainerHighest, - child: Icon( - Icons.menu_book, - color: colorScheme.onSurfaceVariant, - size: 20, - ), + child: Icon(Icons.menu_book, color: colorScheme.onSurfaceVariant, size: 20), ); } @@ -265,24 +227,16 @@ class _MoveToShelfSheetState extends State { return Card( margin: const EdgeInsets.only(bottom: Spacing.xs), elevation: 0, - color: isSelected - ? shelfColor.withValues(alpha: 0.1) - : colorScheme.surfaceContainerLow, + color: isSelected ? shelfColor.withValues(alpha: 0.1) : colorScheme.surfaceContainerLow, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(AppRadius.md), - side: BorderSide( - color: isSelected ? shelfColor : colorScheme.outlineVariant, - width: isSelected ? 2 : 1, - ), + side: BorderSide(color: isSelected ? shelfColor : colorScheme.outlineVariant, width: isSelected ? 2 : 1), ), child: InkWell( onTap: () => _toggleShelf(shelf.id), borderRadius: BorderRadius.circular(AppRadius.md), child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: Spacing.md, - vertical: Spacing.sm, - ), + padding: const EdgeInsets.symmetric(horizontal: Spacing.md, vertical: Spacing.sm), child: Row( children: [ // Shelf icon @@ -303,27 +257,19 @@ class _MoveToShelfSheetState extends State { children: [ Text( shelf.name, - style: textTheme.titleSmall?.copyWith( - fontWeight: FontWeight.w600, - ), + style: textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w600), maxLines: 1, overflow: TextOverflow.ellipsis, ), Text( shelf.bookCountLabel, - style: textTheme.bodySmall?.copyWith( - color: colorScheme.onSurfaceVariant, - ), + style: textTheme.bodySmall?.copyWith(color: colorScheme.onSurfaceVariant), ), ], ), ), // Checkbox - Checkbox( - value: isSelected, - onChanged: (_) => _toggleShelf(shelf.id), - activeColor: shelfColor, - ), + Checkbox(value: isSelected, onChanged: (_) => _toggleShelf(shelf.id), activeColor: shelfColor), ], ), ), diff --git a/app/lib/widgets/shelves/shelf_card.dart b/app/lib/widgets/shelves/shelf_card.dart index 176801e..3c495ab 100644 --- a/app/lib/widgets/shelves/shelf_card.dart +++ b/app/lib/widgets/shelves/shelf_card.dart @@ -38,17 +38,14 @@ class ShelfCard extends StatefulWidget { class _ShelfCardState extends State { bool _isHovered = false; - bool get _isDesktop => - MediaQuery.of(context).size.width >= Breakpoints.desktopSmall; + bool get _isDesktop => MediaQuery.of(context).size.width >= Breakpoints.desktopSmall; @override Widget build(BuildContext context) { return MouseRegion( onEnter: (_) => setState(() => _isHovered = true), onExit: (_) => setState(() => _isHovered = false), - child: widget.isListItem - ? _buildListItem(context) - : _buildGridCard(context), + child: widget.isListItem ? _buildListItem(context) : _buildGridCard(context), ); } @@ -99,18 +96,12 @@ class _ShelfCardState extends State { // Icon and title row Row( children: [ - Icon( - widget.shelf.displayIcon, - size: IconSizes.small, - color: shelfColor, - ), + Icon(widget.shelf.displayIcon, size: IconSizes.small, color: shelfColor), const SizedBox(width: Spacing.xs), Expanded( child: Text( widget.shelf.name, - style: textTheme.titleSmall?.copyWith( - fontWeight: FontWeight.w600, - ), + style: textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w600), maxLines: 1, overflow: TextOverflow.ellipsis, ), @@ -120,9 +111,7 @@ class _ShelfCardState extends State { const SizedBox(height: 2), Text( widget.shelf.bookCountLabel, - style: textTheme.bodySmall?.copyWith( - color: colorScheme.onSurfaceVariant, - ), + style: textTheme.bodySmall?.copyWith(color: colorScheme.onSurfaceVariant), ), ], ), @@ -152,19 +141,10 @@ class _ShelfCardState extends State { gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, - colors: [ - shelfColor.withValues(alpha: 0.3), - shelfColor.withValues(alpha: 0.15), - ], - ), - ), - child: Center( - child: Icon( - widget.shelf.displayIcon, - size: 48, - color: shelfColor.withValues(alpha: 0.6), + colors: [shelfColor.withValues(alpha: 0.3), shelfColor.withValues(alpha: 0.15)], ), ), + child: Center(child: Icon(widget.shelf.displayIcon, size: 48, color: shelfColor.withValues(alpha: 0.6))), ); } @@ -245,10 +225,8 @@ class _ShelfCardState extends State { image: DecorationImage(image: imageProvider, fit: BoxFit.cover), ), ), - errorWidget: (context, url, error) => - _buildCoverPlaceholder(colorScheme, cover.title), - placeholder: (context, url) => - Container(color: colorScheme.surfaceContainerHighest), + errorWidget: (context, url, error) => _buildCoverPlaceholder(colorScheme, cover.title), + placeholder: (context, url) => Container(color: colorScheme.surfaceContainerHighest), ); } @@ -261,20 +239,13 @@ class _ShelfCardState extends State { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon( - Icons.menu_book, - size: IconSizes.display, - color: colorScheme.onSurfaceVariant.withValues(alpha: 0.5), - ), + Icon(Icons.menu_book, size: IconSizes.display, color: colorScheme.onSurfaceVariant.withValues(alpha: 0.5)), const SizedBox(height: Spacing.xs), Padding( padding: const EdgeInsets.symmetric(horizontal: Spacing.sm), child: Text( title, - style: TextStyle( - fontSize: 11, - color: colorScheme.onSurfaceVariant.withValues(alpha: 0.7), - ), + style: TextStyle(fontSize: 11, color: colorScheme.onSurfaceVariant.withValues(alpha: 0.7)), textAlign: TextAlign.center, maxLines: 2, overflow: TextOverflow.ellipsis, @@ -296,11 +267,7 @@ class _ShelfCardState extends State { onTap: widget.onMoreTap, child: Padding( padding: const EdgeInsets.all(6), - child: Icon( - Icons.more_vert, - size: IconSizes.small, - color: Colors.white, - ), + child: Icon(Icons.more_vert, size: IconSizes.small, color: Colors.white), ), ), ); @@ -321,14 +288,9 @@ class _ShelfCardState extends State { onTap: widget.onTap, onLongPress: widget.onLongPress, child: Container( - padding: const EdgeInsets.symmetric( - horizontal: Spacing.md, - vertical: Spacing.sm, - ), + padding: const EdgeInsets.symmetric(horizontal: Spacing.md, vertical: Spacing.sm), decoration: BoxDecoration( - border: Border( - bottom: BorderSide(color: colorScheme.outlineVariant), - ), + border: Border(bottom: BorderSide(color: colorScheme.outlineVariant)), ), child: Row( children: [ @@ -351,18 +313,14 @@ class _ShelfCardState extends State { children: [ Text( widget.shelf.name, - style: textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.w600, - ), + style: textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600), maxLines: 1, overflow: TextOverflow.ellipsis, ), const SizedBox(height: 2), Text( widget.shelf.description ?? widget.shelf.bookCountLabel, - style: textTheme.bodySmall?.copyWith( - color: colorScheme.onSurfaceVariant, - ), + style: textTheme.bodySmall?.copyWith(color: colorScheme.onSurfaceVariant), maxLines: 1, overflow: TextOverflow.ellipsis, ), @@ -371,19 +329,14 @@ class _ShelfCardState extends State { ), // Book count badge Container( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 6, - ), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: BoxDecoration( color: colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(AppRadius.full), ), child: Text( '${widget.shelf.bookCount}', - style: textTheme.labelLarge?.copyWith( - fontWeight: FontWeight.w600, - ), + style: textTheme.labelLarge?.copyWith(fontWeight: FontWeight.w600), ), ), // More button (desktop hover only) diff --git a/app/lib/widgets/statistics/reading_charts.dart b/app/lib/widgets/statistics/reading_charts.dart index 830cc61..fb9be03 100644 --- a/app/lib/widgets/statistics/reading_charts.dart +++ b/app/lib/widgets/statistics/reading_charts.dart @@ -77,20 +77,7 @@ List<_AggregatedBucket> _aggregateWeekly(List activities) { // AXIS HELPERS // ============================================================================= -const _months = [ - 'Jan', - 'Feb', - 'Mar', - 'Apr', - 'May', - 'Jun', - 'Jul', - 'Aug', - 'Sep', - 'Oct', - 'Nov', - 'Dec', -]; +const _months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; /// Formats a minutes value for the Y-axis (e.g., "0", "30m", "1h", "1.5h"). String _formatMinutesLabel(double value) { @@ -111,24 +98,7 @@ String _formatPagesLabel(double value) { ({double interval, double maxY}) _niceYAxis(double maxValue) { if (maxValue <= 0) return (interval: 15.0, maxY: 60.0); final rawInterval = maxValue / 3; - const niceSteps = [ - 5, - 10, - 15, - 20, - 25, - 30, - 50, - 60, - 100, - 120, - 150, - 200, - 250, - 300, - 500, - 1000, - ]; + const niceSteps = [5, 10, 15, 20, 25, 30, 50, 60, 100, 120, 150, 200, 250, 300, 500, 1000]; var interval = (rawInterval / 100).ceil() * 100.0; for (final step in niceSteps) { if (step >= rawInterval) { @@ -141,31 +111,18 @@ String _formatPagesLabel(double value) { } /// Builds a Y-axis title widget. -Widget _buildYAxisLabel( - double value, - TextTheme textTheme, - ColorScheme colorScheme, - String Function(double) formatter, -) { +Widget _buildYAxisLabel(double value, TextTheme textTheme, ColorScheme colorScheme, String Function(double) formatter) { return Padding( padding: const EdgeInsets.only(right: 4), child: Text( formatter(value), - style: textTheme.labelSmall?.copyWith( - color: colorScheme.onSurfaceVariant, - fontSize: 10, - ), + style: textTheme.labelSmall?.copyWith(color: colorScheme.onSurfaceVariant, fontSize: 10), ), ); } /// Builds a bottom axis label for chart data points. -Widget _buildBottomLabel( - double value, - List activities, - TextTheme textTheme, - ColorScheme colorScheme, -) { +Widget _buildBottomLabel(double value, List activities, TextTheme textTheme, ColorScheme colorScheme) { final idx = value.toInt(); if (idx < 0 || idx >= activities.length) return const SizedBox.shrink(); @@ -183,8 +140,7 @@ Widget _buildBottomLabel( label = '${activity.date.day}'; } } else { - if (idx == 0 || - activities[idx].date.month != activities[idx - 1].date.month) { + if (idx == 0 || activities[idx].date.month != activities[idx - 1].date.month) { label = _months[activity.date.month - 1]; } } @@ -196,9 +152,7 @@ Widget _buildBottomLabel( child: Text( label, style: textTheme.labelSmall?.copyWith( - color: activity.isToday - ? colorScheme.primary - : colorScheme.onSurfaceVariant, + color: activity.isToday ? colorScheme.primary : colorScheme.onSurfaceVariant, fontWeight: activity.isToday ? FontWeight.bold : FontWeight.normal, fontSize: 10, ), @@ -217,8 +171,7 @@ Widget _buildBucketBottomLabel( if (idx < 0 || idx >= buckets.length) return const SizedBox.shrink(); final bucket = buckets[idx]; - if (idx > 0 && - buckets[idx].startDate.month == buckets[idx - 1].startDate.month) { + if (idx > 0 && buckets[idx].startDate.month == buckets[idx - 1].startDate.month) { return const SizedBox.shrink(); } @@ -227,9 +180,7 @@ Widget _buildBucketBottomLabel( child: Text( _months[bucket.startDate.month - 1], style: textTheme.labelSmall?.copyWith( - color: bucket.containsToday - ? colorScheme.primary - : colorScheme.onSurfaceVariant, + color: bucket.containsToday ? colorScheme.primary : colorScheme.onSurfaceVariant, fontWeight: bucket.containsToday ? FontWeight.bold : FontWeight.normal, fontSize: 10, ), @@ -247,12 +198,7 @@ class ReadingTimeBarChart extends StatelessWidget { final bool isWeekly; final bool isDesktop; - const ReadingTimeBarChart({ - super.key, - required this.activities, - this.isWeekly = true, - this.isDesktop = false, - }); + const ReadingTimeBarChart({super.key, required this.activities, this.isWeekly = true, this.isDesktop = false}); @override Widget build(BuildContext context) { @@ -263,22 +209,12 @@ class ReadingTimeBarChart extends StatelessWidget { final chartHeight = isDesktop ? 200.0 : 160.0; if (activities.length > 21) { - return _buildAggregatedChart( - context, - colorScheme, - textTheme, - chartHeight, - ); + return _buildAggregatedChart(context, colorScheme, textTheme, chartHeight); } return _buildDailyChart(context, colorScheme, textTheme, chartHeight); } - Widget _buildDailyChart( - BuildContext context, - ColorScheme colorScheme, - TextTheme textTheme, - double chartHeight, - ) { + Widget _buildDailyChart(BuildContext context, ColorScheme colorScheme, TextTheme textTheme, double chartHeight) { final maxMinutes = activities.maxMinutes.toDouble(); final yAxis = _niceYAxis(maxMinutes); @@ -287,10 +223,7 @@ class ReadingTimeBarChart extends StatelessWidget { child: LayoutBuilder( builder: (context, constraints) { final availableWidth = constraints.maxWidth - 60; - final barWidth = (availableWidth / activities.length * 0.6).clamp( - 4.0, - isDesktop ? 20.0 : 16.0, - ); + final barWidth = (availableWidth / activities.length * 0.6).clamp(4.0, isDesktop ? 20.0 : 16.0); return BarChart( BarChartData( @@ -305,30 +238,19 @@ class ReadingTimeBarChart extends StatelessWidget { final activity = activities[groupIndex]; return BarTooltipItem( '${activity.dayName}\n${activity.readingTimeLabel}', - textTheme.bodySmall!.copyWith( - color: colorScheme.onInverseSurface, - ), + textTheme.bodySmall!.copyWith(color: colorScheme.onInverseSurface), ); }, ), ), titlesData: FlTitlesData( show: true, - rightTitles: const AxisTitles( - sideTitles: SideTitles(showTitles: false), - ), - topTitles: const AxisTitles( - sideTitles: SideTitles(showTitles: false), - ), + rightTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)), + topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)), bottomTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, - getTitlesWidget: (value, meta) => _buildBottomLabel( - value, - activities, - textTheme, - colorScheme, - ), + getTitlesWidget: (value, meta) => _buildBottomLabel(value, activities, textTheme, colorScheme), reservedSize: 28, ), ), @@ -337,12 +259,8 @@ class ReadingTimeBarChart extends StatelessWidget { showTitles: true, reservedSize: 40, interval: yAxis.interval, - getTitlesWidget: (value, meta) => _buildYAxisLabel( - value, - textTheme, - colorScheme, - _formatMinutesLabel, - ), + getTitlesWidget: (value, meta) => + _buildYAxisLabel(value, textTheme, colorScheme, _formatMinutesLabel), ), ), ), @@ -350,10 +268,8 @@ class ReadingTimeBarChart extends StatelessWidget { show: true, drawVerticalLine: false, horizontalInterval: yAxis.interval, - getDrawingHorizontalLine: (value) => FlLine( - color: colorScheme.outlineVariant.withValues(alpha: 0.5), - strokeWidth: 1, - ), + getDrawingHorizontalLine: (value) => + FlLine(color: colorScheme.outlineVariant.withValues(alpha: 0.5), strokeWidth: 1), ), borderData: FlBorderData(show: false), barGroups: activities.asMap().entries.map((entry) { @@ -364,13 +280,9 @@ class ReadingTimeBarChart extends StatelessWidget { barRods: [ BarChartRodData( toY: activity.readingMinutes.toDouble(), - color: activity.isToday - ? colorScheme.primary - : colorScheme.primary.withValues(alpha: 0.6), + color: activity.isToday ? colorScheme.primary : colorScheme.primary.withValues(alpha: 0.6), width: barWidth, - borderRadius: const BorderRadius.vertical( - top: Radius.circular(4), - ), + borderRadius: const BorderRadius.vertical(top: Radius.circular(4)), ), ], ); @@ -383,19 +295,11 @@ class ReadingTimeBarChart extends StatelessWidget { ); } - Widget _buildAggregatedChart( - BuildContext context, - ColorScheme colorScheme, - TextTheme textTheme, - double chartHeight, - ) { + Widget _buildAggregatedChart(BuildContext context, ColorScheme colorScheme, TextTheme textTheme, double chartHeight) { final buckets = _aggregateWeekly(activities); if (buckets.isEmpty) return _buildEmptyState(context); - final maxMinutes = buckets - .map((b) => b.totalMinutes) - .reduce((a, b) => math.max(a, b)) - .toDouble(); + final maxMinutes = buckets.map((b) => b.totalMinutes).reduce((a, b) => math.max(a, b)).toDouble(); final yAxis = _niceYAxis(maxMinutes); return SizedBox( @@ -403,10 +307,7 @@ class ReadingTimeBarChart extends StatelessWidget { child: LayoutBuilder( builder: (context, constraints) { final availableWidth = constraints.maxWidth - 60; - final barWidth = (availableWidth / buckets.length * 0.6).clamp( - 6.0, - isDesktop ? 24.0 : 18.0, - ); + final barWidth = (availableWidth / buckets.length * 0.6).clamp(6.0, isDesktop ? 24.0 : 18.0); return BarChart( BarChartData( @@ -420,35 +321,22 @@ class ReadingTimeBarChart extends StatelessWidget { getTooltipItem: (group, groupIndex, rod, rodIndex) { final bucket = buckets[groupIndex]; final hours = bucket.totalMinutes / 60; - final timeLabel = hours >= 1 - ? '${hours.toStringAsFixed(1)}h' - : '${bucket.totalMinutes}m'; + final timeLabel = hours >= 1 ? '${hours.toStringAsFixed(1)}h' : '${bucket.totalMinutes}m'; return BarTooltipItem( '${bucket.label}\n$timeLabel total', - textTheme.bodySmall!.copyWith( - color: colorScheme.onInverseSurface, - ), + textTheme.bodySmall!.copyWith(color: colorScheme.onInverseSurface), ); }, ), ), titlesData: FlTitlesData( show: true, - rightTitles: const AxisTitles( - sideTitles: SideTitles(showTitles: false), - ), - topTitles: const AxisTitles( - sideTitles: SideTitles(showTitles: false), - ), + rightTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)), + topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)), bottomTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, - getTitlesWidget: (value, meta) => _buildBucketBottomLabel( - value, - buckets, - textTheme, - colorScheme, - ), + getTitlesWidget: (value, meta) => _buildBucketBottomLabel(value, buckets, textTheme, colorScheme), reservedSize: 28, ), ), @@ -457,12 +345,8 @@ class ReadingTimeBarChart extends StatelessWidget { showTitles: true, reservedSize: 40, interval: yAxis.interval, - getTitlesWidget: (value, meta) => _buildYAxisLabel( - value, - textTheme, - colorScheme, - _formatMinutesLabel, - ), + getTitlesWidget: (value, meta) => + _buildYAxisLabel(value, textTheme, colorScheme, _formatMinutesLabel), ), ), ), @@ -470,10 +354,8 @@ class ReadingTimeBarChart extends StatelessWidget { show: true, drawVerticalLine: false, horizontalInterval: yAxis.interval, - getDrawingHorizontalLine: (value) => FlLine( - color: colorScheme.outlineVariant.withValues(alpha: 0.5), - strokeWidth: 1, - ), + getDrawingHorizontalLine: (value) => + FlLine(color: colorScheme.outlineVariant.withValues(alpha: 0.5), strokeWidth: 1), ), borderData: FlBorderData(show: false), barGroups: buckets.asMap().entries.map((entry) { @@ -484,13 +366,9 @@ class ReadingTimeBarChart extends StatelessWidget { barRods: [ BarChartRodData( toY: bucket.totalMinutes.toDouble(), - color: bucket.containsToday - ? colorScheme.primary - : colorScheme.primary.withValues(alpha: 0.6), + color: bucket.containsToday ? colorScheme.primary : colorScheme.primary.withValues(alpha: 0.6), width: barWidth, - borderRadius: const BorderRadius.vertical( - top: Radius.circular(4), - ), + borderRadius: const BorderRadius.vertical(top: Radius.circular(4)), ), ], ); @@ -512,9 +390,7 @@ class ReadingTimeBarChart extends StatelessWidget { alignment: Alignment.center, child: Text( 'No reading data for this period', - style: textTheme.bodyMedium?.copyWith( - color: colorScheme.onSurfaceVariant, - ), + style: textTheme.bodyMedium?.copyWith(color: colorScheme.onSurfaceVariant), ), ); } @@ -530,12 +406,7 @@ class PagesReadLineChart extends StatelessWidget { final bool isWeekly; final bool isDesktop; - const PagesReadLineChart({ - super.key, - required this.activities, - this.isWeekly = true, - this.isDesktop = false, - }); + const PagesReadLineChart({super.key, required this.activities, this.isWeekly = true, this.isDesktop = false}); @override Widget build(BuildContext context) { @@ -546,26 +417,13 @@ class PagesReadLineChart extends StatelessWidget { final chartHeight = isDesktop ? 200.0 : 160.0; if (activities.length > 21) { - return _buildAggregatedChart( - context, - colorScheme, - textTheme, - chartHeight, - ); + return _buildAggregatedChart(context, colorScheme, textTheme, chartHeight); } return _buildDailyChart(context, colorScheme, textTheme, chartHeight); } - Widget _buildDailyChart( - BuildContext context, - ColorScheme colorScheme, - TextTheme textTheme, - double chartHeight, - ) { - final maxPages = activities - .map((a) => a.pagesRead) - .reduce((a, b) => math.max(a, b)) - .toDouble(); + Widget _buildDailyChart(BuildContext context, ColorScheme colorScheme, TextTheme textTheme, double chartHeight) { + final maxPages = activities.map((a) => a.pagesRead).reduce((a, b) => math.max(a, b)).toDouble(); final yAxis = _niceYAxis(maxPages); final spots = activities.asMap().entries.map((entry) { @@ -590,9 +448,7 @@ class PagesReadLineChart extends StatelessWidget { final activity = activities[spot.x.toInt()]; return LineTooltipItem( '${activity.dayName}\n${activity.pagesRead} pages', - textTheme.bodySmall!.copyWith( - color: colorScheme.onInverseSurface, - ), + textTheme.bodySmall!.copyWith(color: colorScheme.onInverseSurface), ); }).toList(); }, @@ -600,22 +456,13 @@ class PagesReadLineChart extends StatelessWidget { ), titlesData: FlTitlesData( show: true, - rightTitles: const AxisTitles( - sideTitles: SideTitles(showTitles: false), - ), - topTitles: const AxisTitles( - sideTitles: SideTitles(showTitles: false), - ), + rightTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)), + topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)), bottomTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, interval: 1, - getTitlesWidget: (value, meta) => _buildBottomLabel( - value, - activities, - textTheme, - colorScheme, - ), + getTitlesWidget: (value, meta) => _buildBottomLabel(value, activities, textTheme, colorScheme), reservedSize: 28, ), ), @@ -624,12 +471,7 @@ class PagesReadLineChart extends StatelessWidget { showTitles: true, reservedSize: 40, interval: yAxis.interval, - getTitlesWidget: (value, meta) => _buildYAxisLabel( - value, - textTheme, - colorScheme, - _formatPagesLabel, - ), + getTitlesWidget: (value, meta) => _buildYAxisLabel(value, textTheme, colorScheme, _formatPagesLabel), ), ), ), @@ -637,10 +479,8 @@ class PagesReadLineChart extends StatelessWidget { show: true, drawVerticalLine: false, horizontalInterval: yAxis.interval, - getDrawingHorizontalLine: (value) => FlLine( - color: colorScheme.outlineVariant.withValues(alpha: 0.5), - strokeWidth: 1, - ), + getDrawingHorizontalLine: (value) => + FlLine(color: colorScheme.outlineVariant.withValues(alpha: 0.5), strokeWidth: 1), ), borderData: FlBorderData(show: false), lineBarsData: [ @@ -662,10 +502,7 @@ class PagesReadLineChart extends StatelessWidget { ); }, ), - belowBarData: BarAreaData( - show: true, - color: colorScheme.tertiary.withValues(alpha: 0.1), - ), + belowBarData: BarAreaData(show: true, color: colorScheme.tertiary.withValues(alpha: 0.1)), ), ], ), @@ -674,19 +511,11 @@ class PagesReadLineChart extends StatelessWidget { ); } - Widget _buildAggregatedChart( - BuildContext context, - ColorScheme colorScheme, - TextTheme textTheme, - double chartHeight, - ) { + Widget _buildAggregatedChart(BuildContext context, ColorScheme colorScheme, TextTheme textTheme, double chartHeight) { final buckets = _aggregateWeekly(activities); if (buckets.isEmpty) return _buildEmptyState(context); - final maxPages = buckets - .map((b) => b.totalPages) - .reduce((a, b) => math.max(a, b)) - .toDouble(); + final maxPages = buckets.map((b) => b.totalPages).reduce((a, b) => math.max(a, b)).toDouble(); final yAxis = _niceYAxis(maxPages); final spots = buckets.asMap().entries.map((entry) { @@ -711,9 +540,7 @@ class PagesReadLineChart extends StatelessWidget { final bucket = buckets[spot.x.toInt()]; return LineTooltipItem( '${bucket.label}\n${bucket.totalPages} pages', - textTheme.bodySmall!.copyWith( - color: colorScheme.onInverseSurface, - ), + textTheme.bodySmall!.copyWith(color: colorScheme.onInverseSurface), ); }).toList(); }, @@ -721,22 +548,13 @@ class PagesReadLineChart extends StatelessWidget { ), titlesData: FlTitlesData( show: true, - rightTitles: const AxisTitles( - sideTitles: SideTitles(showTitles: false), - ), - topTitles: const AxisTitles( - sideTitles: SideTitles(showTitles: false), - ), + rightTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)), + topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)), bottomTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, interval: 1, - getTitlesWidget: (value, meta) => _buildBucketBottomLabel( - value, - buckets, - textTheme, - colorScheme, - ), + getTitlesWidget: (value, meta) => _buildBucketBottomLabel(value, buckets, textTheme, colorScheme), reservedSize: 28, ), ), @@ -745,12 +563,7 @@ class PagesReadLineChart extends StatelessWidget { showTitles: true, reservedSize: 40, interval: yAxis.interval, - getTitlesWidget: (value, meta) => _buildYAxisLabel( - value, - textTheme, - colorScheme, - _formatPagesLabel, - ), + getTitlesWidget: (value, meta) => _buildYAxisLabel(value, textTheme, colorScheme, _formatPagesLabel), ), ), ), @@ -758,10 +571,8 @@ class PagesReadLineChart extends StatelessWidget { show: true, drawVerticalLine: false, horizontalInterval: yAxis.interval, - getDrawingHorizontalLine: (value) => FlLine( - color: colorScheme.outlineVariant.withValues(alpha: 0.5), - strokeWidth: 1, - ), + getDrawingHorizontalLine: (value) => + FlLine(color: colorScheme.outlineVariant.withValues(alpha: 0.5), strokeWidth: 1), ), borderData: FlBorderData(show: false), lineBarsData: [ @@ -783,10 +594,7 @@ class PagesReadLineChart extends StatelessWidget { ); }, ), - belowBarData: BarAreaData( - show: true, - color: colorScheme.tertiary.withValues(alpha: 0.1), - ), + belowBarData: BarAreaData(show: true, color: colorScheme.tertiary.withValues(alpha: 0.1)), ), ], ), @@ -804,9 +612,7 @@ class PagesReadLineChart extends StatelessWidget { alignment: Alignment.center, child: Text( 'No reading data for this period', - style: textTheme.bodyMedium?.copyWith( - color: colorScheme.onSurfaceVariant, - ), + style: textTheme.bodyMedium?.copyWith(color: colorScheme.onSurfaceVariant), ), ); } @@ -821,11 +627,7 @@ class BooksPerMonthChart extends StatelessWidget { final List monthlyStats; final bool isDesktop; - const BooksPerMonthChart({ - super.key, - required this.monthlyStats, - this.isDesktop = false, - }); + const BooksPerMonthChart({super.key, required this.monthlyStats, this.isDesktop = false}); @override Widget build(BuildContext context) { @@ -833,26 +635,18 @@ class BooksPerMonthChart extends StatelessWidget { final colorScheme = Theme.of(context).colorScheme; final textTheme = Theme.of(context).textTheme; - final maxBooks = monthlyStats - .map((m) => m.booksRead) - .reduce((a, b) => math.max(a, b)) - .toDouble(); + final maxBooks = monthlyStats.map((m) => m.booksRead).reduce((a, b) => math.max(a, b)).toDouble(); final chartHeight = isDesktop ? 200.0 : 160.0; // Take last 6 months for display - final displayStats = monthlyStats.length > 6 - ? monthlyStats.sublist(monthlyStats.length - 6) - : monthlyStats; + final displayStats = monthlyStats.length > 6 ? monthlyStats.sublist(monthlyStats.length - 6) : monthlyStats; return SizedBox( height: chartHeight, child: LayoutBuilder( builder: (context, constraints) { final availableWidth = constraints.maxWidth - 50; - final barWidth = (availableWidth / displayStats.length * 0.5).clamp( - 8.0, - isDesktop ? 28.0 : 22.0, - ); + final barWidth = (availableWidth / displayStats.length * 0.5).clamp(8.0, isDesktop ? 28.0 : 22.0); return BarChart( BarChartData( @@ -867,21 +661,15 @@ class BooksPerMonthChart extends StatelessWidget { final stats = displayStats[groupIndex]; return BarTooltipItem( '${stats.fullMonthLabel}\n${stats.booksRead} books', - textTheme.bodySmall!.copyWith( - color: colorScheme.onInverseSurface, - ), + textTheme.bodySmall!.copyWith(color: colorScheme.onInverseSurface), ); }, ), ), titlesData: FlTitlesData( show: true, - rightTitles: const AxisTitles( - sideTitles: SideTitles(showTitles: false), - ), - topTitles: const AxisTitles( - sideTitles: SideTitles(showTitles: false), - ), + rightTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)), + topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)), bottomTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, @@ -894,10 +682,7 @@ class BooksPerMonthChart extends StatelessWidget { padding: const EdgeInsets.only(top: 8), child: Text( stats.monthLabel, - style: textTheme.labelSmall?.copyWith( - color: colorScheme.onSurfaceVariant, - fontSize: 10, - ), + style: textTheme.labelSmall?.copyWith(color: colorScheme.onSurfaceVariant, fontSize: 10), ), ); }, @@ -910,17 +695,12 @@ class BooksPerMonthChart extends StatelessWidget { reservedSize: 32, interval: 1, getTitlesWidget: (value, meta) { - if (value == value.truncateToDouble() && - value >= 0 && - value <= meta.max * 0.95) { + if (value == value.truncateToDouble() && value >= 0 && value <= meta.max * 0.95) { return Padding( padding: const EdgeInsets.only(right: 4), child: Text( '${value.toInt()}', - style: textTheme.labelSmall?.copyWith( - color: colorScheme.onSurfaceVariant, - fontSize: 10, - ), + style: textTheme.labelSmall?.copyWith(color: colorScheme.onSurfaceVariant, fontSize: 10), ), ); } @@ -933,10 +713,8 @@ class BooksPerMonthChart extends StatelessWidget { show: true, drawVerticalLine: false, horizontalInterval: 1, - getDrawingHorizontalLine: (value) => FlLine( - color: colorScheme.outlineVariant.withValues(alpha: 0.5), - strokeWidth: 1, - ), + getDrawingHorizontalLine: (value) => + FlLine(color: colorScheme.outlineVariant.withValues(alpha: 0.5), strokeWidth: 1), ), borderData: FlBorderData(show: false), barGroups: displayStats.asMap().entries.map((entry) { @@ -950,15 +728,10 @@ class BooksPerMonthChart extends StatelessWidget { gradient: LinearGradient( begin: Alignment.bottomCenter, end: Alignment.topCenter, - colors: [ - colorScheme.secondary, - colorScheme.secondary.withValues(alpha: 0.7), - ], + colors: [colorScheme.secondary, colorScheme.secondary.withValues(alpha: 0.7)], ), width: barWidth, - borderRadius: const BorderRadius.vertical( - top: Radius.circular(4), - ), + borderRadius: const BorderRadius.vertical(top: Radius.circular(4)), ), ], ); @@ -980,9 +753,7 @@ class BooksPerMonthChart extends StatelessWidget { alignment: Alignment.center, child: Text( 'No books read data available', - style: textTheme.bodyMedium?.copyWith( - color: colorScheme.onSurfaceVariant, - ), + style: textTheme.bodyMedium?.copyWith(color: colorScheme.onSurfaceVariant), ), ); } diff --git a/app/lib/widgets/statistics/stat_card.dart b/app/lib/widgets/statistics/stat_card.dart index de98d30..95394f0 100644 --- a/app/lib/widgets/statistics/stat_card.dart +++ b/app/lib/widgets/statistics/stat_card.dart @@ -43,10 +43,7 @@ class StatCard extends StatelessWidget { decoration: BoxDecoration( color: colorScheme.surfaceContainerLow, borderRadius: BorderRadius.circular(AppRadius.xl), - border: Border.all( - color: colorScheme.outlineVariant, - width: BorderWidths.thin, - ), + border: Border.all(color: colorScheme.outlineVariant, width: BorderWidths.thin), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -64,11 +61,7 @@ class StatCard extends StatelessWidget { color: colorScheme.primaryContainer, borderRadius: BorderRadius.circular(AppRadius.md), ), - child: Icon( - icon, - size: 20, - color: colorScheme.onPrimaryContainer, - ), + child: Icon(icon, size: 20, color: colorScheme.onPrimaryContainer), ) else const SizedBox.shrink(), @@ -78,19 +71,15 @@ class StatCard extends StatelessWidget { if (icon != null || trend != null) const SizedBox(height: Spacing.sm), Text( value, - style: - (isDesktop ? textTheme.headlineMedium : textTheme.headlineSmall) - ?.copyWith( - color: colorScheme.onSurface, - fontWeight: FontWeight.bold, - ), + style: (isDesktop ? textTheme.headlineMedium : textTheme.headlineSmall)?.copyWith( + color: colorScheme.onSurface, + fontWeight: FontWeight.bold, + ), ), const SizedBox(height: Spacing.xs), Text( label, - style: textTheme.bodySmall?.copyWith( - color: colorScheme.onSurfaceVariant, - ), + style: textTheme.bodySmall?.copyWith(color: colorScheme.onSurfaceVariant), maxLines: 2, overflow: TextOverflow.ellipsis, ), @@ -115,18 +104,11 @@ class StatCard extends StatelessWidget { child: Row( mainAxisSize: MainAxisSize.min, children: [ - Icon( - isPositive ? Icons.trending_up : Icons.trending_down, - size: 14, - color: trendColor, - ), + Icon(isPositive ? Icons.trending_up : Icons.trending_down, size: 14, color: trendColor), const SizedBox(width: 4), Text( trend!, - style: textTheme.labelSmall?.copyWith( - color: trendColor, - fontWeight: FontWeight.bold, - ), + style: textTheme.labelSmall?.copyWith(color: trendColor, fontWeight: FontWeight.bold), ), ], ), @@ -145,12 +127,7 @@ class CompactStatCard extends StatelessWidget { /// Whether to use desktop styling. final bool isDesktop; - const CompactStatCard({ - super.key, - required this.value, - required this.label, - this.isDesktop = false, - }); + const CompactStatCard({super.key, required this.value, required this.label, this.isDesktop = false}); @override Widget build(BuildContext context) { @@ -165,28 +142,22 @@ class CompactStatCard extends StatelessWidget { decoration: BoxDecoration( color: colorScheme.surfaceContainerLow, borderRadius: BorderRadius.circular(AppRadius.lg), - border: Border.all( - color: colorScheme.outlineVariant, - width: BorderWidths.thin, - ), + border: Border.all(color: colorScheme.outlineVariant, width: BorderWidths.thin), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ Text( value, - style: (isDesktop ? textTheme.titleLarge : textTheme.titleMedium) - ?.copyWith( - color: colorScheme.primary, - fontWeight: FontWeight.bold, - ), + style: (isDesktop ? textTheme.titleLarge : textTheme.titleMedium)?.copyWith( + color: colorScheme.primary, + fontWeight: FontWeight.bold, + ), ), const SizedBox(height: 2), Text( label, - style: textTheme.labelSmall?.copyWith( - color: colorScheme.onSurfaceVariant, - ), + style: textTheme.labelSmall?.copyWith(color: colorScheme.onSurfaceVariant), textAlign: TextAlign.center, maxLines: 1, overflow: TextOverflow.ellipsis, @@ -211,13 +182,7 @@ class StatSectionCard extends StatelessWidget { /// Whether to use desktop styling. final bool isDesktop; - const StatSectionCard({ - super.key, - required this.title, - this.action, - required this.child, - this.isDesktop = false, - }); + const StatSectionCard({super.key, required this.title, this.action, required this.child, this.isDesktop = false}); @override Widget build(BuildContext context) { @@ -229,10 +194,7 @@ class StatSectionCard extends StatelessWidget { decoration: BoxDecoration( color: colorScheme.surfaceContainerLow, borderRadius: BorderRadius.circular(AppRadius.xl), - border: Border.all( - color: colorScheme.outlineVariant, - width: BorderWidths.thin, - ), + border: Border.all(color: colorScheme.outlineVariant, width: BorderWidths.thin), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, diff --git a/app/lib/widgets/titled_divider.dart b/app/lib/widgets/titled_divider.dart index 25ef008..e4ef191 100644 --- a/app/lib/widgets/titled_divider.dart +++ b/app/lib/widgets/titled_divider.dart @@ -20,27 +20,12 @@ class TitledDivider extends StatelessWidget { padding: EdgeInsets.symmetric(vertical: padding), child: Row( children: [ - Expanded( - child: Divider( - thickness: 1.5, - color: theme.colorScheme.outlineVariant, - ), - ), + Expanded(child: Divider(thickness: 1.5, color: theme.colorScheme.outlineVariant)), Padding( padding: const EdgeInsets.symmetric(horizontal: Spacing.md), - child: Text( - title, - style: theme.textTheme.titleSmall?.copyWith( - color: theme.colorScheme.outline, - ), - ), - ), - Expanded( - child: Divider( - thickness: 1.5, - color: theme.colorScheme.outlineVariant, - ), + child: Text(title, style: theme.textTheme.titleSmall?.copyWith(color: theme.colorScheme.outline)), ), + Expanded(child: Divider(thickness: 1.5, color: theme.colorScheme.outlineVariant)), ], ), ); diff --git a/app/lib/widgets/topics/add_topic_sheet.dart b/app/lib/widgets/topics/add_topic_sheet.dart index 57a59f5..5523c3c 100644 --- a/app/lib/widgets/topics/add_topic_sheet.dart +++ b/app/lib/widgets/topics/add_topic_sheet.dart @@ -10,8 +10,7 @@ class AddTopicSheet extends StatefulWidget { final Tag? topic; /// Called when the topic is saved. - final void Function(String name, String? description, String colorHex)? - onSave; + final void Function(String name, String? description, String colorHex)? onSave; const AddTopicSheet({super.key, this.topic, this.onSave}); @@ -24,9 +23,7 @@ class AddTopicSheet extends StatefulWidget { return showModalBottomSheet( context: context, isScrollControlled: true, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(AppRadius.xl)), - ), + shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(AppRadius.xl))), builder: (context) => AddTopicSheet(topic: topic, onSave: onSave), ); } @@ -46,11 +43,8 @@ class _AddTopicSheetState extends State { void initState() { super.initState(); _nameController = TextEditingController(text: widget.topic?.name ?? ''); - _descriptionController = TextEditingController( - text: widget.topic?.description ?? '', - ); - _selectedColorHex = - widget.topic?.colorHex ?? Tag.availableColors[5]; // Blue default + _descriptionController = TextEditingController(text: widget.topic?.description ?? ''); + _selectedColorHex = widget.topic?.colorHex ?? Tag.availableColors[5]; // Blue default } @override @@ -81,19 +75,11 @@ class _AddTopicSheetState extends State { const BottomSheetHandle(), const SizedBox(height: Spacing.lg), // Title - Text( - _isEditing ? 'Edit topic' : 'Create new topic', - style: textTheme.headlineSmall, - ), + Text(_isEditing ? 'Edit topic' : 'Create new topic', style: textTheme.headlineSmall), const SizedBox(height: Spacing.lg), // Name field - Text( - 'Name', - style: textTheme.titleSmall?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), + Text('Name', style: textTheme.titleSmall?.copyWith(color: colorScheme.onSurfaceVariant)), const SizedBox(height: Spacing.sm), TextFormField( controller: _nameController, @@ -102,24 +88,14 @@ class _AddTopicSheetState extends State { onChanged: (_) => setState(() {}), decoration: InputDecoration( hintText: 'Enter topic name', - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(AppRadius.md), - ), - contentPadding: const EdgeInsets.symmetric( - horizontal: Spacing.md, - vertical: Spacing.sm, - ), + border: OutlineInputBorder(borderRadius: BorderRadius.circular(AppRadius.md)), + contentPadding: const EdgeInsets.symmetric(horizontal: Spacing.md, vertical: Spacing.sm), ), ), const SizedBox(height: Spacing.lg), // Description field - Text( - 'Description (optional)', - style: textTheme.titleSmall?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), + Text('Description (optional)', style: textTheme.titleSmall?.copyWith(color: colorScheme.onSurfaceVariant)), const SizedBox(height: Spacing.sm), TextFormField( controller: _descriptionController, @@ -128,21 +104,14 @@ class _AddTopicSheetState extends State { onChanged: (_) => setState(() {}), decoration: InputDecoration( hintText: 'Add a description', - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(AppRadius.md), - ), + border: OutlineInputBorder(borderRadius: BorderRadius.circular(AppRadius.md)), contentPadding: const EdgeInsets.all(Spacing.md), ), ), const SizedBox(height: Spacing.lg), // Color picker - Text( - 'Color', - style: textTheme.titleSmall?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), + Text('Color', style: textTheme.titleSmall?.copyWith(color: colorScheme.onSurfaceVariant)), const SizedBox(height: Spacing.sm), _buildColorPicker(context), const SizedBox(height: Spacing.xl), @@ -189,22 +158,12 @@ class _AddTopicSheetState extends State { decoration: BoxDecoration( color: color, shape: BoxShape.circle, - border: isSelected - ? Border.all(color: colorScheme.primary, width: 3) - : null, + border: isSelected ? Border.all(color: colorScheme.primary, width: 3) : null, boxShadow: isSelected - ? [ - BoxShadow( - color: color.withValues(alpha: 0.4), - blurRadius: 8, - spreadRadius: 1, - ), - ] + ? [BoxShadow(color: color.withValues(alpha: 0.4), blurRadius: 8, spreadRadius: 1)] : null, ), - child: isSelected - ? Icon(Icons.check, size: 18, color: getContrastColor(color)) - : null, + child: isSelected ? Icon(Icons.check, size: 18, color: getContrastColor(color)) : null, ), ); }).toList(), @@ -229,10 +188,7 @@ class _AddTopicSheetState extends State { Container( width: 12, height: 12, - decoration: BoxDecoration( - color: topicColor, - shape: BoxShape.circle, - ), + decoration: BoxDecoration(color: topicColor, shape: BoxShape.circle), ), const SizedBox(width: Spacing.md), // Info @@ -241,14 +197,10 @@ class _AddTopicSheetState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - _nameController.text.isNotEmpty - ? _nameController.text - : 'Topic name', + _nameController.text.isNotEmpty ? _nameController.text : 'Topic name', style: textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w600, - color: _nameController.text.isEmpty - ? colorScheme.onSurfaceVariant - : null, + color: _nameController.text.isEmpty ? colorScheme.onSurfaceVariant : null, ), maxLines: 1, overflow: TextOverflow.ellipsis, @@ -257,9 +209,7 @@ class _AddTopicSheetState extends State { const SizedBox(height: 2), Text( _descriptionController.text, - style: textTheme.bodySmall?.copyWith( - color: colorScheme.onSurfaceVariant, - ), + style: textTheme.bodySmall?.copyWith(color: colorScheme.onSurfaceVariant), maxLines: 1, overflow: TextOverflow.ellipsis, ), @@ -274,12 +224,7 @@ class _AddTopicSheetState extends State { color: colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(AppRadius.full), ), - child: Text( - 'Preview', - style: textTheme.labelSmall?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), + child: Text('Preview', style: textTheme.labelSmall?.copyWith(color: colorScheme.onSurfaceVariant)), ), ], ), @@ -292,9 +237,7 @@ class _AddTopicSheetState extends State { widget.onSave?.call( name, - _descriptionController.text.trim().isEmpty - ? null - : _descriptionController.text.trim(), + _descriptionController.text.trim().isEmpty ? null : _descriptionController.text.trim(), _selectedColorHex, ); Navigator.of(context).pop(); diff --git a/app/lib/widgets/topics/manage_topics_sheet.dart b/app/lib/widgets/topics/manage_topics_sheet.dart index 3d05b8c..765125d 100644 --- a/app/lib/widgets/topics/manage_topics_sheet.dart +++ b/app/lib/widgets/topics/manage_topics_sheet.dart @@ -20,27 +20,16 @@ class ManageTopicsSheet extends StatefulWidget { /// Called when topic assignments change. final void Function(List tagIds)? onSave; - const ManageTopicsSheet({ - super.key, - this.book, - this.bulkBookIds, - this.onSave, - }); + const ManageTopicsSheet({super.key, this.book, this.bulkBookIds, this.onSave}); bool get isBulkMode => bulkBookIds != null && bulkBookIds!.isNotEmpty; /// Shows the manage topics sheet for a single book. - static Future show( - BuildContext context, { - required Book book, - void Function(List tagIds)? onSave, - }) { + static Future show(BuildContext context, {required Book book, void Function(List tagIds)? onSave}) { return showModalBottomSheet( context: context, isScrollControlled: true, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(AppRadius.xl)), - ), + shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(AppRadius.xl))), builder: (context) => ManageTopicsSheet(book: book, onSave: onSave), ); } @@ -54,11 +43,8 @@ class ManageTopicsSheet extends StatefulWidget { return showModalBottomSheet( context: context, isScrollControlled: true, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(AppRadius.xl)), - ), - builder: (context) => - ManageTopicsSheet(bulkBookIds: bookIds, onSave: onSave), + shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(AppRadius.xl))), + builder: (context) => ManageTopicsSheet(bulkBookIds: bookIds, onSave: onSave), ); } @@ -100,11 +86,7 @@ class _ManageTopicsSheetState extends State { maxChildSize: 0.9, expand: false, builder: (context, scrollController) => Padding( - padding: const EdgeInsets.only( - left: Spacing.lg, - right: Spacing.lg, - top: Spacing.md, - ), + padding: const EdgeInsets.only(left: Spacing.lg, right: Spacing.lg, top: Spacing.md), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, @@ -120,11 +102,7 @@ class _ManageTopicsSheetState extends State { if (!widget.isBulkMode) ...[ ClipRRect( borderRadius: BorderRadius.circular(AppRadius.sm), - child: SizedBox( - width: 40, - height: 60, - child: _buildCover(context), - ), + child: SizedBox(width: 40, height: 60, child: _buildCover(context)), ), const SizedBox(width: Spacing.sm + Spacing.xs), ], @@ -143,9 +121,7 @@ class _ManageTopicsSheetState extends State { const SizedBox(height: 2), Text( widget.book!.title, - style: textTheme.bodySmall?.copyWith( - color: colorScheme.onSurfaceVariant, - ), + style: textTheme.bodySmall?.copyWith(color: colorScheme.onSurfaceVariant), maxLines: 1, overflow: TextOverflow.ellipsis, ), @@ -179,21 +155,13 @@ class _ManageTopicsSheetState extends State { builder: (context) { final filteredTags = _searchQuery.isEmpty ? tags - : tags - .where( - (t) => t.name.toLowerCase().contains( - _searchQuery.toLowerCase(), - ), - ) - .toList(); + : tags.where((t) => t.name.toLowerCase().contains(_searchQuery.toLowerCase())).toList(); if (filteredTags.isEmpty && _searchQuery.isNotEmpty) { return Center( child: Text( 'No topics found', - style: textTheme.bodyMedium?.copyWith( - color: colorScheme.onSurfaceVariant, - ), + style: textTheme.bodyMedium?.copyWith(color: colorScheme.onSurfaceVariant), ), ); } @@ -201,8 +169,7 @@ class _ManageTopicsSheetState extends State { return ListView.builder( controller: scrollController, itemCount: filteredTags.length, - itemBuilder: (context, index) => - _buildTagTile(context, filteredTags[index]), + itemBuilder: (context, index) => _buildTagTile(context, filteredTags[index]), ); }, ), @@ -210,17 +177,11 @@ class _ManageTopicsSheetState extends State { // Action buttons Padding( - padding: EdgeInsets.only( - top: Spacing.md, - bottom: MediaQuery.of(context).viewInsets.bottom + Spacing.md, - ), + padding: EdgeInsets.only(top: Spacing.md, bottom: MediaQuery.of(context).viewInsets.bottom + Spacing.md), child: Row( mainAxisAlignment: MainAxisAlignment.end, children: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: const Text('Cancel'), - ), + TextButton(onPressed: () => Navigator.pop(context), child: const Text('Cancel')), const SizedBox(width: Spacing.md), FilledButton(onPressed: _onSave, child: const Text('Save')), ], @@ -242,22 +203,14 @@ class _ManageTopicsSheetState extends State { fit: BoxFit.cover, errorBuilder: (context, error, stackTrace) => Container( color: colorScheme.surfaceContainerHighest, - child: Icon( - Icons.menu_book, - color: colorScheme.onSurfaceVariant, - size: 20, - ), + child: Icon(Icons.menu_book, color: colorScheme.onSurfaceVariant, size: 20), ), ); } return Container( color: colorScheme.surfaceContainerHighest, - child: Icon( - Icons.menu_book, - color: colorScheme.onSurfaceVariant, - size: 20, - ), + child: Icon(Icons.menu_book, color: colorScheme.onSurfaceVariant, size: 20), ); } @@ -268,25 +221,11 @@ class _ManageTopicsSheetState extends State { return Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon( - Icons.label_outline, - size: IconSizes.display, - color: colorScheme.onSurfaceVariant.withValues(alpha: 0.5), - ), + Icon(Icons.label_outline, size: IconSizes.display, color: colorScheme.onSurfaceVariant.withValues(alpha: 0.5)), const SizedBox(height: Spacing.md), - Text( - 'No topics yet', - style: textTheme.titleMedium?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), + Text('No topics yet', style: textTheme.titleMedium?.copyWith(color: colorScheme.onSurfaceVariant)), const SizedBox(height: Spacing.sm), - Text( - 'Tap + to create a topic', - style: textTheme.bodyMedium?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), + Text('Tap + to create a topic', style: textTheme.bodyMedium?.copyWith(color: colorScheme.onSurfaceVariant)), ], ); } @@ -302,24 +241,16 @@ class _ManageTopicsSheetState extends State { return Card( margin: const EdgeInsets.only(bottom: Spacing.xs), elevation: 0, - color: isSelected - ? tagColor.withValues(alpha: 0.1) - : colorScheme.surfaceContainerLow, + color: isSelected ? tagColor.withValues(alpha: 0.1) : colorScheme.surfaceContainerLow, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(AppRadius.md), - side: BorderSide( - color: isSelected ? tagColor : colorScheme.outlineVariant, - width: isSelected ? 2 : 1, - ), + side: BorderSide(color: isSelected ? tagColor : colorScheme.outlineVariant, width: isSelected ? 2 : 1), ), child: InkWell( onTap: () => _toggleTag(tag.id), borderRadius: BorderRadius.circular(AppRadius.md), child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: Spacing.md, - vertical: Spacing.sm, - ), + padding: const EdgeInsets.symmetric(horizontal: Spacing.md, vertical: Spacing.sm), child: Row( children: [ // Color dot @@ -334,10 +265,7 @@ class _ManageTopicsSheetState extends State { child: Container( width: 16, height: 16, - decoration: BoxDecoration( - color: tagColor, - shape: BoxShape.circle, - ), + decoration: BoxDecoration(color: tagColor, shape: BoxShape.circle), ), ), ), @@ -349,27 +277,19 @@ class _ManageTopicsSheetState extends State { children: [ Text( tag.name, - style: textTheme.titleSmall?.copyWith( - fontWeight: FontWeight.w600, - ), + style: textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w600), maxLines: 1, overflow: TextOverflow.ellipsis, ), Text( '$bookCount ${bookCount == 1 ? 'book' : 'books'}', - style: textTheme.bodySmall?.copyWith( - color: colorScheme.onSurfaceVariant, - ), + style: textTheme.bodySmall?.copyWith(color: colorScheme.onSurfaceVariant), ), ], ), ), // Checkbox - Checkbox( - value: isSelected, - onChanged: (_) => _toggleTag(tag.id), - activeColor: tagColor, - ), + Checkbox(value: isSelected, onChanged: (_) => _toggleTag(tag.id), activeColor: tagColor), ], ), ), diff --git a/app/lib/widgets/topics/topic_detail_sheet.dart b/app/lib/widgets/topics/topic_detail_sheet.dart index 790b994..1106964 100644 --- a/app/lib/widgets/topics/topic_detail_sheet.dart +++ b/app/lib/widgets/topics/topic_detail_sheet.dart @@ -19,9 +19,7 @@ class TopicDetailSheet extends StatelessWidget { static Future show(BuildContext context, {required Tag tag}) { return showModalBottomSheet( context: context, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(AppRadius.xl)), - ), + shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(AppRadius.xl))), builder: (context) => TopicDetailSheet(tag: tag), ); } @@ -52,29 +50,18 @@ class TopicDetailSheet extends StatelessWidget { Container( width: 12, height: 12, - decoration: BoxDecoration( - color: tag.color, - shape: BoxShape.circle, - ), + decoration: BoxDecoration(color: tag.color, shape: BoxShape.circle), ), const SizedBox(width: Spacing.sm), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - tag.name, - style: textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.w600, - ), - ), - if (tag.description != null && - tag.description!.isNotEmpty) + Text(tag.name, style: textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600)), + if (tag.description != null && tag.description!.isNotEmpty) Text( tag.description!, - style: textTheme.bodySmall?.copyWith( - color: colorScheme.onSurfaceVariant, - ), + style: textTheme.bodySmall?.copyWith(color: colorScheme.onSurfaceVariant), maxLines: 2, overflow: TextOverflow.ellipsis, ), @@ -83,19 +70,14 @@ class TopicDetailSheet extends StatelessWidget { ), // Book count badge Container( - padding: const EdgeInsets.symmetric( - horizontal: 8, - vertical: 4, - ), + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(AppRadius.full), ), child: Text( '$bookCount ${bookCount == 1 ? 'book' : 'books'}', - style: textTheme.labelSmall?.copyWith( - color: colorScheme.onSurfaceVariant, - ), + style: textTheme.labelSmall?.copyWith(color: colorScheme.onSurfaceVariant), ), ), ], @@ -113,14 +95,9 @@ class TopicDetailSheet extends StatelessWidget { ), ListTile( leading: Icon(Icons.delete_outline, color: colorScheme.error), - title: Text( - 'Delete topic', - style: TextStyle(color: colorScheme.error), - ), + title: Text('Delete topic', style: TextStyle(color: colorScheme.error)), subtitle: bookCount > 0 - ? Text( - 'Will be removed from $bookCount ${bookCount == 1 ? 'book' : 'books'}', - ) + ? Text('Will be removed from $bookCount ${bookCount == 1 ? 'book' : 'books'}') : null, onTap: () { Navigator.pop(context); @@ -140,13 +117,7 @@ class TopicDetailSheet extends StatelessWidget { context, topic: tag, onSave: (name, description, colorHex) { - dataStore.updateTag( - tag.copyWith( - name: name, - description: description, - colorHex: colorHex, - ), - ); + dataStore.updateTag(tag.copyWith(name: name, description: description, colorHex: colorHex)); }, ); } @@ -166,10 +137,7 @@ class TopicDetailSheet extends StatelessWidget { : 'The topic "${tag.name}" will be permanently deleted.', ), actions: [ - TextButton( - onPressed: () => Navigator.pop(dialogContext), - child: const Text('Cancel'), - ), + TextButton(onPressed: () => Navigator.pop(dialogContext), child: const Text('Cancel')), FilledButton( onPressed: () { Navigator.pop(dialogContext); diff --git a/app/linux/flutter/generated_plugin_registrant.cc b/app/linux/flutter/generated_plugin_registrant.cc index 3792af4..8be520b 100644 --- a/app/linux/flutter/generated_plugin_registrant.cc +++ b/app/linux/flutter/generated_plugin_registrant.cc @@ -6,14 +6,22 @@ #include "generated_plugin_registrant.h" -#include +#include +#include #include +#include void fl_register_plugins(FlPluginRegistry* registry) { - g_autoptr(FlPluginRegistrar) gtk_registrar = - fl_plugin_registry_get_registrar_for_plugin(registry, "GtkPlugin"); - gtk_plugin_register_with_registrar(gtk_registrar); + g_autoptr(FlPluginRegistrar) desktop_webview_window_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "DesktopWebviewWindowPlugin"); + desktop_webview_window_plugin_register_with_registrar(desktop_webview_window_registrar); + g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin"); + flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar); g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); + g_autoptr(FlPluginRegistrar) window_to_front_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "WindowToFrontPlugin"); + window_to_front_plugin_register_with_registrar(window_to_front_registrar); } diff --git a/app/linux/flutter/generated_plugins.cmake b/app/linux/flutter/generated_plugins.cmake index 5d07423..0245e78 100644 --- a/app/linux/flutter/generated_plugins.cmake +++ b/app/linux/flutter/generated_plugins.cmake @@ -3,8 +3,10 @@ # list(APPEND FLUTTER_PLUGIN_LIST - gtk + desktop_webview_window + flutter_secure_storage_linux url_launcher_linux + window_to_front ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/app/macos/Flutter/GeneratedPluginRegistrant.swift b/app/macos/Flutter/GeneratedPluginRegistrant.swift index 1b10278..8eca03a 100644 --- a/app/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/app/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,20 +5,24 @@ import FlutterMacOS import Foundation -import app_links +import desktop_webview_window import file_picker -import google_sign_in_ios +import flutter_secure_storage_darwin +import flutter_web_auth_2 import mobile_scanner import shared_preferences_foundation import sqflite_darwin import url_launcher_macos +import window_to_front func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { - AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin")) + DesktopWebviewWindowPlugin.register(with: registry.registrar(forPlugin: "DesktopWebviewWindowPlugin")) FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin")) - FLTGoogleSignInPlugin.register(with: registry.registrar(forPlugin: "FLTGoogleSignInPlugin")) + FlutterSecureStorageDarwinPlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStorageDarwinPlugin")) + FlutterWebAuth2Plugin.register(with: registry.registrar(forPlugin: "FlutterWebAuth2Plugin")) MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) + WindowToFrontPlugin.register(with: registry.registrar(forPlugin: "WindowToFrontPlugin")) } diff --git a/app/macos/Runner/Info.plist b/app/macos/Runner/Info.plist index 4789daa..90f3099 100644 --- a/app/macos/Runner/Info.plist +++ b/app/macos/Runner/Info.plist @@ -28,5 +28,16 @@ MainMenu NSPrincipalClass NSApplication + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLSchemes + + papyrus + + + diff --git a/app/pubspec.lock b/app/pubspec.lock index 8250a68..36dc6bf 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -1,46 +1,6 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: - adaptive_number: - dependency: transitive - description: - name: adaptive_number - sha256: "3a567544e9b5c9c803006f51140ad544aedc79604fd4f3f2c1380003f97c1d77" - url: "https://pub.dev" - source: hosted - version: "1.0.0" - app_links: - dependency: transitive - description: - name: app_links - sha256: "5f88447519add627fe1cbcab4fd1da3d4fed15b9baf29f28b22535c95ecee3e8" - url: "https://pub.dev" - source: hosted - version: "6.4.1" - app_links_linux: - dependency: transitive - description: - name: app_links_linux - sha256: f5f7173a78609f3dfd4c2ff2c95bd559ab43c80a87dc6a095921d96c05688c81 - url: "https://pub.dev" - source: hosted - version: "1.0.3" - app_links_platform_interface: - dependency: transitive - description: - name: app_links_platform_interface - sha256: "05f5379577c513b534a29ddea68176a4d4802c46180ee8e2e966257158772a3f" - url: "https://pub.dev" - source: hosted - version: "2.0.2" - app_links_web: - dependency: transitive - description: - name: app_links_web - sha256: af060ed76183f9e2b87510a9480e56a5352b6c249778d07bd2c95fc35632a555 - url: "https://pub.dev" - source: hosted - version: "1.0.4" archive: dependency: "direct main" description: @@ -161,14 +121,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.8" - dart_jsonwebtoken: - dependency: transitive - description: - name: dart_jsonwebtoken - sha256: c6ecb3bb991c459b91c5adf9e871113dcb32bbe8fe7ca2c92723f88ffc1e0b7a - url: "https://pub.dev" - source: hosted - version: "3.3.2" dart_mobi: dependency: "direct main" description: @@ -177,14 +129,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.2" - ed25519_edwards: + desktop_webview_window: dependency: transitive description: - name: ed25519_edwards - sha256: "6ce0112d131327ec6d42beede1e5dfd526069b18ad45dcf654f15074ad9276cd" + name: desktop_webview_window + sha256: "57cf20d81689d5cbb1adfd0017e96b669398a669d927906073b0e42fc64111c0" url: "https://pub.dev" source: hosted - version: "0.3.1" + version: "0.2.3" epub_pro: dependency: "direct main" description: @@ -278,120 +230,112 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.33" - flutter_svg: + flutter_secure_storage: dependency: "direct main" description: - name: flutter_svg - sha256: "87fbd7c534435b6c5d9d98b01e1fd527812b82e68ddd8bd35fc45ed0fa8f0a95" + name: flutter_secure_storage + sha256: "8b302d17096ba88f911b7eb317c71d5e691da60a259549f42b38c658d1776d87" url: "https://pub.dev" source: hosted - version: "2.2.3" - flutter_test: - dependency: "direct dev" - description: flutter - source: sdk - version: "0.0.0" - flutter_web_plugins: - dependency: transitive - description: flutter - source: sdk - version: "0.0.0" - functions_client: + version: "10.1.0" + flutter_secure_storage_darwin: dependency: transitive description: - name: functions_client - sha256: "94074d62167ae634127ef6095f536835063a7dc80f2b1aa306d2346ff9023996" + name: flutter_secure_storage_darwin + sha256: "3af15a3cb2bf5b8b776832bd01776f8018766aece55623176e28b406481fb320" url: "https://pub.dev" source: hosted - version: "2.5.0" - glob: + version: "0.3.0" + flutter_secure_storage_linux: dependency: transitive description: - name: glob - sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de + name: flutter_secure_storage_linux + sha256: "2b5c76dce569ab752d55a1cee6a2242bcc11fdba927078fb88c503f150767cda" url: "https://pub.dev" source: hosted - version: "2.1.3" - go_router: - dependency: "direct main" + version: "3.0.0" + flutter_secure_storage_platform_interface: + dependency: transitive description: - name: go_router - sha256: d8f590a69729f719177ea68eb1e598295e8dbc41bbc247fed78b2c8a25660d7c + name: flutter_secure_storage_platform_interface + sha256: "8ceea1223bee3c6ac1a22dabd8feefc550e4729b3675de4b5900f55afcb435d6" url: "https://pub.dev" source: hosted - version: "16.3.0" - google_fonts: - dependency: "direct main" + version: "2.0.1" + flutter_secure_storage_web: + dependency: transitive description: - name: google_fonts - sha256: ba03d03bcaa2f6cb7bd920e3b5027181db75ab524f8891c8bc3aa603885b8055 + name: flutter_secure_storage_web + sha256: "073a62b3aeb866ab4ce795f960413948e51e5a42a9b0c8333b6daf5bb3208a1c" url: "https://pub.dev" source: hosted - version: "6.3.3" - google_identity_services_web: + version: "2.1.1" + flutter_secure_storage_windows: dependency: transitive description: - name: google_identity_services_web - sha256: "5d187c46dc59e02646e10fe82665fc3884a9b71bc1c90c2b8b749316d33ee454" + name: flutter_secure_storage_windows + sha256: "3b7c8e068875dfd46719ff57c90d8c459c87f2302ed6b00ff006b3c9fcad1613" url: "https://pub.dev" source: hosted - version: "0.3.3+1" - google_sign_in: + version: "4.1.0" + flutter_svg: dependency: "direct main" description: - name: google_sign_in - sha256: "521031b65853b4409b8213c0387d57edaad7e2a949ce6dea0d8b2afc9cb29763" + name: flutter_svg + sha256: "87fbd7c534435b6c5d9d98b01e1fd527812b82e68ddd8bd35fc45ed0fa8f0a95" url: "https://pub.dev" source: hosted - version: "7.2.0" - google_sign_in_android: - dependency: transitive + version: "2.2.3" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_auth_2: + dependency: "direct main" description: - name: google_sign_in_android - sha256: f353140580797e01c1f35748810326f326664c52040b6f62d88e7d6d1cd30917 + name: flutter_web_auth_2 + sha256: d354998934ddc338e69b999b2abaeb33c6fd09999d3a5f92ead1a6b49b49712e url: "https://pub.dev" source: hosted - version: "7.2.9" - google_sign_in_ios: + version: "5.0.2" + flutter_web_auth_2_platform_interface: dependency: transitive description: - name: google_sign_in_ios - sha256: ac1e4c1205267cb7999d1d81333fccffdfda29e853f434bbaf71525498bb6950 + name: flutter_web_auth_2_platform_interface + sha256: ba0fbba55bffb47242025f96852ad1ffba34bc451568f56ef36e613612baffab url: "https://pub.dev" source: hosted - version: "6.3.0" - google_sign_in_platform_interface: + version: "5.0.0" + flutter_web_plugins: dependency: transitive - description: - name: google_sign_in_platform_interface - sha256: "7f59208c42b415a3cca203571128d6f84f885fead2d5b53eb65a9e27f2965bb5" - url: "https://pub.dev" - source: hosted - version: "3.1.0" - google_sign_in_web: + description: flutter + source: sdk + version: "0.0.0" + glob: dependency: transitive description: - name: google_sign_in_web - sha256: dac0676af14b96b11691cc3c3e152415a896a38f1224269241d7cc294bdb9102 + name: glob + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de url: "https://pub.dev" source: hosted - version: "1.1.2" - gotrue: - dependency: transitive + version: "2.1.3" + go_router: + dependency: "direct main" description: - name: gotrue - sha256: f7b52008311941a7c3e99f9590c4ee32dfc102a5442e43abf1b287d9f8cc39b2 + name: go_router + sha256: d8f590a69729f719177ea68eb1e598295e8dbc41bbc247fed78b2c8a25660d7c url: "https://pub.dev" source: hosted - version: "2.18.0" - gtk: - dependency: transitive + version: "16.3.0" + google_fonts: + dependency: "direct main" description: - name: gtk - sha256: e8ce9ca4b1df106e4d72dad201d345ea1a036cc12c360f1a7d5a758f78ffa42c + name: google_fonts + sha256: ba03d03bcaa2f6cb7bd920e3b5027181db75ab524f8891c8bc3aa603885b8055 url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "6.3.3" hooks: dependency: transitive description: @@ -432,14 +376,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.19.0" - jwt_decode: - dependency: transitive - description: - name: jwt_decode - sha256: d2e9f68c052b2225130977429d30f187aa1981d789c76ad104a32243cfdebfbb - url: "https://pub.dev" - source: hosted - version: "0.3.1" leak_tracker: dependency: transitive description: @@ -504,14 +440,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.17.0" - mime: - dependency: transitive - description: - name: mime - sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" - url: "https://pub.dev" - source: hosted - version: "2.0.0" mobile_scanner: dependency: "direct main" description: @@ -640,14 +568,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.8" - pointycastle: - dependency: transitive - description: - name: pointycastle - sha256: "92aa3841d083cc4b0f4709b5c74fd6409a3e6ba833ffc7dc6a8fee096366acf5" - url: "https://pub.dev" - source: hosted - version: "4.0.0" posix: dependency: transitive description: @@ -656,14 +576,6 @@ packages: url: "https://pub.dev" source: hosted version: "6.5.0" - postgrest: - dependency: transitive - description: - name: postgrest - sha256: f4b6bb24b465c47649243ef0140475de8a0ec311dc9c75ebe573b2dcabb10460 - url: "https://pub.dev" - source: hosted - version: "2.6.0" provider: dependency: "direct main" description: @@ -680,22 +592,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.0" - realtime_client: - dependency: transitive - description: - name: realtime_client - sha256: "5268afc208d02fb9109854d262c1ebf6ece224cd285199ae1d2f92d2ff49dbf1" - url: "https://pub.dev" - source: hosted - version: "2.7.0" - retry: - dependency: transitive - description: - name: retry - sha256: "822e118d5b3aafed083109c72d5f484c6dc66707885e07c0fbcb8b986bba7efc" - url: "https://pub.dev" - source: hosted - version: "3.1.2" rxdart: dependency: transitive description: @@ -821,14 +717,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.12.1" - storage_client: - dependency: transitive - description: - name: storage_client - sha256: "1c61b19ed9e78f37fdd1ca8b729ab8484e6c8fe82e15c87e070b861951183657" - url: "https://pub.dev" - source: hosted - version: "2.4.1" stream_channel: dependency: transitive description: @@ -845,22 +733,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.1" - supabase: - dependency: transitive - description: - name: supabase - sha256: cc039f63a3168386b3a4f338f3bff342c860d415a3578f3fbe854024aee6f911 - url: "https://pub.dev" - source: hosted - version: "2.10.2" - supabase_flutter: - dependency: "direct main" - description: - name: supabase_flutter - sha256: "92b2416ecb6a5c3ed34cf6e382b35ce6cc8921b64f2a9299d5d28968d42b09bb" - url: "https://pub.dev" - source: hosted - version: "2.12.0" syncfusion_flutter_core: dependency: transitive description: @@ -1037,22 +909,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" - web_socket: - dependency: transitive - description: - name: web_socket - sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" - url: "https://pub.dev" - source: hosted - version: "1.0.1" - web_socket_channel: - dependency: transitive - description: - name: web_socket_channel - sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 - url: "https://pub.dev" - source: hosted - version: "3.0.3" win32: dependency: transitive description: @@ -1061,6 +917,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.15.0" + window_to_front: + dependency: transitive + description: + name: window_to_front + sha256: "7aef379752b7190c10479e12b5fd7c0b9d92adc96817d9e96c59937929512aee" + url: "https://pub.dev" + source: hosted + version: "0.0.3" word_count: dependency: transitive description: @@ -1093,14 +957,6 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.3" - yet_another_json_isolate: - dependency: transitive - description: - name: yet_another_json_isolate - sha256: fe45897501fa156ccefbfb9359c9462ce5dec092f05e8a56109db30be864f01e - url: "https://pub.dev" - source: hosted - version: "2.1.0" sdks: dart: ">=3.10.3 <4.0.0" flutter: ">=3.38.4" diff --git a/app/pubspec.yaml b/app/pubspec.yaml index 8e5dedd..24c71b8 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -14,8 +14,8 @@ dependencies: go_router: ^16.2.1 collection: ^1.19.1 provider: ^6.1.5+1 - google_sign_in: ^7.2.0 flutter_svg: ^2.0.10+1 + flutter_web_auth_2: ^5.0.2 fl_chart: ^0.69.0 intl: ^0.19.0 http: ^1.2.0 @@ -35,7 +35,7 @@ dependencies: web: ^1.1.1 mobile_scanner: ^7.0.1 shared_preferences: ^2.5.4 - supabase_flutter: ^2.12.0 + flutter_secure_storage: ^10.1.0 dev_dependencies: flutter_test: @@ -62,4 +62,4 @@ flutter: fonts: - family: MadimiOne fonts: - - asset: fonts/MadimiOne-Regular.ttf \ No newline at end of file + - asset: fonts/MadimiOne-Regular.ttf diff --git a/app/test/auth/auth_api_client_test.dart b/app/test/auth/auth_api_client_test.dart new file mode 100644 index 0000000..d3a9c97 --- /dev/null +++ b/app/test/auth/auth_api_client_test.dart @@ -0,0 +1,81 @@ +import 'dart:convert'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:http/http.dart' as http; +import 'package:http/testing.dart'; +import 'package:papyrus/auth/auth_api_client.dart'; +import 'package:papyrus/auth/papyrus_api_config.dart'; + +const _authResponse = { + 'access_token': 'access-token', + 'refresh_token': 'refresh-token', + 'token_type': 'Bearer', + 'expires_in': 3600, + 'user': { + 'user_id': '11111111-1111-1111-1111-111111111111', + 'email': 'reader@example.com', + 'display_name': 'Reader', + 'avatar_url': null, + 'email_verified': false, + 'created_at': '2026-05-09T12:00:00Z', + 'last_login_at': null, + }, +}; + +void main() { + test('login maps Papyrus token response', () async { + final client = AuthApiClient( + config: PapyrusApiConfig(serverBaseUri: Uri.parse('http://server.test')), + httpClient: MockClient((request) async { + expect(request.url.path, '/v1/auth/login'); + expect(jsonDecode(request.body), { + 'email': 'reader@example.com', + 'password': 'password123', + 'client_type': 'mobile', + 'device_label': 'test-device', + }); + + return http.Response(jsonEncode(_authResponse), 200); + }), + ); + + final tokens = await client.login( + email: 'reader@example.com', + password: 'password123', + clientType: 'mobile', + deviceLabel: 'test-device', + ); + + expect(tokens.accessToken, 'access-token'); + expect(tokens.refreshToken, 'refresh-token'); + expect(tokens.user.displayName, 'Reader'); + }); + + test('throws AuthApiException for server errors', () async { + final client = AuthApiClient( + config: PapyrusApiConfig(serverBaseUri: Uri.parse('http://server.test')), + httpClient: MockClient((request) async { + return http.Response( + jsonEncode({ + 'error': {'code': 'UNAUTHORIZED', 'message': 'Invalid email or password'}, + }), + 401, + ); + }), + ); + + await expectLater( + client.login(email: 'reader@example.com', password: 'wrong'), + throwsA(isA().having((error) => error.message, 'message', 'Invalid email or password')), + ); + }); + + test('googleOAuthStartUri builds server-owned browser flow URL', () { + final client = AuthApiClient(config: PapyrusApiConfig(serverBaseUri: Uri.parse('http://server.test'))); + + final uri = client.googleOAuthStartUri('papyrus://auth/callback'); + + expect(uri.path, '/v1/auth/oauth/google/start'); + expect(uri.queryParameters['redirect_uri'], 'papyrus://auth/callback'); + }); +} diff --git a/app/test/auth/token_store_test.dart b/app/test/auth/token_store_test.dart new file mode 100644 index 0000000..cd1f140 --- /dev/null +++ b/app/test/auth/token_store_test.dart @@ -0,0 +1,38 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:papyrus/auth/token_store.dart'; + +class MemoryRefreshTokenStorage implements RefreshTokenStorage { + String? value; + + @override + Future delete() async { + value = null; + } + + @override + Future read() async => value; + + @override + Future write(String refreshToken) async { + value = refreshToken; + } +} + +void main() { + test('TokenStore saves, rotates, and clears tokens', () async { + final storage = MemoryRefreshTokenStorage(); + final store = TokenStore(storage); + + await store.saveTokens(accessToken: 'access-one', refreshToken: 'refresh-one'); + expect(store.accessToken, 'access-one'); + expect(await store.readRefreshToken(), 'refresh-one'); + + await store.saveTokens(accessToken: 'access-two', refreshToken: 'refresh-two'); + expect(store.accessToken, 'access-two'); + expect(await store.readRefreshToken(), 'refresh-two'); + + await store.clear(); + expect(store.accessToken, isNull); + expect(await store.readRefreshToken(), isNull); + }); +} diff --git a/app/test/config/app_router_test.dart b/app/test/config/app_router_test.dart new file mode 100644 index 0000000..bc91b84 --- /dev/null +++ b/app/test/config/app_router_test.dart @@ -0,0 +1,102 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:papyrus/auth/auth_api_client.dart'; +import 'package:papyrus/auth/auth_models.dart'; +import 'package:papyrus/auth/auth_repository.dart'; +import 'package:papyrus/auth/papyrus_api_config.dart'; +import 'package:papyrus/auth/token_store.dart'; +import 'package:papyrus/config/app_router.dart'; +import 'package:papyrus/providers/auth_provider.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class MemoryRefreshTokenStorage implements RefreshTokenStorage { + String? value; + + @override + Future delete() async { + value = null; + } + + @override + Future read() async => value; + + @override + Future write(String refreshToken) async { + value = refreshToken; + } +} + +class FakeAuthRepository extends AuthRepository { + FakeAuthRepository() + : super( + apiClient: AuthApiClient(config: PapyrusApiConfig(serverBaseUri: Uri.parse('http://server.test'))), + tokenStore: TokenStore(MemoryRefreshTokenStorage()), + ); + + AuthTokens? bootstrapResult; + + @override + Future bootstrap() async { + return bootstrapResult; + } +} + +void main() { + setUp(() { + SharedPreferences.setMockInitialValues({}); + }); + + test('redirects signed-out users away from protected routes', () async { + final prefs = await SharedPreferences.getInstance(); + final provider = AuthProvider(prefs, repository: FakeAuthRepository(), bootstrapOnCreate: false); + + await provider.bootstrap(); + + final appRouter = AppRouter(authProvider: provider); + + expect(appRouter.redirectForPath('/library/books'), '/'); + expect(appRouter.redirectForPath('/login'), isNull); + }); + + test('redirects signed-in users away from auth routes', () async { + final prefs = await SharedPreferences.getInstance(); + final repository = FakeAuthRepository()..bootstrapResult = _tokens(); + final provider = AuthProvider(prefs, repository: repository, bootstrapOnCreate: false); + + await provider.bootstrap(); + + final appRouter = AppRouter(authProvider: provider); + + expect(appRouter.redirectForPath('/login'), '/library/books'); + expect(appRouter.redirectForPath('/library/books'), isNull); + }); + + test('offline mode bypasses protected-route auth redirect', () async { + final prefs = await SharedPreferences.getInstance(); + final provider = AuthProvider(prefs, repository: FakeAuthRepository(), bootstrapOnCreate: false); + + await provider.bootstrap(); + provider.setOfflineMode(true); + + final appRouter = AppRouter(authProvider: provider); + + expect(appRouter.redirectForPath('/library/books'), isNull); + }); +} + +AuthTokens _tokens() { + return AuthTokens( + accessToken: 'access-token', + refreshToken: 'refresh-token', + tokenType: 'Bearer', + expiresIn: 3600, + user: PapyrusUser( + userId: '11111111-1111-1111-1111-111111111111', + email: 'reader@example.com', + displayName: 'Reader', + avatarUrl: null, + emailVerified: true, + createdAt: null, + lastLoginAt: null, + ), + ); +} diff --git a/app/test/helpers/test_helpers.dart b/app/test/helpers/test_helpers.dart index 8c73d71..90a78fa 100644 --- a/app/test/helpers/test_helpers.dart +++ b/app/test/helpers/test_helpers.dart @@ -23,9 +23,7 @@ Widget createTestApp({ }) { return MultiProvider( providers: [ - ChangeNotifierProvider.value( - value: libraryProvider ?? LibraryProvider(), - ), + ChangeNotifierProvider.value(value: libraryProvider ?? LibraryProvider()), ChangeNotifierProvider.value(value: dataStore ?? DataStore()), ...?additionalProviders, ], @@ -51,9 +49,7 @@ Widget createTestPage({ }) { return MultiProvider( providers: [ - ChangeNotifierProvider.value( - value: libraryProvider ?? LibraryProvider(), - ), + ChangeNotifierProvider.value(value: libraryProvider ?? LibraryProvider()), ChangeNotifierProvider.value(value: dataStore ?? DataStore()), ...?additionalProviders, ], @@ -124,11 +120,7 @@ List createTestBooks() { } /// Creates a [DataStore] pre-loaded with test data. -DataStore createTestDataStore({ - List? books, - List? shelves, - List? tags, -}) { +DataStore createTestDataStore({List? books, List? shelves, List? tags}) { final now = DateTime.now(); final store = DataStore(); store.loadData( @@ -137,28 +129,13 @@ DataStore createTestDataStore({ shelves ?? [ Shelf(id: 'shelf-1', name: 'Fiction', createdAt: now, updatedAt: now), - Shelf( - id: 'shelf-2', - name: 'Science Fiction', - createdAt: now, - updatedAt: now, - ), + Shelf(id: 'shelf-2', name: 'Science Fiction', createdAt: now, updatedAt: now), ], tags: tags ?? [ - Tag( - id: 'tag-1', - name: 'Fantasy', - colorHex: '#4CAF50', - createdAt: now, - ), - Tag( - id: 'tag-2', - name: 'Classic', - colorHex: '#2196F3', - createdAt: now, - ), + Tag(id: 'tag-1', name: 'Fantasy', colorHex: '#4CAF50', createdAt: now), + Tag(id: 'tag-2', name: 'Classic', colorHex: '#2196F3', createdAt: now), ], ); return store; diff --git a/app/test/models/active_filter_test.dart b/app/test/models/active_filter_test.dart index b25519a..a2df62d 100644 --- a/app/test/models/active_filter_test.dart +++ b/app/test/models/active_filter_test.dart @@ -5,62 +5,30 @@ void main() { group('ActiveFilter', () { group('equality', () { test('should be equal when type, label, and value match', () { - const filter1 = ActiveFilter( - type: ActiveFilterType.quick, - label: 'Reading', - value: 'reading', - ); - const filter2 = ActiveFilter( - type: ActiveFilterType.quick, - label: 'Reading', - value: 'reading', - ); + const filter1 = ActiveFilter(type: ActiveFilterType.quick, label: 'Reading', value: 'reading'); + const filter2 = ActiveFilter(type: ActiveFilterType.quick, label: 'Reading', value: 'reading'); expect(filter1, equals(filter2)); expect(filter1.hashCode, equals(filter2.hashCode)); }); test('should not be equal when type differs', () { - const filter1 = ActiveFilter( - type: ActiveFilterType.quick, - label: 'shelf', - value: 'Fiction', - ); - const filter2 = ActiveFilter( - type: ActiveFilterType.query, - label: 'shelf', - value: 'Fiction', - ); + const filter1 = ActiveFilter(type: ActiveFilterType.quick, label: 'shelf', value: 'Fiction'); + const filter2 = ActiveFilter(type: ActiveFilterType.query, label: 'shelf', value: 'Fiction'); expect(filter1, isNot(equals(filter2))); }); test('should not be equal when label differs', () { - const filter1 = ActiveFilter( - type: ActiveFilterType.quick, - label: 'Reading', - value: 'reading', - ); - const filter2 = ActiveFilter( - type: ActiveFilterType.quick, - label: 'Finished', - value: 'reading', - ); + const filter1 = ActiveFilter(type: ActiveFilterType.quick, label: 'Reading', value: 'reading'); + const filter2 = ActiveFilter(type: ActiveFilterType.quick, label: 'Finished', value: 'reading'); expect(filter1, isNot(equals(filter2))); }); test('should not be equal when value differs', () { - const filter1 = ActiveFilter( - type: ActiveFilterType.query, - label: 'shelf', - value: 'Fiction', - ); - const filter2 = ActiveFilter( - type: ActiveFilterType.query, - label: 'shelf', - value: 'Non-Fiction', - ); + const filter1 = ActiveFilter(type: ActiveFilterType.query, label: 'shelf', value: 'Fiction'); + const filter2 = ActiveFilter(type: ActiveFilterType.query, label: 'shelf', value: 'Non-Fiction'); expect(filter1, isNot(equals(filter2))); }); @@ -68,11 +36,7 @@ void main() { group('toString', () { test('should return label for quick filters', () { - const filter = ActiveFilter( - type: ActiveFilterType.quick, - label: 'Reading', - value: 'reading', - ); + const filter = ActiveFilter(type: ActiveFilterType.quick, label: 'Reading', value: 'reading'); expect(filter.toString(), 'Reading'); }); @@ -88,18 +52,11 @@ void main() { expect(filter.toString(), 'author:"Tolkien"'); }); - test( - 'should return label:value for query filters without queryString', - () { - const filter = ActiveFilter( - type: ActiveFilterType.query, - label: 'author', - value: 'Tolkien', - ); + test('should return label:value for query filters without queryString', () { + const filter = ActiveFilter(type: ActiveFilterType.query, label: 'author', value: 'Tolkien'); - expect(filter.toString(), 'author:Tolkien'); - }, - ); + expect(filter.toString(), 'author:Tolkien'); + }); }); }); diff --git a/app/test/models/annotation_test.dart b/app/test/models/annotation_test.dart index 60b0187..a49701d 100644 --- a/app/test/models/annotation_test.dart +++ b/app/test/models/annotation_test.dart @@ -7,11 +7,7 @@ void main() { group('BookLocation', () { group('displayLocation', () { test('shows chapter title, number, and page when all present', () { - const loc = BookLocation( - chapter: 3, - chapterTitle: 'The Quest', - pageNumber: 42, - ); + const loc = BookLocation(chapter: 3, chapterTitle: 'The Quest', pageNumber: 42); expect(loc.displayLocation, 'Chapter 3: The Quest, Page 42'); }); @@ -40,12 +36,7 @@ void main() { group('copyWith', () { test('creates copy with updated fields', () { - const original = BookLocation( - chapter: 1, - chapterTitle: 'Intro', - pageNumber: 5, - percentage: 0.01, - ); + const original = BookLocation(chapter: 1, chapterTitle: 'Intro', pageNumber: 5, percentage: 0.01); final copy = original.copyWith(pageNumber: 10, percentage: 0.05); expect(copy.chapter, 1); @@ -78,15 +69,8 @@ void main() { group('copyWith', () { test('creates copy with updated fields', () { - final original = buildTestAnnotation( - selectedText: 'Original', - color: HighlightColor.yellow, - note: 'Note', - ); - final copy = original.copyWith( - color: HighlightColor.blue, - note: 'Updated note', - ); + final original = buildTestAnnotation(selectedText: 'Original', color: HighlightColor.yellow, note: 'Note'); + final copy = original.copyWith(color: HighlightColor.blue, note: 'Updated note'); expect(copy.selectedText, 'Original'); expect(copy.color, HighlightColor.blue); @@ -103,12 +87,7 @@ void main() { bookId: 'book-1', selectedText: 'Some text', color: HighlightColor.green, - location: const BookLocation( - chapter: 2, - chapterTitle: 'Methods', - pageNumber: 45, - percentage: 0.15, - ), + location: const BookLocation(chapter: 2, chapterTitle: 'Methods', pageNumber: 45, percentage: 0.15), note: 'Great point', createdAt: now, updatedAt: now, @@ -181,12 +160,7 @@ void main() { bookId: 'book-rt', selectedText: 'Roundtrip highlight', color: HighlightColor.purple, - location: const BookLocation( - chapter: 3, - chapterTitle: 'Middle', - pageNumber: 100, - percentage: 0.5, - ), + location: const BookLocation(chapter: 3, chapterTitle: 'Middle', pageNumber: 100, percentage: 0.5), note: 'Roundtrip note', createdAt: DateTime(2025, 3, 15), updatedAt: DateTime(2025, 4, 1), diff --git a/app/test/models/book_test.dart b/app/test/models/book_test.dart index f25eb55..9418405 100644 --- a/app/test/models/book_test.dart +++ b/app/test/models/book_test.dart @@ -25,29 +25,14 @@ void main() { }); test('isReading returns true only for inProgress status', () { - expect( - buildTestBook(readingStatus: ReadingStatus.inProgress).isReading, - true, - ); - expect( - buildTestBook(readingStatus: ReadingStatus.completed).isReading, - false, - ); - expect( - buildTestBook(readingStatus: ReadingStatus.notStarted).isReading, - false, - ); + expect(buildTestBook(readingStatus: ReadingStatus.inProgress).isReading, true); + expect(buildTestBook(readingStatus: ReadingStatus.completed).isReading, false); + expect(buildTestBook(readingStatus: ReadingStatus.notStarted).isReading, false); }); test('isFinished returns true only for completed status', () { - expect( - buildTestBook(readingStatus: ReadingStatus.completed).isFinished, - true, - ); - expect( - buildTestBook(readingStatus: ReadingStatus.inProgress).isFinished, - false, - ); + expect(buildTestBook(readingStatus: ReadingStatus.completed).isFinished, true); + expect(buildTestBook(readingStatus: ReadingStatus.inProgress).isFinished, false); }); test('hasProgress returns true when currentPosition > 0', () { @@ -73,10 +58,7 @@ void main() { }); test('allAuthors joins author with co-authors', () { - final book = buildTestBook( - author: 'Alice', - coAuthors: ['Bob', 'Charlie'], - ); + final book = buildTestBook(author: 'Alice', coAuthors: ['Bob', 'Charlie']); expect(book.allAuthors, 'Alice, Bob, Charlie'); }); @@ -289,12 +271,7 @@ void main() { }); test('defaults for missing optional fields', () { - final json = { - 'id': 'test', - 'title': 'Title', - 'author': 'Author', - 'added_at': '2025-01-01T00:00:00.000', - }; + final json = {'id': 'test', 'title': 'Title', 'author': 'Author', 'added_at': '2025-01-01T00:00:00.000'}; final book = Book.fromJson(json); diff --git a/app/test/models/bookmark_test.dart b/app/test/models/bookmark_test.dart index afd91b1..3db7397 100644 --- a/app/test/models/bookmark_test.dart +++ b/app/test/models/bookmark_test.dart @@ -18,10 +18,7 @@ void main() { }); test('displayLocation shows chapter and page when both present', () { - final bookmark = buildTestBookmark( - chapterTitle: 'Chapter 1', - pageNumber: 42, - ); + final bookmark = buildTestBookmark(chapterTitle: 'Chapter 1', pageNumber: 42); expect(bookmark.displayLocation, 'Chapter 1, Page 42'); }); @@ -50,11 +47,7 @@ void main() { group('copyWith sentinel pattern', () { test('keeps nullable fields when not passed', () { - final bookmark = buildTestBookmark( - pageNumber: 10, - chapterTitle: 'Ch1', - note: 'A note', - ); + final bookmark = buildTestBookmark(pageNumber: 10, chapterTitle: 'Ch1', note: 'A note'); final copy = bookmark.copyWith(colorHex: '#FF0000'); expect(copy.pageNumber, 10); @@ -160,12 +153,7 @@ void main() { }); test('defaults colorHex when missing', () { - final json = { - 'id': 'bm-1', - 'book_id': 'book-1', - 'position': 0.5, - 'created_at': '2025-01-01T00:00:00.000', - }; + final json = {'id': 'bm-1', 'book_id': 'book-1', 'position': 0.5, 'created_at': '2025-01-01T00:00:00.000'}; final bookmark = Bookmark.fromJson(json); expect(bookmark.colorHex, '#FF5722'); diff --git a/app/test/models/note_test.dart b/app/test/models/note_test.dart index a36eda4..8608265 100644 --- a/app/test/models/note_test.dart +++ b/app/test/models/note_test.dart @@ -21,9 +21,7 @@ void main() { }); test('hasLocation is true when location is set', () { - final note = buildTestNote( - location: const BookLocation(pageNumber: 10), - ); + final note = buildTestNote(location: const BookLocation(pageNumber: 10)); expect(note.hasLocation, true); }); @@ -43,10 +41,7 @@ void main() { }); test('formattedDate uses updatedAt when present', () { - final note = buildTestNote( - createdAt: DateTime(2025, 1, 15), - updatedAt: DateTime(2025, 6, 20), - ); + final note = buildTestNote(createdAt: DateTime(2025, 1, 15), updatedAt: DateTime(2025, 6, 20)); expect(note.formattedDate, 'Jun 20, 2025'); }); @@ -56,10 +51,7 @@ void main() { }); test('dateLabel says Edited when updatedAt present', () { - final note = buildTestNote( - createdAt: DateTime(2025, 1, 1), - updatedAt: DateTime(2025, 6, 20), - ); + final note = buildTestNote(createdAt: DateTime(2025, 1, 1), updatedAt: DateTime(2025, 6, 20)); expect(note.dateLabel, 'Edited Jun 20, 2025'); }); @@ -71,17 +63,8 @@ void main() { group('copyWith', () { test('creates copy with updated fields', () { - final original = buildTestNote( - title: 'Original', - content: 'Original content', - tags: ['tag1'], - isPinned: false, - ); - final copy = original.copyWith( - title: 'Updated', - isPinned: true, - tags: ['tag1', 'tag2'], - ); + final original = buildTestNote(title: 'Original', content: 'Original content', tags: ['tag1'], isPinned: false); + final copy = original.copyWith(title: 'Updated', isPinned: true, tags: ['tag1', 'tag2']); expect(copy.title, 'Updated'); expect(copy.content, 'Original content'); @@ -99,12 +82,7 @@ void main() { bookId: 'book-1', title: 'Test', content: 'Content', - location: const BookLocation( - chapter: 2, - chapterTitle: 'Ch 2', - pageNumber: 30, - percentage: 0.1, - ), + location: const BookLocation(chapter: 2, chapterTitle: 'Ch 2', pageNumber: 30, percentage: 0.1), tags: ['review', 'important'], isPinned: true, createdAt: now, @@ -207,12 +185,7 @@ void main() { bookId: 'book-rt', title: 'Roundtrip', content: 'Roundtrip content', - location: const BookLocation( - chapter: 3, - chapterTitle: 'Middle', - pageNumber: 100, - percentage: 0.5, - ), + location: const BookLocation(chapter: 3, chapterTitle: 'Middle', pageNumber: 100, percentage: 0.5), tags: ['test', 'roundtrip'], isPinned: true, createdAt: DateTime(2025, 3, 15), diff --git a/app/test/models/search_filter_test.dart b/app/test/models/search_filter_test.dart index b223ea7..5f8ab8b 100644 --- a/app/test/models/search_filter_test.dart +++ b/app/test/models/search_filter_test.dart @@ -52,31 +52,19 @@ void main() { group('SearchFilter', () { group('SearchField.title', () { test('contains matches substring', () { - final filter = SearchFilter( - field: SearchField.title, - operator: SearchOperator.contains, - value: 'Hobbit', - ); + final filter = SearchFilter(field: SearchField.title, operator: SearchOperator.contains, value: 'Hobbit'); expect(filter.matches(makeBook()), true); expect(filter.matches(makeBook(title: 'Dune')), false); }); test('equals matches exact value (case-insensitive)', () { - final filter = SearchFilter( - field: SearchField.title, - operator: SearchOperator.equals, - value: 'the hobbit', - ); + final filter = SearchFilter(field: SearchField.title, operator: SearchOperator.equals, value: 'the hobbit'); expect(filter.matches(makeBook()), true); expect(filter.matches(makeBook(title: 'The Hobbit Part 2')), false); }); test('notEquals excludes matching value', () { - final filter = SearchFilter( - field: SearchField.title, - operator: SearchOperator.notEquals, - value: 'The Hobbit', - ); + final filter = SearchFilter(field: SearchField.title, operator: SearchOperator.notEquals, value: 'The Hobbit'); expect(filter.matches(makeBook()), false); expect(filter.matches(makeBook(title: 'Dune')), true); }); @@ -84,11 +72,7 @@ void main() { group('SearchField.author', () { test('contains matches author name', () { - final filter = SearchFilter( - field: SearchField.author, - operator: SearchOperator.contains, - value: 'Tolkien', - ); + final filter = SearchFilter(field: SearchField.author, operator: SearchOperator.contains, value: 'Tolkien'); expect(filter.matches(makeBook()), true); expect(filter.matches(makeBook(author: 'Frank Herbert')), false); }); @@ -96,21 +80,13 @@ void main() { group('SearchField.format', () { test('equals matches format label', () { - final filter = SearchFilter( - field: SearchField.format, - operator: SearchOperator.equals, - value: 'epub', - ); + final filter = SearchFilter(field: SearchField.format, operator: SearchOperator.equals, value: 'epub'); expect(filter.matches(makeBook(fileFormat: BookFormat.epub)), true); expect(filter.matches(makeBook(fileFormat: BookFormat.pdf)), false); }); test('matches physical books', () { - final filter = SearchFilter( - field: SearchField.format, - operator: SearchOperator.equals, - value: 'physical', - ); + final filter = SearchFilter(field: SearchField.format, operator: SearchOperator.equals, value: 'physical'); expect(filter.matches(makeBook(isPhysical: true)), true); expect(filter.matches(makeBook(isPhysical: false)), false); }); @@ -118,98 +94,49 @@ void main() { group('SearchField.status', () { test('matches reading status', () { - final filter = SearchFilter( - field: SearchField.status, - operator: SearchOperator.equals, - value: 'reading', - ); - expect( - filter.matches(makeBook(readingStatus: ReadingStatus.inProgress)), - true, - ); - expect( - filter.matches(makeBook(readingStatus: ReadingStatus.notStarted)), - false, - ); + final filter = SearchFilter(field: SearchField.status, operator: SearchOperator.equals, value: 'reading'); + expect(filter.matches(makeBook(readingStatus: ReadingStatus.inProgress)), true); + expect(filter.matches(makeBook(readingStatus: ReadingStatus.notStarted)), false); }); test('matches finished status', () { - final filter = SearchFilter( - field: SearchField.status, - operator: SearchOperator.equals, - value: 'finished', - ); - expect( - filter.matches(makeBook(readingStatus: ReadingStatus.completed)), - true, - ); + final filter = SearchFilter(field: SearchField.status, operator: SearchOperator.equals, value: 'finished'); + expect(filter.matches(makeBook(readingStatus: ReadingStatus.completed)), true); }); test('matches unread status', () { - final filter = SearchFilter( - field: SearchField.status, - operator: SearchOperator.equals, - value: 'unread', - ); - expect( - filter.matches( - makeBook( - readingStatus: ReadingStatus.notStarted, - currentPosition: 0.0, - ), - ), - true, - ); + final filter = SearchFilter(field: SearchField.status, operator: SearchOperator.equals, value: 'unread'); + expect(filter.matches(makeBook(readingStatus: ReadingStatus.notStarted, currentPosition: 0.0)), true); }); }); group('SearchField.progress', () { test('greaterThan compares numerically', () { - final filter = SearchFilter( - field: SearchField.progress, - operator: SearchOperator.greaterThan, - value: '50', - ); + final filter = SearchFilter(field: SearchField.progress, operator: SearchOperator.greaterThan, value: '50'); expect(filter.matches(makeBook(currentPosition: 0.75)), true); expect(filter.matches(makeBook(currentPosition: 0.25)), false); }); test('lessThan compares numerically', () { - final filter = SearchFilter( - field: SearchField.progress, - operator: SearchOperator.lessThan, - value: '50', - ); + final filter = SearchFilter(field: SearchField.progress, operator: SearchOperator.lessThan, value: '50'); expect(filter.matches(makeBook(currentPosition: 0.25)), true); expect(filter.matches(makeBook(currentPosition: 0.75)), false); }); test('greaterThan returns false for non-numeric value', () { - final filter = SearchFilter( - field: SearchField.progress, - operator: SearchOperator.greaterThan, - value: 'abc', - ); + final filter = SearchFilter(field: SearchField.progress, operator: SearchOperator.greaterThan, value: 'abc'); expect(filter.matches(makeBook(currentPosition: 0.5)), false); }); }); group('SearchField.any', () { test('matches title or author', () { - final filter = SearchFilter( - field: SearchField.any, - operator: SearchOperator.contains, - value: 'Tolkien', - ); + final filter = SearchFilter(field: SearchField.any, operator: SearchOperator.contains, value: 'Tolkien'); expect(filter.matches(makeBook()), true); }); test('matches title in combined field', () { - final filter = SearchFilter( - field: SearchField.any, - operator: SearchOperator.contains, - value: 'Hobbit', - ); + final filter = SearchFilter(field: SearchField.any, operator: SearchOperator.contains, value: 'Hobbit'); expect(filter.matches(makeBook()), true); }); }); @@ -217,47 +144,27 @@ void main() { group('SearchField.shelf with DataStore', () { test('matches book on shelf via junction table', () { final book = makeBook(id: 'b1'); - final shelf = Shelf( - id: 's1', - name: 'Fiction', - createdAt: now, - updatedAt: now, - ); + final shelf = Shelf(id: 's1', name: 'Fiction', createdAt: now, updatedAt: now); final dataStore = buildDataStore( books: [book], shelves: [shelf], - bookShelfRelations: [ - BookShelfRelation(bookId: 'b1', shelfId: 's1', addedAt: now), - ], + bookShelfRelations: [BookShelfRelation(bookId: 'b1', shelfId: 's1', addedAt: now)], ); - final filter = SearchFilter( - field: SearchField.shelf, - operator: SearchOperator.contains, - value: 'Fiction', - ); + final filter = SearchFilter(field: SearchField.shelf, operator: SearchOperator.contains, value: 'Fiction'); expect(filter.matches(book, dataStore: dataStore), true); }); test('does not match book not on shelf', () { final book = makeBook(id: 'b1'); - final shelf = Shelf( - id: 's1', - name: 'Fiction', - createdAt: now, - updatedAt: now, - ); + final shelf = Shelf(id: 's1', name: 'Fiction', createdAt: now, updatedAt: now); final dataStore = buildDataStore( books: [book], shelves: [shelf], // No relation ); - final filter = SearchFilter( - field: SearchField.shelf, - operator: SearchOperator.contains, - value: 'Fiction', - ); + final filter = SearchFilter(field: SearchField.shelf, operator: SearchOperator.contains, value: 'Fiction'); expect(filter.matches(book, dataStore: dataStore), false); }); @@ -281,11 +188,7 @@ void main() { operator: SearchOperator.contains, value: 'Fiction', ); - final favFilter = SearchFilter( - field: SearchField.shelf, - operator: SearchOperator.contains, - value: 'Favorites', - ); + final favFilter = SearchFilter(field: SearchField.shelf, operator: SearchOperator.contains, value: 'Favorites'); expect(fictionFilter.matches(book, dataStore: dataStore), true); expect(favFilter.matches(book, dataStore: dataStore), true); }); @@ -294,11 +197,7 @@ void main() { group('SearchField.shelf without DataStore (fallback)', () { test('falls back to book.shelves (empty list)', () { final book = makeBook(); - final filter = SearchFilter( - field: SearchField.shelf, - operator: SearchOperator.contains, - value: 'Fiction', - ); + final filter = SearchFilter(field: SearchField.shelf, operator: SearchOperator.contains, value: 'Fiction'); // book.shelves returns const [], so no match expect(filter.matches(book), false); }); @@ -307,47 +206,27 @@ void main() { group('SearchField.topic with DataStore', () { test('matches book with tag via junction table', () { final book = makeBook(id: 'b1'); - final tag = Tag( - id: 't1', - name: 'Science', - colorHex: '#FF0000', - createdAt: now, - ); + final tag = Tag(id: 't1', name: 'Science', colorHex: '#FF0000', createdAt: now); final dataStore = buildDataStore( books: [book], tags: [tag], - bookTagRelations: [ - BookTagRelation(bookId: 'b1', tagId: 't1', createdAt: now), - ], + bookTagRelations: [BookTagRelation(bookId: 'b1', tagId: 't1', createdAt: now)], ); - final filter = SearchFilter( - field: SearchField.topic, - operator: SearchOperator.contains, - value: 'Science', - ); + final filter = SearchFilter(field: SearchField.topic, operator: SearchOperator.contains, value: 'Science'); expect(filter.matches(book, dataStore: dataStore), true); }); test('does not match book without tag', () { final book = makeBook(id: 'b1'); - final tag = Tag( - id: 't1', - name: 'Science', - colorHex: '#FF0000', - createdAt: now, - ); + final tag = Tag(id: 't1', name: 'Science', colorHex: '#FF0000', createdAt: now); final dataStore = buildDataStore( books: [book], tags: [tag], // No relation ); - final filter = SearchFilter( - field: SearchField.topic, - operator: SearchOperator.contains, - value: 'Science', - ); + final filter = SearchFilter(field: SearchField.topic, operator: SearchOperator.contains, value: 'Science'); expect(filter.matches(book, dataStore: dataStore), false); }); }); @@ -355,11 +234,7 @@ void main() { group('SearchField.topic without DataStore (fallback)', () { test('falls back to book.topics (empty list)', () { final book = makeBook(); - final filter = SearchFilter( - field: SearchField.topic, - operator: SearchOperator.contains, - value: 'Science', - ); + final filter = SearchFilter(field: SearchField.topic, operator: SearchOperator.contains, value: 'Science'); expect(filter.matches(book), false); }); }); @@ -370,16 +245,8 @@ void main() { test('matches when all filters pass', () { final query = SearchQuery( filters: [ - SearchFilter( - field: SearchField.title, - operator: SearchOperator.contains, - value: 'Hobbit', - ), - SearchFilter( - field: SearchField.author, - operator: SearchOperator.contains, - value: 'Tolkien', - ), + SearchFilter(field: SearchField.title, operator: SearchOperator.contains, value: 'Hobbit'), + SearchFilter(field: SearchField.author, operator: SearchOperator.contains, value: 'Tolkien'), ], operators: [LogicalOperator.and], ); @@ -389,16 +256,8 @@ void main() { test('fails when one filter does not match', () { final query = SearchQuery( filters: [ - SearchFilter( - field: SearchField.title, - operator: SearchOperator.contains, - value: 'Hobbit', - ), - SearchFilter( - field: SearchField.author, - operator: SearchOperator.contains, - value: 'Herbert', - ), + SearchFilter(field: SearchField.title, operator: SearchOperator.contains, value: 'Hobbit'), + SearchFilter(field: SearchField.author, operator: SearchOperator.contains, value: 'Herbert'), ], operators: [LogicalOperator.and], ); @@ -410,16 +269,8 @@ void main() { test('matches when at least one filter passes', () { final query = SearchQuery( filters: [ - SearchFilter( - field: SearchField.title, - operator: SearchOperator.contains, - value: 'Dune', - ), - SearchFilter( - field: SearchField.author, - operator: SearchOperator.contains, - value: 'Tolkien', - ), + SearchFilter(field: SearchField.title, operator: SearchOperator.contains, value: 'Dune'), + SearchFilter(field: SearchField.author, operator: SearchOperator.contains, value: 'Tolkien'), ], operators: [LogicalOperator.or], ); @@ -429,16 +280,8 @@ void main() { test('fails when no filter matches', () { final query = SearchQuery( filters: [ - SearchFilter( - field: SearchField.title, - operator: SearchOperator.contains, - value: 'Dune', - ), - SearchFilter( - field: SearchField.author, - operator: SearchOperator.contains, - value: 'Herbert', - ), + SearchFilter(field: SearchField.title, operator: SearchOperator.contains, value: 'Dune'), + SearchFilter(field: SearchField.author, operator: SearchOperator.contains, value: 'Herbert'), ], operators: [LogicalOperator.or], ); @@ -449,40 +292,16 @@ void main() { group('NOT filters', () { test('excludes book when NOT filter matches', () { final query = SearchQuery( - filters: [ - SearchFilter( - field: SearchField.any, - operator: SearchOperator.contains, - value: 'Hobbit', - ), - ], - notFilters: [ - SearchFilter( - field: SearchField.author, - operator: SearchOperator.contains, - value: 'Tolkien', - ), - ], + filters: [SearchFilter(field: SearchField.any, operator: SearchOperator.contains, value: 'Hobbit')], + notFilters: [SearchFilter(field: SearchField.author, operator: SearchOperator.contains, value: 'Tolkien')], ); expect(query.matches(makeBook()), false); }); test('includes book when NOT filter does not match', () { final query = SearchQuery( - filters: [ - SearchFilter( - field: SearchField.any, - operator: SearchOperator.contains, - value: 'Hobbit', - ), - ], - notFilters: [ - SearchFilter( - field: SearchField.author, - operator: SearchOperator.contains, - value: 'Herbert', - ), - ], + filters: [SearchFilter(field: SearchField.any, operator: SearchOperator.contains, value: 'Hobbit')], + notFilters: [SearchFilter(field: SearchField.author, operator: SearchOperator.contains, value: 'Herbert')], ); expect(query.matches(makeBook()), true); }); @@ -491,57 +310,31 @@ void main() { group('DataStore passthrough', () { test('passes dataStore to shelf filter in query', () { final book = makeBook(id: 'b1'); - final shelf = Shelf( - id: 's1', - name: 'Fiction', - createdAt: now, - updatedAt: now, - ); + final shelf = Shelf(id: 's1', name: 'Fiction', createdAt: now, updatedAt: now); final dataStore = buildDataStore( books: [book], shelves: [shelf], - bookShelfRelations: [ - BookShelfRelation(bookId: 'b1', shelfId: 's1', addedAt: now), - ], + bookShelfRelations: [BookShelfRelation(bookId: 'b1', shelfId: 's1', addedAt: now)], ); final query = SearchQuery( - filters: [ - SearchFilter( - field: SearchField.shelf, - operator: SearchOperator.contains, - value: 'Fiction', - ), - ], + filters: [SearchFilter(field: SearchField.shelf, operator: SearchOperator.contains, value: 'Fiction')], ); expect(query.matches(book, dataStore: dataStore), true); }); test('passes dataStore to NOT filter', () { final book = makeBook(id: 'b1'); - final shelf = Shelf( - id: 's1', - name: 'Fiction', - createdAt: now, - updatedAt: now, - ); + final shelf = Shelf(id: 's1', name: 'Fiction', createdAt: now, updatedAt: now); final dataStore = buildDataStore( books: [book], shelves: [shelf], - bookShelfRelations: [ - BookShelfRelation(bookId: 'b1', shelfId: 's1', addedAt: now), - ], + bookShelfRelations: [BookShelfRelation(bookId: 'b1', shelfId: 's1', addedAt: now)], ); final query = SearchQuery( filters: [], - notFilters: [ - SearchFilter( - field: SearchField.shelf, - operator: SearchOperator.contains, - value: 'Fiction', - ), - ], + notFilters: [SearchFilter(field: SearchField.shelf, operator: SearchOperator.contains, value: 'Fiction')], ); // Book IS on Fiction shelf, so NOT filter excludes it expect(query.matches(book, dataStore: dataStore), false); diff --git a/app/test/pages/library_page_test.dart b/app/test/pages/library_page_test.dart index c05dbb0..81cea9b 100644 --- a/app/test/pages/library_page_test.dart +++ b/app/test/pages/library_page_test.dart @@ -22,11 +22,7 @@ void main() { dataStore = createTestDataStore(); }); - Widget buildPage({ - Size screenSize = const Size(400, 800), - LibraryProvider? provider, - DataStore? store, - }) { + Widget buildPage({Size screenSize = const Size(400, 800), LibraryProvider? provider, DataStore? store}) { return createTestPage( page: const LibraryPage(), libraryProvider: provider ?? libraryProvider, @@ -77,9 +73,7 @@ void main() { }); testWidgets('displays singular "book" when only 1 book', (tester) async { - final singleBookStore = createTestDataStore( - books: [createTestBooks().first], - ); + final singleBookStore = createTestDataStore(books: [createTestBooks().first]); await tester.pumpWidget(buildPage(store: singleBookStore)); await tester.pumpAndSettle(); @@ -102,9 +96,7 @@ void main() { expect(find.byIcon(Icons.view_list), findsOneWidget); }); - testWidgets('switches to list view when list mode is selected', ( - tester, - ) async { + testWidgets('switches to list view when list mode is selected', (tester) async { libraryProvider.setViewMode(LibraryViewMode.list); await tester.pumpWidget(buildPage()); await tester.pumpAndSettle(); @@ -113,19 +105,14 @@ void main() { expect(find.byType(BookCard), findsNothing); }); - testWidgets('shows empty state when no books match filter', ( - tester, - ) async { + testWidgets('shows empty state when no books match filter', (tester) async { final emptyStore = createTestDataStore(books: []); await tester.pumpWidget(buildPage(store: emptyStore)); await tester.pumpAndSettle(); expect(find.byType(EmptyState), findsOneWidget); expect(find.text('No books found'), findsOneWidget); - expect( - find.text('Try adjusting your filters or add some books'), - findsOneWidget, - ); + expect(find.text('Try adjusting your filters or add some books'), findsOneWidget); }); }); @@ -197,9 +184,7 @@ void main() { testWidgets('shows empty state when no books on desktop', (tester) async { final emptyStore = createTestDataStore(books: []); - await tester.pumpWidget( - buildPage(screenSize: desktopSize, store: emptyStore), - ); + await tester.pumpWidget(buildPage(screenSize: desktopSize, store: emptyStore)); await tester.pumpAndSettle(); expect(find.byType(EmptyState), findsOneWidget); @@ -254,9 +239,7 @@ void main() { expect(find.text('2 books'), findsOneWidget); }); - testWidgets('shows empty state when no books match filter', ( - tester, - ) async { + testWidgets('shows empty state when no books match filter', (tester) async { // Add a filter and clear all books libraryProvider.addFilter(LibraryFilterType.reading); final storeWithNoReadingBooks = createTestDataStore( @@ -321,9 +304,7 @@ void main() { // ======================================================================== group('view mode switching', () { - testWidgets('toggling view mode on mobile updates the display', ( - tester, - ) async { + testWidgets('toggling view mode on mobile updates the display', (tester) async { await tester.pumpWidget(buildPage()); await tester.pumpAndSettle(); @@ -338,9 +319,7 @@ void main() { expect(find.byType(BookCard), findsNothing); }); - testWidgets('tapping grid segment on mobile selects grid view', ( - tester, - ) async { + testWidgets('tapping grid segment on mobile selects grid view', (tester) async { libraryProvider.setViewMode(LibraryViewMode.list); await tester.pumpWidget(buildPage()); await tester.pumpAndSettle(); @@ -352,9 +331,7 @@ void main() { expect(libraryProvider.viewMode, LibraryViewMode.grid); }); - testWidgets('tapping list segment on mobile selects list view', ( - tester, - ) async { + testWidgets('tapping list segment on mobile selects list view', (tester) async { await tester.pumpWidget(buildPage()); await tester.pumpAndSettle(); @@ -365,9 +342,7 @@ void main() { expect(libraryProvider.viewMode, LibraryViewMode.list); }); - testWidgets('tapping toggle on desktop switches view mode', ( - tester, - ) async { + testWidgets('tapping toggle on desktop switches view mode', (tester) async { const desktopSize = Size(1200, 800); await tester.pumpWidget(buildPage(screenSize: desktopSize)); await tester.pumpAndSettle(); @@ -442,9 +417,7 @@ void main() { // ======================================================================== group('desktop compact layout', () { - testWidgets('uses compact layout at narrow desktop width', ( - tester, - ) async { + testWidgets('uses compact layout at narrow desktop width', (tester) async { // 850px is >= desktopSmall (840) but < 800 in maxWidth // after padding. Let's use exactly 860 to trigger desktop // but be narrow enough for compact layout. diff --git a/app/test/providers/auth_provider_test.dart b/app/test/providers/auth_provider_test.dart new file mode 100644 index 0000000..f5a8930 --- /dev/null +++ b/app/test/providers/auth_provider_test.dart @@ -0,0 +1,125 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:papyrus/auth/auth_api_client.dart'; +import 'package:papyrus/auth/auth_models.dart'; +import 'package:papyrus/auth/auth_repository.dart'; +import 'package:papyrus/auth/papyrus_api_config.dart'; +import 'package:papyrus/auth/token_store.dart'; +import 'package:papyrus/providers/auth_provider.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class MemoryRefreshTokenStorage implements RefreshTokenStorage { + String? value; + + @override + Future delete() async { + value = null; + } + + @override + Future read() async => value; + + @override + Future write(String refreshToken) async { + value = refreshToken; + } +} + +class FakeAuthRepository extends AuthRepository { + FakeAuthRepository() + : super( + apiClient: AuthApiClient(config: PapyrusApiConfig(serverBaseUri: Uri.parse('http://server.test'))), + tokenStore: TokenStore(MemoryRefreshTokenStorage()), + ); + + AuthTokens? bootstrapResult; + Object? bootstrapError; + AuthTokens? refreshResult; + Object? refreshError; + bool clearCalled = false; + + @override + Future bootstrap() async { + if (bootstrapError != null) { + throw bootstrapError!; + } + + return bootstrapResult; + } + + @override + Future refresh() async { + if (refreshError != null) { + throw refreshError!; + } + + return refreshResult ?? _tokens('refresh-user'); + } + + @override + Future clearTokens() async { + clearCalled = true; + } +} + +void main() { + setUp(() { + SharedPreferences.setMockInitialValues({}); + }); + + test('bootstraps signed in state from stored refresh token', () async { + final prefs = await SharedPreferences.getInstance(); + final repository = FakeAuthRepository()..bootstrapResult = _tokens('Bootstrap User'); + final provider = AuthProvider(prefs, repository: repository, bootstrapOnCreate: false); + + await provider.bootstrap(); + + expect(provider.isSignedIn, isTrue); + expect(provider.user?.displayName, 'Bootstrap User'); + }); + + test('clears auth state when refresh fails', () async { + final prefs = await SharedPreferences.getInstance(); + final repository = FakeAuthRepository() + ..bootstrapResult = _tokens('Bootstrap User') + ..refreshError = const AuthApiException(statusCode: 401, message: 'Invalid refresh token'); + final provider = AuthProvider(prefs, repository: repository, bootstrapOnCreate: false); + + await provider.bootstrap(); + final refreshed = await provider.refresh(); + + expect(refreshed, isFalse); + expect(provider.isSignedIn, isFalse); + expect(provider.user, isNull); + }); + + test('offline mode clears tokens and bypasses signed in state', () async { + final prefs = await SharedPreferences.getInstance(); + final repository = FakeAuthRepository()..bootstrapResult = _tokens('Bootstrap User'); + final provider = AuthProvider(prefs, repository: repository, bootstrapOnCreate: false); + + await provider.bootstrap(); + provider.setOfflineMode(true); + + expect(provider.isOfflineMode, isTrue); + expect(provider.isSignedIn, isFalse); + expect(repository.clearCalled, isTrue); + }); +} + +AuthTokens _tokens(String displayName) { + return AuthTokens( + accessToken: 'access-token', + refreshToken: 'refresh-token', + tokenType: 'Bearer', + expiresIn: 3600, + user: PapyrusUser( + userId: '11111111-1111-1111-1111-111111111111', + email: 'reader@example.com', + displayName: displayName, + avatarUrl: null, + emailVerified: true, + createdAt: null, + lastLoginAt: null, + ), + ); +} diff --git a/app/test/providers/book_details_provider_test.dart b/app/test/providers/book_details_provider_test.dart index d5721c7..df1b793 100644 --- a/app/test/providers/book_details_provider_test.dart +++ b/app/test/providers/book_details_provider_test.dart @@ -25,32 +25,15 @@ void main() { isFavorite: false, pageCount: 300, ), - buildTestBook( - id: 'book-2', - title: 'Another Book', - author: 'Author 2', - ), + buildTestBook(id: 'book-2', title: 'Another Book', author: 'Author 2'), ], bookmarks: [ buildTestBookmark(id: 'bm-1', bookId: 'book-1', position: 0.3), buildTestBookmark(id: 'bm-2', bookId: 'book-1', position: 0.6), buildTestBookmark(id: 'bm-3', bookId: 'book-2', position: 0.1), ], - annotations: [ - buildTestAnnotation( - id: 'ann-1', - bookId: 'book-1', - selectedText: 'Highlight 1', - ), - ], - notes: [ - buildTestNote( - id: 'note-1', - bookId: 'book-1', - title: 'Note 1', - content: 'Content 1', - ), - ], + annotations: [buildTestAnnotation(id: 'ann-1', bookId: 'book-1', selectedText: 'Highlight 1')], + notes: [buildTestNote(id: 'note-1', bookId: 'book-1', title: 'Note 1', content: 'Content 1')], ); provider.setDataStore(dataStore); }); @@ -234,12 +217,7 @@ void main() { }); test('addNote persists to DataStore and notifies', () { - final note = buildTestNote( - id: 'new-note', - bookId: 'book-1', - title: 'New Note', - content: 'New content', - ); + final note = buildTestNote(id: 'new-note', bookId: 'book-1', title: 'New Note', content: 'New content'); var notified = false; provider.addListener(() => notified = true); @@ -279,11 +257,7 @@ void main() { }); test('addBookmark persists to DataStore', () { - final bookmark = buildTestBookmark( - id: 'new-bm', - bookId: 'book-1', - position: 0.8, - ); + final bookmark = buildTestBookmark(id: 'new-bm', bookId: 'book-1', position: 0.8); provider.addBookmark(bookmark); @@ -336,11 +310,7 @@ void main() { }); test('addAnnotation persists to DataStore', () { - final annotation = buildTestAnnotation( - id: 'new-ann', - bookId: 'book-1', - selectedText: 'New highlight', - ); + final annotation = buildTestAnnotation(id: 'new-ann', bookId: 'book-1', selectedText: 'New highlight'); provider.addAnnotation(annotation); diff --git a/app/test/providers/library_provider_test.dart b/app/test/providers/library_provider_test.dart index 6f6084f..2b2c1f2 100644 --- a/app/test/providers/library_provider_test.dart +++ b/app/test/providers/library_provider_test.dart @@ -95,12 +95,7 @@ void main() { provider.addFilter(LibraryFilterType.reading); // Set uses unique values - expect( - provider.activeFilters - .where((f) => f == LibraryFilterType.reading) - .length, - 1, - ); + expect(provider.activeFilters.where((f) => f == LibraryFilterType.reading).length, 1); }); }); @@ -156,9 +151,7 @@ void main() { provider.selectShelf('Fiction'); provider.selectTopic('Science'); provider.addFilter(LibraryFilterType.reading); - provider.setSearchQuery( - 'author:tolkien shelf:"Fiction" topic:"Science"', - ); + provider.setSearchQuery('author:tolkien shelf:"Fiction" topic:"Science"'); provider.clearSearch(); diff --git a/app/test/services/book_import_service_test.dart b/app/test/services/book_import_service_test.dart index 943b59e..d9e6517 100644 --- a/app/test/services/book_import_service_test.dart +++ b/app/test/services/book_import_service_test.dart @@ -8,9 +8,7 @@ import 'package:path_provider_platform_interface/path_provider_platform_interfac import 'package:plugin_platform_interface/plugin_platform_interface.dart'; /// Fake path_provider that returns a temporary directory for testing. -class _FakePathProvider extends Fake - with MockPlatformInterfaceMixin - implements PathProviderPlatform { +class _FakePathProvider extends Fake with MockPlatformInterfaceMixin implements PathProviderPlatform { final Directory tempDir; _FakePathProvider(this.tempDir); @@ -124,9 +122,7 @@ void main() { final result = await service.importBook(bytes, 'book1.epub'); - final storedFile = File( - p.join(tempDir.path, 'books', '${result.bookId}.epub'), - ); + final storedFile = File(p.join(tempDir.path, 'books', '${result.bookId}.epub')); expect(storedFile.existsSync(), isTrue); expect(storedFile.lengthSync(), bytes.length); }); @@ -150,14 +146,9 @@ void main() { }); test('imports txt file with author-title pattern', () async { - final bytes = Uint8List.fromList( - 'Hello, this is a test book with some content.'.codeUnits, - ); - - final result = await service.importBook( - bytes, - 'Jane Austen - Pride and Prejudice.txt', - ); + final bytes = Uint8List.fromList('Hello, this is a test book with some content.'.codeUnits); + + final result = await service.importBook(bytes, 'Jane Austen - Pride and Prejudice.txt'); expect(result.title, 'Pride and Prejudice'); expect(result.author, 'Jane Austen'); diff --git a/app/test/services/file_metadata_service_test.dart b/app/test/services/file_metadata_service_test.dart index 212e0ef..4c61818 100644 --- a/app/test/services/file_metadata_service_test.dart +++ b/app/test/services/file_metadata_service_test.dart @@ -33,124 +33,97 @@ void main() { expect(result.language, 'de'); }); - test( - 'parses 2.mobi without crash (MOBI v6, limited EXTH support)', - () async { - final bytes = loadTestFile('2.mobi'); - if (bytes == null) return; - final result = await service.extractMetadata(bytes, '2.mobi'); - - // MOBI v6 has limited EXTH support in dart_mobi — metadata - // fields may be null, but extraction should not crash. - expect(result.warnings, isNotEmpty); - }, - ); + test('parses 2.mobi without crash (MOBI v6, limited EXTH support)', () async { + final bytes = loadTestFile('2.mobi'); + if (bytes == null) return; + final result = await service.extractMetadata(bytes, '2.mobi'); + + // MOBI v6 has limited EXTH support in dart_mobi — metadata + // fields may be null, but extraction should not crash. + expect(result.warnings, isNotEmpty); + }); }); group('EPUB extraction', () { - test( - 'parses 3.epub metadata (EPUB 2, title, author, cover, date)', - () async { - final bytes = loadTestFile('3.epub'); - if (bytes == null) return; - final result = await service.extractMetadata(bytes, '3.epub'); - - expect(result.title, 'Im Kampf um Ideale'); - expect(result.authors, contains('Georg Bonne')); - expect(result.language, 'de'); - expect(result.coverImageBytes, isNotNull); - expect(result.coverImageMimeType, 'image/png'); - expect(result.publishedDate, isNotNull); - }, - ); - - test( - 'parses 4.epub metadata (EPUB 3, title, author, cover, date)', - () async { - final bytes = loadTestFile('4.epub'); - if (bytes == null) return; - final result = await service.extractMetadata(bytes, '4.epub'); - - expect(result.title, 'Im Kampf um Ideale'); - expect(result.authors, contains('Georg Bonne')); - expect(result.language, 'de'); - expect(result.coverImageBytes, isNotNull); - expect(result.coverImageMimeType, 'image/png'); - expect(result.publishedDate, isNotNull); - }, - ); - - test( - 'parses 5.epub metadata (EPUB 2, title, author, cover, date)', - () async { - final bytes = loadTestFile('5.epub'); - if (bytes == null) return; - final result = await service.extractMetadata(bytes, '5.epub'); - - expect(result.title, 'Im Kampf um Ideale'); - expect(result.authors, contains('Georg Bonne')); - expect(result.language, 'de'); - expect(result.coverImageBytes, isNotNull); - expect(result.coverImageMimeType, 'image/png'); - expect(result.publishedDate, isNotNull); - }, - ); + test('parses 3.epub metadata (EPUB 2, title, author, cover, date)', () async { + final bytes = loadTestFile('3.epub'); + if (bytes == null) return; + final result = await service.extractMetadata(bytes, '3.epub'); + + expect(result.title, 'Im Kampf um Ideale'); + expect(result.authors, contains('Georg Bonne')); + expect(result.language, 'de'); + expect(result.coverImageBytes, isNotNull); + expect(result.coverImageMimeType, 'image/png'); + expect(result.publishedDate, isNotNull); + }); + + test('parses 4.epub metadata (EPUB 3, title, author, cover, date)', () async { + final bytes = loadTestFile('4.epub'); + if (bytes == null) return; + final result = await service.extractMetadata(bytes, '4.epub'); + + expect(result.title, 'Im Kampf um Ideale'); + expect(result.authors, contains('Georg Bonne')); + expect(result.language, 'de'); + expect(result.coverImageBytes, isNotNull); + expect(result.coverImageMimeType, 'image/png'); + expect(result.publishedDate, isNotNull); + }); + + test('parses 5.epub metadata (EPUB 2, title, author, cover, date)', () async { + final bytes = loadTestFile('5.epub'); + if (bytes == null) return; + final result = await service.extractMetadata(bytes, '5.epub'); + + expect(result.title, 'Im Kampf um Ideale'); + expect(result.authors, contains('Georg Bonne')); + expect(result.language, 'de'); + expect(result.coverImageBytes, isNotNull); + expect(result.coverImageMimeType, 'image/png'); + expect(result.publishedDate, isNotNull); + }); }); group('AZW3 extraction', () { - test( - 'parses 6.azw3 metadata (title, author, publisher, description, language)', - () async { - final bytes = loadTestFile('6.azw3'); - if (bytes == null) return; - final result = await service.extractMetadata(bytes, '6.azw3'); - - expect(result.title, 'Dracula'); - expect(result.authors, contains('Bram Stoker')); - expect(result.publisher, 'Standard Ebooks'); - expect(result.description, contains('undead')); - expect(result.language, 'en'); - }, - ); + test('parses 6.azw3 metadata (title, author, publisher, description, language)', () async { + final bytes = loadTestFile('6.azw3'); + if (bytes == null) return; + final result = await service.extractMetadata(bytes, '6.azw3'); + + expect(result.title, 'Dracula'); + expect(result.authors, contains('Bram Stoker')); + expect(result.publisher, 'Standard Ebooks'); + expect(result.description, contains('undead')); + expect(result.language, 'en'); + }); }); group('CBZ extraction', () { - test( - 'parses 7.cbz (no ComicInfo.xml, cover from first image, warning)', - () async { - final bytes = loadTestFile('7.cbz'); - if (bytes == null) return; - final result = await service.extractMetadata(bytes, '7.cbz'); - - expect(result.coverImageBytes, isNotNull); - expect( - result.warnings, - contains('No ComicInfo.xml found in CBZ archive'), - ); - }, - ); + test('parses 7.cbz (no ComicInfo.xml, cover from first image, warning)', () async { + final bytes = loadTestFile('7.cbz'); + if (bytes == null) return; + final result = await service.extractMetadata(bytes, '7.cbz'); + + expect(result.coverImageBytes, isNotNull); + expect(result.warnings, contains('No ComicInfo.xml found in CBZ archive')); + }); }); group('CBR extraction', () { - test( - 'handles 8.cbr RAR v4 gracefully (returns warning, no crash)', - () async { - final bytes = loadTestFile('8.cbr'); - if (bytes == null) return; - final result = await service.extractMetadata(bytes, '8.cbr'); - - expect(result.warnings, isNotEmpty); - }, - ); + test('handles 8.cbr RAR v4 gracefully (returns warning, no crash)', () async { + final bytes = loadTestFile('8.cbr'); + if (bytes == null) return; + final result = await service.extractMetadata(bytes, '8.cbr'); + + expect(result.warnings, isNotEmpty); + }); }); group('TXT extraction', () { test('parses "Author - Title" filename pattern', () async { final bytes = Uint8List.fromList(utf8.encode('Some book content.')); - final result = await service.extractMetadata( - bytes, - 'Author Name - Book Title.txt', - ); + final result = await service.extractMetadata(bytes, 'Author Name - Book Title.txt'); expect(result.title, 'Book Title'); expect(result.authors, ['Author Name']); @@ -158,10 +131,7 @@ void main() { test('preserves multiple hyphens in title', () async { final bytes = Uint8List.fromList(utf8.encode('content')); - final result = await service.extractMetadata( - bytes, - 'Author - Part 1 - The Beginning.txt', - ); + final result = await service.extractMetadata(bytes, 'Author - Part 1 - The Beginning.txt'); expect(result.title, 'Part 1 - The Beginning'); expect(result.authors, ['Author']); @@ -172,10 +142,7 @@ void main() { final result = await service.extractMetadata(bytes, 'JustATitle.txt'); expect(result.title, 'JustATitle'); - expect( - result.warnings, - contains('Could not detect author from filename'), - ); + expect(result.warnings, contains('Could not detect author from filename')); }); test('estimates page count from content length', () async { @@ -223,23 +190,9 @@ void main() { final corruptedBytes = Uint8List.fromList([0, 1, 2, 3, 4, 5]); // Should not throw for any supported format - for (final filename in [ - 'bad.epub', - 'bad.mobi', - 'bad.azw3', - 'bad.cbz', - 'bad.cbr', - 'bad.pdf', - ]) { - final result = await service.extractMetadata( - corruptedBytes, - filename, - ); - expect( - result.warnings, - isNotEmpty, - reason: 'Expected warnings for $filename', - ); + for (final filename in ['bad.epub', 'bad.mobi', 'bad.azw3', 'bad.cbz', 'bad.cbr', 'bad.pdf']) { + final result = await service.extractMetadata(corruptedBytes, filename); + expect(result.warnings, isNotEmpty, reason: 'Expected warnings for $filename'); } }); }); diff --git a/app/test/utils/search_query_parser_test.dart b/app/test/utils/search_query_parser_test.dart index b610f4e..5d7a25c 100644 --- a/app/test/utils/search_query_parser_test.dart +++ b/app/test/utils/search_query_parser_test.dart @@ -126,9 +126,7 @@ void main() { }); test('should parse explicit AND operator', () { - final result = SearchQueryParser.parse( - 'author:tolkien AND format:epub', - ); + final result = SearchQueryParser.parse('author:tolkien AND format:epub'); expect(result.filters.length, 2); expect(result.operators.length, 1); @@ -136,9 +134,7 @@ void main() { }); test('should parse OR operator', () { - final result = SearchQueryParser.parse( - 'author:tolkien OR author:lewis', - ); + final result = SearchQueryParser.parse('author:tolkien OR author:lewis'); expect(result.filters.length, 2); expect(result.operators.length, 1); @@ -177,10 +173,7 @@ void main() { test('should provide status suggestions', () { expect(SearchQueryParser.statusSuggestions, contains('status:reading')); - expect( - SearchQueryParser.statusSuggestions, - contains('status:finished'), - ); + expect(SearchQueryParser.statusSuggestions, contains('status:finished')); expect(SearchQueryParser.statusSuggestions, contains('status:unread')); }); @@ -200,13 +193,7 @@ void main() { test('isNotEmpty should be true when filters exist', () { const query = SearchQuery( - filters: [ - SearchFilter( - field: SearchField.author, - operator: SearchOperator.contains, - value: 'tolkien', - ), - ], + filters: [SearchFilter(field: SearchField.author, operator: SearchOperator.contains, value: 'tolkien')], ); expect(query.isEmpty, false); expect(query.isNotEmpty, true); @@ -214,13 +201,7 @@ void main() { test('isNotEmpty should be true when notFilters exist', () { const query = SearchQuery( - notFilters: [ - SearchFilter( - field: SearchField.status, - operator: SearchOperator.equals, - value: 'finished', - ), - ], + notFilters: [SearchFilter(field: SearchField.status, operator: SearchOperator.equals, value: 'finished')], ); expect(query.isEmpty, false); expect(query.isNotEmpty, true); diff --git a/app/test/widgets/book/book_annotations_test.dart b/app/test/widgets/book/book_annotations_test.dart index 3824b18..9b07018 100644 --- a/app/test/widgets/book/book_annotations_test.dart +++ b/app/test/widgets/book/book_annotations_test.dart @@ -31,8 +31,7 @@ void main() { Annotation( id: 'ann-3', bookId: 'book-1', - selectedText: - 'The journey of a thousand miles begins with a single step.', + selectedText: 'The journey of a thousand miles begins with a single step.', color: HighlightColor.green, location: const BookLocation(chapter: 2, pageNumber: 30), note: 'Great motivational quote', @@ -85,9 +84,7 @@ void main() { expect(find.byType(AnnotationCard), findsNWidgets(4)); }); - testWidgets('shows empty state when annotations list is empty', ( - tester, - ) async { + testWidgets('shows empty state when annotations list is empty', (tester) async { await tester.pumpWidget(buildAnnotations(annotations: [])); expect(find.text('No annotations yet'), findsOneWidget); @@ -163,16 +160,12 @@ void main() { await tester.pumpWidget(buildAnnotations()); // Newest first: ann-3 (Jul), ann-1 (Jun), ann-2 (May), ann-4 (Apr) - final items = tester.widgetList( - find.byType(AnnotationCard), - ); + final items = tester.widgetList(find.byType(AnnotationCard)); expect(items.first.annotation.id, 'ann-3'); expect(items.last.annotation.id, 'ann-4'); }); - testWidgets('selecting by position reorders by page number', ( - tester, - ) async { + testWidgets('selecting by position reorders by page number', (tester) async { await tester.pumpWidget(buildAnnotations()); await tester.tap(find.byIcon(Icons.sort)); @@ -181,16 +174,12 @@ void main() { await tester.pumpAndSettle(); // By page: ann-1 (p10), ann-3 (p30), ann-2 (p55), ann-4 (p88) - final items = tester.widgetList( - find.byType(AnnotationCard), - ); + final items = tester.widgetList(find.byType(AnnotationCard)); expect(items.first.annotation.id, 'ann-1'); expect(items.last.annotation.id, 'ann-4'); }); - testWidgets('selecting by color reorders by color enum index', ( - tester, - ) async { + testWidgets('selecting by color reorders by color enum index', (tester) async { await tester.pumpWidget(buildAnnotations()); await tester.tap(find.byIcon(Icons.sort)); @@ -199,9 +188,7 @@ void main() { await tester.pumpAndSettle(); // By color index: yellow(0), green(1), blue(2), pink(3) - final items = tester.widgetList( - find.byType(AnnotationCard), - ); + final items = tester.widgetList(find.byType(AnnotationCard)); expect(items.first.annotation.color, HighlightColor.yellow); expect(items.last.annotation.color, HighlightColor.pink); }); @@ -210,9 +197,7 @@ void main() { group('callbacks', () { testWidgets('tap calls onAnnotationTap', (tester) async { Annotation? tappedAnnotation; - await tester.pumpWidget( - buildAnnotations(onAnnotationTap: (a) => tappedAnnotation = a), - ); + await tester.pumpWidget(buildAnnotations(onAnnotationTap: (a) => tappedAnnotation = a)); await tester.tap(find.byType(AnnotationCard).first); await tester.pump(); @@ -222,9 +207,7 @@ void main() { testWidgets('long press calls onAnnotationActions', (tester) async { Annotation? actionAnnotation; - await tester.pumpWidget( - buildAnnotations(onAnnotationActions: (a) => actionAnnotation = a), - ); + await tester.pumpWidget(buildAnnotations(onAnnotationActions: (a) => actionAnnotation = a)); await tester.longPress(find.byType(AnnotationCard).first); await tester.pump(); @@ -235,17 +218,13 @@ void main() { group('responsive', () { testWidgets('desktop layout shows action menu on items', (tester) async { - await tester.pumpWidget( - buildAnnotations(screenSize: const Size(1200, 800)), - ); + await tester.pumpWidget(buildAnnotations(screenSize: const Size(1200, 800))); expect(find.byIcon(Icons.more_vert), findsAtLeastNWidgets(1)); }); testWidgets('mobile layout hides action menu on items', (tester) async { - await tester.pumpWidget( - buildAnnotations(screenSize: const Size(400, 800)), - ); + await tester.pumpWidget(buildAnnotations(screenSize: const Size(400, 800))); expect(find.byIcon(Icons.more_vert), findsNothing); }); diff --git a/app/test/widgets/book/book_bookmarks_test.dart b/app/test/widgets/book/book_bookmarks_test.dart index f88e5a1..f115c9f 100644 --- a/app/test/widgets/book/book_bookmarks_test.dart +++ b/app/test/widgets/book/book_bookmarks_test.dart @@ -78,9 +78,7 @@ void main() { expect(find.byType(BookmarkListItem), findsNWidgets(3)); }); - testWidgets('shows empty state when bookmarks list is empty', ( - tester, - ) async { + testWidgets('shows empty state when bookmarks list is empty', (tester) async { await tester.pumpWidget(buildBookmarks(bookmarks: [])); expect(find.text('No bookmarks yet'), findsOneWidget); @@ -108,9 +106,7 @@ void main() { expect(find.byType(BookmarkListItem), findsOneWidget); }); - testWidgets('shows no results when search has no matches', ( - tester, - ) async { + testWidgets('shows no results when search has no matches', (tester) async { await tester.pumpWidget(buildBookmarks()); await tester.enterText(find.byType(TextField), 'zzzznonexistent'); @@ -135,9 +131,7 @@ void main() { }); group('sorting', () { - testWidgets('tapping sort button opens popup menu with 3 options', ( - tester, - ) async { + testWidgets('tapping sort button opens popup menu with 3 options', (tester) async { await tester.pumpWidget(buildBookmarks()); await tester.tap(find.byIcon(Icons.sort)); @@ -156,10 +150,7 @@ void main() { // The check icon next to "Newest first" should be visible (primary color) // while others should be transparent - final newestItem = find.ancestor( - of: find.text('Newest first'), - matching: find.byType(Row), - ); + final newestItem = find.ancestor(of: find.text('Newest first'), matching: find.byType(Row)); expect(newestItem, findsOneWidget); }); @@ -167,9 +158,7 @@ void main() { await tester.pumpWidget(buildBookmarks()); // Default: newest first — bm-3 (5h ago), bm-1 (1d ago), bm-2 (3d ago) - var items = tester.widgetList( - find.byType(BookmarkListItem), - ); + var items = tester.widgetList(find.byType(BookmarkListItem)); expect(items.first.bookmark.id, 'bm-3'); // Select "Oldest first" @@ -179,9 +168,7 @@ void main() { await tester.pumpAndSettle(); // Now: bm-2 (3d ago), bm-1 (1d ago), bm-3 (5h ago) - items = tester.widgetList( - find.byType(BookmarkListItem), - ); + items = tester.widgetList(find.byType(BookmarkListItem)); expect(items.first.bookmark.id, 'bm-2'); }); @@ -194,22 +181,16 @@ void main() { await tester.pumpAndSettle(); // By position: bm-3 (0.1), bm-1 (0.3), bm-2 (0.6) - final items = tester.widgetList( - find.byType(BookmarkListItem), - ); + final items = tester.widgetList(find.byType(BookmarkListItem)); expect(items.first.bookmark.id, 'bm-3'); expect(items.last.bookmark.id, 'bm-2'); }); }); group('callbacks', () { - testWidgets('long press on bookmark calls onBookmarkActions', ( - tester, - ) async { + testWidgets('long press on bookmark calls onBookmarkActions', (tester) async { Bookmark? actionBookmark; - await tester.pumpWidget( - buildBookmarks(onBookmarkActions: (b) => actionBookmark = b), - ); + await tester.pumpWidget(buildBookmarks(onBookmarkActions: (b) => actionBookmark = b)); await tester.longPress(find.byType(BookmarkListItem).first); await tester.pump(); @@ -221,17 +202,13 @@ void main() { group('responsive', () { testWidgets('desktop layout shows action menu on items', (tester) async { - await tester.pumpWidget( - buildBookmarks(screenSize: const Size(1200, 800)), - ); + await tester.pumpWidget(buildBookmarks(screenSize: const Size(1200, 800))); expect(find.byIcon(Icons.more_vert), findsNWidgets(3)); }); testWidgets('mobile layout hides action menu on items', (tester) async { - await tester.pumpWidget( - buildBookmarks(screenSize: const Size(400, 800)), - ); + await tester.pumpWidget(buildBookmarks(screenSize: const Size(400, 800))); expect(find.byIcon(Icons.more_vert), findsNothing); }); diff --git a/app/test/widgets/book/book_notes_test.dart b/app/test/widgets/book/book_notes_test.dart index 23c653d..7d75cd0 100644 --- a/app/test/widgets/book/book_notes_test.dart +++ b/app/test/widgets/book/book_notes_test.dart @@ -33,8 +33,7 @@ void main() { id: 'note-3', bookId: 'book-1', title: 'Zen of Python', - content: - 'Beautiful is better than ugly. Explicit is better than implicit.', + content: 'Beautiful is better than ugly. Explicit is better than implicit.', tags: ['philosophy'], createdAt: DateTime(2025, 7, 20), ), @@ -200,9 +199,7 @@ void main() { testWidgets('long press calls onNoteActions', (tester) async { Note? actionNote; - await tester.pumpWidget( - buildNotes(onNoteActions: (n) => actionNote = n), - ); + await tester.pumpWidget(buildNotes(onNoteActions: (n) => actionNote = n)); await tester.longPress(find.byType(NoteCard).first); await tester.pump(); @@ -218,9 +215,7 @@ void main() { expect(find.text('Add note'), findsOneWidget); }); - testWidgets('mobile does not show add note button in header', ( - tester, - ) async { + testWidgets('mobile does not show add note button in header', (tester) async { await tester.pumpWidget(buildNotes(screenSize: const Size(400, 800))); expect(find.text('Add note'), findsNothing); diff --git a/app/test/widgets/book_details/annotation_card_test.dart b/app/test/widgets/book_details/annotation_card_test.dart index 0d59b65..8961b6d 100644 --- a/app/test/widgets/book_details/annotation_card_test.dart +++ b/app/test/widgets/book_details/annotation_card_test.dart @@ -25,11 +25,7 @@ void main() { bookId: 'book-1', selectedText: 'To be or not to be, that is the question.', color: HighlightColor.pink, - location: const BookLocation( - chapter: 1, - chapterTitle: 'Act III', - pageNumber: 12, - ), + location: const BookLocation(chapter: 1, chapterTitle: 'Act III', pageNumber: 12), createdAt: DateTime(2025, 3, 10), ); }); @@ -52,15 +48,10 @@ void main() { group('AnnotationCard', () { group('rendering', () { - testWidgets('displays highlight text in italic with quotes', ( - tester, - ) async { + testWidgets('displays highlight text in italic with quotes', (tester) async { await tester.pumpWidget(buildCard()); - expect( - find.text('"The quick brown fox jumps over the lazy dog."'), - findsOneWidget, - ); + expect(find.text('"The quick brown fox jumps over the lazy dog."'), findsOneWidget); }); testWidgets('displays location', (tester) async { @@ -75,29 +66,19 @@ void main() { expect(find.text('Jun 15, 2025'), findsOneWidget); }); - testWidgets('shows note section when annotation has a note', ( - tester, - ) async { + testWidgets('shows note section when annotation has a note', (tester) async { await tester.pumpWidget(buildCard()); - expect( - find.text('A classic pangram used in typography.'), - findsOneWidget, - ); + expect(find.text('A classic pangram used in typography.'), findsOneWidget); }); testWidgets('hides note section when no note', (tester) async { await tester.pumpWidget(buildCard(annotation: annotationWithoutNote)); - expect( - find.text('A classic pangram used in typography.'), - findsNothing, - ); + expect(find.text('A classic pangram used in typography.'), findsNothing); }); - testWidgets('shows colored left border matching highlight color', ( - tester, - ) async { + testWidgets('shows colored left border matching highlight color', (tester) async { await tester.pumpWidget(buildCard()); // Find the container with the left border decoration @@ -114,25 +95,19 @@ void main() { expect(container, findsOneWidget); }); - testWidgets('shows action menu when showActionMenu is true', ( - tester, - ) async { + testWidgets('shows action menu when showActionMenu is true', (tester) async { await tester.pumpWidget(buildCard(showActionMenu: true)); expect(find.byIcon(Icons.more_vert), findsOneWidget); }); - testWidgets('hides action menu when showActionMenu is false', ( - tester, - ) async { + testWidgets('hides action menu when showActionMenu is false', (tester) async { await tester.pumpWidget(buildCard(showActionMenu: false)); expect(find.byIcon(Icons.more_vert), findsNothing); }); - testWidgets('displays location with chapter title when present', ( - tester, - ) async { + testWidgets('displays location with chapter title when present', (tester) async { await tester.pumpWidget(buildCard(annotation: annotationWithoutNote)); expect(find.text('Ch. 1, p. 12'), findsOneWidget); @@ -152,9 +127,7 @@ void main() { testWidgets('long press calls onLongPress', (tester) async { var longPressed = false; - await tester.pumpWidget( - buildCard(onLongPress: () => longPressed = true), - ); + await tester.pumpWidget(buildCard(onLongPress: () => longPressed = true)); await tester.longPress(find.byType(AnnotationCard)); await tester.pump(); diff --git a/app/test/widgets/book_details/book_action_buttons_test.dart b/app/test/widgets/book_details/book_action_buttons_test.dart index 0786045..b2692b2 100644 --- a/app/test/widgets/book_details/book_action_buttons_test.dart +++ b/app/test/widgets/book_details/book_action_buttons_test.dart @@ -19,11 +19,7 @@ void main() { child: BookActionButtons( book: book ?? - buildTestBook( - fileFormat: BookFormat.epub, - currentPosition: 0.5, - readingStatus: ReadingStatus.inProgress, - ), + buildTestBook(fileFormat: BookFormat.epub, currentPosition: 0.5, readingStatus: ReadingStatus.inProgress), onContinueReading: onContinueReading, onUpdateProgress: onUpdateProgress, onToggleFavorite: onToggleFavorite, @@ -34,63 +30,33 @@ void main() { } group('digital book', () { - testWidgets('shows Continue button when book has progress', ( - tester, - ) async { - await tester.pumpWidget( - buildWidget( - book: buildTestBook( - currentPosition: 0.5, - fileFormat: BookFormat.epub, - ), - ), - ); + testWidgets('shows Continue button when book has progress', (tester) async { + await tester.pumpWidget(buildWidget(book: buildTestBook(currentPosition: 0.5, fileFormat: BookFormat.epub))); expect(find.text('Continue'), findsOneWidget); expect(find.byIcon(Icons.play_arrow), findsOneWidget); }); - testWidgets('shows Read button when book has no progress (mobile)', ( - tester, - ) async { - await tester.pumpWidget( - buildWidget( - book: buildTestBook( - currentPosition: 0.0, - fileFormat: BookFormat.epub, - ), - ), - ); + testWidgets('shows Read button when book has no progress (mobile)', (tester) async { + await tester.pumpWidget(buildWidget(book: buildTestBook(currentPosition: 0.0, fileFormat: BookFormat.epub))); expect(find.text('Read'), findsOneWidget); expect(find.byIcon(Icons.menu_book), findsOneWidget); }); - testWidgets( - 'shows Start reading button when book has no progress (desktop)', - (tester) async { - await tester.pumpWidget( - buildWidget( - book: buildTestBook( - currentPosition: 0.0, - fileFormat: BookFormat.epub, - ), - isDesktop: true, - ), - ); - - expect(find.text('Start reading'), findsOneWidget); - }, - ); + testWidgets('shows Start reading button when book has no progress (desktop)', (tester) async { + await tester.pumpWidget( + buildWidget(book: buildTestBook(currentPosition: 0.0, fileFormat: BookFormat.epub), isDesktop: true), + ); + + expect(find.text('Start reading'), findsOneWidget); + }); testWidgets('calls onContinueReading when tapped', (tester) async { var called = false; await tester.pumpWidget( buildWidget( - book: buildTestBook( - currentPosition: 0.5, - fileFormat: BookFormat.epub, - ), + book: buildTestBook(currentPosition: 0.5, fileFormat: BookFormat.epub), onContinueReading: () => called = true, ), ); @@ -102,9 +68,7 @@ void main() { group('physical book', () { testWidgets('shows Update progress button', (tester) async { - await tester.pumpWidget( - buildWidget(book: buildTestBook(isPhysical: true)), - ); + await tester.pumpWidget(buildWidget(book: buildTestBook(isPhysical: true))); expect(find.text('Update progress'), findsOneWidget); expect(find.byIcon(Icons.edit_note), findsOneWidget); @@ -113,10 +77,7 @@ void main() { testWidgets('calls onUpdateProgress when tapped', (tester) async { var called = false; await tester.pumpWidget( - buildWidget( - book: buildTestBook(isPhysical: true), - onUpdateProgress: () => called = true, - ), + buildWidget(book: buildTestBook(isPhysical: true), onUpdateProgress: () => called = true), ); await tester.tap(find.text('Update progress')); @@ -126,21 +87,13 @@ void main() { group('favorite button', () { testWidgets('shows outlined heart when not favorite', (tester) async { - await tester.pumpWidget( - buildWidget( - book: buildTestBook(isFavorite: false, fileFormat: BookFormat.epub), - ), - ); + await tester.pumpWidget(buildWidget(book: buildTestBook(isFavorite: false, fileFormat: BookFormat.epub))); expect(find.byIcon(Icons.favorite_border), findsOneWidget); }); testWidgets('shows filled heart when favorite', (tester) async { - await tester.pumpWidget( - buildWidget( - book: buildTestBook(isFavorite: true, fileFormat: BookFormat.epub), - ), - ); + await tester.pumpWidget(buildWidget(book: buildTestBook(isFavorite: true, fileFormat: BookFormat.epub))); expect(find.byIcon(Icons.favorite), findsOneWidget); }); @@ -166,9 +119,7 @@ void main() { group('edit button', () { testWidgets('shows edit icon', (tester) async { - await tester.pumpWidget( - buildWidget(book: buildTestBook(fileFormat: BookFormat.epub)), - ); + await tester.pumpWidget(buildWidget(book: buildTestBook(fileFormat: BookFormat.epub))); expect(find.byIcon(Icons.edit_outlined), findsOneWidget); }); @@ -182,10 +133,7 @@ void main() { ), ); - final editButton = find.ancestor( - of: find.byIcon(Icons.edit_outlined), - matching: find.byType(OutlinedButton), - ); + final editButton = find.ancestor(of: find.byIcon(Icons.edit_outlined), matching: find.byType(OutlinedButton)); await tester.tap(editButton); expect(called, true); }); diff --git a/app/test/widgets/book_details/book_progress_bar_test.dart b/app/test/widgets/book_details/book_progress_bar_test.dart index 6868b31..9056df2 100644 --- a/app/test/widgets/book_details/book_progress_bar_test.dart +++ b/app/test/widgets/book_details/book_progress_bar_test.dart @@ -38,9 +38,7 @@ void main() { }); testWidgets('shows page numbers when provided', (tester) async { - await tester.pumpWidget( - buildWidget(progress: 0.5, currentPage: 150, totalPages: 300), - ); + await tester.pumpWidget(buildWidget(progress: 0.5, currentPage: 150, totalPages: 300)); expect(find.text('150 / 300 (50%)'), findsOneWidget); }); @@ -70,9 +68,7 @@ void main() { }); group('onTap', () { - testWidgets('wraps in GestureDetector when onTap provided', ( - tester, - ) async { + testWidgets('wraps in GestureDetector when onTap provided', (tester) async { var called = false; await tester.pumpWidget(buildWidget(onTap: () => called = true)); @@ -82,9 +78,7 @@ void main() { expect(called, true); }); - testWidgets('does not wrap in GestureDetector when onTap is null', ( - tester, - ) async { + testWidgets('does not wrap in GestureDetector when onTap is null', (tester) async { await tester.pumpWidget(buildWidget()); expect(find.byType(GestureDetector), findsNothing); diff --git a/app/test/widgets/book_details/bookmark_list_item_test.dart b/app/test/widgets/book_details/bookmark_list_item_test.dart index 6043aab..0a41c97 100644 --- a/app/test/widgets/book_details/bookmark_list_item_test.dart +++ b/app/test/widgets/book_details/bookmark_list_item_test.dart @@ -31,12 +31,7 @@ void main() { ); }); - Widget buildItem({ - Bookmark? bookmark, - bool showActionMenu = true, - VoidCallback? onTap, - VoidCallback? onLongPress, - }) { + Widget buildItem({Bookmark? bookmark, bool showActionMenu = true, VoidCallback? onTap, VoidCallback? onLongPress}) { return createTestApp( child: BookmarkListItem( bookmark: bookmark ?? bookmarkWithNote, @@ -65,10 +60,7 @@ void main() { testWidgets('shows note text when bookmark has a note', (tester) async { await tester.pumpWidget(buildItem()); - expect( - find.text('This is an important passage to remember.'), - findsOneWidget, - ); + expect(find.text('This is an important passage to remember.'), findsOneWidget); }); testWidgets('hides note section when no note', (tester) async { @@ -91,25 +83,19 @@ void main() { expect(dot, findsOneWidget); }); - testWidgets('shows action menu when showActionMenu is true', ( - tester, - ) async { + testWidgets('shows action menu when showActionMenu is true', (tester) async { await tester.pumpWidget(buildItem(showActionMenu: true)); expect(find.byIcon(Icons.more_vert), findsOneWidget); }); - testWidgets('hides action menu when showActionMenu is false', ( - tester, - ) async { + testWidgets('hides action menu when showActionMenu is false', (tester) async { await tester.pumpWidget(buildItem(showActionMenu: false)); expect(find.byIcon(Icons.more_vert), findsNothing); }); - testWidgets('displays page-only location when no chapter', ( - tester, - ) async { + testWidgets('displays page-only location when no chapter', (tester) async { await tester.pumpWidget(buildItem(bookmark: bookmarkWithoutNote)); expect(find.text('Page 100'), findsOneWidget); @@ -119,9 +105,7 @@ void main() { group('callbacks', () { testWidgets('long press calls onLongPress', (tester) async { var longPressed = false; - await tester.pumpWidget( - buildItem(onLongPress: () => longPressed = true), - ); + await tester.pumpWidget(buildItem(onLongPress: () => longPressed = true)); await tester.longPress(find.byType(BookmarkListItem)); await tester.pump(); diff --git a/app/test/widgets/book_details/empty_annotations_state_test.dart b/app/test/widgets/book_details/empty_annotations_state_test.dart index 055204b..aee3b65 100644 --- a/app/test/widgets/book_details/empty_annotations_state_test.dart +++ b/app/test/widgets/book_details/empty_annotations_state_test.dart @@ -6,15 +6,9 @@ import '../../helpers/test_helpers.dart'; void main() { group('EmptyAnnotationsState', () { - Widget buildWidget({ - bool isPhysical = false, - VoidCallback? onAddAnnotation, - }) { + Widget buildWidget({bool isPhysical = false, VoidCallback? onAddAnnotation}) { return createTestApp( - child: EmptyAnnotationsState( - isPhysical: isPhysical, - onAddAnnotation: onAddAnnotation, - ), + child: EmptyAnnotationsState(isPhysical: isPhysical, onAddAnnotation: onAddAnnotation), ); } @@ -36,10 +30,7 @@ void main() { testWidgets('shows digital book description', (tester) async { await tester.pumpWidget(buildWidget()); - expect( - find.text('Highlight text while reading to create annotations.'), - findsOneWidget, - ); + expect(find.text('Highlight text while reading to create annotations.'), findsOneWidget); }); testWidgets('does not show add annotation button', (tester) async { @@ -53,12 +44,7 @@ void main() { testWidgets('shows physical book description', (tester) async { await tester.pumpWidget(buildWidget(isPhysical: true)); - expect( - find.text( - "Add passages you've highlighted or underlined in your book.", - ), - findsOneWidget, - ); + expect(find.text("Add passages you've highlighted or underlined in your book."), findsOneWidget); }); testWidgets('shows add annotation button', (tester) async { @@ -70,9 +56,7 @@ void main() { testWidgets('add button calls onAddAnnotation', (tester) async { var called = false; - await tester.pumpWidget( - buildWidget(isPhysical: true, onAddAnnotation: () => called = true), - ); + await tester.pumpWidget(buildWidget(isPhysical: true, onAddAnnotation: () => called = true)); await tester.tap(find.text('Add annotation')); expect(called, true); diff --git a/app/test/widgets/book_details/empty_bookmarks_state_test.dart b/app/test/widgets/book_details/empty_bookmarks_state_test.dart index 87d424c..9638ac6 100644 --- a/app/test/widgets/book_details/empty_bookmarks_state_test.dart +++ b/app/test/widgets/book_details/empty_bookmarks_state_test.dart @@ -8,10 +8,7 @@ void main() { group('EmptyBookmarksState', () { Widget buildWidget({bool isPhysical = false, VoidCallback? onAddBookmark}) { return createTestApp( - child: EmptyBookmarksState( - isPhysical: isPhysical, - onAddBookmark: onAddBookmark, - ), + child: EmptyBookmarksState(isPhysical: isPhysical, onAddBookmark: onAddBookmark), ); } @@ -33,10 +30,7 @@ void main() { testWidgets('shows digital book description', (tester) async { await tester.pumpWidget(buildWidget()); - expect( - find.text('Bookmarks you create while reading will appear here.'), - findsOneWidget, - ); + expect(find.text('Bookmarks you create while reading will appear here.'), findsOneWidget); }); testWidgets('does not show add bookmark button', (tester) async { @@ -50,10 +44,7 @@ void main() { testWidgets('shows physical book description', (tester) async { await tester.pumpWidget(buildWidget(isPhysical: true)); - expect( - find.text('Save pages you want to return to later.'), - findsOneWidget, - ); + expect(find.text('Save pages you want to return to later.'), findsOneWidget); }); testWidgets('shows add bookmark button', (tester) async { @@ -65,9 +56,7 @@ void main() { testWidgets('add button calls onAddBookmark', (tester) async { var called = false; - await tester.pumpWidget( - buildWidget(isPhysical: true, onAddBookmark: () => called = true), - ); + await tester.pumpWidget(buildWidget(isPhysical: true, onAddBookmark: () => called = true)); await tester.tap(find.text('Add bookmark')); expect(called, true); diff --git a/app/test/widgets/book_details/note_card_test.dart b/app/test/widgets/book_details/note_card_test.dart index 3b27846..06aa2fe 100644 --- a/app/test/widgets/book_details/note_card_test.dart +++ b/app/test/widgets/book_details/note_card_test.dart @@ -34,12 +34,7 @@ void main() { ); }); - Widget buildCard({ - Note? note, - bool showActionMenu = true, - VoidCallback? onTap, - VoidCallback? onLongPress, - }) { + Widget buildCard({Note? note, bool showActionMenu = true, VoidCallback? onTap, VoidCallback? onLongPress}) { return createTestApp( child: NoteCard( note: note ?? noteWithLocation, @@ -84,17 +79,13 @@ void main() { expect(find.byIcon(Icons.location_on_outlined), findsNothing); }); - testWidgets('shows action menu when showActionMenu is true', ( - tester, - ) async { + testWidgets('shows action menu when showActionMenu is true', (tester) async { await tester.pumpWidget(buildCard(showActionMenu: true)); expect(find.byIcon(Icons.more_vert), findsOneWidget); }); - testWidgets('hides action menu when showActionMenu is false', ( - tester, - ) async { + testWidgets('hides action menu when showActionMenu is false', (tester) async { await tester.pumpWidget(buildCard(showActionMenu: false)); expect(find.byIcon(Icons.more_vert), findsNothing); @@ -132,9 +123,7 @@ void main() { testWidgets('long press calls onLongPress', (tester) async { var longPressed = false; - await tester.pumpWidget( - buildCard(onLongPress: () => longPressed = true), - ); + await tester.pumpWidget(buildCard(onLongPress: () => longPressed = true)); await tester.longPress(find.byType(NoteCard)); await tester.pump(); diff --git a/app/test/widgets/library/book_card_test.dart b/app/test/widgets/library/book_card_test.dart index bb9a900..a9d4729 100644 --- a/app/test/widgets/library/book_card_test.dart +++ b/app/test/widgets/library/book_card_test.dart @@ -71,9 +71,7 @@ void main() { expect(find.byType(LinearProgressIndicator), findsOneWidget); }); - testWidgets('hides progress bar when showProgress is false', ( - tester, - ) async { + testWidgets('hides progress bar when showProgress is false', (tester) async { await tester.pumpWidget(buildCard(showProgress: false)); expect(find.byType(LinearProgressIndicator), findsNothing); }); @@ -94,16 +92,9 @@ void main() { expect(find.byIcon(Icons.favorite), findsOneWidget); }); - testWidgets('calls onToggleFavorite when favorite button tapped', ( - tester, - ) async { + testWidgets('calls onToggleFavorite when favorite button tapped', (tester) async { bool? tappedValue; - await tester.pumpWidget( - buildCard( - isFavorite: false, - onToggleFavorite: (current) => tappedValue = current, - ), - ); + await tester.pumpWidget(buildCard(isFavorite: false, onToggleFavorite: (current) => tappedValue = current)); // Find and tap the favorite button (InkWell wrapping the heart icon) final favoriteIcon = find.byIcon(Icons.favorite_border); diff --git a/app/test/widgets/library/book_grid_test.dart b/app/test/widgets/library/book_grid_test.dart index 3ddd596..a06a510 100644 --- a/app/test/widgets/library/book_grid_test.dart +++ b/app/test/widgets/library/book_grid_test.dart @@ -14,11 +14,7 @@ void main() { testBooks = createTestBooks(); }); - Widget buildGrid({ - List? books, - void Function(Book)? onBookTap, - Size screenSize = const Size(400, 800), - }) { + Widget buildGrid({List? books, void Function(Book)? onBookTap, Size screenSize = const Size(400, 800)}) { return createTestApp( dataStore: createTestDataStore(books: books ?? testBooks), child: BookGrid(books: books ?? testBooks, onBookTap: onBookTap), @@ -44,9 +40,7 @@ void main() { testWidgets('passes onBookTap to each card', (tester) async { Book? tappedBook; - await tester.pumpWidget( - buildGrid(onBookTap: (book) => tappedBook = book), - ); + await tester.pumpWidget(buildGrid(onBookTap: (book) => tappedBook = book)); await tester.pumpAndSettle(); // Tap the first card diff --git a/app/test/widgets/library/book_list_item_test.dart b/app/test/widgets/library/book_list_item_test.dart index 7f3c194..bbe597a 100644 --- a/app/test/widgets/library/book_list_item_test.dart +++ b/app/test/widgets/library/book_list_item_test.dart @@ -22,19 +22,9 @@ void main() { ); }); - Widget buildListItem({ - Book? book, - bool isFavorite = false, - VoidCallback? onTap, - bool showProgress = true, - }) { + Widget buildListItem({Book? book, bool isFavorite = false, VoidCallback? onTap, bool showProgress = true}) { return createTestApp( - child: BookListItem( - book: book ?? testBook, - isFavorite: isFavorite, - onTap: onTap, - showProgress: showProgress, - ), + child: BookListItem(book: book ?? testBook, isFavorite: isFavorite, onTap: onTap, showProgress: showProgress), ); } @@ -53,9 +43,7 @@ void main() { expect(find.text('EPUB'), findsOneWidget); }); - testWidgets('shows progress bar and label when progress > 0', ( - tester, - ) async { + testWidgets('shows progress bar and label when progress > 0', (tester) async { await tester.pumpWidget(buildListItem()); expect(find.byType(LinearProgressIndicator), findsOneWidget); expect(find.text('50%'), findsOneWidget); @@ -104,10 +92,7 @@ void main() { }); testWidgets('displays 100% progress for finished book', (tester) async { - final finishedBook = testBook.copyWith( - readingStatus: ReadingStatus.completed, - currentPosition: 1.0, - ); + final finishedBook = testBook.copyWith(readingStatus: ReadingStatus.completed, currentPosition: 1.0); await tester.pumpWidget(buildListItem(book: finishedBook)); expect(find.text('100%'), findsOneWidget); }); diff --git a/app/test/widgets/library/library_drawer_test.dart b/app/test/widgets/library/library_drawer_test.dart index edb24dc..c35e667 100644 --- a/app/test/widgets/library/library_drawer_test.dart +++ b/app/test/widgets/library/library_drawer_test.dart @@ -9,10 +9,8 @@ void main() { home: Scaffold( drawer: LibraryDrawer(currentPath: currentPath), body: Builder( - builder: (context) => ElevatedButton( - onPressed: () => Scaffold.of(context).openDrawer(), - child: const Text('Open Drawer'), - ), + builder: (context) => + ElevatedButton(onPressed: () => Scaffold.of(context).openDrawer(), child: const Text('Open Drawer')), ), ), ); @@ -56,9 +54,7 @@ void main() { await tester.pumpAndSettle(); // Books item should be selected (ListTile with selected: true) - final booksItem = tester.widget( - find.ancestor(of: find.text('Books'), matching: find.byType(ListTile)), - ); + final booksItem = tester.widget(find.ancestor(of: find.text('Books'), matching: find.byType(ListTile))); expect(booksItem.selected, true); }); @@ -67,24 +63,17 @@ void main() { await tester.tap(find.text('Open Drawer')); await tester.pumpAndSettle(); - final booksItem = tester.widget( - find.ancestor(of: find.text('Books'), matching: find.byType(ListTile)), - ); + final booksItem = tester.widget(find.ancestor(of: find.text('Books'), matching: find.byType(ListTile))); expect(booksItem.selected, true); }); - testWidgets('highlights Shelves when on /library/shelves path', ( - tester, - ) async { + testWidgets('highlights Shelves when on /library/shelves path', (tester) async { await tester.pumpWidget(buildDrawer(currentPath: '/library/shelves')); await tester.tap(find.text('Open Drawer')); await tester.pumpAndSettle(); final shelvesItem = tester.widget( - find.ancestor( - of: find.text('Shelves'), - matching: find.byType(ListTile), - ), + find.ancestor(of: find.text('Shelves'), matching: find.byType(ListTile)), ); expect(shelvesItem.selected, true); }); @@ -95,16 +84,11 @@ void main() { await tester.pumpAndSettle(); final shelvesItem = tester.widget( - find.ancestor( - of: find.text('Shelves'), - matching: find.byType(ListTile), - ), + find.ancestor(of: find.text('Shelves'), matching: find.byType(ListTile)), ); expect(shelvesItem.selected, false); - final notesItem = tester.widget( - find.ancestor(of: find.text('Notes'), matching: find.byType(ListTile)), - ); + final notesItem = tester.widget(find.ancestor(of: find.text('Notes'), matching: find.byType(ListTile))); expect(notesItem.selected, false); }); diff --git a/app/test/widgets/library/library_filter_chips_test.dart b/app/test/widgets/library/library_filter_chips_test.dart index 5ff8ae9..e59b778 100644 --- a/app/test/widgets/library/library_filter_chips_test.dart +++ b/app/test/widgets/library/library_filter_chips_test.dart @@ -14,10 +14,7 @@ void main() { }); Widget buildChips({LibraryProvider? provider}) { - return createTestApp( - libraryProvider: provider ?? libraryProvider, - child: const LibraryFilterChips(), - ); + return createTestApp(libraryProvider: provider ?? libraryProvider, child: const LibraryFilterChips()); } testWidgets('displays all five filter chips', (tester) async { @@ -33,15 +30,11 @@ void main() { testWidgets('"All" chip is selected by default', (tester) async { await tester.pumpWidget(buildChips()); - final allChip = tester.widget( - find.ancestor(of: find.text('All'), matching: find.byType(FilterChip)), - ); + final allChip = tester.widget(find.ancestor(of: find.text('All'), matching: find.byType(FilterChip))); expect(allChip.selected, true); }); - testWidgets('tapping "Reading" chip toggles reading filter', ( - tester, - ) async { + testWidgets('tapping "Reading" chip toggles reading filter', (tester) async { await tester.pumpWidget(buildChips()); await tester.tap(find.text('Reading')); @@ -51,9 +44,7 @@ void main() { expect(libraryProvider.isFilterActive(LibraryFilterType.all), false); }); - testWidgets('tapping "Favorites" chip toggles favorites filter', ( - tester, - ) async { + testWidgets('tapping "Favorites" chip toggles favorites filter', (tester) async { await tester.pumpWidget(buildChips()); await tester.tap(find.text('Favorites')); @@ -62,9 +53,7 @@ void main() { expect(libraryProvider.isFilterActive(LibraryFilterType.favorites), true); }); - testWidgets('tapping "Finished" chip toggles finished filter', ( - tester, - ) async { + testWidgets('tapping "Finished" chip toggles finished filter', (tester) async { await tester.pumpWidget(buildChips()); await tester.tap(find.text('Finished')); @@ -94,10 +83,7 @@ void main() { expect(libraryProvider.isFilterActive(LibraryFilterType.all), true); expect(libraryProvider.isFilterActive(LibraryFilterType.reading), false); - expect( - libraryProvider.isFilterActive(LibraryFilterType.favorites), - false, - ); + expect(libraryProvider.isFilterActive(LibraryFilterType.favorites), false); }); testWidgets('tapping a filter chip twice deactivates it', (tester) async { diff --git a/app/test/widgets/search/library_search_bar_test.dart b/app/test/widgets/search/library_search_bar_test.dart index aa65e4d..0bfffe7 100644 --- a/app/test/widgets/search/library_search_bar_test.dart +++ b/app/test/widgets/search/library_search_bar_test.dart @@ -42,9 +42,7 @@ void main() { testWidgets('calls onQueryChanged when text is entered', (tester) async { String? lastQuery; - await tester.pumpWidget( - buildSearchBar(onQueryChanged: (q) => lastQuery = q), - ); + await tester.pumpWidget(buildSearchBar(onQueryChanged: (q) => lastQuery = q)); await tester.enterText(find.byType(TextField), 'tolkien'); await tester.pump(); @@ -52,13 +50,9 @@ void main() { expect(lastQuery, 'tolkien'); }); - testWidgets('calls onFilterTap when filter button is tapped', ( - tester, - ) async { + testWidgets('calls onFilterTap when filter button is tapped', (tester) async { var filterTapped = false; - await tester.pumpWidget( - buildSearchBar(onFilterTap: () => filterTapped = true), - ); + await tester.pumpWidget(buildSearchBar(onFilterTap: () => filterTapped = true)); await tester.tap(find.byIcon(Icons.tune)); await tester.pump(); @@ -66,17 +60,13 @@ void main() { expect(filterTapped, true); }); - testWidgets('shows filter badge when activeFilterCount > 0', ( - tester, - ) async { + testWidgets('shows filter badge when activeFilterCount > 0', (tester) async { await tester.pumpWidget(buildSearchBar(activeFilterCount: 3)); expect(find.text('3'), findsOneWidget); }); - testWidgets('does not show filter badge when activeFilterCount is 0', ( - tester, - ) async { + testWidgets('does not show filter badge when activeFilterCount is 0', (tester) async { await tester.pumpWidget(buildSearchBar(activeFilterCount: 0)); // Badge number should not be present @@ -92,12 +82,7 @@ void main() { testWidgets('clears text when clear button is tapped', (tester) async { String? lastQuery; - await tester.pumpWidget( - buildSearchBar( - initialQuery: 'test', - onQueryChanged: (q) => lastQuery = q, - ), - ); + await tester.pumpWidget(buildSearchBar(initialQuery: 'test', onQueryChanged: (q) => lastQuery = q)); await tester.pump(); await tester.tap(find.byIcon(Icons.clear)); @@ -119,9 +104,7 @@ void main() { }); group('suggestions', () { - testWidgets('shows field suggestions when typing a field prefix', ( - tester, - ) async { + testWidgets('shows field suggestions when typing a field prefix', (tester) async { await tester.pumpWidget(buildSearchBar()); // Focus and type a field prefix @@ -135,9 +118,7 @@ void main() { expect(find.text('author:'), findsOneWidget); }); - testWidgets('shows status suggestions when typing status:', ( - tester, - ) async { + testWidgets('shows status suggestions when typing status:', (tester) async { await tester.pumpWidget(buildSearchBar()); await tester.tap(find.byType(TextField)); @@ -152,9 +133,7 @@ void main() { expect(find.text('status:unread'), findsOneWidget); }); - testWidgets('shows format suggestions when typing format:', ( - tester, - ) async { + testWidgets('shows format suggestions when typing format:', (tester) async { await tester.pumpWidget(buildSearchBar()); await tester.tap(find.byType(TextField)); @@ -170,9 +149,7 @@ void main() { testWidgets('applies suggestion when tapped', (tester) async { String? lastQuery; - await tester.pumpWidget( - buildSearchBar(onQueryChanged: (q) => lastQuery = q), - ); + await tester.pumpWidget(buildSearchBar(onQueryChanged: (q) => lastQuery = q)); await tester.tap(find.byType(TextField)); await tester.pump(); @@ -208,9 +185,7 @@ void main() { expect(find.text('author:'), findsNothing); }); - testWidgets('does not show suggestions for non-matching text', ( - tester, - ) async { + testWidgets('does not show suggestions for non-matching text', (tester) async { await tester.pumpWidget(buildSearchBar()); await tester.tap(find.byType(TextField)); @@ -224,9 +199,7 @@ void main() { expect(find.text('title:'), findsNothing); }); - testWidgets('shows multiple field suggestions for partial match', ( - tester, - ) async { + testWidgets('shows multiple field suggestions for partial match', (tester) async { await tester.pumpWidget(buildSearchBar()); await tester.tap(find.byType(TextField)); @@ -261,9 +234,7 @@ void main() { }); group('didUpdateWidget', () { - testWidgets('updates text when initialQuery changes and not focused', ( - tester, - ) async { + testWidgets('updates text when initialQuery changes and not focused', (tester) async { // Start with empty query await tester.pumpWidget(buildSearchBar(initialQuery: '')); diff --git a/app/test/widgets/shared/bottom_sheet_header_test.dart b/app/test/widgets/shared/bottom_sheet_header_test.dart index ee64ccd..574b957 100644 --- a/app/test/widgets/shared/bottom_sheet_header_test.dart +++ b/app/test/widgets/shared/bottom_sheet_header_test.dart @@ -48,13 +48,9 @@ void main() { expect(saved, isTrue); }); - testWidgets('save button is disabled when canSave is false', ( - tester, - ) async { + testWidgets('save button is disabled when canSave is false', (tester) async { var saved = false; - await tester.pumpWidget( - buildHeader(canSave: false, onSave: () => saved = true), - ); + await tester.pumpWidget(buildHeader(canSave: false, onSave: () => saved = true)); await tester.tap(find.text('Save')); expect(saved, isFalse); @@ -70,10 +66,7 @@ void main() { testWidgets('save button is a FilledButton', (tester) async { await tester.pumpWidget(buildHeader()); - final filledButton = find.ancestor( - of: find.text('Save'), - matching: find.byType(FilledButton), - ); + final filledButton = find.ancestor(of: find.text('Save'), matching: find.byType(FilledButton)); expect(filledButton, findsOneWidget); }); }); diff --git a/app/test/widgets/shared/empty_state_test.dart b/app/test/widgets/shared/empty_state_test.dart index aea0910..71c6c1f 100644 --- a/app/test/widgets/shared/empty_state_test.dart +++ b/app/test/widgets/shared/empty_state_test.dart @@ -13,21 +13,13 @@ void main() { }) { return MaterialApp( home: Scaffold( - body: EmptyState( - icon: icon, - title: title, - subtitle: subtitle, - action: action, - iconSize: iconSize, - ), + body: EmptyState(icon: icon, title: title, subtitle: subtitle, action: action, iconSize: iconSize), ), ); } testWidgets('displays icon', (tester) async { - await tester.pumpWidget( - buildEmptyState(icon: Icons.library_books_outlined), - ); + await tester.pumpWidget(buildEmptyState(icon: Icons.library_books_outlined)); expect(find.byIcon(Icons.library_books_outlined), findsOneWidget); }); @@ -37,9 +29,7 @@ void main() { }); testWidgets('displays subtitle when provided', (tester) async { - await tester.pumpWidget( - buildEmptyState(subtitle: 'Try adjusting your filters'), - ); + await tester.pumpWidget(buildEmptyState(subtitle: 'Try adjusting your filters')); expect(find.text('Try adjusting your filters'), findsOneWidget); }); @@ -52,10 +42,7 @@ void main() { testWidgets('displays action widget when provided', (tester) async { await tester.pumpWidget( buildEmptyState( - action: ElevatedButton( - onPressed: () {}, - child: const Text('Add books'), - ), + action: ElevatedButton(onPressed: () {}, child: const Text('Add books')), ), ); expect(find.text('Add books'), findsOneWidget); @@ -75,9 +62,7 @@ void main() { testWidgets('uses custom icon size', (tester) async { await tester.pumpWidget(buildEmptyState(iconSize: 100)); - final icon = tester.widget( - find.byIcon(Icons.library_books_outlined), - ); + final icon = tester.widget(find.byIcon(Icons.library_books_outlined)); expect(icon.size, 100); }); diff --git a/app/windows/flutter/generated_plugin_registrant.cc b/app/windows/flutter/generated_plugin_registrant.cc index 785a046..66aef1d 100644 --- a/app/windows/flutter/generated_plugin_registrant.cc +++ b/app/windows/flutter/generated_plugin_registrant.cc @@ -6,12 +6,18 @@ #include "generated_plugin_registrant.h" -#include +#include +#include #include +#include void RegisterPlugins(flutter::PluginRegistry* registry) { - AppLinksPluginCApiRegisterWithRegistrar( - registry->GetRegistrarForPlugin("AppLinksPluginCApi")); + DesktopWebviewWindowPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("DesktopWebviewWindowPlugin")); + FlutterSecureStorageWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin")); UrlLauncherWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("UrlLauncherWindows")); + WindowToFrontPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("WindowToFrontPlugin")); } diff --git a/app/windows/flutter/generated_plugins.cmake b/app/windows/flutter/generated_plugins.cmake index 8f8ee4f..59e47fd 100644 --- a/app/windows/flutter/generated_plugins.cmake +++ b/app/windows/flutter/generated_plugins.cmake @@ -3,8 +3,10 @@ # list(APPEND FLUTTER_PLUGIN_LIST - app_links + desktop_webview_window + flutter_secure_storage_windows url_launcher_windows + window_to_front ) list(APPEND FLUTTER_FFI_PLUGIN_LIST From a422987f0ed514a275a5bcc6df96a9a915ced3fb Mon Sep 17 00:00:00 2001 From: Eoic Date: Sat, 9 May 2026 16:46:01 +0300 Subject: [PATCH 03/16] fix: an issue where successfull Google login wouldn't redirect to library --- app/lib/auth/auth_repository.dart | 9 ++++++++- app/lib/main.dart | 3 +++ app/pubspec.lock | 2 +- app/pubspec.yaml | 2 ++ 4 files changed, 14 insertions(+), 2 deletions(-) diff --git a/app/lib/auth/auth_repository.dart b/app/lib/auth/auth_repository.dart index e8b09c9..f1042db 100644 --- a/app/lib/auth/auth_repository.dart +++ b/app/lib/auth/auth_repository.dart @@ -181,6 +181,13 @@ class AuthRepository { } String _webOAuthRedirectUri() { - return Uri.base.replace(path: '/auth/callback', query: '').toString(); + final base = Uri.base; + + return Uri( + scheme: base.scheme, + host: base.host, + port: base.hasPort ? base.port : null, + path: '/auth/callback', + ).toString(); } } diff --git a/app/lib/main.dart b/app/lib/main.dart index f488da0..b9a67db 100644 --- a/app/lib/main.dart +++ b/app/lib/main.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter_web_plugins/url_strategy.dart'; import 'package:papyrus/auth/auth_api_client.dart'; import 'package:papyrus/auth/auth_repository.dart'; import 'package:papyrus/auth/papyrus_api_config.dart'; @@ -17,6 +18,8 @@ import 'config/app_router.dart'; Future main() async { WidgetsFlutterBinding.ensureInitialized(); + usePathUrlStrategy(); + final prefs = await SharedPreferences.getInstance(); runApp(Papyrus(prefs: prefs)); } diff --git a/app/pubspec.lock b/app/pubspec.lock index 36dc6bf..c01d27a 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -308,7 +308,7 @@ packages: source: hosted version: "5.0.0" flutter_web_plugins: - dependency: transitive + dependency: "direct main" description: flutter source: sdk version: "0.0.0" diff --git a/app/pubspec.yaml b/app/pubspec.yaml index 24c71b8..7ba8930 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -9,6 +9,8 @@ environment: dependencies: flutter: sdk: flutter + flutter_web_plugins: + sdk: flutter cupertino_icons: ^1.0.8 go_router: ^16.2.1 From f45b1040191a577d9dcf02232dcd8382e3831f24 Mon Sep 17 00:00:00 2001 From: Eoic Date: Sat, 9 May 2026 17:24:52 +0300 Subject: [PATCH 04/16] feat: do not use webview for linux/windows Google oauth --- app/lib/auth/auth_repository.dart | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/app/lib/auth/auth_repository.dart b/app/lib/auth/auth_repository.dart index f1042db..c719cb9 100644 --- a/app/lib/auth/auth_repository.dart +++ b/app/lib/auth/auth_repository.dart @@ -9,6 +9,7 @@ import 'package:papyrus/platform/web_redirect.dart'; class AuthRepository { static const nativeOAuthRedirectUri = 'papyrus://auth/callback'; + static const desktopOAuthRedirectUri = 'http://localhost:43821/auth/callback'; final AuthApiClient apiClient; final TokenStore tokenStore; @@ -19,6 +20,14 @@ class AuthRepository { String? get accessToken => tokenStore.accessToken; + bool get _usesDesktopLoopbackOAuth { + if (kIsWeb) { + return false; + } + + return defaultTargetPlatform == TargetPlatform.linux || defaultTargetPlatform == TargetPlatform.windows; + } + Future bootstrap() async { final refreshToken = await tokenStore.readRefreshToken(); @@ -90,7 +99,12 @@ class AuthRepository { } Future signInWithGoogle({required String clientType, String? deviceLabel}) async { - final redirectUri = kIsWeb ? _webOAuthRedirectUri() : nativeOAuthRedirectUri; + final redirectUri = kIsWeb + ? _webOAuthRedirectUri() + : _usesDesktopLoopbackOAuth + ? desktopOAuthRedirectUri + : nativeOAuthRedirectUri; + final startUri = apiClient.googleOAuthStartUri(redirectUri); if (kIsWeb) { @@ -98,7 +112,12 @@ class AuthRepository { return null; } - final callbackUrl = await FlutterWebAuth2.authenticate(url: startUri.toString(), callbackUrlScheme: 'papyrus'); + final callbackUrl = await FlutterWebAuth2.authenticate( + url: startUri.toString(), + callbackUrlScheme: _usesDesktopLoopbackOAuth ? desktopOAuthRedirectUri : Uri.parse(nativeOAuthRedirectUri).scheme, + options: const FlutterWebAuth2Options(useWebview: false), + ); + final callbackUri = Uri.parse(callbackUrl); return completeGoogleSignIn(callbackUri, clientType: clientType, deviceLabel: deviceLabel); } From e623a2402771fa12460825fae9b8f9ea01516d7d Mon Sep 17 00:00:00 2001 From: Eoic Date: Sat, 9 May 2026 23:58:09 +0300 Subject: [PATCH 05/16] Wire Flutter books to PowerSync --- app/lib/auth/auth_api_client.dart | 10 + app/lib/auth/auth_models.dart | 11 + app/lib/auth/auth_repository.dart | 25 + app/lib/auth/papyrus_api_config.dart | 14 +- app/lib/data/data_store.dart | 31 + app/lib/main.dart | 49 +- .../papyrus_powersync_connector.dart | 66 + app/lib/powersync/papyrus_schema.dart | 34 + app/lib/powersync/powersync_book_mapper.dart | 280 + app/lib/powersync/powersync_service.dart | 142 + app/lib/services/book_import_service.dart | 6 +- .../services/book_import_service_stub.dart | 6 +- .../add_book/add_physical_book_sheet.dart | 3 +- app/pubspec.lock | 106 +- app/pubspec.yaml | 2 + app/test/auth/auth_api_client_test.dart | 48 + .../auth/auth_repository_powersync_test.dart | 86 + app/test/data/data_store_sync_test.dart | 57 + .../papyrus_powersync_connector_test.dart | 34 + .../powersync/powersync_book_mapper_test.dart | 62 + .../services/book_import_service_test.dart | 10 +- app/web/powersync_db.worker.js | 18082 ++++++++++++++++ app/web/sqlite3.wasm | Bin 0 -> 1148768 bytes 23 files changed, 19127 insertions(+), 37 deletions(-) create mode 100644 app/lib/powersync/papyrus_powersync_connector.dart create mode 100644 app/lib/powersync/papyrus_schema.dart create mode 100644 app/lib/powersync/powersync_book_mapper.dart create mode 100644 app/lib/powersync/powersync_service.dart create mode 100644 app/test/auth/auth_repository_powersync_test.dart create mode 100644 app/test/data/data_store_sync_test.dart create mode 100644 app/test/powersync/papyrus_powersync_connector_test.dart create mode 100644 app/test/powersync/powersync_book_mapper_test.dart create mode 100644 app/web/powersync_db.worker.js create mode 100644 app/web/sqlite3.wasm diff --git a/app/lib/auth/auth_api_client.dart b/app/lib/auth/auth_api_client.dart index ad33150..6463997 100644 --- a/app/lib/auth/auth_api_client.dart +++ b/app/lib/auth/auth_api_client.dart @@ -125,6 +125,16 @@ class AuthApiClient { return json['message'] as String? ?? 'If the email is registered, a verification link has been sent'; } + Future powerSyncToken(String accessToken) async { + final json = await _postJson(config.endpoint('/auth/powersync-token'), accessToken: accessToken); + + return PowerSyncToken.fromJson(json); + } + + Future uploadPowerSyncBatch(String accessToken, List> batch) async { + await _postJson(config.endpoint('/sync/powersync-upload'), accessToken: accessToken, body: {'batch': batch}); + } + Future> _getJson(Uri uri, {String? accessToken}) async { final response = await _httpClient.get(uri, headers: _headers(accessToken)); diff --git a/app/lib/auth/auth_models.dart b/app/lib/auth/auth_models.dart index 4a83969..63e851e 100644 --- a/app/lib/auth/auth_models.dart +++ b/app/lib/auth/auth_models.dart @@ -58,6 +58,17 @@ class AuthTokens { } } +class PowerSyncToken { + final String token; + final int expiresIn; + + const PowerSyncToken({required this.token, required this.expiresIn}); + + factory PowerSyncToken.fromJson(Map json) { + return PowerSyncToken(token: json['token'] as String, expiresIn: json['expires_in'] as int); + } +} + DateTime? _parseDateTime(Object? value) { if (value is! String || value.isEmpty) { return null; diff --git a/app/lib/auth/auth_repository.dart b/app/lib/auth/auth_repository.dart index c719cb9..fb37ace 100644 --- a/app/lib/auth/auth_repository.dart +++ b/app/lib/auth/auth_repository.dart @@ -180,6 +180,18 @@ class AuthRepository { return apiClient.resendVerification(email); } + Future createPowerSyncToken() { + return _withFreshAccessToken((accessToken) { + return apiClient.powerSyncToken(accessToken); + }); + } + + Future uploadPowerSyncBatch(List> batch) { + return _withFreshAccessToken((accessToken) { + return apiClient.uploadPowerSyncBatch(accessToken, batch); + }); + } + Future clearTokens() { return tokenStore.clear(); } @@ -199,6 +211,19 @@ class AuthRepository { return tokenStore.saveTokens(accessToken: tokens.accessToken, refreshToken: tokens.refreshToken); } + Future _withFreshAccessToken(Future Function(String accessToken) action) async { + try { + return await action(await _requireAccessToken()); + } on AuthApiException catch (error) { + if (error.statusCode != 401) { + rethrow; + } + + final tokens = await refresh(); + return action(tokens.accessToken); + } + } + String _webOAuthRedirectUri() { final base = Uri.base; diff --git a/app/lib/auth/papyrus_api_config.dart b/app/lib/auth/papyrus_api_config.dart index 3c849c9..ca18757 100644 --- a/app/lib/auth/papyrus_api_config.dart +++ b/app/lib/auth/papyrus_api_config.dart @@ -1,15 +1,25 @@ class PapyrusApiConfig { static const _defaultBaseUrl = 'http://localhost:8080'; + static const _defaultPowerSyncServiceUrl = 'http://localhost:8081'; final Uri serverBaseUri; + final Uri powerSyncServiceUri; final String apiPrefix; - const PapyrusApiConfig({required this.serverBaseUri, this.apiPrefix = '/v1'}); + PapyrusApiConfig({required this.serverBaseUri, Uri? powerSyncServiceUri, this.apiPrefix = '/v1'}) + : powerSyncServiceUri = powerSyncServiceUri ?? Uri.parse(_defaultPowerSyncServiceUrl); factory PapyrusApiConfig.fromEnvironment() { const rawBaseUrl = String.fromEnvironment('PAPYRUS_API_BASE_URL', defaultValue: _defaultBaseUrl); + const rawPowerSyncServiceUrl = String.fromEnvironment( + 'POWERSYNC_SERVICE_URL', + defaultValue: _defaultPowerSyncServiceUrl, + ); - return PapyrusApiConfig(serverBaseUri: Uri.parse(rawBaseUrl)); + return PapyrusApiConfig( + serverBaseUri: Uri.parse(rawBaseUrl), + powerSyncServiceUri: Uri.parse(rawPowerSyncServiceUrl), + ); } Uri get apiBaseUri { diff --git a/app/lib/data/data_store.dart b/app/lib/data/data_store.dart index 43e8e74..cd3ae98 100644 --- a/app/lib/data/data_store.dart +++ b/app/lib/data/data_store.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/foundation.dart'; import 'package:papyrus/models/annotation.dart'; import 'package:papyrus/models/book.dart'; @@ -11,6 +13,12 @@ import 'package:papyrus/models/series.dart'; import 'package:papyrus/models/shelf.dart'; import 'package:papyrus/models/tag.dart'; +abstract class BookSyncWriter { + Future upsertBook(Book book); + + Future deleteBook(String id); +} + /// Central in-memory data store - the single source of truth. /// All repositories read from and write to this store. class DataStore extends ChangeNotifier { @@ -30,6 +38,7 @@ class DataStore extends ChangeNotifier { final List _bookTagRelations = []; bool _isLoaded = false; + BookSyncWriter? _bookSyncWriter; // ============================================================ // Getters for read access @@ -62,13 +71,19 @@ class DataStore extends ChangeNotifier { Book? getBook(String id) => _books[id]; + void attachBookSyncWriter(BookSyncWriter? writer) { + _bookSyncWriter = writer; + } + void addBook(Book book) { _books[book.id] = book; + unawaited(_bookSyncWriter?.upsertBook(book)); notifyListeners(); } void updateBook(Book book) { _books[book.id] = book; + unawaited(_bookSyncWriter?.upsertBook(book)); notifyListeners(); } @@ -81,6 +96,22 @@ class DataStore extends ChangeNotifier { _notes.removeWhere((key, n) => n.bookId == id); _bookmarks.removeWhere((key, b) => b.bookId == id); _readingSessions.removeWhere((key, s) => s.bookId == id); + unawaited(_bookSyncWriter?.deleteBook(id)); + notifyListeners(); + } + + void replaceBooksFromSync(List books) { + final syncedIds = books.map((book) => book.id).toSet(); + _books + ..clear() + ..addEntries(books.map((book) => MapEntry(book.id, book))); + _bookShelfRelations.removeWhere((relation) => !syncedIds.contains(relation.bookId)); + _bookTagRelations.removeWhere((relation) => !syncedIds.contains(relation.bookId)); + _annotations.removeWhere((key, annotation) => !syncedIds.contains(annotation.bookId)); + _notes.removeWhere((key, note) => !syncedIds.contains(note.bookId)); + _bookmarks.removeWhere((key, bookmark) => !syncedIds.contains(bookmark.bookId)); + _readingSessions.removeWhere((key, session) => !syncedIds.contains(session.bookId)); + _isLoaded = true; notifyListeners(); } diff --git a/app/lib/main.dart b/app/lib/main.dart index b9a67db..e9c5f04 100644 --- a/app/lib/main.dart +++ b/app/lib/main.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_web_plugins/url_strategy.dart'; import 'package:papyrus/auth/auth_api_client.dart'; @@ -5,7 +7,7 @@ import 'package:papyrus/auth/auth_repository.dart'; import 'package:papyrus/auth/papyrus_api_config.dart'; import 'package:papyrus/auth/token_store.dart'; import 'package:papyrus/data/data_store.dart'; -import 'package:papyrus/data/sample_data.dart'; +import 'package:papyrus/powersync/powersync_service.dart'; import 'package:papyrus/providers/auth_provider.dart'; import 'package:papyrus/providers/library_provider.dart'; import 'package:papyrus/providers/preferences_provider.dart'; @@ -34,7 +36,9 @@ class Papyrus extends StatefulWidget { } class _PapyrusState extends State { + late final DataStore _dataStore; late final AuthProvider _authProvider; + late final PapyrusPowerSyncService _powerSyncService; late final AppRouter _appRouter; @override @@ -48,37 +52,48 @@ class _PapyrusState extends State { tokenStore: tokenStore, ); + _dataStore = DataStore(); _authProvider = AuthProvider(widget.prefs, repository: authRepository); + _powerSyncService = PapyrusPowerSyncService( + authRepository: authRepository, + config: apiConfig, + dataStore: _dataStore, + ); _appRouter = AppRouter(authProvider: _authProvider); + _authProvider.addListener(_syncPowerSyncAuthState); + _syncPowerSyncAuthState(); } @override void dispose() { + _authProvider.removeListener(_syncPowerSyncAuthState); + unawaited(_powerSyncService.close()); _authProvider.dispose(); super.dispose(); } + void _syncPowerSyncAuthState() { + if (_authProvider.isSignedIn) { + unawaited(_powerSyncService.connect()); + return; + } + + if (_authProvider.isOfflineMode) { + unawaited(_powerSyncService.showOfflineSampleData()); + return; + } + + if (!_authProvider.isBootstrapping) { + unawaited(_powerSyncService.disconnectAndClear()); + } + } + @override Widget build(BuildContext context) { return MultiProvider( providers: [ // Core data store - single source of truth - ChangeNotifierProvider( - create: (_) => DataStore() - ..loadData( - books: SampleData.books, - shelves: SampleData.shelves, - tags: SampleData.tags, - series: SampleData.seriesList, - annotations: SampleData.annotations, - notes: SampleData.notes, - bookmarks: SampleData.bookmarks, - readingSessions: SampleData.readingSessions, - readingGoals: SampleData.readingGoals, - bookShelfRelations: SampleData.bookShelfRelations, - bookTagRelations: SampleData.bookTagRelations, - ), - ), + ChangeNotifierProvider.value(value: _dataStore), // Auth and UI state providers ChangeNotifierProvider.value(value: _authProvider), ChangeNotifierProvider(create: (_) => SidebarProvider()), diff --git a/app/lib/powersync/papyrus_powersync_connector.dart b/app/lib/powersync/papyrus_powersync_connector.dart new file mode 100644 index 0000000..43c0325 --- /dev/null +++ b/app/lib/powersync/papyrus_powersync_connector.dart @@ -0,0 +1,66 @@ +import 'package:papyrus/auth/auth_api_client.dart'; +import 'package:papyrus/auth/auth_repository.dart'; +import 'package:papyrus/auth/papyrus_api_config.dart'; +import 'package:papyrus/powersync/powersync_book_mapper.dart'; +import 'package:powersync/powersync.dart'; + +class PapyrusPowerSyncConnector extends PowerSyncBackendConnector { + final AuthRepository authRepository; + final PapyrusApiConfig config; + + PapyrusPowerSyncConnector({required this.authRepository, required this.config}); + + @override + Future fetchCredentials() async { + try { + final token = await authRepository.createPowerSyncToken(); + + return PowerSyncCredentials( + endpoint: config.powerSyncServiceUri.toString(), + token: token.token, + expiresAt: DateTime.now().add(Duration(seconds: token.expiresIn)), + ); + } on AuthApiException catch (error) { + if (error.statusCode == 401) { + return null; + } + + rethrow; + } + } + + @override + Future uploadData(PowerSyncDatabase database) async { + while (true) { + final transaction = await database.getNextCrudTransaction(); + + if (transaction == null) { + return; + } + + final batch = powerSyncUploadBatchFromCrud(transaction.crud); + + if (batch.isEmpty) { + await transaction.complete(); + continue; + } + + await authRepository.uploadPowerSyncBatch(batch); + await transaction.complete(); + } + } +} + +List> powerSyncUploadBatchFromCrud(List entries) { + return entries.map(powerSyncCrudEntryToJson).toList(); +} + +Map powerSyncCrudEntryToJson(CrudEntry entry) { + final json = entry.toJson(); + + if (entry.table == 'books') { + json['data'] = PowerSyncBookMapper.decodeUploadData(entry.opData); + } + + return json; +} diff --git a/app/lib/powersync/papyrus_schema.dart b/app/lib/powersync/papyrus_schema.dart new file mode 100644 index 0000000..a6e14be --- /dev/null +++ b/app/lib/powersync/papyrus_schema.dart @@ -0,0 +1,34 @@ +import 'package:powersync/powersync.dart'; + +const papyrusPowerSyncSchema = Schema([ + Table( + 'books', + [ + Column.text('owner_user_id'), + Column.text('title'), + Column.text('subtitle'), + Column.text('author'), + Column.text('co_authors'), + Column.text('isbn'), + Column.text('isbn13'), + Column.text('publisher'), + Column.text('language'), + Column.integer('page_count'), + Column.text('description'), + Column.text('cover_image_url'), + Column.text('reading_status'), + Column.integer('current_page'), + Column.real('current_position'), + Column.text('current_cfi'), + Column.integer('is_favorite'), + Column.integer('rating'), + Column.text('custom_metadata'), + Column.text('added_at'), + Column.text('updated_at'), + ], + indexes: [ + Index('books_added_at', [IndexedColumn('added_at')]), + Index('books_title', [IndexedColumn('title')]), + ], + ), +]); diff --git a/app/lib/powersync/powersync_book_mapper.dart b/app/lib/powersync/powersync_book_mapper.dart new file mode 100644 index 0000000..ce079ba --- /dev/null +++ b/app/lib/powersync/powersync_book_mapper.dart @@ -0,0 +1,280 @@ +import 'dart:convert'; + +import 'package:papyrus/models/book.dart'; + +const syncedBookColumns = [ + 'title', + 'subtitle', + 'author', + 'co_authors', + 'isbn', + 'isbn13', + 'publisher', + 'language', + 'page_count', + 'description', + 'cover_image_url', + 'reading_status', + 'current_page', + 'current_position', + 'current_cfi', + 'is_favorite', + 'rating', + 'custom_metadata', + 'added_at', + 'updated_at', +]; + +class PowerSyncBookMapper { + static Book fromRow(Map row) { + final metadata = _decodeObject(row['custom_metadata']); + + return Book( + id: row['id'] as String, + title: row['title'] as String? ?? 'Untitled Book', + subtitle: row['subtitle'] as String?, + author: row['author'] as String? ?? 'Unknown Author', + coAuthors: _decodeStringList(row['co_authors']), + isbn: row['isbn'] as String?, + isbn13: row['isbn13'] as String?, + publicationDate: _parseDate(metadata['publication_date']), + publisher: row['publisher'] as String?, + language: row['language'] as String?, + pageCount: _toInt(row['page_count']), + description: row['description'] as String?, + coverUrl: row['cover_image_url'] as String?, + fileFormat: _bookFormat(metadata['file_format']), + fileSize: _toInt(metadata['file_size']), + fileHash: metadata['file_hash'] as String?, + isPhysical: _toBool(metadata['is_physical']), + physicalLocation: metadata['physical_location'] as String?, + lentTo: metadata['lent_to'] as String?, + lentAt: _parseDate(metadata['lent_at']), + readingStatus: _readingStatus(row['reading_status']), + currentPage: _toInt(row['current_page']), + currentPosition: _toDouble(row['current_position']) ?? 0.0, + currentCfi: row['current_cfi'] as String?, + isFavorite: _toBool(row['is_favorite']), + rating: _toInt(row['rating']), + customMetadata: _decodeNestedMetadata(metadata['custom_metadata']), + seriesId: metadata['series_id'] as String?, + seriesName: metadata['series_name'] as String?, + seriesNumber: _toDouble(metadata['series_number']), + addedAt: _parseDate(row['added_at']) ?? DateTime.now(), + startedAt: _parseDate(metadata['started_at']), + completedAt: _parseDate(metadata['completed_at']), + lastReadAt: _parseDate(metadata['last_read_at']), + ); + } + + static Map toRow(Book book) { + final metadata = { + if (book.publicationDate != null) 'publication_date': book.publicationDate!.toIso8601String(), + if (book.fileFormat != null) 'file_format': book.fileFormat!.name, + if (book.fileSize != null) 'file_size': book.fileSize, + if (book.fileHash != null) 'file_hash': book.fileHash, + 'is_physical': book.isPhysical, + if (book.physicalLocation != null) 'physical_location': book.physicalLocation, + if (book.lentTo != null) 'lent_to': book.lentTo, + if (book.lentAt != null) 'lent_at': book.lentAt!.toIso8601String(), + if (book.customMetadata != null) 'custom_metadata': book.customMetadata, + if (book.seriesId != null) 'series_id': book.seriesId, + if (book.seriesName != null) 'series_name': book.seriesName, + if (book.seriesNumber != null) 'series_number': book.seriesNumber, + if (book.startedAt != null) 'started_at': book.startedAt!.toIso8601String(), + if (book.completedAt != null) 'completed_at': book.completedAt!.toIso8601String(), + if (book.lastReadAt != null) 'last_read_at': book.lastReadAt!.toIso8601String(), + }; + final now = DateTime.now().toIso8601String(); + + return { + 'id': book.id, + 'title': book.title, + 'subtitle': book.subtitle, + 'author': book.author, + 'co_authors': jsonEncode(book.coAuthors), + 'isbn': book.isbn, + 'isbn13': book.isbn13, + 'publisher': book.publisher, + 'language': book.language, + 'page_count': book.pageCount, + 'description': book.description, + 'cover_image_url': _remoteCoverUrl(book.coverUrl), + 'reading_status': book.readingStatus.name, + 'current_page': book.currentPage, + 'current_position': book.currentPosition, + 'current_cfi': book.currentCfi, + 'is_favorite': book.isFavorite ? 1 : 0, + 'rating': book.rating, + 'custom_metadata': jsonEncode(metadata), + 'added_at': book.addedAt.toIso8601String(), + 'updated_at': now, + }; + } + + static Map? decodeUploadData(Map? data) { + if (data == null) { + return null; + } + + final decoded = Map.from(data); + decoded['co_authors'] = _decodeStringList(decoded['co_authors']); + decoded['custom_metadata'] = _decodeObject(decoded['custom_metadata']); + return decoded; + } + + static List rowParameters(Map row) { + return [row['id'], ...syncedBookColumns.map((column) => row[column])]; + } + + static String insertSql() { + return ''' +INSERT INTO books (id, ${syncedBookColumns.join(', ')}) +VALUES (${List.filled(syncedBookColumns.length + 1, '?').join(', ')}) +'''; + } + + static String updateSql() { + return ''' +UPDATE books +SET ${syncedBookColumns.map((column) => '$column = ?').join(', ')} +WHERE id = ? +'''; + } + + static List updateParameters(Map row) { + return [...syncedBookColumns.map((column) => row[column]), row['id']]; + } + + static String? _remoteCoverUrl(String? coverUrl) { + if (coverUrl == null || coverUrl.startsWith('data:')) { + return null; + } + + return coverUrl; + } + + static Map _decodeObject(Object? value) { + if (value is Map) { + return value; + } + + if (value is Map) { + return value; + } + + if (value is String && value.isNotEmpty) { + final decoded = jsonDecode(value); + + if (decoded is Map) { + return decoded; + } + } + + return {}; + } + + static Map? _decodeNestedMetadata(Object? value) { + if (value is Map) { + return value; + } + + if (value is Map) { + return Map.from(value); + } + + return null; + } + + static List _decodeStringList(Object? value) { + if (value is List) { + return value.map((item) => item.toString()).toList(); + } + + if (value is String && value.isNotEmpty) { + final decoded = jsonDecode(value); + + if (decoded is List) { + return decoded.map((item) => item.toString()).toList(); + } + } + + return []; + } + + static DateTime? _parseDate(Object? value) { + if (value is! String || value.isEmpty) { + return null; + } + + return DateTime.tryParse(value); + } + + static int? _toInt(Object? value) { + if (value is int) { + return value; + } + + if (value is num) { + return value.toInt(); + } + + if (value is String) { + return int.tryParse(value); + } + + return null; + } + + static double? _toDouble(Object? value) { + if (value is num) { + return value.toDouble(); + } + + if (value is String) { + return double.tryParse(value); + } + + return null; + } + + static bool _toBool(Object? value) { + if (value is bool) { + return value; + } + + if (value is num) { + return value != 0; + } + + if (value is String) { + return {'1', 'true', 'yes', 'on'}.contains(value.toLowerCase()); + } + + return false; + } + + static ReadingStatus _readingStatus(Object? value) { + return switch (value) { + 'inProgress' || 'in_progress' || 'reading' => ReadingStatus.inProgress, + 'completed' || 'finished' => ReadingStatus.completed, + 'paused' => ReadingStatus.paused, + 'abandoned' => ReadingStatus.abandoned, + _ => ReadingStatus.notStarted, + }; + } + + static BookFormat? _bookFormat(Object? value) { + if (value is! String) { + return null; + } + + for (final format in BookFormat.values) { + if (format.name == value) { + return format; + } + } + + return null; + } +} diff --git a/app/lib/powersync/powersync_service.dart b/app/lib/powersync/powersync_service.dart new file mode 100644 index 0000000..787a442 --- /dev/null +++ b/app/lib/powersync/powersync_service.dart @@ -0,0 +1,142 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:papyrus/auth/auth_repository.dart'; +import 'package:papyrus/auth/papyrus_api_config.dart'; +import 'package:papyrus/data/data_store.dart'; +import 'package:papyrus/data/sample_data.dart'; +import 'package:papyrus/models/book.dart'; +import 'package:papyrus/powersync/papyrus_powersync_connector.dart'; +import 'package:papyrus/powersync/papyrus_schema.dart'; +import 'package:papyrus/powersync/powersync_book_mapper.dart'; +import 'package:path/path.dart' as path; +import 'package:path_provider/path_provider.dart'; +import 'package:powersync/powersync.dart'; + +class PapyrusPowerSyncService implements BookSyncWriter { + final AuthRepository authRepository; + final PapyrusApiConfig config; + final DataStore dataStore; + + PowerSyncDatabase? _database; + StreamSubscription? _booksSubscription; + Future? _connectOperation; + bool _isConnected = false; + + PapyrusPowerSyncService({required this.authRepository, required this.config, required this.dataStore}); + + Future connect() { + if (_isConnected) { + return Future.value(); + } + + _connectOperation ??= _connect().whenComplete(() { + _connectOperation = null; + }); + + return _connectOperation!; + } + + Future showOfflineSampleData() async { + await disconnectAndClear(); + dataStore.loadData( + books: SampleData.books, + shelves: SampleData.shelves, + tags: SampleData.tags, + series: SampleData.seriesList, + annotations: SampleData.annotations, + notes: SampleData.notes, + bookmarks: SampleData.bookmarks, + readingSessions: SampleData.readingSessions, + readingGoals: SampleData.readingGoals, + bookShelfRelations: SampleData.bookShelfRelations, + bookTagRelations: SampleData.bookTagRelations, + ); + } + + Future disconnectAndClear() async { + _isConnected = false; + dataStore.attachBookSyncWriter(null); + await _booksSubscription?.cancel(); + _booksSubscription = null; + + final database = _database; + + if (database != null) { + await database.disconnectAndClear(); + } + + dataStore.clear(); + } + + Future close() async { + _isConnected = false; + dataStore.attachBookSyncWriter(null); + await _booksSubscription?.cancel(); + await _database?.close(); + } + + @override + Future upsertBook(Book book) async { + final database = await _openDatabase(); + final row = PowerSyncBookMapper.toRow(book); + final existing = await database.getOptional('SELECT id FROM books WHERE id = ?', [book.id]); + + if (existing == null) { + await database.execute(PowerSyncBookMapper.insertSql(), PowerSyncBookMapper.rowParameters(row)); + return; + } + + await database.execute(PowerSyncBookMapper.updateSql(), PowerSyncBookMapper.updateParameters(row)); + } + + @override + Future deleteBook(String id) async { + final database = await _openDatabase(); + await database.execute('DELETE FROM books WHERE id = ?', [id]); + } + + Future _connect() async { + final database = await _openDatabase(); + dataStore.clear(); + dataStore.attachBookSyncWriter(this); + _watchBooks(database); + + await database.connect( + connector: PapyrusPowerSyncConnector(authRepository: authRepository, config: config), + ); + _isConnected = true; + } + + Future _openDatabase() async { + final existing = _database; + + if (existing != null) { + return existing; + } + + final database = PowerSyncDatabase(schema: papyrusPowerSyncSchema, path: await _databasePath()); + await database.initialize(); + _database = database; + return database; + } + + Future _databasePath() async { + if (kIsWeb) { + return 'papyrus-powersync.db'; + } + + final directory = await getApplicationSupportDirectory(); + return path.join(directory.path, 'papyrus-powersync.db'); + } + + void _watchBooks(PowerSyncDatabase database) { + unawaited(_booksSubscription?.cancel()); + _booksSubscription = database + .watch('SELECT * FROM books ORDER BY added_at DESC', triggerOnTables: ['books']) + .listen((rows) { + final books = rows.map((row) => PowerSyncBookMapper.fromRow(Map.from(row))).toList(); + dataStore.replaceBooksFromSync(books); + }); + } +} diff --git a/app/lib/services/book_import_service.dart b/app/lib/services/book_import_service.dart index b2611a0..3dd9d87 100644 --- a/app/lib/services/book_import_service.dart +++ b/app/lib/services/book_import_service.dart @@ -4,6 +4,7 @@ import 'dart:js_interop_unsafe'; import 'package:flutter/foundation.dart'; import 'package:papyrus/services/book_import_result.dart'; +import 'package:uuid/uuid.dart'; import 'package:web/web.dart' as web; export 'package:papyrus/services/book_import_result.dart'; @@ -18,9 +19,6 @@ class BookImportService { /// Timeout for worker operations before they are considered failed. static const _timeout = Duration(seconds: 30); - /// Incrementing counter used to guarantee unique book IDs. - int _nextId = 0; - /// Pending requests keyed by '$action:$bookId'. final Map> _pending = {}; @@ -122,7 +120,7 @@ class BookImportService { throw ArgumentError('Unsupported format: $ext. Only epub is supported.'); } - final bookId = 'book-${DateTime.now().millisecondsSinceEpoch}-${_nextId++}'; + final bookId = const Uuid().v4(); final completer = Completer(); final worker = _getWorker(); diff --git a/app/lib/services/book_import_service_stub.dart b/app/lib/services/book_import_service_stub.dart index e40e7a5..27beb47 100644 --- a/app/lib/services/book_import_service_stub.dart +++ b/app/lib/services/book_import_service_stub.dart @@ -6,6 +6,7 @@ import 'package:papyrus/services/book_import_result.dart'; import 'package:papyrus/services/file_metadata_service.dart'; import 'package:path/path.dart' as p; import 'package:path_provider/path_provider.dart'; +import 'package:uuid/uuid.dart'; export 'package:papyrus/services/book_import_result.dart'; @@ -16,9 +17,6 @@ export 'package:papyrus/services/book_import_result.dart'; class BookImportService { final _metadataService = FileMetadataService(); - /// Incrementing counter used to guarantee unique book IDs. - int _nextId = 0; - /// Imports a book file: extracts metadata and stores the file locally. /// /// Supports all formats handled by [FileMetadataService]: @@ -29,7 +27,7 @@ class BookImportService { throw ArgumentError('Filename has no extension: $filename'); } - final bookId = 'book-${DateTime.now().millisecondsSinceEpoch}-${_nextId++}'; + final bookId = const Uuid().v4(); // Extract metadata final metadata = await _metadataService.extractMetadata(bytes, filename); diff --git a/app/lib/widgets/add_book/add_physical_book_sheet.dart b/app/lib/widgets/add_book/add_physical_book_sheet.dart index 874e873..e8fce5f 100644 --- a/app/lib/widgets/add_book/add_physical_book_sheet.dart +++ b/app/lib/widgets/add_book/add_physical_book_sheet.dart @@ -15,6 +15,7 @@ import 'package:papyrus/widgets/book_form/co_author_editor.dart'; import 'package:papyrus/widgets/shared/bottom_sheet_handle.dart'; import 'package:papyrus/widgets/shared/bottom_sheet_header.dart'; import 'package:provider/provider.dart'; +import 'package:uuid/uuid.dart'; /// ISBN lookup states. enum _IsbnLookupState { idle, fetching, found, notFound, error } @@ -218,7 +219,7 @@ class _PhysicalBookContentState extends State<_PhysicalBookContent> { } final book = Book( - id: 'book-${now.millisecondsSinceEpoch}', + id: const Uuid().v4(), title: _titleController.text.trim(), author: _authorController.text.trim(), subtitle: _nullIfEmpty(_subtitleController.text), diff --git a/app/pubspec.lock b/app/pubspec.lock index c01d27a..4732b3a 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -65,6 +65,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.1" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f" + url: "https://pub.dev" + source: hosted + version: "2.0.4" clock: dependency: transitive description: @@ -340,10 +348,10 @@ packages: dependency: transitive description: name: hooks - sha256: "7a08a0d684cb3b8fb604b78455d5d352f502b68079f7b80b831c62220ab0a4f6" + sha256: "025f060e86d2d4c3c47b56e33caf7f93bf9283340f26d23424ebcfccf34f621e" url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.0.3" http: dependency: "direct main" description: @@ -376,6 +384,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.19.0" + json_annotation: + dependency: transitive + description: + name: json_annotation + sha256: cb09e7dac6210041fad964ed7fbee004f14258b4eca4040f72d1234062ace4c8 + url: "https://pub.dev" + source: hosted + version: "4.11.0" leak_tracker: dependency: transitive description: @@ -452,10 +468,10 @@ packages: dependency: transitive description: name: native_toolchain_c - sha256: "89e83885ba09da5fdf2cdacc8002a712ca238c28b7f717910b34bcd27b0d03ac" + sha256: "6ba77bb18063eebe9de401f5e6437e95e1438af0a87a3a39084fbd37c90df572" url: "https://pub.dev" source: hosted - version: "0.17.4" + version: "0.17.6" nested: dependency: transitive description: @@ -576,6 +592,22 @@ packages: url: "https://pub.dev" source: hosted version: "6.5.0" + powersync: + dependency: "direct main" + description: + name: powersync + sha256: bd1f1b2ee9b79b6f821f2cc77e7ae6ef1a7159d2a9779990e1b8dc791e97e91c + url: "https://pub.dev" + source: hosted + version: "2.1.0" + powersync_flutter_libs: + dependency: transitive + description: + name: powersync_flutter_libs + sha256: "24324947bffee25912abd3534203d7ab070b99d3186a8de658205ffb44902971" + url: "https://pub.dev" + source: hosted + version: "0.5.0+eol" provider: dependency: "direct main" description: @@ -592,6 +624,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.0" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" + url: "https://pub.dev" + source: hosted + version: "1.5.0" + record_use: + dependency: transitive + description: + name: record_use + sha256: "2551bd8eecfe95d14ae75f6021ad0248be5c27f138c2ec12fcb52b500b3ba1ed" + url: "https://pub.dev" + source: hosted + version: "0.6.0" rxdart: dependency: transitive description: @@ -709,6 +757,54 @@ packages: url: "https://pub.dev" source: hosted version: "2.4.0" + sqlcipher_flutter_libs: + dependency: transitive + description: + name: sqlcipher_flutter_libs + sha256: "38d62d659d2fb8739bf25a42c9a350d1fdd6c29a5a61f13a946778ec75d27929" + url: "https://pub.dev" + source: hosted + version: "0.7.0+eol" + sqlite3: + dependency: transitive + description: + name: sqlite3 + sha256: "56da3e13ed7d28a66f930aa2b2b29db6736a233f08283326e96321dd812030f5" + url: "https://pub.dev" + source: hosted + version: "3.3.1" + sqlite3_connection_pool: + dependency: transitive + description: + name: sqlite3_connection_pool + sha256: "90b25972c7699d84da97df1c5919804275560b4ab8a158bbec890434b9718f65" + url: "https://pub.dev" + source: hosted + version: "0.2.4" + sqlite3_flutter_libs: + dependency: transitive + description: + name: sqlite3_flutter_libs + sha256: "3ed7553eee7bb368f8950f58ba29f634e06e813c029aff6a0d60862b96de8454" + url: "https://pub.dev" + source: hosted + version: "0.6.0+eol" + sqlite3_web: + dependency: transitive + description: + name: sqlite3_web + sha256: d876398a9f2cbf115d93fc34901f8fa129b58b13b5fa9377156ed3a9a05695e3 + url: "https://pub.dev" + source: hosted + version: "0.7.1" + sqlite_async: + dependency: transitive + description: + name: sqlite_async + sha256: "4c243c5386eba3a7102f98999388a7e0a7f2632e4e06dafb3b4f5a44170a26f6" + url: "https://pub.dev" + source: hosted + version: "0.14.1" stack_trace: dependency: transitive description: @@ -854,7 +950,7 @@ packages: source: hosted version: "3.1.5" uuid: - dependency: transitive + dependency: "direct main" description: name: uuid sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489" diff --git a/app/pubspec.yaml b/app/pubspec.yaml index 7ba8930..b6c469d 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -34,10 +34,12 @@ dependencies: path: ^1.9.1 crypto: ^3.0.6 path_provider: ^2.1.5 + powersync: ^2.1.0 web: ^1.1.1 mobile_scanner: ^7.0.1 shared_preferences: ^2.5.4 flutter_secure_storage: ^10.1.0 + uuid: ^4.5.1 dev_dependencies: flutter_test: diff --git a/app/test/auth/auth_api_client_test.dart b/app/test/auth/auth_api_client_test.dart index d3a9c97..c0e4f88 100644 --- a/app/test/auth/auth_api_client_test.dart +++ b/app/test/auth/auth_api_client_test.dart @@ -78,4 +78,52 @@ void main() { expect(uri.path, '/v1/auth/oauth/google/start'); expect(uri.queryParameters['redirect_uri'], 'papyrus://auth/callback'); }); + + test('powerSyncToken maps Papyrus token response', () async { + final client = AuthApiClient( + config: PapyrusApiConfig(serverBaseUri: Uri.parse('http://server.test')), + httpClient: MockClient((request) async { + expect(request.url.path, '/v1/auth/powersync-token'); + expect(request.headers['Authorization'], 'Bearer access-token'); + + return http.Response(jsonEncode({'token': 'powersync-token', 'expires_in': 300}), 200); + }), + ); + + final token = await client.powerSyncToken('access-token'); + + expect(token.token, 'powersync-token'); + expect(token.expiresIn, 300); + }); + + test('uploadPowerSyncBatch posts PowerSync mutations', () async { + final client = AuthApiClient( + config: PapyrusApiConfig(serverBaseUri: Uri.parse('http://server.test')), + httpClient: MockClient((request) async { + expect(request.url.path, '/v1/sync/powersync-upload'); + expect(request.headers['Authorization'], 'Bearer access-token'); + expect(jsonDecode(request.body), { + 'batch': [ + { + 'type': 'books', + 'op': 'PUT', + 'id': 'book-id', + 'data': {'title': 'Book'}, + }, + ], + }); + + return http.Response(jsonEncode({'applied_count': 1}), 200); + }), + ); + + await client.uploadPowerSyncBatch('access-token', [ + { + 'type': 'books', + 'op': 'PUT', + 'id': 'book-id', + 'data': {'title': 'Book'}, + }, + ]); + }); } diff --git a/app/test/auth/auth_repository_powersync_test.dart b/app/test/auth/auth_repository_powersync_test.dart new file mode 100644 index 0000000..f0581ae --- /dev/null +++ b/app/test/auth/auth_repository_powersync_test.dart @@ -0,0 +1,86 @@ +import 'dart:convert'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:http/http.dart' as http; +import 'package:http/testing.dart'; +import 'package:papyrus/auth/auth_api_client.dart'; +import 'package:papyrus/auth/auth_repository.dart'; +import 'package:papyrus/auth/papyrus_api_config.dart'; +import 'package:papyrus/auth/token_store.dart'; + +class MemoryRefreshTokenStorage implements RefreshTokenStorage { + String? value; + + @override + Future delete() async { + value = null; + } + + @override + Future read() async => value; + + @override + Future write(String refreshToken) async { + value = refreshToken; + } +} + +void main() { + test('createPowerSyncToken refreshes Papyrus auth once after 401', () async { + var powerSyncTokenCalls = 0; + final store = TokenStore(MemoryRefreshTokenStorage()); + await store.saveTokens(accessToken: 'expired-access', refreshToken: 'refresh-token'); + final repository = AuthRepository( + apiClient: AuthApiClient( + config: PapyrusApiConfig(serverBaseUri: Uri.parse('http://server.test')), + httpClient: MockClient((request) async { + if (request.url.path == '/v1/auth/powersync-token') { + powerSyncTokenCalls += 1; + + if (powerSyncTokenCalls == 1) { + return http.Response( + jsonEncode({ + 'error': {'message': 'Expired'}, + }), + 401, + ); + } + + expect(request.headers['Authorization'], 'Bearer fresh-access'); + return http.Response(jsonEncode({'token': 'powersync-token', 'expires_in': 300}), 200); + } + + if (request.url.path == '/v1/auth/refresh') { + expect(jsonDecode(request.body), {'refresh_token': 'refresh-token'}); + return http.Response(jsonEncode(_authResponse), 200); + } + + throw StateError('Unexpected request: ${request.url}'); + }), + ), + tokenStore: store, + ); + + final token = await repository.createPowerSyncToken(); + + expect(token.token, 'powersync-token'); + expect(powerSyncTokenCalls, 2); + expect(store.accessToken, 'fresh-access'); + }); +} + +const _authResponse = { + 'access_token': 'fresh-access', + 'refresh_token': 'fresh-refresh', + 'token_type': 'Bearer', + 'expires_in': 3600, + 'user': { + 'user_id': '11111111-1111-1111-1111-111111111111', + 'email': 'reader@example.com', + 'display_name': 'Reader', + 'avatar_url': null, + 'email_verified': true, + 'created_at': null, + 'last_login_at': null, + }, +}; diff --git a/app/test/data/data_store_sync_test.dart b/app/test/data/data_store_sync_test.dart new file mode 100644 index 0000000..e2aa5c5 --- /dev/null +++ b/app/test/data/data_store_sync_test.dart @@ -0,0 +1,57 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:papyrus/data/data_store.dart'; +import 'package:papyrus/models/book.dart'; + +class FakeBookSyncWriter implements BookSyncWriter { + final upserts = []; + final deletes = []; + + @override + Future deleteBook(String id) async { + deletes.add(id); + } + + @override + Future upsertBook(Book book) async { + upserts.add(book); + } +} + +void main() { + test('DataStore delegates local book writes to sync writer', () { + final dataStore = DataStore(); + final writer = FakeBookSyncWriter(); + final book = Book( + id: '11111111-1111-1111-1111-111111111111', + title: 'Book', + author: 'Author', + addedAt: DateTime(2026, 5, 9), + ); + + dataStore.attachBookSyncWriter(writer); + dataStore.addBook(book); + dataStore.updateBook(book.copyWith(title: 'Updated')); + dataStore.deleteBook(book.id); + + expect(writer.upserts.map((book) => book.title), ['Book', 'Updated']); + expect(writer.deletes, [book.id]); + }); + + test('replaceBooksFromSync updates books without delegating writes', () { + final dataStore = DataStore(); + final writer = FakeBookSyncWriter(); + final book = Book( + id: '11111111-1111-1111-1111-111111111111', + title: 'Synced Book', + author: 'Author', + addedAt: DateTime(2026, 5, 9), + ); + + dataStore.attachBookSyncWriter(writer); + dataStore.replaceBooksFromSync([book]); + + expect(dataStore.books.single.title, 'Synced Book'); + expect(writer.upserts, isEmpty); + expect(writer.deletes, isEmpty); + }); +} diff --git a/app/test/powersync/papyrus_powersync_connector_test.dart b/app/test/powersync/papyrus_powersync_connector_test.dart new file mode 100644 index 0000000..a176cbb --- /dev/null +++ b/app/test/powersync/papyrus_powersync_connector_test.dart @@ -0,0 +1,34 @@ +import 'dart:convert'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:papyrus/powersync/papyrus_powersync_connector.dart'; +import 'package:powersync/powersync.dart'; + +void main() { + test('serializes CRUD entries for Papyrus upload endpoint', () { + final batch = powerSyncUploadBatchFromCrud([ + CrudEntry(11, UpdateType.put, 'books', '11111111-1111-1111-1111-111111111111', 22, { + 'title': 'Book', + 'co_authors': jsonEncode(['Co Author']), + 'custom_metadata': jsonEncode({'file_format': 'epub'}), + }), + ]); + + expect(batch, [ + { + 'op_id': 11, + 'op': 'PUT', + 'type': 'books', + 'id': '11111111-1111-1111-1111-111111111111', + 'tx_id': 22, + 'data': { + 'title': 'Book', + 'co_authors': ['Co Author'], + 'custom_metadata': {'file_format': 'epub'}, + }, + 'metadata': null, + 'old': null, + }, + ]); + }); +} diff --git a/app/test/powersync/powersync_book_mapper_test.dart b/app/test/powersync/powersync_book_mapper_test.dart new file mode 100644 index 0000000..e8c90c1 --- /dev/null +++ b/app/test/powersync/powersync_book_mapper_test.dart @@ -0,0 +1,62 @@ +import 'dart:convert'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:papyrus/models/book.dart'; +import 'package:papyrus/powersync/powersync_book_mapper.dart'; + +void main() { + test('maps Book to synced row without file path or embedded cover bytes', () { + final book = Book( + id: '11111111-1111-1111-1111-111111111111', + title: 'Synced Book', + author: 'Author', + coAuthors: const ['Co Author'], + coverUrl: 'data:image/png;base64,abc', + filePath: '/local/book.epub', + fileFormat: BookFormat.epub, + fileSize: 1024, + fileHash: 'hash', + isPhysical: true, + physicalLocation: 'Shelf', + readingStatus: ReadingStatus.inProgress, + currentPosition: 0.4, + isFavorite: true, + addedAt: DateTime.parse('2026-05-09T12:00:00Z'), + ); + + final row = PowerSyncBookMapper.toRow(book); + final metadata = jsonDecode(row['custom_metadata']! as String) as Map; + + expect(row['cover_image_url'], isNull); + expect(row.containsKey('file_path'), isFalse); + expect(row['co_authors'], jsonEncode(['Co Author'])); + expect(row['reading_status'], 'inProgress'); + expect(row['is_favorite'], 1); + expect(metadata['file_format'], 'epub'); + expect(metadata['file_size'], 1024); + expect(metadata['file_hash'], 'hash'); + expect(metadata['is_physical'], true); + expect(metadata['physical_location'], 'Shelf'); + }); + + test('maps synced row to Book', () { + final book = PowerSyncBookMapper.fromRow({ + 'id': '11111111-1111-1111-1111-111111111111', + 'title': 'Synced Book', + 'author': 'Author', + 'co_authors': jsonEncode(['Co Author']), + 'reading_status': 'in_progress', + 'current_position': 0.5, + 'is_favorite': 1, + 'custom_metadata': jsonEncode({'file_format': 'epub', 'is_physical': false}), + 'added_at': '2026-05-09T12:00:00Z', + }); + + expect(book.title, 'Synced Book'); + expect(book.coAuthors, ['Co Author']); + expect(book.readingStatus, ReadingStatus.inProgress); + expect(book.currentPosition, 0.5); + expect(book.isFavorite, isTrue); + expect(book.fileFormat, BookFormat.epub); + }); +} diff --git a/app/test/services/book_import_service_test.dart b/app/test/services/book_import_service_test.dart index d9e6517..1b85f3e 100644 --- a/app/test/services/book_import_service_test.dart +++ b/app/test/services/book_import_service_test.dart @@ -18,6 +18,8 @@ class _FakePathProvider extends Fake with MockPlatformInterfaceMixin implements } void main() { + final uuidMatcher = matches(RegExp(r'^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$')); + late BookImportService service; late Directory tempDir; @@ -51,7 +53,7 @@ void main() { final result = await service.importBook(bytes, 'book1.epub'); - expect(result.bookId, startsWith('book-')); + expect(result.bookId, uuidMatcher); expect(result.title, isNotEmpty); expect(result.fileSize, bytes.length); expect(result.fileHash, isNotEmpty); @@ -65,7 +67,7 @@ void main() { final result = await service.importBook(bytes, 'book2.epub'); - expect(result.bookId, startsWith('book-')); + expect(result.bookId, uuidMatcher); expect(result.title, isNotEmpty); expect(result.fileSize, bytes.length); expect(result.fileHash, isNotEmpty); @@ -78,7 +80,7 @@ void main() { final result = await service.importBook(bytes, 'book3.epub'); - expect(result.bookId, startsWith('book-')); + expect(result.bookId, uuidMatcher); expect(result.title, isNotEmpty); expect(result.fileSize, bytes.length); expect(result.fileHash, isNotEmpty); @@ -139,7 +141,7 @@ void main() { final result = await service.importBook(bytes, 'bad.epub'); // Should still return a result (FileMetadataService never throws) - expect(result.bookId, startsWith('book-')); + expect(result.bookId, uuidMatcher); expect(result.fileSize, 6); expect(result.fileHash, isNotEmpty); expect(result.fileExtension, 'epub'); diff --git a/app/web/powersync_db.worker.js b/app/web/powersync_db.worker.js new file mode 100644 index 0000000..e400dda --- /dev/null +++ b/app/web/powersync_db.worker.js @@ -0,0 +1,18082 @@ +(function dartProgram(){function copyProperties(a,b){var s=Object.keys(a) +for(var r=0;r=0)return true +if(typeof version=="function"&&version.length==0){var q=version() +if(/^\d+\.\d+\.\d+\.\d+$/.test(q))return true}}catch(p){}return false}() +function inherit(a,b){a.prototype.constructor=a +a.prototype["$i"+a.name]=a +if(b!=null){if(z){Object.setPrototypeOf(a.prototype,b.prototype) +return}var s=Object.create(b.prototype) +copyProperties(a.prototype,s) +a.prototype=s}}function inheritMany(a,b){for(var s=0;s4294967295)throw A.a(A.a0(a,0,4294967295,"length",null)) +return J.zB(new Array(a),b)}, +us(a,b){if(a<0)throw A.a(A.K("Length must be a non-negative integer: "+a,null)) +return A.v(new Array(a),b.h("A<0>"))}, +zB(a,b){var s=A.v(a,b.h("A<0>")) +s.$flags=1 +return s}, +zC(a,b){return J.vv(a,b)}, +dy(a){if(typeof a=="number"){if(Math.floor(a)==a)return J.fc.prototype +return J.ip.prototype}if(typeof a=="string")return J.cl.prototype +if(a==null)return J.dP.prototype +if(typeof a=="boolean")return J.io.prototype +if(Array.isArray(a))return J.A.prototype +if(typeof a!="object"){if(typeof a=="function")return J.b0.prototype +if(typeof a=="symbol")return J.dR.prototype +if(typeof a=="bigint")return J.aO.prototype +return a}if(a instanceof A.k)return a +return J.tI(a)}, +a2(a){if(typeof a=="string")return J.cl.prototype +if(a==null)return a +if(Array.isArray(a))return J.A.prototype +if(typeof a!="object"){if(typeof a=="function")return J.b0.prototype +if(typeof a=="symbol")return J.dR.prototype +if(typeof a=="bigint")return J.aO.prototype +return a}if(a instanceof A.k)return a +return J.tI(a)}, +bq(a){if(a==null)return a +if(Array.isArray(a))return J.A.prototype +if(typeof a!="object"){if(typeof a=="function")return J.b0.prototype +if(typeof a=="symbol")return J.dR.prototype +if(typeof a=="bigint")return J.aO.prototype +return a}if(a instanceof A.k)return a +return J.tI(a)}, +Dd(a){if(typeof a=="number")return J.dQ.prototype +if(typeof a=="string")return J.cl.prototype +if(a==null)return a +if(!(a instanceof A.k))return J.d1.prototype +return a}, +tH(a){if(typeof a=="string")return J.cl.prototype +if(a==null)return a +if(!(a instanceof A.k))return J.d1.prototype +return a}, +ve(a){if(a==null)return a +if(typeof a!="object"){if(typeof a=="function")return J.b0.prototype +if(typeof a=="symbol")return J.dR.prototype +if(typeof a=="bigint")return J.aO.prototype +return a}if(a instanceof A.k)return a +return J.tI(a)}, +y(a,b){if(a==null)return b==null +if(typeof a!="object")return b!=null&&a===b +return J.dy(a).H(a,b)}, +kP(a,b){if(typeof b==="number")if(Array.isArray(a)||typeof a=="string"||A.y0(a,a[v.dispatchPropertyName]))if(b>>>0===b&&b>>0===b&&b").J(c).h("h6<1,2>")) +return new A.cJ(a,b.h("@<0>").J(c).h("cJ<1,2>"))}, +vY(a){return new A.cQ("Field '"+a+"' has been assigned during initialization.")}, +vZ(a){return new A.cQ("Field '"+a+"' has not been initialized.")}, +zH(a){return new A.cQ("Field '"+a+"' has already been initialized.")}, +tL(a){var s,r=a^48 +if(r<=9)return r +s=a|32 +if(97<=s&&s<=102)return s-87 +return-1}, +F(a,b){a=a+b&536870911 +a=a+((a&524287)<<10)&536870911 +return a^a>>>6}, +c4(a){a=a+((a&67108863)<<3)&536870911 +a^=a>>>11 +return a+((a&16383)<<15)&536870911}, +wp(a,b,c){return A.c4(A.F(A.F(c,a),b))}, +bd(a,b,c){return a}, +vh(a){var s,r +for(s=$.du.length,r=0;rc)A.p(A.a0(b,0,c,"start",null))}return new A.cZ(a,b,c,d.h("cZ<0>"))}, +fl(a,b,c,d){if(t.O.b(a))return new A.cM(a,b,c.h("@<0>").J(d).h("cM<1,2>")) +return new A.bZ(a,b,c.h("@<0>").J(d).h("bZ<1,2>"))}, +wq(a,b,c){var s="takeCount" +A.hM(b,s) +A.aI(b,s) +if(t.O.b(a))return new A.eZ(a,b,c.h("eZ<0>")) +return new A.d0(a,b,c.h("d0<0>"))}, +wm(a,b,c){var s="count" +if(t.O.b(a)){A.hM(b,s) +A.aI(b,s) +return new A.dL(a,b,c.h("dL<0>"))}A.hM(b,s) +A.aI(b,s) +return new A.c2(a,b,c.h("c2<0>"))}, +ck(){return new A.b7("No element")}, +vU(){return new A.b7("Too few elements")}, +j1(a,b,c,d){if(c-b<=32)A.Aj(a,b,c,d) +else A.Ai(a,b,c,d)}, +Aj(a,b,c,d){var s,r,q,p,o +for(s=b+1,r=J.a2(a);s<=c;++s){q=r.i(a,s) +p=s +for(;;){if(!(p>b&&d.$2(r.i(a,p-1),q)>0))break +o=p-1 +r.m(a,p,r.i(a,o)) +p=o}r.m(a,p,q)}}, +Ai(a3,a4,a5,a6){var s,r,q,p,o,n,m,l,k,j,i=B.b.M(a5-a4+1,6),h=a4+i,g=a5-i,f=B.b.M(a4+a5,2),e=f-i,d=f+i,c=J.a2(a3),b=c.i(a3,h),a=c.i(a3,e),a0=c.i(a3,f),a1=c.i(a3,d),a2=c.i(a3,g) +if(a6.$2(b,a)>0){s=a +a=b +b=s}if(a6.$2(a1,a2)>0){s=a2 +a2=a1 +a1=s}if(a6.$2(b,a0)>0){s=a0 +a0=b +b=s}if(a6.$2(a,a0)>0){s=a0 +a0=a +a=s}if(a6.$2(b,a1)>0){s=a1 +a1=b +b=s}if(a6.$2(a0,a1)>0){s=a1 +a1=a0 +a0=s}if(a6.$2(a,a2)>0){s=a2 +a2=a +a=s}if(a6.$2(a,a0)>0){s=a0 +a0=a +a=s}if(a6.$2(a1,a2)>0){s=a2 +a2=a1 +a1=s}c.m(a3,h,b) +c.m(a3,f,a0) +c.m(a3,g,a2) +c.m(a3,e,c.i(a3,a4)) +c.m(a3,d,c.i(a3,a5)) +r=a4+1 +q=a5-1 +p=J.y(a6.$2(a,a1),0) +if(p)for(o=r;o<=q;++o){n=c.i(a3,o) +m=a6.$2(n,a) +if(m===0)continue +if(m<0){if(o!==r){c.m(a3,o,c.i(a3,r)) +c.m(a3,r,n)}++r}else for(;;){m=a6.$2(c.i(a3,q),a) +if(m>0){--q +continue}else{l=q-1 +if(m<0){c.m(a3,o,c.i(a3,r)) +k=r+1 +c.m(a3,r,c.i(a3,q)) +c.m(a3,q,n) +q=l +r=k +break}else{c.m(a3,o,c.i(a3,q)) +c.m(a3,q,n) +q=l +break}}}}else for(o=r;o<=q;++o){n=c.i(a3,o) +if(a6.$2(n,a)<0){if(o!==r){c.m(a3,o,c.i(a3,r)) +c.m(a3,r,n)}++r}else if(a6.$2(n,a1)>0)for(;;)if(a6.$2(c.i(a3,q),a1)>0){--q +if(qg){while(J.y(a6.$2(c.i(a3,r),a),0))++r +while(J.y(a6.$2(c.i(a3,q),a1),0))--q +for(o=r;o<=q;++o){n=c.i(a3,o) +if(a6.$2(n,a)===0){if(o!==r){c.m(a3,o,c.i(a3,r)) +c.m(a3,r,n)}++r}else if(a6.$2(n,a1)===0)for(;;)if(a6.$2(c.i(a3,q),a1)===0){--q +if(q65535)return A.A2(a)}return A.w5(a)}, +A3(a,b,c){var s,r,q,p +if(c<=500&&b===0&&c===a.length)return String.fromCharCode.apply(null,a) +for(s=b,r="";s>>0,s&1023|56320)}}throw A.a(A.a0(a,0,1114111,null,null))}, +aP(a){if(a.date===void 0)a.date=new Date(a.a) +return a.date}, +wc(a){return a.c?A.aP(a).getUTCFullYear()+0:A.aP(a).getFullYear()+0}, +wa(a){return a.c?A.aP(a).getUTCMonth()+1:A.aP(a).getMonth()+1}, +w7(a){return a.c?A.aP(a).getUTCDate()+0:A.aP(a).getDate()+0}, +w8(a){return a.c?A.aP(a).getUTCHours()+0:A.aP(a).getHours()+0}, +w9(a){return a.c?A.aP(a).getUTCMinutes()+0:A.aP(a).getMinutes()+0}, +wb(a){return a.c?A.aP(a).getUTCSeconds()+0:A.aP(a).getSeconds()+0}, +A0(a){return a.c?A.aP(a).getUTCMilliseconds()+0:A.aP(a).getMilliseconds()+0}, +A1(a){return B.b.aU((a.c?A.aP(a).getUTCDay()+0:A.aP(a).getDay()+0)+6,7)+1}, +A_(a){var s=a.$thrownJsError +if(s==null)return null +return A.N(s)}, +iP(a,b){var s +if(a.$thrownJsError==null){s=new Error() +A.ao(a,s) +a.$thrownJsError=s +s.stack=b.j(0)}}, +eJ(a,b){var s,r="index" +if(!A.eD(b))return new A.a3(!0,b,r,null) +s=J.ay(a) +if(b<0||b>=s)return A.ih(b,s,a,null,r) +return A.nF(b,r)}, +D6(a,b,c){if(a<0||a>c)return A.a0(a,0,c,"start",null) +if(b!=null)if(bc)return A.a0(b,a,c,"end",null) +return new A.a3(!0,b,"end",null)}, +dv(a){return new A.a3(!0,a,null,null)}, +a(a){return A.ao(a,new Error())}, +ao(a,b){var s +if(a==null)a=new A.c5() +b.dartException=a +s=A.DL +if("defineProperty" in Object){Object.defineProperty(b,"message",{get:s}) +b.name=""}else b.toString=s +return b}, +DL(){return J.aZ(this.dartException)}, +p(a,b){throw A.ao(a,b==null?new Error():b)}, +D(a,b,c){var s +if(b==null)b=0 +if(c==null)c=0 +s=Error() +A.p(A.BS(a,b,c),s)}, +BS(a,b,c){var s,r,q,p,o,n,m,l,k +if(typeof b=="string")s=b +else{r="[]=;add;removeWhere;retainWhere;removeRange;setRange;setInt8;setInt16;setInt32;setUint8;setUint16;setUint32;setFloat32;setFloat64".split(";") +q=r.length +p=b +if(p>q){c=p/q|0 +p%=q}s=r[p]}o=typeof c=="string"?c:"modify;remove from;add to".split(";")[c] +n=t.j.b(a)?"list":"ByteData" +m=a.$flags|0 +l="a " +if((m&4)!==0)k="constant " +else if((m&2)!==0){k="unmodifiable " +l="an "}else k=(m&1)!==0?"fixed-length ":"" +return new A.fO("'"+s+"': Cannot "+o+" "+l+k+n)}, +a9(a){throw A.a(A.am(a))}, +c6(a){var s,r,q,p,o,n +a=A.y7(a.replace(String({}),"$receiver$")) +s=a.match(/\\\$[a-zA-Z]+\\\$/g) +if(s==null)s=A.v([],t.s) +r=s.indexOf("\\$arguments\\$") +q=s.indexOf("\\$argumentsExpr\\$") +p=s.indexOf("\\$expr\\$") +o=s.indexOf("\\$method\\$") +n=s.indexOf("\\$receiver\\$") +return new A.oP(a.replace(new RegExp("\\\\\\$arguments\\\\\\$","g"),"((?:x|[^x])*)").replace(new RegExp("\\\\\\$argumentsExpr\\\\\\$","g"),"((?:x|[^x])*)").replace(new RegExp("\\\\\\$expr\\\\\\$","g"),"((?:x|[^x])*)").replace(new RegExp("\\\\\\$method\\\\\\$","g"),"((?:x|[^x])*)").replace(new RegExp("\\\\\\$receiver\\\\\\$","g"),"((?:x|[^x])*)"),r,q,p,o,n)}, +oQ(a){return function($expr$){var $argumentsExpr$="$arguments$" +try{$expr$.$method$($argumentsExpr$)}catch(s){return s.message}}(a)}, +wt(a){return function($expr$){try{$expr$.$method$}catch(s){return s.message}}(a)}, +uv(a,b){var s=b==null,r=s?null:b.method +return new A.ir(a,r,s?null:b.receiver)}, +H(a){if(a==null)return new A.iK(a) +if(a instanceof A.f_)return A.cG(a,a.a) +if(typeof a!=="object")return a +if("dartException" in a)return A.cG(a,a.dartException) +return A.CF(a)}, +cG(a,b){if(t.C.b(b))if(b.$thrownJsError==null)b.$thrownJsError=a +return b}, +CF(a){var s,r,q,p,o,n,m,l,k,j,i,h,g +if(!("message" in a))return a +s=a.message +if("number" in a&&typeof a.number=="number"){r=a.number +q=r&65535 +if((B.b.Y(r,16)&8191)===10)switch(q){case 438:return A.cG(a,A.uv(A.o(s)+" (Error "+q+")",null)) +case 445:case 5007:A.o(s) +return A.cG(a,new A.ft())}}if(a instanceof TypeError){p=$.yh() +o=$.yi() +n=$.yj() +m=$.yk() +l=$.yn() +k=$.yo() +j=$.ym() +$.yl() +i=$.yq() +h=$.yp() +g=p.b7(s) +if(g!=null)return A.cG(a,A.uv(s,g)) +else{g=o.b7(s) +if(g!=null){g.method="call" +return A.cG(a,A.uv(s,g))}else if(n.b7(s)!=null||m.b7(s)!=null||l.b7(s)!=null||k.b7(s)!=null||j.b7(s)!=null||m.b7(s)!=null||i.b7(s)!=null||h.b7(s)!=null)return A.cG(a,new A.ft())}return A.cG(a,new A.jj(typeof s=="string"?s:""))}if(a instanceof RangeError){if(typeof s=="string"&&s.indexOf("call stack")!==-1)return new A.fC() +s=function(b){try{return String(b)}catch(f){}return null}(a) +return A.cG(a,new A.a3(!1,null,null,typeof s=="string"?s.replace(/^RangeError:\s*/,""):s))}if(typeof InternalError=="function"&&a instanceof InternalError)if(typeof s=="string"&&s==="too much recursion")return new A.fC() +return a}, +N(a){var s +if(a instanceof A.f_)return a.b +if(a==null)return new A.hp(a) +s=a.$cachedTrace +if(s!=null)return s +s=new A.hp(a) +if(typeof a==="object")a.$cachedTrace=s +return s}, +kH(a){if(a==null)return J.z(a) +if(typeof a=="object")return A.fv(a) +return J.z(a)}, +Db(a,b){var s,r,q,p=a.length +for(s=0;s=0 +else if(b instanceof A.fd){s=B.a.X(a,c) +return b.b.test(s)}else return!J.yN(b,B.a.X(a,c)).gG(0)}, +D8(a){if(a.indexOf("$",0)>=0)return a.replace(/\$/g,"$$$$") +return a}, +y7(a){if(/[[\]{}()*+?.\\^$|]/.test(a))return a.replace(/[[\]{}()*+?.\\^$|]/g,"\\$&") +return a}, +hG(a,b,c){var s=A.DH(a,b,c) +return s}, +DH(a,b,c){var s,r,q +if(b===""){if(a==="")return c +s=a.length +for(r=c,q=0;q=0)return a.split(b).join(c) +return a.replace(new RegExp(A.y7(b),"g"),A.D8(c))}, +xL(a){return a}, +ya(a,b,c,d){var s,r,q,p,o,n,m +for(s=b.e6(0,a),s=new A.jB(s.a,s.b,s.c),r=t.lu,q=0,p="";s.l();){o=s.d +if(o==null)o=r.a(o) +n=o.b +m=n.index +p=p+A.o(A.xL(B.a.t(a,q,m)))+A.o(c.$1(o)) +q=m+n[0].length}s=p+A.o(A.xL(B.a.X(a,q))) +return s.charCodeAt(0)==0?s:s}, +DI(a,b,c,d){var s=a.indexOf(b,d) +if(s<0)return a +return A.yb(a,s,s+b.length,c)}, +yb(a,b,c,d){return a.substring(0,b)+d+a.substring(c)}, +hj:function hj(a){this.a=a}, +au:function au(a,b){this.a=a +this.b=b}, +hk:function hk(a,b){this.a=a +this.b=b}, +hl:function hl(a,b){this.a=a +this.b=b}, +k8:function k8(a,b){this.a=a +this.b=b}, +dj:function dj(a,b){this.a=a +this.b=b}, +k9:function k9(a,b){this.a=a +this.b=b}, +ka:function ka(a,b){this.a=a +this.b=b}, +hm:function hm(a,b,c){this.a=a +this.b=b +this.c=c}, +kb:function kb(a,b,c){this.a=a +this.b=b +this.c=c}, +kc:function kc(a,b,c){this.a=a +this.b=b +this.c=c}, +kd:function kd(a,b,c){this.a=a +this.b=b +this.c=c}, +ke:function ke(a){this.a=a}, +eS:function eS(){}, +lB:function lB(a,b,c){this.a=a +this.b=b +this.c=c}, +bw:function bw(a,b,c){this.a=a +this.b=b +this.$ti=c}, +hc:function hc(a,b){this.a=a +this.$ti=b}, +ek:function ek(a,b,c){var _=this +_.a=a +_.b=b +_.c=0 +_.d=null +_.$ti=c}, +eT:function eT(){}, +eU:function eU(a,b,c){this.a=a +this.b=b +this.$ti=c}, +n1:function n1(){}, +fb:function fb(a,b){this.a=a +this.$ti=b}, +fx:function fx(){}, +oP:function oP(a,b,c,d,e,f){var _=this +_.a=a +_.b=b +_.c=c +_.d=d +_.e=e +_.f=f}, +ft:function ft(){}, +ir:function ir(a,b,c){this.a=a +this.b=b +this.c=c}, +jj:function jj(a){this.a=a}, +iK:function iK(a){this.a=a}, +f_:function f_(a,b){this.a=a +this.b=b}, +hp:function hp(a){this.a=a +this.b=null}, +cK:function cK(){}, +lo:function lo(){}, +lp:function lp(){}, +oD:function oD(){}, +o6:function o6(){}, +eO:function eO(a,b){this.a=a +this.b=b}, +iW:function iW(a){this.a=a}, +b2:function b2(a){var _=this +_.a=0 +_.f=_.e=_.d=_.c=_.b=null +_.r=0 +_.$ti=a}, +na:function na(a){this.a=a}, +ne:function ne(a,b){var _=this +_.a=a +_.b=b +_.d=_.c=null}, +bx:function bx(a,b){this.a=a +this.$ti=b}, +fg:function fg(a,b,c){var _=this +_.a=a +_.b=b +_.c=c +_.d=null}, +bf:function bf(a,b){this.a=a +this.$ti=b}, +by:function by(a,b,c){var _=this +_.a=a +_.b=b +_.c=c +_.d=null}, +az:function az(a,b){this.a=a +this.$ti=b}, +iy:function iy(a,b,c,d){var _=this +_.a=a +_.b=b +_.c=c +_.d=null +_.$ti=d}, +fe:function fe(a){var _=this +_.a=0 +_.f=_.e=_.d=_.c=_.b=null +_.r=0 +_.$ti=a}, +tM:function tM(a){this.a=a}, +tN:function tN(a){this.a=a}, +tO:function tO(a){this.a=a}, +di:function di(){}, +k5:function k5(){}, +k4:function k4(){}, +k6:function k6(){}, +k7:function k7(){}, +fd:function fd(a,b){var _=this +_.a=a +_.b=b +_.e=_.d=_.c=null}, +en:function en(a){this.b=a}, +jA:function jA(a,b,c){this.a=a +this.b=b +this.c=c}, +jB:function jB(a,b,c){var _=this +_.a=a +_.b=b +_.c=c +_.d=null}, +fI:function fI(a,b){this.a=a +this.c=b}, +kp:function kp(a,b,c){this.a=a +this.b=b +this.c=c}, +rs:function rs(a,b,c){var _=this +_.a=a +_.b=b +_.c=c +_.d=null}, +DJ(a){throw A.ao(A.vY(a),new Error())}, +B(){throw A.ao(A.vZ(""),new Error())}, +ua(){throw A.ao(A.zH(""),new Error())}, +vm(){throw A.ao(A.vY(""),new Error())}, +uT(){var s=new A.jK("") +return s.b=s}, +q5(a){var s=new A.jK(a) +return s.b=s}, +jK:function jK(a){this.a=a +this.b=null}, +kD(a,b,c){}, +xp(a){return a}, +zS(a){return new DataView(new ArrayBuffer(a))}, +zT(a,b,c){var s +A.kD(a,b,c) +s=new DataView(a,b) +return s}, +c1(a,b,c){A.kD(a,b,c) +c=B.b.M(a.byteLength-b,4) +return new Int32Array(a,b,c)}, +zU(a){return new Int8Array(a)}, +zV(a,b,c){A.kD(a,b,c) +return new Uint32Array(a,b,c)}, +zW(a){return new Uint8Array(a)}, +bg(a,b,c){A.kD(a,b,c) +return c==null?new Uint8Array(a,b):new Uint8Array(a,b,c)}, +cd(a,b,c){if(a>>>0!==a||a>=c)throw A.a(A.eJ(b,a))}, +xl(a,b,c){var s +if(!(a>>>0!==a))s=b>>>0!==b||a>b||b>c +else s=!0 +if(s)throw A.a(A.D6(a,b,c)) +return b}, +dX:function dX(){}, +dW:function dW(){}, +fp:function fp(){}, +kx:function kx(a){this.a=a}, +cS:function cS(){}, +dZ:function dZ(){}, +co:function co(){}, +b4:function b4(){}, +iC:function iC(){}, +iD:function iD(){}, +iE:function iE(){}, +dY:function dY(){}, +iF:function iF(){}, +iG:function iG(){}, +fq:function fq(){}, +fr:function fr(){}, +cT:function cT(){}, +hf:function hf(){}, +hg:function hg(){}, +hh:function hh(){}, +hi:function hi(){}, +uA(a,b){var s=b.c +return s==null?b.c=A.hu(a,"r",[b.x]):s}, +wi(a){var s=a.w +if(s===6||s===7)return A.wi(a.x) +return s===11||s===12}, +Ad(a){return a.as}, +Dx(a,b){var s,r=b.length +for(s=0;s") +for(r=1;r=0)p+=" "+r[q];++q}return p+"})"}, +xs(a1,a2,a3){var s,r,q,p,o,n,m,l,k,j,i,h,g,f,e,d,c,b,a=", ",a0=null +if(a3!=null){s=a3.length +if(a2==null)a2=A.v([],t.s) +else a0=a2.length +r=a2.length +for(q=s;q>0;--q)a2.push("T"+(r+q)) +for(p=t.X,o="<",n="",q=0;q0){c+=b+"[" +for(b="",q=0;q0){c+=b+"{" +for(b="",q=0;q "+d}, +bb(a,b){var s,r,q,p,o,n,m=a.w +if(m===5)return"erased" +if(m===2)return"dynamic" +if(m===3)return"void" +if(m===1)return"Never" +if(m===4)return"any" +if(m===6){s=a.x +r=A.bb(s,b) +q=s.w +return(q===11||q===12?"("+r+")":r)+"?"}if(m===7)return"FutureOr<"+A.bb(a.x,b)+">" +if(m===8){p=A.CE(a.x) +o=a.y +return o.length>0?p+("<"+A.xH(o,b)+">"):p}if(m===10)return A.Cn(a,b) +if(m===11)return A.xs(a,b,null) +if(m===12)return A.xs(a.x,b,a.y) +if(m===13){n=a.x +return b[b.length-1-n]}return"?"}, +CE(a){var s=v.mangledGlobalNames[a] +if(s!=null)return s +return"minified:"+a}, +Bq(a,b){var s=a.tR[b] +while(typeof s=="string")s=a.tR[s] +return s}, +Bp(a,b){var s,r,q,p,o,n=a.eT,m=n[b] +if(m==null)return A.rH(a,b,!1) +else if(typeof m=="number"){s=m +r=A.hv(a,5,"#") +q=A.rQ(s) +for(p=0;p0)p+="<"+A.ht(c)+">" +s=a.eC.get(p) +if(s!=null)return s +r=new A.bA(null,null) +r.w=8 +r.x=b +r.y=c +if(c.length>0)r.c=c[0] +r.as=p +q=A.cC(a,r) +a.eC.set(p,q) +return q}, +uX(a,b,c){var s,r,q,p,o,n +if(b.w===9){s=b.x +r=b.y.concat(c)}else{r=c +s=b}q=s.as+(";<"+A.ht(r)+">") +p=a.eC.get(q) +if(p!=null)return p +o=new A.bA(null,null) +o.w=9 +o.x=s +o.y=r +o.as=q +n=A.cC(a,o) +a.eC.set(q,n) +return n}, +wZ(a,b,c){var s,r,q="+"+(b+"("+A.ht(c)+")"),p=a.eC.get(q) +if(p!=null)return p +s=new A.bA(null,null) +s.w=10 +s.x=b +s.y=c +s.as=q +r=A.cC(a,s) +a.eC.set(q,r) +return r}, +wW(a,b,c){var s,r,q,p,o,n=b.as,m=c.a,l=m.length,k=c.b,j=k.length,i=c.c,h=i.length,g="("+A.ht(m) +if(j>0){s=l>0?",":"" +g+=s+"["+A.ht(k)+"]"}if(h>0){s=l>0?",":"" +g+=s+"{"+A.Bi(i)+"}"}r=n+(g+")") +q=a.eC.get(r) +if(q!=null)return q +p=new A.bA(null,null) +p.w=11 +p.x=b +p.y=c +p.as=r +o=A.cC(a,p) +a.eC.set(r,o) +return o}, +uY(a,b,c,d){var s,r=b.as+("<"+A.ht(c)+">"),q=a.eC.get(r) +if(q!=null)return q +s=A.Bk(a,b,c,r,d) +a.eC.set(r,s) +return s}, +Bk(a,b,c,d,e){var s,r,q,p,o,n,m,l +if(e){s=c.length +r=A.rQ(s) +for(q=0,p=0;p0){n=A.cE(a,b,r,0) +m=A.eH(a,c,r,0) +return A.uY(a,n,m,c!==m)}}l=new A.bA(null,null) +l.w=12 +l.x=b +l.y=c +l.as=d +return A.cC(a,l)}, +wQ(a,b,c,d){return{u:a,e:b,r:c,s:[],p:0,n:d}}, +wS(a){var s,r,q,p,o,n,m,l=a.r,k=a.s +for(s=l.length,r=0;r=48&&q<=57)r=A.B8(r+1,q,l,k) +else if((((q|32)>>>0)-97&65535)<26||q===95||q===36||q===124)r=A.wR(a,r,l,k,!1) +else if(q===46)r=A.wR(a,r,l,k,!0) +else{++r +switch(q){case 44:break +case 58:k.push(!1) +break +case 33:k.push(!0) +break +case 59:k.push(A.dh(a.u,a.e,k.pop())) +break +case 94:k.push(A.Bm(a.u,k.pop())) +break +case 35:k.push(A.hv(a.u,5,"#")) +break +case 64:k.push(A.hv(a.u,2,"@")) +break +case 126:k.push(A.hv(a.u,3,"~")) +break +case 60:k.push(a.p) +a.p=k.length +break +case 62:A.Ba(a,k) +break +case 38:A.B9(a,k) +break +case 63:p=a.u +k.push(A.wY(p,A.dh(p,a.e,k.pop()),a.n)) +break +case 47:p=a.u +k.push(A.wX(p,A.dh(p,a.e,k.pop()),a.n)) +break +case 40:k.push(-3) +k.push(a.p) +a.p=k.length +break +case 41:A.B7(a,k) +break +case 91:k.push(a.p) +a.p=k.length +break +case 93:o=k.splice(a.p) +A.wT(a.u,a.e,o) +a.p=k.pop() +k.push(o) +k.push(-1) +break +case 123:k.push(a.p) +a.p=k.length +break +case 125:o=k.splice(a.p) +A.Bc(a.u,a.e,o) +a.p=k.pop() +k.push(o) +k.push(-2) +break +case 43:n=l.indexOf("(",r) +k.push(l.substring(r,n)) +k.push(-4) +k.push(a.p) +a.p=k.length +r=n+1 +break +default:throw"Bad character "+q}}}m=k.pop() +return A.dh(a.u,a.e,m)}, +B8(a,b,c,d){var s,r,q=b-48 +for(s=c.length;a=48&&r<=57))break +q=q*10+(r-48)}d.push(q) +return a}, +wR(a,b,c,d,e){var s,r,q,p,o,n,m=b+1 +for(s=c.length;m>>0)-97&65535)<26||r===95||r===36||r===124))q=r>=48&&r<=57 +else q=!0 +if(!q)break}}p=c.substring(b,m) +if(e){s=a.u +o=a.e +if(o.w===9)o=o.x +n=A.Bq(s,o.x)[p] +if(n==null)A.p('No "'+p+'" in "'+A.Ad(o)+'"') +d.push(A.hw(s,o,n))}else d.push(p) +return m}, +Ba(a,b){var s,r=a.u,q=A.wP(a,b),p=b.pop() +if(typeof p=="string")b.push(A.hu(r,p,q)) +else{s=A.dh(r,a.e,p) +switch(s.w){case 11:b.push(A.uY(r,s,q,a.n)) +break +default:b.push(A.uX(r,s,q)) +break}}}, +B7(a,b){var s,r,q,p=a.u,o=b.pop(),n=null,m=null +if(typeof o=="number")switch(o){case-1:n=b.pop() +break +case-2:m=b.pop() +break +default:b.push(o) +break}else b.push(o) +s=A.wP(a,b) +o=b.pop() +switch(o){case-3:o=b.pop() +if(n==null)n=p.sEA +if(m==null)m=p.sEA +r=A.dh(p,a.e,o) +q=new A.jT() +q.a=s +q.b=n +q.c=m +b.push(A.wW(p,r,q)) +return +case-4:b.push(A.wZ(p,b.pop(),s)) +return +default:throw A.a(A.hR("Unexpected state under `()`: "+A.o(o)))}}, +B9(a,b){var s=b.pop() +if(0===s){b.push(A.hv(a.u,1,"0&")) +return}if(1===s){b.push(A.hv(a.u,4,"1&")) +return}throw A.a(A.hR("Unexpected extended operation "+A.o(s)))}, +wP(a,b){var s=b.splice(a.p) +A.wT(a.u,a.e,s) +a.p=b.pop() +return s}, +dh(a,b,c){if(typeof c=="string")return A.hu(a,c,a.sEA) +else if(typeof c=="number"){b.toString +return A.Bb(a,b,c)}else return c}, +wT(a,b,c){var s,r=c.length +for(s=0;sn)return!1 +m=n-o +l=s.b +k=r.b +j=l.length +i=k.length +if(o+j=d)return!1 +a1=f[b] +b+=3 +if(a00?new Array(q):v.typeUniverse.sEA +for(o=0;o0?new Array(a):v.typeUniverse.sEA}, +bA:function bA(a,b){var _=this +_.a=a +_.b=b +_.r=_.f=_.d=_.c=null +_.w=0 +_.as=_.Q=_.z=_.y=_.x=null}, +jT:function jT(){this.c=this.b=this.a=null}, +rF:function rF(a){this.a=a}, +jP:function jP(){}, +hs:function hs(a){this.a=a}, +AF(){var s,r,q +if(self.scheduleImmediate!=null)return A.CG() +if(self.MutationObserver!=null&&self.document!=null){s={} +r=self.document.createElement("div") +q=self.document.createElement("span") +s.a=null +new self.MutationObserver(A.cF(new A.pM(s),1)).observe(r,{childList:true}) +return new A.pL(s,r,q)}else if(self.setImmediate!=null)return A.CH() +return A.CI()}, +AG(a){self.scheduleImmediate(A.cF(new A.pN(a),0))}, +AH(a){self.setImmediate(A.cF(new A.pO(a),0))}, +AI(a){A.uF(B.a2,a)}, +uF(a,b){var s=B.b.M(a.a,1000) +return A.Bg(s<0?0:s,b)}, +Bg(a,b){var s=new A.kt(!0) +s.kA(a,b) +return s}, +Bh(a,b){var s=new A.kt(!1) +s.kB(a,b) +return s}, +j(a){return new A.h_(new A.l($.n,a.h("l<0>")),a.h("h_<0>"))}, +i(a,b){a.$2(0,null) +b.b=!0 +return b.a}, +c(a,b){A.xj(a,b)}, +h(a,b){b.W(a)}, +f(a,b){b.b6(A.H(a),A.N(a))}, +xj(a,b){var s,r,q=new A.rV(b),p=new A.rW(b) +if(a instanceof A.l)a.iD(q,p,t.z) +else{s=t.z +if(a instanceof A.l)a.b9(q,p,s) +else{r=new A.l($.n,t._) +r.a=8 +r.c=a +r.iD(q,p,s)}}}, +e(a){var s=function(b,c){return function(d,e){while(true){try{b(d,e) +break}catch(r){e=r +d=c}}}}(a,1) +return $.n.cD(new A.tv(s),t.H,t.S,t.z)}, +kC(a,b,c){var s,r,q,p +if(b===0){s=c.c +if(s!=null)s.bS(null) +else{s=c.a +s===$&&A.B() +s.n()}return}else if(b===1){s=c.c +if(s!=null){r=A.H(a) +q=A.N(a) +s.a7(new A.a6(r,q))}else{s=A.H(a) +r=A.N(a) +q=c.a +q===$&&A.B() +q.a2(s,r) +c.a.n()}return}if(a instanceof A.hb){if(c.c!=null){b.$2(2,null) +return}s=a.b +if(s===0){s=a.a +r=c.a +r===$&&A.B() +r.q(0,s) +A.eM(new A.rT(c,b)) +return}else if(s===1){p=a.a +s=c.a +s===$&&A.B() +s.e5(p,!1).b8(new A.rU(c,b),t.P) +return}}A.xj(a,b)}, +Cy(a){var s=a.a +s===$&&A.B() +return new A.O(s,A.q(s).h("O<1>"))}, +AJ(a,b){var s=new A.jD(b.h("jD<0>")) +s.kv(a,b) +return s}, +Ce(a,b){return A.AJ(a,b)}, +B1(a){return new A.hb(a,1)}, +wN(a){return new A.hb(a,0)}, +wV(a,b,c){return 0}, +cI(a){var s +if(t.C.b(a)){s=a.gcf() +if(s!=null)return s}return B.r}, +un(a,b){var s=new A.l($.n,b.h("l<0>")) +A.oO(B.a2,new A.mv(a,s)) +return s}, +dO(a,b){var s,r,q,p,o,n,m,l=null +try{l=a.$0()}catch(q){s=A.H(q) +r=A.N(q) +p=new A.l($.n,b.h("l<0>")) +o=s +n=r +m=A.ds(o,n) +if(m==null)o=new A.a6(o,n==null?A.cI(o):n) +else o=m +p.R(o) +return p}return b.h("r<0>").b(l)?l:A.h8(l,b)}, +mu(a,b){var s +b.a(a) +s=new A.l($.n,b.h("l<0>")) +s.aB(a) +return s}, +ms(a,b){var s +if(!b.b(null))throw A.a(A.aH(null,"computation","The type parameter is not nullable")) +s=new A.l($.n,b.h("l<0>")) +A.oO(a,new A.mt(null,s,b)) +return s}, +f5(a,b){var s,r,q,p,o,n,m,l,k,j,i={},h=null,g=!1,f=new A.l($.n,b.h("l>")) +i.a=null +i.b=0 +i.c=i.d=null +s=new A.mz(i,h,g,f) +try{for(n=J.U(a),m=t.P;n.l();){r=n.gp() +q=i.b +r.b9(new A.my(i,q,f,b,h,g),s,m);++i.b}n=i.b +if(n===0){n=f +n.bS(A.v([],b.h("A<0>"))) +return n}i.a=A.aW(n,null,!1,b.h("0?"))}catch(l){p=A.H(l) +o=A.N(l) +if(i.b===0||g){n=f +m=p +k=o +j=A.ds(m,k) +if(j==null)m=new A.a6(m,k==null?A.cI(m):k) +else m=j +n.R(m) +return n}else{i.d=p +i.c=o}}return f}, +zp(a,b){var s,r,q=new A.l($.n,b.h("l<0>")),p=new A.M(q,b.h("M<0>")),o=new A.mx(p,b),n=new A.mw(p) +for(s=t.H,r=0;r<2;++r)a[r].b9(o,n,s) +return q}, +mn(a,b,c,d){var s=new A.mo(d,null,b,c),r=$.n,q=new A.l(r,c.h("l<0>")) +if(r!==B.e)s=r.cD(s,c.h("0/"),t.K,t.l) +a.cj(new A.bl(q,2,null,s,a.$ti.h("@<1>").J(c).h("bl<1,2>"))) +return q}, +ds(a,b){var s,r,q,p=$.n +if(p===B.e)return null +s=p.j_(a,b) +if(s==null)return null +r=s.a +q=s.b +if(t.C.b(r))A.iP(r,q) +return s}, +aw(a,b){var s +if($.n!==B.e){s=A.ds(a,b) +if(s!=null)return s}if(b==null)if(t.C.b(a)){b=a.gcf() +if(b==null){A.iP(a,B.r) +b=B.r}}else b=B.r +else if(t.C.b(a))A.iP(a,b) +return new A.a6(a,b)}, +AX(a,b,c){var s=new A.l(b,c.h("l<0>")) +s.a=8 +s.c=a +return s}, +h8(a,b){var s=new A.l($.n,b.h("l<0>")) +s.a=8 +s.c=a +return s}, +qJ(a,b,c){var s,r,q,p={},o=p.a=a +while(s=o.a,(s&4)!==0){o=o.c +p.a=o}if(o===b){s=A.fD() +b.R(new A.a6(new A.a3(!0,o,null,"Cannot complete a future with itself"),s)) +return}r=b.a&1 +s=o.a=s|r +if((s&24)===0){q=b.c +b.a=b.a&1|4 +b.c=o +o.ig(q) +return}if(!c)if(b.c==null)o=(s&16)===0||r!==0 +else o=!1 +else o=!0 +if(o){q=b.cY() +b.dK(p.a) +A.dg(b,q) +return}b.a^=2 +b.b.bN(new A.qK(p,b))}, +dg(a,b){var s,r,q,p,o,n,m,l,k,j,i,h,g={},f=g.a=a +for(;;){s={} +r=f.a +q=(r&16)===0 +p=!q +if(b==null){if(p&&(r&1)===0){r=f.c +f.b.cq(r.a,r.b)}return}s.a=b +o=b.a +for(f=b;o!=null;f=o,o=n){f.a=null +A.dg(g.a,f) +s.a=o +n=o.a}r=g.a +m=r.c +s.b=p +s.c=m +if(q){l=f.c +l=(l&1)!==0||(l&15)===8}else l=!0 +if(l){k=f.b.b +if(p){f=r.b +f=!(f===k||f.gbi()===k.gbi())}else f=!1 +if(f){f=g.a +r=f.c +f.b.cq(r.a,r.b) +return}j=$.n +if(j!==k)$.n=k +else j=null +f=s.a.c +if((f&15)===8)new A.qO(s,g,p).$0() +else if(q){if((f&1)!==0)new A.qN(s,m).$0()}else if((f&2)!==0)new A.qM(g,s).$0() +if(j!=null)$.n=j +f=s.c +if(f instanceof A.l){r=s.a.$ti +r=r.h("r<2>").b(f)||!r.y[1].b(f)}else r=!1 +if(r){i=s.a.b +if((f.a&24)!==0){h=i.c +i.c=null +b=i.dP(h) +i.a=f.a&30|i.a&1 +i.c=f.c +g.a=f +continue}else A.qJ(f,i,!0) +return}}i=s.a.b +h=i.c +i.c=null +b=i.dP(h) +f=s.b +r=s.c +if(!f){i.a=8 +i.c=r}else{i.a=i.a&1|16 +i.c=r}g.a=i +f=i}}, +xB(a,b){if(t.d.b(a))return b.cD(a,t.z,t.K,t.l) +if(t.mq.b(a))return b.bo(a,t.z,t.K) +throw A.a(A.aH(a,"onError",u.w))}, +Cg(){var s,r +for(s=$.eF;s!=null;s=$.eF){$.hD=null +r=s.b +$.eF=r +if(r==null)$.hC=null +s.a.$0()}}, +Cx(){$.v8=!0 +try{A.Cg()}finally{$.hD=null +$.v8=!1 +if($.eF!=null)$.vq().$1(A.xQ())}}, +xJ(a){var s=new A.jC(a),r=$.hC +if(r==null){$.eF=$.hC=s +if(!$.v8)$.vq().$1(A.xQ())}else $.hC=r.b=s}, +Cu(a){var s,r,q,p=$.eF +if(p==null){A.xJ(a) +$.hD=$.hC +return}s=new A.jC(a) +r=$.hD +if(r==null){s.b=p +$.eF=$.hD=s}else{q=r.b +s.b=q +$.hD=r.b=s +if(q==null)$.hC=s}}, +eM(a){var s,r=null,q=$.n +if(B.e===q){A.ti(r,r,B.e,a) +return}if(B.e===q.gfE().a)s=B.e.gbi()===q.gbi() +else s=!1 +if(s){A.ti(r,r,q,q.b0(a,t.H)) +return}s=$.n +s.bN(s.e8(a))}, +E2(a){return new A.bU(A.bd(a,"stream",t.K))}, +bi(a,b,c,d,e,f){return e?new A.cB(b,c,d,a,f.h("cB<0>")):new A.bT(b,c,d,a,f.h("bT<0>"))}, +cY(a,b){var s=null +return a?new A.dl(s,s,b.h("dl<0>")):new A.h0(s,s,b.h("h0<0>"))}, +kE(a){var s,r,q +if(a==null)return +try{a.$0()}catch(q){s=A.H(q) +r=A.N(q) +$.n.cq(s,r)}}, +AV(a,b,c,d,e,f){var s=$.n,r=e?1:0,q=c!=null?32:0,p=A.jG(s,b,f),o=A.jH(s,c),n=d==null?A.tw():d +return new A.cx(a,p,o,s.b0(n,t.H),s,r|q,f.h("cx<0>"))}, +AD(a,b,c){var s=$.n,r=a.geW(),q=a.gdI() +return new A.fZ(new A.l(s,t._),b.A(r,!1,a.gf2(),q))}, +AE(a){return new A.pJ(a)}, +jG(a,b,c){var s=b==null?A.CJ():b +return a.bo(s,t.H,c)}, +jH(a,b){if(b==null)b=A.CK() +if(t.v.b(b))return a.cD(b,t.z,t.K,t.l) +if(t.i6.b(b))return a.bo(b,t.z,t.K) +throw A.a(A.K(u.y,null))}, +Ch(a){}, +Cj(a,b){$.n.cq(a,b)}, +Ci(){}, +wJ(a,b){var s=$.n,r=new A.ef(s,b.h("ef<0>")) +A.eM(r.gic()) +if(a!=null)r.c=s.b0(a,t.H) +return r}, +Ct(a,b,c){var s,r,q,p +try{b.$1(a.$0())}catch(p){s=A.H(p) +r=A.N(p) +q=A.ds(s,r) +if(q!=null)c.$2(q.a,q.b) +else c.$2(s,r)}}, +BL(a,b,c){var s=a.u() +if(s!==$.cH())s.O(new A.rZ(b,c)) +else b.a7(c)}, +BM(a,b){return new A.rY(a,b)}, +BN(a,b,c){var s=a.u() +if(s!==$.cH())s.O(new A.t_(b,c)) +else b.bd(c)}, +xe(a,b,c){var s=A.ds(b,c) +if(s!=null){b=s.a +c=s.b}a.au(b,c)}, +oO(a,b){var s=$.n +if(s===B.e)return s.fV(a,b) +return s.fV(a,s.e8(b))}, +Cr(a,b,c,d,e){A.hE(d,e)}, +hE(a,b){A.Cu(new A.te(a,b))}, +tf(a,b,c,d){var s,r=$.n +if(r===c)return d.$0() +$.n=c +s=r +try{r=d.$0() +return r}finally{$.n=s}}, +th(a,b,c,d,e){var s,r=$.n +if(r===c)return d.$1(e) +$.n=c +s=r +try{r=d.$1(e) +return r}finally{$.n=s}}, +tg(a,b,c,d,e,f){var s,r=$.n +if(r===c)return d.$2(e,f) +$.n=c +s=r +try{r=d.$2(e,f) +return r}finally{$.n=s}}, +xF(a,b,c,d){return d}, +xG(a,b,c,d){return d}, +xE(a,b,c,d){return d}, +Cq(a,b,c,d,e){return null}, +ti(a,b,c,d){var s,r +if(B.e!==c){s=B.e.gbi() +r=c.gbi() +d=s!==r?c.e8(d):c.fQ(d,t.H)}A.xJ(d)}, +Cp(a,b,c,d,e){return A.uF(d,B.e!==c?c.fQ(e,t.H):e)}, +Co(a,b,c,d,e){var s +if(B.e!==c)e=c.iQ(e,t.H,t.hU) +s=B.b.M(d.a,1000) +return A.Bh(s<0?0:s,e)}, +Cs(a,b,c,d){A.vk(d)}, +Ck(a){$.n.jp(a)}, +xD(a,b,c,d,e){var s,r,q,p,o,n,m,l,k,j,i,h,g,f +$.y5=A.CL() +if(e==null)s=c.gi9() +else{r=t.X +s=A.zq(e,r,r)}r=c.git() +q=c.giv() +p=c.giu() +o=c.gio() +n=c.gip() +m=c.gim() +l=c.ghU() +k=c.gfE() +j=c.ghO() +i=c.ghN() +h=c.gih() +g=c.ghZ() +f=c.gfq() +return new A.jM(r,q,p,o,n,m,l,k,j,i,h,g,f,c,s)}, +pM:function pM(a){this.a=a}, +pL:function pL(a,b,c){this.a=a +this.b=b +this.c=c}, +pN:function pN(a){this.a=a}, +pO:function pO(a){this.a=a}, +kt:function kt(a){this.a=a +this.b=null +this.c=0}, +rE:function rE(a,b){this.a=a +this.b=b}, +rD:function rD(a,b,c,d){var _=this +_.a=a +_.b=b +_.c=c +_.d=d}, +h_:function h_(a,b){this.a=a +this.b=!1 +this.$ti=b}, +rV:function rV(a){this.a=a}, +rW:function rW(a){this.a=a}, +tv:function tv(a){this.a=a}, +rT:function rT(a,b){this.a=a +this.b=b}, +rU:function rU(a,b){this.a=a +this.b=b}, +jD:function jD(a){var _=this +_.a=$ +_.b=!1 +_.c=null +_.$ti=a}, +pQ:function pQ(a){this.a=a}, +pR:function pR(a){this.a=a}, +pT:function pT(a){this.a=a}, +pU:function pU(a,b){this.a=a +this.b=b}, +pS:function pS(a,b){this.a=a +this.b=b}, +pP:function pP(a){this.a=a}, +hb:function hb(a,b){this.a=a +this.b=b}, +kr:function kr(a){var _=this +_.a=a +_.e=_.d=_.c=_.b=null}, +ex:function ex(a,b){this.a=a +this.$ti=b}, +a6:function a6(a,b){this.a=a +this.b=b}, +aJ:function aJ(a,b){this.a=a +this.$ti=b}, +d8:function d8(a,b,c,d,e,f,g){var _=this +_.ay=0 +_.CW=_.ch=null +_.w=a +_.a=b +_.b=c +_.c=d +_.d=e +_.e=f +_.r=_.f=null +_.$ti=g}, +c8:function c8(){}, +dl:function dl(a,b,c){var _=this +_.a=a +_.b=b +_.c=0 +_.r=_.f=_.e=_.d=null +_.$ti=c}, +ru:function ru(a,b){this.a=a +this.b=b}, +rw:function rw(a,b,c){this.a=a +this.b=b +this.c=c}, +rv:function rv(a){this.a=a}, +h0:function h0(a,b,c){var _=this +_.a=a +_.b=b +_.c=0 +_.r=_.f=_.e=_.d=null +_.$ti=c}, +mv:function mv(a,b){this.a=a +this.b=b}, +mt:function mt(a,b,c){this.a=a +this.b=b +this.c=c}, +mz:function mz(a,b,c,d){var _=this +_.a=a +_.b=b +_.c=c +_.d=d}, +my:function my(a,b,c,d,e,f){var _=this +_.a=a +_.b=b +_.c=c +_.d=d +_.e=e +_.f=f}, +mx:function mx(a,b){this.a=a +this.b=b}, +mw:function mw(a){this.a=a}, +mo:function mo(a,b,c,d){var _=this +_.a=a +_.b=b +_.c=c +_.d=d}, +d9:function d9(){}, +as:function as(a,b){this.a=a +this.$ti=b}, +M:function M(a,b){this.a=a +this.$ti=b}, +bl:function bl(a,b,c,d,e){var _=this +_.a=null +_.b=a +_.c=b +_.d=c +_.e=d +_.$ti=e}, +l:function l(a,b){var _=this +_.a=0 +_.b=a +_.c=null +_.$ti=b}, +qG:function qG(a,b){this.a=a +this.b=b}, +qL:function qL(a,b){this.a=a +this.b=b}, +qK:function qK(a,b){this.a=a +this.b=b}, +qI:function qI(a,b){this.a=a +this.b=b}, +qH:function qH(a,b){this.a=a +this.b=b}, +qO:function qO(a,b,c){this.a=a +this.b=b +this.c=c}, +qP:function qP(a,b){this.a=a +this.b=b}, +qQ:function qQ(a){this.a=a}, +qN:function qN(a,b){this.a=a +this.b=b}, +qM:function qM(a,b){this.a=a +this.b=b}, +qR:function qR(a,b,c,d){var _=this +_.a=a +_.b=b +_.c=c +_.d=d}, +qS:function qS(a,b,c){this.a=a +this.b=b +this.c=c}, +qT:function qT(a,b){this.a=a +this.b=b}, +jC:function jC(a){this.a=a +this.b=null}, +G:function G(){}, +od:function od(a,b,c){this.a=a +this.b=b +this.c=c}, +oc:function oc(a,b,c,d){var _=this +_.a=a +_.b=b +_.c=c +_.d=d}, +oi:function oi(a,b){this.a=a +this.b=b}, +oj:function oj(a,b,c,d,e,f){var _=this +_.a=a +_.b=b +_.c=c +_.d=d +_.e=e +_.f=f}, +og:function og(a,b,c,d){var _=this +_.a=a +_.b=b +_.c=c +_.d=d}, +oh:function oh(a,b){this.a=a +this.b=b}, +ok:function ok(a,b){this.a=a +this.b=b}, +ol:function ol(a,b){this.a=a +this.b=b}, +oe:function oe(a){this.a=a}, +of:function of(a,b,c){this.a=a +this.b=b +this.c=c}, +fH:function fH(){}, +jc:function jc(){}, +cz:function cz(){}, +ro:function ro(a){this.a=a}, +rn:function rn(a){this.a=a}, +ks:function ks(){}, +jE:function jE(){}, +bT:function bT(a,b,c,d,e){var _=this +_.a=null +_.b=0 +_.c=null +_.d=a +_.e=b +_.f=c +_.r=d +_.$ti=e}, +cB:function cB(a,b,c,d,e){var _=this +_.a=null +_.b=0 +_.c=null +_.d=a +_.e=b +_.f=c +_.r=d +_.$ti=e}, +O:function O(a,b){this.a=a +this.$ti=b}, +cx:function cx(a,b,c,d,e,f,g){var _=this +_.w=a +_.a=b +_.b=c +_.c=d +_.d=e +_.e=f +_.r=_.f=null +_.$ti=g}, +ev:function ev(a){this.a=a}, +fZ:function fZ(a,b){this.a=a +this.b=b}, +pJ:function pJ(a){this.a=a}, +pI:function pI(a){this.a=a}, +ko:function ko(a,b,c){this.c=a +this.a=b +this.b=c}, +at:function at(){}, +q2:function q2(a,b,c){this.a=a +this.b=b +this.c=c}, +q1:function q1(a){this.a=a}, +eu:function eu(){}, +jO:function jO(){}, +c9:function c9(a){this.b=a +this.a=null}, +ed:function ed(a,b){this.b=a +this.c=b +this.a=null}, +qy:function qy(){}, +er:function er(){this.a=0 +this.c=this.b=null}, +r8:function r8(a,b){this.a=a +this.b=b}, +ef:function ef(a,b){var _=this +_.a=1 +_.b=a +_.c=null +_.$ti=b}, +bU:function bU(a){this.a=null +this.b=a +this.c=!1}, +de:function de(a){this.$ti=a}, +bH:function bH(a,b,c){this.a=a +this.b=b +this.$ti=c}, +r7:function r7(a,b){this.a=a +this.b=b}, +he:function he(a,b,c,d,e){var _=this +_.a=null +_.b=0 +_.c=null +_.d=a +_.e=b +_.f=c +_.r=d +_.$ti=e}, +rZ:function rZ(a,b){this.a=a +this.b=b}, +rY:function rY(a,b){this.a=a +this.b=b}, +t_:function t_(a,b){this.a=a +this.b=b}, +b9:function b9(){}, +ej:function ej(a,b,c,d,e,f,g){var _=this +_.w=a +_.x=null +_.a=b +_.b=c +_.c=d +_.d=e +_.e=f +_.r=_.f=null +_.$ti=g}, +dq:function dq(a,b,c){this.b=a +this.a=b +this.$ti=c}, +bG:function bG(a,b,c){this.b=a +this.a=b +this.$ti=c}, +h7:function h7(a){this.a=a}, +es:function es(a,b,c,d,e,f){var _=this +_.w=$ +_.x=null +_.a=a +_.b=b +_.c=c +_.d=d +_.e=e +_.r=_.f=null +_.$ti=f}, +c7:function c7(a,b,c){this.a=a +this.b=b +this.$ti=c}, +kn:function kn(a){this.a=a}, +aN:function aN(a,b){this.a=a +this.b=b}, +kA:function kA(){}, +jM:function jM(a,b,c,d,e,f,g,h,i,j,k,l,m,n,o){var _=this +_.a=a +_.b=b +_.c=c +_.d=d +_.e=e +_.f=f +_.r=g +_.w=h +_.x=i +_.y=j +_.z=k +_.Q=l +_.as=m +_.at=null +_.ax=n +_.ay=o}, +qs:function qs(a,b,c){this.a=a +this.b=b +this.c=c}, +qu:function qu(a,b,c,d){var _=this +_.a=a +_.b=b +_.c=c +_.d=d}, +qr:function qr(a,b){this.a=a +this.b=b}, +qt:function qt(a,b,c){this.a=a +this.b=b +this.c=c}, +kj:function kj(){}, +rc:function rc(a,b,c){this.a=a +this.b=b +this.c=c}, +re:function re(a,b,c,d){var _=this +_.a=a +_.b=b +_.c=c +_.d=d}, +rb:function rb(a,b){this.a=a +this.b=b}, +rd:function rd(a,b,c){this.a=a +this.b=b +this.c=c}, +eA:function eA(){}, +te:function te(a,b){this.a=a +this.b=b}, +mC(a,b,c,d,e){if(c==null)if(b==null){if(a==null)return new A.ca(d.h("@<0>").J(e).h("ca<1,2>")) +b=A.vc()}else{if(A.xT()===b&&A.xS()===a)return new A.cy(d.h("@<0>").J(e).h("cy<1,2>")) +if(a==null)a=A.vb()}else{if(b==null)b=A.vc() +if(a==null)a=A.vb()}return A.AW(a,b,c,d,e)}, +wL(a,b){var s=a[b] +return s===a?null:s}, +uV(a,b,c){if(c==null)a[b]=a +else a[b]=c}, +uU(){var s=Object.create(null) +A.uV(s,"",s) +delete s[""] +return s}, +AW(a,b,c,d,e){var s=c!=null?c:new A.qq(d) +return new A.h4(a,b,s,d.h("@<0>").J(e).h("h4<1,2>"))}, +uw(a,b,c,d){if(b==null){if(a==null)return new A.b2(c.h("@<0>").J(d).h("b2<1,2>")) +b=A.vc()}else{if(A.xT()===b&&A.xS()===a)return new A.fe(c.h("@<0>").J(d).h("fe<1,2>")) +if(a==null)a=A.vb()}return A.B6(a,b,null,c,d)}, +bJ(a,b,c){return A.Db(a,new A.b2(b.h("@<0>").J(c).h("b2<1,2>")))}, +P(a,b){return new A.b2(a.h("@<0>").J(b).h("b2<1,2>"))}, +B6(a,b,c,d,e){return new A.hd(a,b,new A.r5(d),d.h("@<0>").J(e).h("hd<1,2>"))}, +ux(a){return new A.cb(a.h("cb<0>"))}, +bK(a){return new A.cb(a.h("cb<0>"))}, +uW(){var s=Object.create(null) +s[""]=s +delete s[""] +return s}, +BP(a,b){return J.y(a,b)}, +BQ(a){return J.z(a)}, +zq(a,b,c){var s=A.mC(null,null,null,b,c) +a.a4(0,new A.mD(s,b,c)) +return s}, +zz(a){var s=new A.kg(a) +if(s.l())return s.gp() +return null}, +w_(a,b,c){var s=A.uw(null,null,b,c) +a.a4(0,new A.nf(s,b,c)) +return s}, +zI(a,b){var s,r,q=A.ux(b) +for(s=a.length,r=0;r"))}, +zL(a){return 8}, +ca:function ca(a){var _=this +_.a=0 +_.e=_.d=_.c=_.b=null +_.$ti=a}, +cy:function cy(a){var _=this +_.a=0 +_.e=_.d=_.c=_.b=null +_.$ti=a}, +h4:function h4(a,b,c,d){var _=this +_.f=a +_.r=b +_.w=c +_.a=0 +_.e=_.d=_.c=_.b=null +_.$ti=d}, +qq:function qq(a){this.a=a}, +ha:function ha(a,b){this.a=a +this.$ti=b}, +jU:function jU(a,b,c){var _=this +_.a=a +_.b=b +_.c=0 +_.d=null +_.$ti=c}, +hd:function hd(a,b,c,d){var _=this +_.w=a +_.x=b +_.y=c +_.a=0 +_.f=_.e=_.d=_.c=_.b=null +_.r=0 +_.$ti=d}, +r5:function r5(a){this.a=a}, +cb:function cb(a){var _=this +_.a=0 +_.f=_.e=_.d=_.c=_.b=null +_.r=0 +_.$ti=a}, +r6:function r6(a){this.a=a +this.c=this.b=null}, +k0:function k0(a,b,c){var _=this +_.a=a +_.b=b +_.d=_.c=null +_.$ti=c}, +d2:function d2(a,b){this.a=a +this.$ti=b}, +mD:function mD(a,b,c){this.a=a +this.b=b +this.c=c}, +nf:function nf(a,b,c){this.a=a +this.b=b +this.c=c}, +fh:function fh(a){var _=this +_.b=_.a=0 +_.c=null +_.$ti=a}, +k1:function k1(a,b,c,d){var _=this +_.a=a +_.b=b +_.c=null +_.d=c +_.e=!1 +_.$ti=d}, +aV:function aV(){}, +C:function C(){}, +L:function L(){}, +ni:function ni(a){this.a=a}, +nk:function nk(a,b){this.a=a +this.b=b}, +kw:function kw(){}, +fk:function fk(){}, +fN:function fN(a,b){this.a=a +this.$ti=b}, +fi:function fi(a,b){var _=this +_.a=a +_.d=_.c=_.b=0 +_.$ti=b}, +k2:function k2(a,b,c,d,e){var _=this +_.a=a +_.b=b +_.c=c +_.d=d +_.e=null +_.$ti=e}, +cq:function cq(){}, +ho:function ho(){}, +hx:function hx(){}, +xy(a,b){var s,r,q,p=null +try{p=JSON.parse(a)}catch(r){s=A.H(r) +q=A.ai(String(s),null,null) +throw A.a(q)}q=A.t4(p) +return q}, +t4(a){var s +if(a==null)return null +if(typeof a!="object")return a +if(!Array.isArray(a))return new A.jY(a,Object.create(null)) +for(s=0;s>>2,k=3-(h&3) +for(s=J.a2(b),r=f.$flags|0,q=c,p=0;q>>0 +l=(l<<8|o)&16777215;--k +if(k===0){n=g+1 +r&2&&A.D(f) +f[g]=a.charCodeAt(l>>>18&63) +g=n+1 +f[n]=a.charCodeAt(l>>>12&63) +n=g+1 +f[g]=a.charCodeAt(l>>>6&63) +g=n+1 +f[n]=a.charCodeAt(l&63) +l=0 +k=3}}if(p>=0&&p<=255){if(e&&k<3){n=g+1 +m=n+1 +if(3-k===1){r&2&&A.D(f) +f[g]=a.charCodeAt(l>>>2&63) +f[n]=a.charCodeAt(l<<4&63) +f[m]=61 +f[m+1]=61}else{r&2&&A.D(f) +f[g]=a.charCodeAt(l>>>10&63) +f[n]=a.charCodeAt(l>>>4&63) +f[m]=a.charCodeAt(l<<2&63) +f[m+1]=61}return 0}return(l<<2|3-k)>>>0}for(q=c;q255)break;++q}throw A.a(A.aH(b,"Not a byte value at index "+q+": 0x"+B.b.op(s.i(b,q),16),null))}, +vP(a){return B.bI.i(0,a.toLowerCase())}, +vX(a,b,c){return new A.ff(a,b)}, +BR(a){return a.eA()}, +B2(a,b){return new A.r0(a,[],A.D2())}, +B3(a,b,c){var s,r=new A.X("") +A.wO(a,r,b,c) +s=r.a +return s.charCodeAt(0)==0?s:s}, +wO(a,b,c,d){var s=A.B2(b,c) +s.eF(a)}, +B4(a,b,c){var s,r,q +for(s=J.a2(a),r=b,q=0;r>>0 +if(q>=0&&q<=255)return +A.B5(a,b,c)}, +B5(a,b,c){var s,r,q +for(s=J.a2(a),r=b;r255)throw A.a(A.ai("Source contains non-Latin-1 characters.",a,r))}}, +xc(a){switch(a){case 65:return"Missing extension byte" +case 67:return"Unexpected extension byte" +case 69:return"Invalid UTF-8 byte" +case 71:return"Overlong encoding" +case 73:return"Out of unicode range" +case 75:return"Encoded surrogate" +case 77:return"Unfinished UTF-8 octet sequence" +default:return""}}, +jY:function jY(a,b){this.a=a +this.b=b +this.c=null}, +jZ:function jZ(a){this.a=a}, +qZ:function qZ(a,b,c){this.b=a +this.c=b +this.a=c}, +rO:function rO(){}, +rN:function rN(){}, +hN:function hN(){}, +kv:function kv(){}, +hP:function hP(a){this.a=a}, +rG:function rG(a,b){this.a=a +this.b=b}, +ku:function ku(){}, +hO:function hO(a,b){this.a=a +this.b=b}, +qB:function qB(a){this.a=a}, +rf:function rf(a){this.a=a}, +l7:function l7(){}, +hU:function hU(){}, +pV:function pV(){}, +q0:function q0(a){this.c=null +this.a=0 +this.b=a}, +pW:function pW(){}, +pK:function pK(a,b){this.a=a +this.b=b}, +lg:function lg(){}, +jI:function jI(a){this.a=a}, +jJ:function jJ(a,b){this.a=a +this.b=b +this.c=0}, +i0:function i0(){}, +db:function db(a,b){this.a=a +this.b=b}, +i1:function i1(){}, +ah:function ah(){}, +lE:function lE(a){this.a=a}, +cO:function cO(){}, +mg:function mg(){}, +mh:function mh(){}, +ff:function ff(a,b){this.a=a +this.b=b}, +is:function is(a,b){this.a=a +this.b=b}, +nb:function nb(){}, +iu:function iu(a){this.b=a}, +r_:function r_(a,b,c){var _=this +_.a=a +_.b=b +_.c=c +_.d=!1}, +it:function it(a){this.a=a}, +r1:function r1(){}, +r2:function r2(a,b){this.a=a +this.b=b}, +r0:function r0(a,b,c){this.c=a +this.a=b +this.b=c}, +iv:function iv(){}, +ix:function ix(a){this.a=a}, +iw:function iw(a,b){this.a=a +this.b=b}, +k_:function k_(a){this.a=a}, +r3:function r3(a){this.a=a}, +nc:function nc(){}, +nd:function nd(){}, +r4:function r4(){}, +el:function el(a,b){var _=this +_.e=a +_.a=b +_.c=_.b=null +_.d=!1}, +je:function je(){}, +rt:function rt(a,b){this.a=a +this.b=b}, +hr:function hr(){}, +dk:function dk(a){this.a=a}, +ky:function ky(a,b,c){this.a=a +this.b=b +this.c=c}, +jq:function jq(){}, +js:function js(){}, +kz:function kz(a){this.b=this.a=0 +this.c=a}, +rP:function rP(a,b){var _=this +_.d=a +_.b=_.a=0 +_.c=b}, +jr:function jr(a){this.a=a}, +dp:function dp(a){this.a=a +this.b=16 +this.c=0}, +kB:function kB(){}, +vD(a){var s=A.wG(a,null) +if(s==null)A.p(A.ai("Could not parse BigInt",a,null)) +return s}, +wH(a,b){var s=A.wG(a,b) +if(s==null)throw A.a(A.ai("Could not parse BigInt",a,null)) +return s}, +AO(a,b){var s,r,q=$.cf(),p=a.length,o=4-p%4 +if(o===4)o=0 +for(s=0,r=0;r=16)return null +r=r*16+o}n=h-1 +i[h]=r +for(;s=16)return null +r=r*16+o}m=n-1 +i[n]=r}if(j===1&&i[0]===0)return $.cf() +l=A.bk(j,i) +return new A.aC(l===0?!1:c,i,l)}, +wG(a,b){var s,r,q,p,o +if(a==="")return null +s=$.ys().j2(a) +if(s==null)return null +r=s.b +q=r[1]==="-" +p=r[4] +o=r[3] +if(p!=null)return A.AO(p,q) +if(o!=null)return A.AP(o,2,q) +return null}, +bk(a,b){for(;;){if(!(a>0&&b[a-1]===0))break;--a}return a}, +uR(a,b,c,d){var s,r=new Uint16Array(d),q=c-b +for(s=0;s=0;--s){q=a[s] +r&2&&A.D(d) +d[s+c]=q}for(s=c-1;s>=0;--s){r&2&&A.D(d) +d[s]=0}return b+c}, +AN(a,b,c,d){var s,r,q,p,o,n=B.b.M(c,16),m=B.b.aU(c,16),l=16-m,k=B.b.cL(1,l)-1 +for(s=b-1,r=d.$flags|0,q=0;s>=0;--s){p=a[s] +o=B.b.cM(p,l) +r&2&&A.D(d) +d[s+n+1]=(o|q)>>>0 +q=B.b.cL((p&k)>>>0,m)}r&2&&A.D(d) +d[n]=q}, +wA(a,b,c,d){var s,r,q,p,o=B.b.M(c,16) +if(B.b.aU(c,16)===0)return A.uS(a,b,o,d) +s=b+o+1 +A.AN(a,b,c,d) +for(r=d.$flags|0,q=o;--q,q>=0;){r&2&&A.D(d) +d[q]=0}p=s-1 +return d[p]===0?p:s}, +AQ(a,b,c,d){var s,r,q,p,o=B.b.M(c,16),n=B.b.aU(c,16),m=16-n,l=B.b.cL(1,n)-1,k=B.b.cM(a[o],n),j=b-o-1 +for(s=d.$flags|0,r=0;r>>0,m) +s&2&&A.D(d) +d[r]=(p|k)>>>0 +k=B.b.cM(q,n)}s&2&&A.D(d) +d[j]=k}, +pY(a,b,c,d){var s,r=b-d +if(r===0)for(s=b-1;s>=0;--s){r=a[s]-c[s] +if(r!==0)return r}return r}, +AL(a,b,c,d,e){var s,r,q +for(s=e.$flags|0,r=0,q=0;q=0;e=o,c=q){q=c+1 +p=a*b[c]+d[e]+r +o=e+1 +s&2&&A.D(d) +d[e]=p&65535 +r=B.b.M(p,65536)}for(;r!==0;e=o){n=d[e]+r +o=e+1 +s&2&&A.D(d) +d[e]=n&65535 +r=B.b.M(n,65536)}}, +AM(a,b,c){var s,r=b[c] +if(r===a)return 65535 +s=B.b.hv((r<<16|b[c-1])>>>0,a) +if(s>65535)return 65535 +return s}, +Dj(a){return A.kH(a)}, +zm(a){if(A.dt(a)||typeof a=="number"||typeof a=="string"||a instanceof A.di)A.vQ(a)}, +vQ(a){throw A.a(A.aH(a,"object","Expandos are not allowed on strings, numbers, bools, records or null"))}, +jS(a,b){var s=$.yt() +s=s==null?null:new s(A.cF(A.DP(a,b),1)) +return new A.jR(s,b.h("jR<0>"))}, +xZ(a){var s=A.uz(a,null) +if(s!=null)return s +throw A.a(A.ai(a,null,null))}, +zl(a,b){a=A.ao(a,new Error()) +a.stack=b.j(0) +throw a}, +aW(a,b,c,d){var s,r=c?J.us(a,d):J.ur(a,d) +if(a!==0&&b!=null)for(s=0;s")) +for(s=J.U(a);s.l();)r.push(s.gp()) +r.$flags=1 +return r}, +an(a,b){var s,r +if(Array.isArray(a))return A.v(a.slice(0),b.h("A<0>")) +s=A.v([],b.h("A<0>")) +for(r=J.U(a);r.l();)s.push(r.gp()) +return s}, +iA(a,b){var s=A.zN(a,!1,b) +s.$flags=3 +return s}, +bR(a,b,c){var s,r,q,p,o +A.aI(b,"start") +s=c==null +r=!s +if(r){q=c-b +if(q<0)throw A.a(A.a0(c,b,null,"end",null)) +if(q===0)return""}if(Array.isArray(a)){p=a +o=p.length +if(s)c=o +return A.we(b>0||c0)a=J.kT(a,b) +s=A.an(a,t.S) +return A.we(s)}, +Am(a,b,c){var s=a.length +if(b>=s)return"" +return A.A3(a,b,c==null||c>s?s:c)}, +ar(a,b){return new A.fd(a,A.ut(a,!1,b,!1,!1,""))}, +Di(a,b){return a==null?b==null:a===b}, +uE(a,b,c){var s=J.U(b) +if(!s.l())return a +if(c.length===0){do a+=A.o(s.gp()) +while(s.l())}else{a+=A.o(s.gp()) +while(s.l())a=a+c+A.o(s.gp())}return a}, +fS(){var s,r,q=A.zZ() +if(q==null)throw A.a(A.R("'Uri.base' is not supported")) +s=$.wx +if(s!=null&&q===$.ww)return s +r=A.d3(q) +$.wx=r +$.ww=q +return r}, +fD(){return A.N(new Error())}, +i8(a,b,c){var s="microsecond" +if(b<0||b>999)throw A.a(A.a0(b,0,999,s,null)) +if(a<-864e13||a>864e13)throw A.a(A.a0(a,-864e13,864e13,"millisecondsSinceEpoch",null)) +if(a===864e13&&b!==0)throw A.a(A.aH(b,s,u.C)) +A.bd(c,"isUtc",t.y) +return a}, +zg(a){var s=Math.abs(a),r=a<0?"-":"" +if(s>=1000)return""+a +if(s>=100)return r+"0"+s +if(s>=10)return r+"00"+s +return r+"000"+s}, +vO(a){if(a>=100)return""+a +if(a>=10)return"0"+a +return"00"+a}, +i7(a){if(a>=10)return""+a +return"0"+a}, +mf(a,b){return new A.b_(a+1000*b)}, +ia(a,b){var s,r,q +for(s=a.length,r=0;rc)throw A.a(A.a0(a,b,c,d,null)) +return a}, +aL(a,b,c){if(0>a||a>c)throw A.a(A.a0(a,0,c,"start",null)) +if(b!=null){if(a>b||b>c)throw A.a(A.a0(b,a,c,"end",null)) +return b}return c}, +aI(a,b){if(a<0)throw A.a(A.a0(a,0,null,b,null)) +return a}, +vT(a,b){var s=b.b +return new A.f9(s,!0,a,null,"Index out of range")}, +ih(a,b,c,d,e){return new A.f9(b,!0,a,e,"Index out of range")}, +zu(a,b,c,d,e){if(0>a||a>=b)throw A.a(A.ih(a,b,c,d,e==null?"index":e)) +return a}, +R(a){return new A.fO(a)}, +uI(a){return new A.ji(a)}, +u(a){return new A.b7(a)}, +am(a){return new A.i2(a)}, +uj(a){return new A.jQ(a)}, +ai(a,b,c){return new A.aU(a,b,c)}, +zA(a,b,c){var s,r +if(A.vh(a)){if(b==="("&&c===")")return"(...)" +return b+"..."+c}s=A.v([],t.s) +$.du.push(a) +try{A.Cd(a,s)}finally{$.du.pop()}r=A.uE(b,s,", ")+c +return r.charCodeAt(0)==0?r:r}, +n8(a,b,c){var s,r +if(A.vh(a))return b+"..."+c +s=new A.X(b) +$.du.push(a) +try{r=s +r.a=A.uE(r.a,a,", ")}finally{$.du.pop()}s.a+=c +r=s.a +return r.charCodeAt(0)==0?r:r}, +Cd(a,b){var s,r,q,p,o,n,m,l=a.gv(a),k=0,j=0 +for(;;){if(!(k<80||j<3))break +if(!l.l())return +s=A.o(l.gp()) +b.push(s) +k+=s.length+2;++j}if(!l.l()){if(j<=5)return +r=b.pop() +q=b.pop()}else{p=l.gp();++j +if(!l.l()){if(j<=4){b.push(A.o(p)) +return}r=A.o(p) +q=b.pop() +k+=r.length+2}else{o=l.gp();++j +for(;l.l();p=o,o=n){n=l.gp();++j +if(j>100){for(;;){if(!(k>75&&j>3))break +k-=b.pop().length+2;--j}b.push("...") +return}}q=A.o(p) +r=A.o(o) +k+=r.length+q.length+4}}if(j>b.length+2){k+=5 +m="..."}else m=null +for(;;){if(!(k>80&&b.length>3))break +k-=b.pop().length+2 +if(m==null){k+=5 +m="..."}}if(m!=null)b.push(m) +b.push(q) +b.push(r)}, +bN(a,b,c,d,e,f,g,h,i,j){var s +if(B.c===c)return A.wp(J.z(a),J.z(b),$.bX()) +if(B.c===d){s=J.z(a) +b=J.z(b) +c=J.z(c) +return A.c4(A.F(A.F(A.F($.bX(),s),b),c))}if(B.c===e){s=J.z(a) +b=J.z(b) +c=J.z(c) +d=J.z(d) +return A.c4(A.F(A.F(A.F(A.F($.bX(),s),b),c),d))}if(B.c===f){s=J.z(a) +b=J.z(b) +c=J.z(c) +d=J.z(d) +e=J.z(e) +return A.c4(A.F(A.F(A.F(A.F(A.F($.bX(),s),b),c),d),e))}if(B.c===g){s=J.z(a) +b=J.z(b) +c=J.z(c) +d=J.z(d) +e=J.z(e) +f=J.z(f) +return A.c4(A.F(A.F(A.F(A.F(A.F(A.F($.bX(),s),b),c),d),e),f))}if(B.c===h){s=J.z(a) +b=J.z(b) +c=J.z(c) +d=J.z(d) +e=J.z(e) +f=J.z(f) +g=J.z(g) +return A.c4(A.F(A.F(A.F(A.F(A.F(A.F(A.F($.bX(),s),b),c),d),e),f),g))}if(B.c===i){s=J.z(a) +b=J.z(b) +c=J.z(c) +d=J.z(d) +e=J.z(e) +f=J.z(f) +g=J.z(g) +h=J.z(h) +return A.c4(A.F(A.F(A.F(A.F(A.F(A.F(A.F(A.F($.bX(),s),b),c),d),e),f),g),h))}if(B.c===j){s=J.z(a) +b=J.z(b) +c=J.z(c) +d=J.z(d) +e=J.z(e) +f=J.z(f) +g=J.z(g) +h=J.z(h) +i=J.z(i) +return A.c4(A.F(A.F(A.F(A.F(A.F(A.F(A.F(A.F(A.F($.bX(),s),b),c),d),e),f),g),h),i))}s=J.z(a) +b=J.z(b) +c=J.z(c) +d=J.z(d) +e=J.z(e) +f=J.z(f) +g=J.z(g) +h=J.z(h) +i=J.z(i) +j=J.z(j) +j=A.c4(A.F(A.F(A.F(A.F(A.F(A.F(A.F(A.F(A.F(A.F($.bX(),s),b),c),d),e),f),g),h),i),j)) +return j}, +zX(a){var s,r,q=$.bX() +for(s=a.length,r=0;r>>16)>>>0)*569420461>>>0 +o=((o^o>>>15)>>>0)*3545902487>>>0 +r=r+((o^o>>>15)>>>0)&1073741823;++q}return A.wp(r,q,0)}, +u4(a){var s=A.o(a),r=$.y5 +if(r==null)A.vk(s) +else r.$1(s)}, +d3(a5){var s,r,q,p,o,n,m,l,k,j,i,h,g,f,e,d,c,b,a,a0,a1,a2,a3=null,a4=a5.length +if(a4>=5){s=((a5.charCodeAt(4)^58)*3|a5.charCodeAt(0)^100|a5.charCodeAt(1)^97|a5.charCodeAt(2)^116|a5.charCodeAt(3)^97)>>>0 +if(s===0)return A.wv(a4=14)r[7]=a4 +q=r[1] +if(q>=0)if(A.xI(a5,0,q,20,r)===20)r[7]=q +p=r[2]+1 +o=r[3] +n=r[4] +m=r[5] +l=r[6] +if(lq+3)){i=o>0 +if(!(i&&o+1===n)){if(!B.a.P(a5,"\\",n))if(p>0)h=B.a.P(a5,"\\",p-1)||B.a.P(a5,"\\",p-2) +else h=!1 +else h=!0 +if(!h){if(!(mn+2&&B.a.P(a5,"/..",m-3) +else h=!0 +if(!h)if(q===4){if(B.a.P(a5,"file",0)){if(p<=0){if(!B.a.P(a5,"/",n)){g="file:///" +s=3}else{g="file://" +s=2}a5=g+B.a.t(a5,n,a4) +m+=s +l+=s +a4=a5.length +p=7 +o=7 +n=7}else if(n===m){++l +f=m+1 +a5=B.a.c2(a5,n,m,"/");++a4 +m=f}j="file"}else if(B.a.P(a5,"http",0)){if(i&&o+3===n&&B.a.P(a5,"80",o+1)){l-=3 +e=n-3 +m-=3 +a5=B.a.c2(a5,o,n,"") +a4-=3 +n=e}j="http"}}else if(q===5&&B.a.P(a5,"https",0)){if(i&&o+4===n&&B.a.P(a5,"443",o+1)){l-=4 +e=n-4 +m-=4 +a5=B.a.c2(a5,o,n,"") +a4-=3 +n=e}j="https"}k=!h}}}}if(k)return new A.bn(a40)j=A.v_(a5,0,q) +else{if(q===0)A.ez(a5,0,"Invalid empty scheme") +j=""}d=a3 +if(p>0){c=q+3 +b=c=c?0:a.charCodeAt(q) +m=n^48 +if(m<=9){if(o!==0||q===r){o=o*10+m +if(o<=255){++q +continue}A.jp("each part must be in the range 0..255",a,r)}A.jp("parts must not have leading zeros",a,r)}if(q===r){if(q===c)break +A.jp(k,a,q)}l=p+1 +s&2&&A.D(d) +d[e+p]=o +if(n===46){if(l<4){++q +p=l +r=q +o=0 +continue}break}if(q===c){if(l===4)return +break}A.jp(k,a,q) +p=l}A.jp("IPv4 address should contain exactly 4 parts",a,q)}, +Aw(a,b,c){var s +if(b===c)throw A.a(A.ai("Empty IP address",a,b)) +if(a.charCodeAt(b)===118){s=A.Ax(a,b,c) +if(s!=null)throw A.a(s) +return!1}A.wy(a,b,c) +return!0}, +Ax(a,b,c){var s,r,q,p,o="Missing hex-digit in IPvFuture address";++b +for(s=b;;s=r){if(s=97&&p<=102)continue +if(q===46){if(r-1===b)return new A.aU(o,a,r) +s=r +break}return new A.aU("Unexpected character",a,r-1)}if(s-1===b)return new A.aU(o,a,s) +return new A.aU("Missing '.' in IPvFuture address",a,s)}if(s===c)return new A.aU("Missing address in IPvFuture address, host, cursor",null,null) +for(;;){if((u.S.charCodeAt(a.charCodeAt(s))&16)!==0){++s +if(s=a3?0:a1.charCodeAt(p) +A:{k=l^48 +j=!1 +if(k<=9)i=k +else{h=l|32 +if(h>=97&&h<=102)i=h-87 +else break A +m=j}if(po){if(l===46){if(m){if(q<=6){A.Av(a1,o,a3,s,q*2) +q+=2 +p=a3 +break}a0.$2(a,o)}break}g=q*2 +s[g]=B.b.Y(n,8) +s[g+1]=n&255;++q +if(l===58){if(q<8){++p +o=p +n=0 +m=!0 +continue}a0.$2(a,p)}break}if(l===58){if(r<0){f=q+1;++p +r=q +q=f +o=p +continue}a0.$2("only one wildcard `::` is allowed",p)}if(r!==q-1)a0.$2("missing part",p) +break}if(p0){c=e*2 +b=16-d*2 +B.f.L(s,b,16,s,c) +B.f.h1(s,c,b,0)}}return s}, +hz(a,b,c,d,e,f,g){return new A.hy(a,b,c,d,e,f,g)}, +x0(a){if(a==="http")return 80 +if(a==="https")return 443 +return 0}, +ez(a,b,c){throw A.a(A.ai(c,a,b))}, +Bs(a,b){var s,r,q +for(s=a.length,r=0;r=b&&s=b&&s=p){if(i==null)i=new A.X("") +if(r=o){if(q==null)q=new A.X("") +if(r=a.length)return"%" +s=a.charCodeAt(b+1) +r=a.charCodeAt(n) +q=A.tL(s) +p=A.tL(r) +if(q<0||p<0)return"%" +o=q*16+p +if(o<127&&(u.S.charCodeAt(o)&1)!==0)return A.aQ(c&&65<=o&&90>=o?(o|32)>>>0:o) +if(s>=97||r>=97)return B.a.t(a,b,b+3).toUpperCase() +return null}, +uZ(a){var s,r,q,p,o,n="0123456789ABCDEF" +if(a<=127){s=new Uint8Array(3) +s[0]=37 +s[1]=n.charCodeAt(a>>>4) +s[2]=n.charCodeAt(a&15)}else{if(a>2047)if(a>65535){r=240 +q=4}else{r=224 +q=3}else{r=192 +q=2}s=new Uint8Array(3*q) +for(p=0;--q,q>=0;r=128){o=B.b.m_(a,6*q)&63|r +s[p]=37 +s[p+1]=n.charCodeAt(o>>>4) +s[p+2]=n.charCodeAt(o&15) +p+=3}}return A.bR(s,0,null)}, +hA(a,b,c,d,e,f){var s=A.x9(a,b,c,d,e,f) +return s==null?B.a.t(a,b,c):s}, +x9(a,b,c,d,e,f){var s,r,q,p,o,n,m,l,k,j=null,i=u.S +for(s=!e,r=b,q=r,p=j;r=2&&A.x2(a.charCodeAt(0)))for(s=1;s127||(u.S.charCodeAt(r)&8)===0)break}return a}, +Bx(a,b){if(a.em("package")&&a.c==null)return A.xK(b,0,b.length) +return-1}, +Bu(a,b){var s,r,q +for(s=0,r=0;r<2;++r){q=a.charCodeAt(b+r) +if(48<=q&&q<=57)s=s*16+q-48 +else{q|=32 +if(97<=q&&q<=102)s=s*16+q-87 +else throw A.a(A.K("Invalid URL encoding",null))}}return s}, +v2(a,b,c,d,e){var s,r,q,p,o=b +for(;;){if(!(o127)throw A.a(A.K("Illegal percent encoding in URI",null)) +if(r===37){if(o+3>q)throw A.a(A.K("Truncated URI",null)) +p.push(A.Bu(a,o+1)) +o+=2}else p.push(r)}}return d.aO(p)}, +x2(a){var s=a|32 +return 97<=s&&s<=122}, +wv(a,b,c){var s,r,q,p,o,n,m,l,k="Invalid MIME type",j=A.v([b-1],t.t) +for(s=a.length,r=b,q=-1,p=null;rb)throw A.a(A.ai(k,a,r)) +while(p!==44){j.push(r);++r +for(o=-1;r=0)j.push(o) +else{n=B.d.gaS(j) +if(p!==44||r!==n+7||!B.a.P(a,"base64",n+1))throw A.a(A.ai("Expecting '='",a,r)) +break}}j.push(r) +m=r+1 +if((j.length&1)===1)a=B.aW.o7(a,m,s) +else{l=A.x9(a,m,s,256,!0,!1) +if(l!=null)a=B.a.c2(a,m,s,l)}return new A.p0(a,j,c)}, +xI(a,b,c,d,e){var s,r,q +for(s=b;s95)r=31 +q='\xe1\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\xe1\xe1\xe1\x01\xe1\xe1\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\xe1\xe3\xe1\xe1\x01\xe1\x01\xe1\xcd\x01\xe1\x01\x01\x01\x01\x01\x01\x01\x01\x0e\x03\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01"\x01\xe1\x01\xe1\xac\xe1\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\xe1\xe1\xe1\x01\xe1\xe1\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\xe1\xea\xe1\xe1\x01\xe1\x01\xe1\xcd\x01\xe1\x01\x01\x01\x01\x01\x01\x01\x01\x01\n\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01"\x01\xe1\x01\xe1\xac\xeb\x8b\x8b\x8b\x8b\x8b\x8b\x8b\x8b\x8b\x8b\x8b\x8b\x8b\x8b\x8b\x8b\x8b\x8b\x8b\x8b\x8b\x8b\x8b\x8b\x8b\x8b\xeb\xeb\xeb\x8b\xeb\xeb\x8b\x8b\x8b\x8b\x8b\x8b\x8b\x8b\x8b\x8b\x8b\x8b\x8b\x8b\x8b\x8b\x8b\x8b\x8b\x8b\x8b\x8b\x8b\x8b\x8b\x8b\xeb\x83\xeb\xeb\x8b\xeb\x8b\xeb\xcd\x8b\xeb\x8b\x8b\x8b\x8b\x8b\x8b\x8b\x8b\x92\x83\x8b\x8b\x8b\x8b\x8b\x8b\x8b\x8b\x8b\x8b\xeb\x8b\xeb\x8b\xeb\xac\xeb\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\xeb\xeb\xeb\v\xeb\xeb\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\xebD\xeb\xeb\v\xeb\v\xeb\xcd\v\xeb\v\v\v\v\v\v\v\v\x12D\v\v\v\v\v\v\v\v\v\v\xeb\v\xeb\v\xeb\xac\xe5\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05\xe5\xe5\xe5\x05\xe5D\xe5\xe5\xe5\xe5\xe5\xe5\xe5\xe5\xe5\xe5\xe5\xe5\xe5\xe5\xe5\xe5\xe5\xe5\xe5\xe5\xe5\xe5\xe5\xe5\xe5\xe5\xe8\x8a\xe5\xe5\x05\xe5\x05\xe5\xcd\x05\xe5\x05\x05\x05\x05\x05\x05\x05\x05\x05\x8a\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05f\x05\xe5\x05\xe5\xac\xe5\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05\xe5\xe5\xe5\x05\xe5D\xe5\xe5\xe5\xe5\xe5\xe5\xe5\xe5\xe5\xe5\xe5\xe5\xe5\xe5\xe5\xe5\xe5\xe5\xe5\xe5\xe5\xe5\xe5\xe5\xe5\xe5\xe5\x8a\xe5\xe5\x05\xe5\x05\xe5\xcd\x05\xe5\x05\x05\x05\x05\x05\x05\x05\x05\x05\x8a\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05f\x05\xe5\x05\xe5\xac\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7D\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\x8a\xe7\xe7\xe7\xe7\xe7\xe7\xcd\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\x8a\xe7\x07\x07\x07\x07\x07\x07\x07\x07\x07\xe7\xe7\xe7\xe7\xe7\xac\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7D\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\x8a\xe7\xe7\xe7\xe7\xe7\xe7\xcd\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\x8a\x07\x07\x07\x07\x07\x07\x07\x07\x07\x07\xe7\xe7\xe7\xe7\xe7\xac\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\x05\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\xeb\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\xeb\xeb\xeb\v\xeb\xeb\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\xeb\xea\xeb\xeb\v\xeb\v\xeb\xcd\v\xeb\v\v\v\v\v\v\v\v\x10\xea\v\v\v\v\v\v\v\v\v\v\xeb\v\xeb\v\xeb\xac\xeb\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\xeb\xeb\xeb\v\xeb\xeb\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\xeb\xea\xeb\xeb\v\xeb\v\xeb\xcd\v\xeb\v\v\v\v\v\v\v\v\x12\n\v\v\v\v\v\v\v\v\v\v\xeb\v\xeb\v\xeb\xac\xeb\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\xeb\xeb\xeb\v\xeb\xeb\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\xeb\xea\xeb\xeb\v\xeb\v\xeb\xcd\v\xeb\v\v\v\v\v\v\v\v\v\n\v\v\v\v\v\v\v\v\v\v\xeb\v\xeb\v\xeb\xac\xec\f\f\f\f\f\f\f\f\f\f\f\f\f\f\f\f\f\f\f\f\f\f\f\f\f\f\xec\xec\xec\f\xec\xec\f\f\f\f\f\f\f\f\f\f\f\f\f\f\f\f\f\f\f\f\f\f\f\f\f\f\xec\xec\xec\xec\f\xec\f\xec\xcd\f\xec\f\f\f\f\f\f\f\f\f\xec\f\f\f\f\f\f\f\f\f\f\xec\f\xec\f\xec\f\xed\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\xed\xed\xed\r\xed\xed\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\xed\xed\xed\xed\r\xed\r\xed\xed\r\xed\r\r\r\r\r\r\r\r\r\xed\r\r\r\r\r\r\r\r\r\r\xed\r\xed\r\xed\r\xe1\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\xe1\xe1\xe1\x01\xe1\xe1\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\xe1\xea\xe1\xe1\x01\xe1\x01\xe1\xcd\x01\xe1\x01\x01\x01\x01\x01\x01\x01\x01\x0f\xea\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01"\x01\xe1\x01\xe1\xac\xe1\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\xe1\xe1\xe1\x01\xe1\xe1\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\xe1\xe9\xe1\xe1\x01\xe1\x01\xe1\xcd\x01\xe1\x01\x01\x01\x01\x01\x01\x01\x01\x01\t\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01"\x01\xe1\x01\xe1\xac\xeb\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\xeb\xeb\xeb\v\xeb\xeb\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\xeb\xea\xeb\xeb\v\xeb\v\xeb\xcd\v\xeb\v\v\v\v\v\v\v\v\x11\xea\v\v\v\v\v\v\v\v\v\v\xeb\v\xeb\v\xeb\xac\xeb\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\xeb\xeb\xeb\v\xeb\xeb\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\xeb\xe9\xeb\xeb\v\xeb\v\xeb\xcd\v\xeb\v\v\v\v\v\v\v\v\v\t\v\v\v\v\v\v\v\v\v\v\xeb\v\xeb\v\xeb\xac\xeb\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\xeb\xeb\xeb\v\xeb\xeb\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\xeb\xea\xeb\xeb\v\xeb\v\xeb\xcd\v\xeb\v\v\v\v\v\v\v\v\x13\xea\v\v\v\v\v\v\v\v\v\v\xeb\v\xeb\v\xeb\xac\xeb\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\xeb\xeb\xeb\v\xeb\xeb\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\xeb\xea\xeb\xeb\v\xeb\v\xeb\xcd\v\xeb\v\v\v\v\v\v\v\v\v\xea\v\v\v\v\v\v\v\v\v\v\xeb\v\xeb\v\xeb\xac\xf5\x15\x15\x15\x15\x15\x15\x15\x15\x15\x15\x15\x15\x15\x15\x15\x15\x15\x15\x15\x15\x15\x15\x15\x15\x15\x15\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\x15\x15\x15\x15\x15\x15\x15\x15\x15\x15\x15\x15\x15\x15\x15\x15\x15\x15\x15\x15\x15\x15\x15\x15\x15\x15\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\x15\xf5\x15\x15\xf5\x15\x15\x15\x15\x15\x15\x15\x15\x15\x15\xf5\xf5\xf5\xf5\xf5\xf5'.charCodeAt(d*96+r) +d=q&31 +e[q>>>5]=s}return d}, +wU(a){if(a.b===7&&B.a.I(a.a,"package")&&a.c<=0)return A.xK(a.a,a.e,a.f) +return-1}, +xK(a,b,c){var s,r,q +for(s=b,r=0;s1e4&&$.eB.a===0){$.kN().clearMarks() +$.kN().clearMeasures() +$.eE=0}s=c===1||c===5 +r=c===2||c===7 +q=A.xn(s,r,d,a) +if(s){p=$.eB.i(0,q) +if(p==null)p=0 +$.eB.m(0,q,p+1) +q=A.xA(q)}o=$.kN() +o.toString +o.mark(q,$.yB().parse(e)) +$.eE=$.eE+1 +if(r){n=A.xn(!0,!1,d,a) +o=$.kN() +o.toString +o.measure(d,A.xA(n),q) +$.eE=$.eE+1 +A.BO(n)}B.b.mF($.eE,0,10001)}, +Ev(a){if(a==null||a.a===0)return"{}" +return B.h.bB(a)}, +tb:function tb(){}, +t9:function t9(){}, +uN:function uN(a,b){this.a=a +this.b=b}, +Dg(){return v.G}, +zM(a){return a}, +zD(a){return a}, +zG(a){return a}, +uq(a,b){var s,r,q,p,o +if(b.length===0)return!1 +s=b.split(".") +r=v.G +for(q=s.length,p=0;p=1)return a.$1(b) +return a.$0()}, +BH(a,b,c,d){if(d>=2)return a.$2(b,c) +if(d===1)return a.$1(b) +return a.$0()}, +BI(a,b,c,d,e){if(e>=3)return a.$3(b,c,d) +if(e===2)return a.$2(b,c) +if(e===1)return a.$1(b) +return a.$0()}, +BJ(a,b,c,d,e,f){if(f>=4)return a.$4(b,c,d,e) +if(f===3)return a.$3(b,c,d) +if(f===2)return a.$2(b,c) +if(f===1)return a.$1(b) +return a.$0()}, +BK(a,b,c,d,e,f,g){if(g>=5)return a.$5(b,c,d,e,f) +if(g===4)return a.$4(b,c,d,e) +if(g===3)return a.$3(b,c,d) +if(g===2)return a.$2(b,c) +if(g===1)return a.$1(b) +return a.$0()}, +xx(a){return a==null||A.dt(a)||typeof a=="number"||typeof a=="string"||t.jx.b(a)||t.p.b(a)||t.nn.b(a)||t.m6.b(a)||t.hM.b(a)||t.bW.b(a)||t.mC.b(a)||t.pk.b(a)||t.kI.b(a)||t.lo.b(a)||t.fW.b(a)}, +vi(a){if(A.xx(a))return a +return new A.tQ(new A.cy(t.mp)).$1(a)}, +tJ(a,b){return a[b]}, +xR(a,b,c){return a[b].apply(a,c)}, +dw(a,b){var s,r +if(b==null)return new a() +if(b instanceof Array)switch(b.length){case 0:return new a() +case 1:return new a(b[0]) +case 2:return new a(b[0],b[1]) +case 3:return new a(b[0],b[1],b[2]) +case 4:return new a(b[0],b[1],b[2],b[3])}s=[null] +B.d.a8(s,b) +r=a.bind.apply(a,s) +String(r) +return new r()}, +ac(a,b){var s=new A.l($.n,b.h("l<0>")),r=new A.as(s,b.h("as<0>")) +a.then(A.cF(new A.u5(r),1),A.cF(new A.u6(r),1)) +return s}, +xw(a){return a==null||typeof a==="boolean"||typeof a==="number"||typeof a==="string"||a instanceof Int8Array||a instanceof Uint8Array||a instanceof Uint8ClampedArray||a instanceof Int16Array||a instanceof Uint16Array||a instanceof Int32Array||a instanceof Uint32Array||a instanceof Float32Array||a instanceof Float64Array||a instanceof ArrayBuffer||a instanceof DataView}, +xV(a){if(A.xw(a))return a +return new A.tC(new A.cy(t.mp)).$1(a)}, +tQ:function tQ(a){this.a=a}, +u5:function u5(a){this.a=a}, +u6:function u6(a){this.a=a}, +tC:function tC(a){this.a=a}, +y1(a,b){return Math.max(a,b)}, +A4(){return B.be}, +qW:function qW(){}, +qX:function qX(a){this.a=a}, +j_:function j_(a){this.$ti=a}, +nZ:function nZ(a){this.a=a}, +o_:function o_(a,b){this.a=a +this.b=b}, +fG:function fG(a,b,c){var _=this +_.a=$ +_.b=!1 +_.c=a +_.e=b +_.$ti=c}, +oa:function oa(){}, +ob:function ob(a,b){this.a=a +this.b=b}, +o9:function o9(){}, +o8:function o8(a){this.a=a}, +o7:function o7(a,b){this.a=a +this.b=b}, +et:function et(a){this.a=a}, +T:function T(){}, +li:function li(a){this.a=a}, +lj:function lj(a){this.a=a}, +lk:function lk(a,b){this.a=a +this.b=b}, +ll:function ll(a){this.a=a}, +lm:function lm(a,b,c,d){var _=this +_.a=a +_.b=b +_.c=c +_.d=d}, +eX:function eX(){}, +iz:function iz(a){this.$ti=a}, +ey:function ey(){}, +cW:function cW(a){this.$ti=a}, +em:function em(a,b,c){this.a=a +this.b=b +this.c=c}, +dU:function dU(a){this.$ti=a}, +w3(){throw A.a(A.R(u.O))}, +iH:function iH(){}, +jl:function jl(){}, +kV:function kV(){}, +fw:function fw(a,b){this.a=a +this.b=b}, +l8:function l8(){}, +hV:function hV(){}, +hW:function hW(){}, +hX:function hX(){}, +l9:function l9(){}, +xM(a,b){var s +if(t.m.b(a)&&"AbortError"===a.name)return new A.fw("Request aborted by `abortTrigger`",b.b) +if(!(a instanceof A.bY)){s=J.aZ(a) +if(B.a.I(s,"TypeError: "))s=B.a.X(s,11) +a=new A.bY(s,b.b)}return a}, +xC(a,b,c){A.uh(A.xM(a,c),b)}, +BE(a,b){return new A.bH(!1,new A.rX(a,b),t.fb)}, +eG(a,b,c){return A.Cm(a,b,c)}, +Cm(a0,a1,a2){var s=0,r=A.j(t.H),q,p=2,o=[],n,m,l,k,j,i,h,g,f,e,d,c,b,a +var $async$eG=A.e(function(a3,a4){if(a3===1){o.push(a4) +s=p}for(;;)switch(s){case 0:d={} +c=a1.body +b=c==null?null:c.getReader() +s=b==null?3:4 +break +case 3:s=5 +return A.c(a2.n(),$async$eG) +case 5:s=1 +break +case 4:d.a=null +d.b=d.c=!1 +a2.f=new A.tc(d) +a2.r=new A.td(d,b,a0) +c=t.Z,k=t.m,j=t.D,i=t.h +case 6:n=null +p=9 +s=12 +return A.c(A.ac(b.read(),k),$async$eG) +case 12:n=a4 +p=2 +s=11 +break +case 9:p=8 +a=o.pop() +m=A.H(a) +l=A.N(a) +s=!d.c?13:14 +break +case 13:d.b=!0 +c=A.xM(m,a0) +k=l +j=a2.b +if(j>=4)A.p(a2.aL()) +if((j&1)!==0){g=a2.a +if((j&8)!==0)g=g.c +g.au(c,k==null?B.r:k)}s=15 +return A.c(a2.n(),$async$eG) +case 15:case 14:s=7 +break +s=11 +break +case 8:s=2 +break +case 11:if(n.done){a2.iU() +s=7 +break}else{f=n.value +f.toString +c.a(f) +e=a2.b +if(e>=4)A.p(a2.aL()) +if((e&1)!==0){g=a2.a;((e&8)!==0?g.c:g).af(f)}}f=a2.b +if((f&1)!==0){g=a2.a +e=(((f&8)!==0?g.c:g).e&4)!==0 +f=e}else f=(f&2)===0 +s=f?16:17 +break +case 16:f=d.a +s=18 +return A.c((f==null?d.a=new A.as(new A.l($.n,j),i):f).a,$async$eG) +case 18:case 17:if((a2.b&1)===0){s=7 +break}s=6 +break +case 7:case 1:return A.h(q,r) +case 2:return A.f(o.at(-1),r)}}) +return A.i($async$eG,r)}, +la:function la(a){this.b=!1 +this.c=a}, +lb:function lb(a){this.a=a}, +lc:function lc(a){this.a=a}, +rX:function rX(a,b){this.a=a +this.b=b}, +tc:function tc(a){this.a=a}, +td:function td(a,b,c){this.a=a +this.b=b +this.c=c}, +dF:function dF(a){this.a=a}, +lh:function lh(a){this.a=a}, +vJ(a,b){return new A.bY(a,b)}, +bY:function bY(a,b){this.a=a +this.b=b}, +A7(a,b){var s=new Uint8Array(0),r=$.vn() +if(!r.b.test(a))A.p(A.aH(a,"method","Not a valid method")) +r=t.N +return new A.iU(B.i,s,a,b,A.uw(new A.hW(),new A.hX(),r,r))}, +yX(a,b,c){var s=new Uint8Array(0),r=$.vn() +if(!r.b.test(a))A.p(A.aH(a,"method","Not a valid method")) +r=t.N +return new A.hL(c,B.i,s,a,b,A.uw(new A.hW(),new A.hX(),r,r))}, +iU:function iU(a,b,c,d,e){var _=this +_.x=a +_.y=b +_.a=c +_.b=d +_.r=e +_.w=!1}, +hL:function hL(a,b,c,d,e,f){var _=this +_.cx=a +_.x=b +_.y=c +_.a=d +_.b=e +_.r=f +_.w=!1}, +jz:function jz(){}, +nT(a){var s=0,r=A.j(t.cD),q,p,o,n,m,l,k,j +var $async$nT=A.e(function(b,c){if(b===1)return A.f(c,r) +for(;;)switch(s){case 0:s=3 +return A.c(a.w.jw(),$async$nT) +case 3:p=c +o=a.b +n=a.a +m=a.e +l=a.c +k=A.yc(p) +j=p.length +k=new A.iV(k,n,o,l,j,m,!1,!0) +k.hw(o,j,m,!1,!0,l,n) +q=k +s=1 +break +case 1:return A.h(q,r)}}) +return A.i($async$nT,r)}, +xm(a){var s=a.i(0,"content-type") +if(s!=null)return A.w2(s) +return A.nl("application","octet-stream",null)}, +iV:function iV(a,b,c,d,e,f,g,h){var _=this +_.w=a +_.a=b +_.b=c +_.c=d +_.d=e +_.e=f +_.f=g +_.r=h}, +cs:function cs(){}, +jd:function jd(a,b,c,d,e,f,g,h){var _=this +_.w=a +_.a=b +_.b=c +_.c=d +_.d=e +_.e=f +_.f=g +_.r=h}, +z1(a){return a.toLowerCase()}, +eQ:function eQ(a,b,c){this.a=a +this.c=b +this.$ti=c}, +w2(a){return A.DN("media type",a,new A.nm(a))}, +nl(a,b,c){var s=t.N +if(c==null)s=A.P(s,s) +else{s=new A.eQ(A.CZ(),A.P(s,t.gc),t.kj) +s.a8(0,c)}return new A.fm(a.toLowerCase(),b.toLowerCase(),new A.fN(s,t.oP))}, +fm:function fm(a,b,c){this.a=a +this.b=b +this.c=c}, +nm:function nm(a){this.a=a}, +no:function no(a){this.a=a}, +nn:function nn(){}, +Da(a){var s +a.j1($.yE(),"quoted string") +s=a.ghc().i(0,0) +return A.ya(B.a.t(s,1,s.length-1),$.yD(),new A.tE(),null)}, +tE:function tE(){}, +cn:function cn(a,b){this.a=a +this.b=b}, +dS:function dS(a,b,c,d,e,f){var _=this +_.a=a +_.b=b +_.d=c +_.e=d +_.r=e +_.w=f}, +uy(a){return $.zO.cB(a,new A.nh(a))}, +w1(a,b,c){var s=new A.dT(a,b,c) +if(b==null)s.c=B.j +else b.d.m(0,a,s) +return s}, +dT:function dT(a,b,c){var _=this +_.a=a +_.b=b +_.c=null +_.d=c +_.f=null}, +nh:function nh(a){this.a=a}, +vL(a,b){if(a==null)a="." +return new A.i3(b,a)}, +xz(a){return a}, +xN(a,b){var s,r,q,p,o,n,m,l +for(s=b.length,r=1;r=1;s=q){q=s-1 +if(b[q]!=null)break}p=new A.X("") +o=a+"(" +p.a=o +n=A.a1(b) +m=n.h("cZ<1>") +l=new A.cZ(b,0,s,m) +l.ks(b,0,s,n.c) +m=o+new A.a8(l,new A.tu(),m.h("a8")).bF(0,", ") +p.a=m +p.a=m+("): part "+(r-1)+" was null, but part "+r+" was not.") +throw A.a(A.K(p.j(0),null))}}, +i3:function i3(a,b){this.a=a +this.b=b}, +lC:function lC(){}, +lD:function lD(){}, +tu:function tu(){}, +ep:function ep(a){this.a=a}, +eq:function eq(a){this.a=a}, +n5:function n5(){}, +iM(a,b){var s,r,q,p,o,n=b.jX(a) +b.aR(a) +if(n!=null)a=B.a.X(a,n.length) +s=t.s +r=A.v([],s) +q=A.v([],s) +s=a.length +if(s!==0&&b.N(a.charCodeAt(0))){q.push(a[0]) +p=1}else{q.push("") +p=0}for(o=p;o"),r=s.h("dq") +return new A.eR(new A.dq(new A.u2(),new A.bG(new A.u3(),a,s),r),r.h("eR"))}, +u3:function u3(){}, +u2:function u2(){}, +vM(a){return new A.eV(a)}, +oy(a){return A.Aq(a)}, +Aq(a){var s=0,r=A.j(t.jM),q,p=2,o=[],n,m,l,k +var $async$oy=A.e(function(b,c){if(b===1){o.push(c) +s=p}for(;;)switch(s){case 0:p=4 +s=7 +return A.c(B.i.mN(a.w),$async$oy) +case 7:n=c +m=A.wn(a,n) +q=m +s=1 +break +p=2 +s=6 +break +case 4:p=3 +k=o.pop() +if(t.L.b(A.H(k))){q=A.wo(a) +s=1 +break}else throw k +s=6 +break +case 3:s=2 +break +case 6:case 1:return A.h(q,r) +case 2:return A.f(o.at(-1),r)}}) +return A.i($async$oy,r)}, +Ap(a){var s,r,q +try{s=A.xX(A.xm(a.e)).aO(a.w) +r=A.wn(a,s) +return r}catch(q){if(t.L.b(A.H(q)))return A.wo(a) +else throw q}}, +wn(a,b){var s,r,q=J.kP(B.h.cn(b,null),"error") +A:{if(t.f.b(q)){s=A.Ao(q) +break A}s=null +break A}r=s==null?b:s +return new A.d_(a.b,a.c+": "+r)}, +wo(a){return new A.d_(a.b,a.c)}, +Ao(a){var s,r=a.i(0,"code"),q=a.i(0,"description"),p=a.i(0,"name"),o=a.i(0,"details") +if(typeof r!="string"||typeof q!="string")return null +s=(typeof p=="string"?r+("("+p+")"):r)+": "+q +if(typeof o=="string")s=s+", "+o +return s.charCodeAt(0)==0?s:s}, +eV:function eV(a){this.a=a}, +e_:function e_(a){this.a=a}, +d_:function d_(a,b){this.a=a +this.b=b}, +Cf(){var s=A.w1("PowerSync",null,A.P(t.N,t.Y)) +if(s.b!=null)A.p(A.R('Please set "hierarchicalLoggingEnabled" to true if you want to change the level on a non-root logger.')) +J.y(s.c,B.v) +s.c=B.v +s.fh().Z(new A.ta()) +return s}, +ta:function ta(){}, +v5(a){var s,r,q,p=A.bK(t.N) +for(s=a.gv(a);s.l();){r=s.gp() +q=A.Dc(r) +if(q!=null)p.q(0,q) +else if(!B.a.I(r,"ps_"))p.q(0,r)}return p}, +bh:function bh(a){this.a=a}, +ld:function ld(){}, +lf:function lf(a,b){this.a=a +this.b=b}, +le:function le(a,b){this.a=a +this.b=b}, +zw(a){return A.zv(a)}, +zv(a){var s,r,q,p,o,n,m,l,k="UpdateSyncStatus",j="EstablishSyncStream",i="FetchCredentials",h="CloseSyncStream",g="FlushFileSystem",f="DidCompleteSync" +A:{s=a.i(0,"LogLine") +if(s==null)r=a.F("LogLine") +else r=!0 +if(r){t.f.a(s) +r=new A.fj(A.av(s.i(0,"severity")),A.av(s.i(0,"line"))) +break A}q=a.i(0,k) +if(q==null)r=a.F(k) +else r=!0 +if(r){r=t.f +r=new A.fP(A.zd(r.a(r.a(q).i(0,"status")))) +break A}p=a.i(0,j) +if(p==null)r=a.F(j) +else r=!0 +if(r){r=t.f +r=new A.dM(r.a(r.a(p).i(0,"request"))) +break A}o=a.i(0,i) +if(o==null)r=a.F(i) +else r=!0 +if(r){r=new A.f1(A.aT(t.f.a(o).i(0,"did_expire"))) +break A}n=a.i(0,h) +if(n==null)r=a.F(h) +else r=!0 +if(r){t.f.a(n) +r=new A.dH(A.aT(n.i(0,"hide_disconnect"))) +break A}m=a.i(0,g) +if(m==null)r=a.F(g) +else r=!0 +if(r){r=B.aY +break A}l=a.i(0,f) +if(l==null)r=a.F(f) +else r=!0 +if(r){r=B.aX +break A}r=new A.fM(a) +break A}return r}, +zd(a){var s,r,q,p=A.aT(a.i(0,"connected")),o=A.aT(a.i(0,"connecting")),n=A.v([],t.cH) +for(s=J.U(t.j.a(a.i(0,"priority_status"))),r=t.f;s.l();)n.push(A.ze(r.a(s.gp()))) +q=a.i(0,"downloading") +A:{if(q==null){s=null +break A}s=A.zh(r.a(q)) +break A}r=J.hK(t.ia.a(a.i(0,"streams")),new A.lG(),t.em) +r=A.an(r,r.$ti.h("W.E")) +return new A.lF(p,o,n,s,r)}, +ze(a){var s,r=A.S(a.i(0,"priority")),q=A.v3(a.i(0,"has_synced")),p=a.i(0,"last_synced_at") +A:{if(p==null){s=null +break A}s=new A.aK(A.i8(A.S(p)*1000,0,!1),0,!1) +break A}return new A.kd(q,s,r)}, +zh(a){return new A.md(t.f.a(a.i(0,"buckets")).cw(0,new A.me(),t.N,t.cV))}, +fj:function fj(a,b){this.a=a +this.b=b}, +dM:function dM(a){this.a=a}, +fP:function fP(a){this.a=a}, +lF:function lF(a,b,c,d,e){var _=this +_.a=a +_.b=b +_.c=c +_.d=d +_.e=e}, +lG:function lG(){}, +md:function md(a){this.a=a}, +me:function me(){}, +f1:function f1(a){this.a=a}, +dH:function dH(a){this.a=a}, +f4:function f4(){}, +eY:function eY(){}, +fM:function fM(a){this.a=a}, +q3:function q3(a,b,c){this.a=a +this.b=b +this.c=c}, +fo:function fo(a){var _=this +_.d=_.c=_.b=_.a=!1 +_.e=null +_.f=a +_.y=_.x=_.w=_.r=null}, +np:function np(){}, +oz:function oz(a,b,c){this.a=a +this.b=b +this.c=c}, +A8(a){var s=a.a +return s==null?B.J:s}, +A9(a){var s=a.b +return s==null?B.I:s}, +fJ:function fJ(a,b,c,d,e,f){var _=this +_.a=a +_.b=b +_.c=c +_.d=d +_.e=e +_.f=f}, +jg:function jg(a,b){this.a=a +this.b=b}, +zc(a){var s,r,q,p,o,n,m,l,k,j,i=A.av(a.i(0,"name")),h=t.h9.a(a.i(0,"parameters")),g=A.xh(a.i(0,"priority")) +A:{if(g!=null){s=g +break A}s=2147483647 +break A}r=t.f.a(a.i(0,"progress")) +q=A.S(r.i(0,"total")) +r=A.S(r.i(0,"downloaded")) +p=A.aT(a.i(0,"active")) +o=A.aT(a.i(0,"is_default")) +n=A.aT(a.i(0,"has_explicit_subscription")) +m=a.i(0,"expires_at") +B:{if(m==null){l=null +break B}l=new A.aK(A.i8(A.S(m)*1000,0,!1),0,!1) +break B}k=a.i(0,"last_synced_at") +C:{if(k==null){j=null +break C}j=new A.aK(A.i8(A.S(k)*1000,0,!1),0,!1) +break C}return new A.dK(i,h,s,new A.k8(r,q),p,o,n,l,j)}, +dK:function dK(a,b,c,d,e,f,g,h,i){var _=this +_.a=a +_.b=b +_.c=c +_.d=d +_.e=e +_.f=f +_.r=g +_.w=h +_.x=i}, +y2(a,b){var s=null,r={},q=A.bi(s,s,s,s,!0,b) +r.a=null +r.b=!1 +q.d=new A.tY(r,a,q,b) +q.r=new A.tZ(r) +q.e=new A.u_(r) +q.f=new A.u0(r) +return new A.O(q,A.q(q).h("O<1>"))}, +Dy(a){var s,r +for(s=a.length,r=0;r>")),t.H),$async$kF) +case 2:return A.h(null,r)}}) +return A.i($async$kF,r)}, +DD(a,b){var s=null,r={},q=A.bi(s,s,s,s,!0,b) +r.a=!1 +q.r=new A.u7(r,a.b9(new A.u8(q,b),new A.u9(r,q),t.P)) +return new A.O(q,A.q(q).h("O<1>"))}, +AR(a){return new A.e9(a,new DataView(new ArrayBuffer(4)))}, +tY:function tY(a,b,c,d){var _=this +_.a=a +_.b=b +_.c=c +_.d=d}, +tX:function tX(a,b,c){this.a=a +this.b=b +this.c=c}, +tV:function tV(a,b){this.a=a +this.b=b}, +tW:function tW(a,b){this.a=a +this.b=b}, +tZ:function tZ(a){this.a=a}, +u_:function u_(a){this.a=a}, +u0:function u0(a){this.a=a}, +tx:function tx(){}, +u8:function u8(a,b){this.a=a +this.b=b}, +u9:function u9(a,b){this.a=a +this.b=b}, +u7:function u7(a,b){this.a=a +this.b=b}, +e9:function e9(a,b){var _=this +_.a=a +_.b=b +_.c=4 +_.d=null}, +CB(a){var s="Sync service error" +if(a instanceof A.bY)return s +else if(a instanceof A.d_)if(a.a===401)return"Authorization error" +else return s +else if(a instanceof A.a3||t.lW.b(a))return"Configuration error" +else if(a instanceof A.eV)return"Credentials error" +else if(a instanceof A.e_)return"Protocol error" +else return J.vx(a).j(0)+": "+A.o(a)}, +A5(a){return new A.cp(a)}, +om:function om(a,b,c,d,e,f,g,h,i,j,k,l,m,n){var _=this +_.a=a +_.b=b +_.c=c +_.d=d +_.e=e +_.f=f +_.r=g +_.w=h +_.x=i +_.y=j +_.z=null +_.Q=k +_.as=l +_.at=null +_.ax=m +_.ay=n +_.ch=null}, +ou:function ou(a,b){this.a=a +this.b=b}, +ov:function ov(a){this.a=a}, +os:function os(a){this.a=a}, +on:function on(){}, +oo:function oo(){}, +op:function op(a){this.a=a}, +oq:function oq(a){this.a=a}, +or:function or(){}, +ot:function ot(a,b){this.a=a +this.b=b}, +pB:function pB(a,b){this.a=a +this.b=b +this.c=!1}, +pC:function pC(){}, +pH:function pH(){}, +pD:function pD(a){this.a=a}, +pE:function pE(a){this.a=a}, +pF:function pF(a){this.a=a}, +pG:function pG(){}, +dJ:function dJ(a,b){this.a=a +this.b=b}, +cp:function cp(a){this.a=a}, +fR:function fR(){}, +fL:function fL(){}, +f7:function f7(a){this.a=a}, +zx(a){var s=A.q(a).h("bf<2>"),r=t.S,q=s.h("m.E") +return new A.il(a,A.vV(A.fl(new A.bf(a,s),new A.n6(),q,r)),A.vV(A.fl(new A.bf(a,s),new A.n7(),q,r)))}, +ct:function ct(a,b,c,d,e,f,g,h,i,j,k){var _=this +_.a=a +_.b=b +_.c=c +_.d=d +_.e=e +_.f=f +_.r=g +_.w=h +_.x=i +_.y=j +_.z=k}, +oA:function oA(a,b){this.a=a +this.b=b}, +il:function il(a,b,c){this.c=a +this.a=b +this.b=c}, +n6:function n6(){}, +n7:function n7(){}, +ny:function ny(){}, +AT(a,b){var s=new A.da(b) +s.kx(a,b) +return s}, +Bf(a){var s=null,r=new A.fG(B.aQ,A.P(t.ir,t.mQ),t.a9),q=t.pp +r.a=A.bi(r.glo(),r.glv(),r.gm2(),r.gm4(),!0,q) +q=new A.ew(a,new A.fJ(s,s,s,s,B.M,s),r,A.bi(s,s,s,s,!1,q),A.P(t.eV,t.eL),A.v([],t.bN)) +q.kz(a) +return q}, +oB:function oB(a){this.a=a}, +oC:function oC(a){this.a=a}, +da:function da(a){var _=this +_.a=$ +_.b=a +_.d=_.c=null}, +qh:function qh(a){this.a=a}, +qi:function qi(a){this.a=a}, +ew:function ew(a,b,c,d,e,f){var _=this +_.a=a +_.b=b +_.c="{}" +_.d=c +_.e=d +_.w=_.r=_.f=null +_.x=e +_.y=f}, +rC:function rC(a){this.a=a}, +rx:function rx(a,b,c){this.a=a +this.b=b +this.c=c}, +ry:function ry(a,b,c){this.a=a +this.b=b +this.c=c}, +rz:function rz(a,b){this.a=a +this.b=b}, +rA:function rA(a){this.a=a}, +rB:function rB(a){this.a=a}, +fY:function fY(a,b,c,d){var _=this +_.a=a +_.b=b +_.c=c +_.d=d}, +hn:function hn(a){this.a=a}, +h5:function h5(a){this.a=a}, +h3:function h3(a,b){this.a=a +this.b=b}, +fX:function fX(){}, +wu(a){var s=a.content +s=B.d.bm(s,new A.p_(),t.E) +s=A.an(s,s.$ti.h("W.E")) +return s}, +wj(a){var s,r,q,p=null,o=a.endpoint,n=a.token,m=a.userId +if(m==null)m=p +if(a.expiresAt==null)s=p +else{s=a.expiresAt +s.toString +A.S(s) +r=B.b.aU(s,1000) +s=B.b.M(s-r,1000) +if(s<-864e13||s>864e13)A.p(A.a0(s,-864e13,864e13,"millisecondsSinceEpoch",p)) +if(s===864e13&&r!==0)A.p(A.aH(r,"microsecond",u.C)) +A.bd(!1,"isUtc",t.y) +s=new A.aK(s,r,!1)}q=A.d3(o) +if(!q.em("http")&&!q.em("https")||q.gbE().length===0)A.p(A.aH(o,"PowerSync endpoint must be a valid URL",p)) +return new A.bO(o,n,m,s)}, +Ah(a){var s,r,q,p=A.v([],t.W) +for(s=new A.az(a,A.q(a).h("az<1,2>")).gv(0);s.l();){r=s.d +q=r.a +r=r.b.a +p.push({name:q,priority:r[1],atLast:r[0],sinceLast:r[2],targetCount:r[3]})}return p}, +wk(a){var s,r,q,p,o,n,m,l,k,j=null,i=a.f +i=i==null?j:1000*i.a+i.b +s=a.w +s=s==null?j:J.aZ(s) +r=a.x +r=r==null?j:J.aZ(r) +q=A.v([],t.fT) +for(p=J.U(a.y);p.l();){o=p.gp() +n=o.c +m=o.b +m=m==null?j:1000*m.a+m.b +l=o.a +q.push([n,m,l==null?j:l])}k=a.d +A:{if(k==null){p=j +break A}p=A.Ah(k.c) +break A}return{connected:a.a,connecting:a.b,downloading:a.c,uploading:a.e,lastSyncedAt:i,hasSyned:a.r,uploadError:s,downloadError:r,priorityStatusEntries:q,syncProgress:p,streamSubscriptions:B.h.bB(a.z)}}, +AA(a,b){var s=null,r=A.bi(s,s,s,s,!1,t.l4),q=$.vt() +r=new A.jx(A.P(t.S,t.kn),a,b,r,q) +r.ku(s,s,a,b) +return r}, +aE:function aE(a,b){this.a=a +this.b=b}, +p_:function p_(){}, +jx:function jx(a,b,c,d,e){var _=this +_.a=a +_.b=0 +_.c=!1 +_.f=b +_.r=c +_.w=d +_.x=e}, +pw:function pw(a){this.a=a}, +pg:function pg(a,b){this.b=a +this.a=b}, +Du(){var s=null,r=A.fS(),q=t.m,p=A.bi(s,s,s,s,!0,q),o=t.cj +new A.px(new A.qA(new A.nx(new A.qx(r)),new A.O(p,A.q(p).h("O<1>"))),new A.nw(),A.v([],t.az),A.P(t.S,t.lp),new A.dV(A.ng(o)),new A.dV(A.ng(o))).bD() +r=v.G +if($.yz())A.aF(r,"connect",new A.tR(new A.tT(new A.tS(new A.oB(A.P(t.N,t.lG)),p))),!1,q) +else A.aF(r,"message",p.gd4(p),!1,q)}, +tS:function tS(a,b){this.a=a +this.b=b}, +tT:function tT(a){this.a=a}, +tR:function tR(a){this.a=a}, +qA:function qA(a,b){this.a=a +this.b=b}, +nw:function nw(){}, +nx:function nx(a){this.a=a}, +uk(a,b){if(b<0)A.p(A.aA("Offset may not be negative, was "+b+".")) +else if(b>a.c.length)A.p(A.aA("Offset "+b+u.D+a.gk(0)+".")) +return new A.ie(a,b)}, +o0:function o0(a,b,c){var _=this +_.a=a +_.b=b +_.c=c +_.d=null}, +ie:function ie(a,b){this.a=a +this.b=b}, +ei:function ei(a,b,c){this.a=a +this.b=b +this.c=c}, +zr(a,b){var s=A.zs(A.v([A.AY(a,!0)],t.g7)),r=new A.mY(b).$0(),q=B.b.j(B.d.gaS(s).b+1),p=A.zt(s)?0:3,o=A.a1(s) +return new A.mE(s,r,null,1+Math.max(q.length,p),new A.a8(s,new A.mG(),o.h("a8<1,b>")).og(0,B.aV),!A.Dq(new A.a8(s,new A.mH(),o.h("a8<1,k?>"))),new A.X(""))}, +zt(a){var s,r,q +for(s=0;s") +r=s.h("f0") +s=A.an(new A.f0(new A.az(q,s),new A.mL(),r),r.h("m.E")) +return s}, +AY(a,b){var s=new A.qU(a).$0() +return new A.aM(s,!0,null)}, +B_(a){var s,r,q,p,o,n,m=a.gae() +if(!B.a.T(m,"\r\n"))return a +s=a.gC().ga5() +for(r=m.length-1,q=0;q")),r=new A.M(s,b.h("M<0>")),q=t.m +A.aF(a,"success",new A.lt(r,a,b),!1,q) +A.aF(a,"error",new A.lu(r,a),!1,q) +return s}, +za(a,b){var s=new A.l($.n,b.h("l<0>")),r=new A.M(s,b.h("M<0>")),q=t.m +A.aF(a,"success",new A.ly(r,a,b),!1,q) +A.aF(a,"error",new A.lz(r,a),!1,q) +A.aF(a,"blocked",new A.lA(r,a),!1,q) +return s}, +dd:function dd(a,b){var _=this +_.c=_.b=_.a=null +_.d=a +_.$ti=b}, +qo:function qo(a,b){this.a=a +this.b=b}, +qp:function qp(a,b){this.a=a +this.b=b}, +lt:function lt(a,b,c){this.a=a +this.b=b +this.c=c}, +lu:function lu(a,b){this.a=a +this.b=b}, +ly:function ly(a,b,c){this.a=a +this.b=b +this.c=c}, +lz:function lz(a,b){this.a=a +this.b=b}, +lA:function lA(a,b){this.a=a +this.b=b}, +kI(){var s=v.G.navigator +if("storage" in s)return s.storage +return null}, +mk(a,b,c){var s=a.read(b,c) +return s}, +um(a,b,c){var s=a.write(b,c) +return s}, +ul(a,b){return A.ac(a.removeEntry(b,{recursive:!1}),t.X)}, +zn(a){var s=t.om +if(!(v.G.Symbol.asyncIterator in a))A.p(A.K("Target object does not implement the async iterable interface",null)) +return new A.bG(new A.mj(),new A.eN(a,s),s.h("bG"))}, +mj:function mj(){}, +p9:function p9(a){this.a=a}, +pa:function pa(a){this.a=a}, +pc(a,b){var s=0,r=A.j(t.n),q,p,o,n +var $async$pc=A.e(function(c,d){if(c===1)return A.f(d,r) +for(;;)switch(s){case 0:p=v.G +o=a.gjg()?new p.URL(a.j(0)):new p.URL(a.j(0),A.fS().j(0)) +n=A +s=3 +return A.c(A.ac(p.fetch(o,null),t.m),$async$pc) +case 3:q=n.pb(d,null) +s=1 +break +case 1:return A.h(q,r)}}) +return A.i($async$pc,r)}, +pb(a,b){var s=0,r=A.j(t.n),q,p,o,n,m +var $async$pb=A.e(function(c,d){if(c===1)return A.f(d,r) +for(;;)switch(s){case 0:p=new A.i5(A.P(t.S,t.ie)) +o=A +n=A +m=A +s=3 +return A.c(new A.p9(p).ep(a),$async$pb) +case 3:q=new o.e7(new n.pd(m.Az(d,p))) +s=1 +break +case 1:return A.h(q,r)}}) +return A.i($async$pb,r)}, +e7:function e7(a){this.a=a}, +fU:function fU(a,b,c,d,e){var _=this +_.d=a +_.e=b +_.r=c +_.b=d +_.a=e}, +ju:function ju(a,b){this.a=a +this.b=b +this.c=0}, +wg(a){var s=J.y(a.byteLength,8) +if(!s)throw A.a(A.K("Must be 8 in length",null)) +s=v.G.Int32Array +return new A.nS(t.jS.a(A.dw(s,[a])))}, +zP(a){return B.l}, +zQ(a){var s=a.b +return new A.ab(s.getInt32(0,!1),s.getInt32(4,!1),s.getInt32(8,!1))}, +zR(a){var s=a.b +return new A.b3(B.i.aO(A.uC(a.a,16,s.getInt32(12,!1))),s.getInt32(0,!1),s.getInt32(4,!1),s.getInt32(8,!1))}, +nS:function nS(a){this.b=a}, +bM:function bM(a,b,c){this.a=a +this.b=b +this.c=c}, +ap:function ap(a,b,c,d,e){var _=this +_.c=a +_.d=b +_.a=c +_.b=d +_.$ti=e}, +c_:function c_(){}, +be:function be(){}, +ab:function ab(a,b,c){this.a=a +this.b=b +this.c=c}, +b3:function b3(a,b,c,d){var _=this +_.d=a +_.a=b +_.b=c +_.c=d}, +jt(a){var s=0,r=A.j(t.a1),q,p,o,n,m,l,k,j,i +var $async$jt=A.e(function(b,c){if(b===1)return A.f(c,r) +for(;;)switch(s){case 0:k=t.m +s=3 +return A.c(A.ac(A.kI().getDirectory(),k),$async$jt) +case 3:j=c +i=$.hI().cO(0,a.root) +p=i.length,o=0 +case 4:if(!(o")),new A.ev(o),!0,p) +q.a=m +s=A.vS(new A.O(o,A.q(o).h("O<1>")),new A.ev(n),!0,p) +q.b=s +a.start() +A.aF(a,"message",new A.t0(q),!1,p) +m=m.b +m===$&&A.B() +new A.O(m,A.q(m).h("O<1>")).nV(new A.t1(a),new A.t2(a,c)) +if(c==null&&b!=null)$.ue().ju(b).b8(new A.t3(q),t.P) +return s}, +t0:function t0(a){this.a=a}, +t1:function t1(a){this.a=a}, +t2:function t2(a,b){this.a=a +this.b=b}, +t3:function t3(a){this.a=a}, +iQ:function iQ(){}, +nD:function nD(a){this.a=a}, +nB:function nB(a){this.a=a}, +nA:function nA(a){this.a=a}, +nz:function nz(a){this.a=a}, +nC:function nC(){}, +nE:function nE(a,b,c){this.a=a +this.b=b +this.c=c}, +A6(a,b){var s=t.H +s=new A.iT(a,b,new A.as(new A.l($.n,t.ny),t.mE),A.cY(!1,t.e1),new A.jL(A.cY(!1,s)),new A.jL(A.cY(!1,s))) +s.kp(a,b) +return s}, +AB(a,b){var s=t.m,r=A.cY(!1,s),q=t.S +s=new A.jy(r,b,a,A.P(q,t.br),A.P(q,s)) +s.hx(a) +q=a.a +q===$&&A.B() +q.c.a.O(r.gag()) +return s}, +zf(a,b,c,d){var s=A.ng(t.cj) +return new A.lX(d,new A.dV(s),A.bK(t.jC))}, +jL:function jL(a){this.a=null +this.b=a}, +iT:function iT(a,b,c,d,e,f){var _=this +_.a=a +_.b=b +_.c=c +_.d=null +_.e=d +_.f=e +_.r=f +_.w=$}, +nL:function nL(a){this.a=a}, +nM:function nM(a){this.a=a}, +nH:function nH(a){this.a=a}, +nN:function nN(a){this.a=a}, +nO:function nO(a){this.a=a}, +nP:function nP(a){this.a=a}, +nJ:function nJ(a,b,c,d){var _=this +_.a=a +_.b=b +_.c=c +_.d=d}, +nI:function nI(a,b,c){this.a=a +this.b=b +this.c=c}, +nK:function nK(a,b,c){this.a=a +this.b=b +this.c=c}, +nQ:function nQ(a){this.a=a}, +jy:function jy(a,b,c,d,e){var _=this +_.e=a +_.f=b +_.a=c +_.b=0 +_.c=d +_.d=e}, +lX:function lX(a,b,c){this.d=a +this.e=b +this.z=c}, +lY:function lY(){}, +i4:function i4(a){this.a=a}, +lI:function lI(a,b){this.c=a +this.a=b}, +d6:function d6(){}, +qw:function qw(){}, +pn:function pn(a){this.a=a}, +po:function po(a){this.a=a}, +pp:function pp(a){this.a=a}, +cj:function cj(a){this.a=a}, +m9:function m9(a,b,c){this.a=a +this.b=b +this.c=c}, +dV:function dV(a){this.a=!1 +this.b=a}, +ns:function ns(a,b){this.a=a +this.b=b}, +nr:function nr(a,b,c,d,e){var _=this +_.a=a +_.b=b +_.c=c +_.d=d +_.e=e}, +nq:function nq(a,b,c,d){var _=this +_.a=a +_.b=b +_.c=c +_.d=d}, +z7(a){var s,r,q,p,o,n,m=A.v([],t.kC),l=t.c.a(a.a),k=t.o.b(l)?l:new A.al(l,A.a1(l).h("al<1,d>")) +for(s=J.a2(k),r=0;r=8,m=o,l=o,k=o,j=o,i=o,h=o,g=o +if(n){s=a[0] +m=a[1] +l=a[2] +k=a[3] +j=a[4] +i=a[5] +h=a[6] +g=a[7]}else s=o +if(!n)throw A.a(A.u("Pattern matching error")) +n=new A.mi() +l=A.S(A.cD(l)) +A.av(s) +r=n.$1(m) +q=n.$1(j) +p=i!=null&&h!=null?A.uG(t.c.a(i),t.a.a(h)):o +n=n.$1(k) +A.xg(g) +return new A.cX(s,r,l,g==null?o:A.S(g),n,q,p)}, +zk(a){var s,r,q,p,o,n,m=null,l=a.r +A:{if(l==null){s=m +break A}s=A.uH(l) +break A}r=a.b +if(r==null)r=m +q=a.e +if(q==null)q=m +p=a.f +if(p==null)p=m +o=s==null +n=o?m:s.a +s=o?m:s.b +o=a.d +if(o==null)o=m +return[a.a,r,a.c,q,p,n,s,o]}, +Ac(a,a0,a1,a2){var s,r,q,p,o,n,m,l,k,j,i,h=t.bb,g=A.v([],h),f=a2.a,e=f.length,d=a2.d,c=d.length,b=new Uint8Array(c*e) +for(c=t.X,s=0;s")) +s=J.hK(s,new A.nU(),t.N) +r=A.an(s,s.$ti.h("W.E")) +s=a.n +if(s==null)q=g +else{s=t.fi.b(s)?s:new A.al(s,A.a1(s).h("al<1,d?>")) +s=J.hK(s,new A.nV(),t.jv) +q=A.an(s,s.$ti.h("W.E"))}s=a.v +p=s==null?g:A.bg(s,0,g) +o=A.v([],t.dO) +s=a.r +s.toString +if(!t.mu.b(s))s=new A.al(s,A.a1(s).h("al<1,A>")) +s=J.U(s) +n=p!=null +m=0 +while(s.l()){l=s.gp() +k=[] +l=B.d.gv(l) +while(l.l()){j=l.gp() +if(n){i=p[m] +h=i>=8?B.x:B.a8[i]}else h=B.x +k.push(h.iX(j));++m}o.push(k)}return A.wh(r,q,o)}else return g}, +Dr(a){if(a==="sharedCompatibilityCheck"||a==="dedicatedCompatibilityCheck"||a==="dedicatedInSharedCompatibilityCheck")return!0 +else return!1}, +mi:function mi(){}, +nU:function nU(){}, +nV:function nV(){}, +y3(a,b,c,d,e,f,g){return{c:b,n:f,v:g,r:e,x:a,y:c,i:d,t:"rowsResponse"}}, +tF(a){var s,r,q,p,o,n=v.G,m=new n.Array() +switch(a.t){case"connect":m.push(a.r.port) +break +case"fileSystemAccess":s=a.b +if(s!=null)m.push(s) +break +case"runQuery":r=a.v +if(r!=null)m.push(r) +break +case"simpleSuccessResponse":q=a.r +if(q!=null){n=n.ArrayBuffer +n=q instanceof n +p=q}else{p=null +n=!1}if(n)m.push(p) +break +case"endpointResponse":m.push(a.r.port) +break +case"rowsResponse":o=a.v +if(o!=null)m.push(o) +break}return m}, +D7(a,b,c,d,e,f){switch(a.t){case"startFileSystemServer":return f.$1(a) +case"abort":return b.$1(a) +case"notifyUpdate":case"notifyCommit":case"notifyRollback":return c.$1(a) +case"simpleSuccessResponse":case"endpointResponse":case"rowsResponse":case"errorResponse":return e.$1(a) +default:return d.$1(a)}}, +fn:function fn(a,b){this.a=a +this.b=b}, +nR:function nR(){}, +zo(a){var s,r +for(s=0;s<5;++s){r=B.bF[s] +if(r.c===a)return r}throw A.a(A.K("Unknown FS implementation: "+a,null))}, +ws(a){var s,r,q,p,o,n,m,l,k,j=null +A:{if(a==null){s=j +r=B.ax +break A}q=A.eD(a) +p=q?a:j +if(q){s=p +r=B.as +break A}q=a instanceof A.aC +o=q?a:j +if(q){s=v.G.BigInt(o.j(0)) +r=B.at +break A}q=typeof a=="number" +n=q?a:j +if(q){s=n +r=B.au +break A}q=typeof a=="string" +m=q?a:j +if(q){s=m +r=B.av +break A}q=t.p.b(a) +l=q?a:j +if(q){s=l +r=B.aw +break A}q=A.dt(a) +k=q?a:j +if(q){s=k +r=B.ay +break A}s=A.vi(a) +r=B.x}return new A.au(r,s)}, +uH(a){var s,r,q=[],p=a.length,o=new Uint8Array(p) +for(s=0;s=8?B.x:B.a8[q]}else p=B.x +m[r]=p.iX(a[r])}return m}, +ci:function ci(a,b,c){this.c=a +this.a=b +this.b=c}, +bD:function bD(a,b){this.a=a +this.b=b}, +tA(){var s=0,r=A.j(t.y),q,p=2,o=[],n,m,l,k,j +var $async$tA=A.e(function(a,b){if(a===1){o.push(b) +s=p}for(;;)switch(s){case 0:k=v.G +if(!("indexedDB" in k)||!("FileReader" in k)){q=!1 +s=1 +break}n=A.a4(k.indexedDB) +p=4 +s=7 +return A.c(A.z9(n.open("drift_mock_db"),t.m),$async$tA) +case 7:m=b +m.close() +n.deleteDatabase("drift_mock_db") +p=2 +s=6 +break +case 4:p=3 +j=o.pop() +q=!1 +s=1 +break +s=6 +break +case 3:s=2 +break +case 6:q=!0 +s=1 +break +case 1:return A.h(q,r) +case 2:return A.f(o.at(-1),r)}}) +return A.i($async$tA,r)}, +ty(a){return A.D_(a)}, +D_(a){var s=0,r=A.j(t.y),q,p=2,o=[],n,m,l,k,j,i +var $async$ty=A.e(function(b,c){if(b===1){o.push(c) +s=p}for(;;)switch(s){case 0:j={} +j.a=null +p=4 +n=A.a4(v.G.indexedDB) +m=n.open(a,1) +m.onupgradeneeded=A.bV(new A.tz(j,m)) +s=7 +return A.c(A.z8(m,t.m),$async$ty) +case 7:l=c +if(j.a==null)j.a=!0 +l.close() +p=2 +s=6 +break +case 4:p=3 +i=o.pop() +s=6 +break +case 3:s=2 +break +case 6:j=j.a +q=j===!0 +s=1 +break +case 1:return A.h(q,r) +case 2:return A.f(o.at(-1),r)}}) +return A.i($async$ty,r)}, +eL(){var s=0,r=A.j(t.o),q,p=2,o=[],n=[],m,l,k,j,i,h,g +var $async$eL=A.e(function(a,b){if(a===1){o.push(b) +s=p}for(;;)switch(s){case 0:h=A.kI() +if(h==null){q=B.H +s=1 +break}j=t.m +s=3 +return A.c(A.ac(h.getDirectory(),j),$async$eL) +case 3:m=b +p=5 +s=8 +return A.c(A.ac(m.getDirectoryHandle("drift_db",{create:!1}),j),$async$eL) +case 8:m=b +p=2 +s=7 +break +case 5:p=4 +g=o.pop() +q=B.H +s=1 +break +s=7 +break +case 4:s=2 +break +case 7:l=A.v([],t.s) +j=new A.bU(A.bd(A.zn(m),"stream",t.K)) +p=9 +case 12:s=14 +return A.c(j.l(),$async$eL) +case 14:if(!b){s=13 +break}k=j.gp() +if(J.y(k.kind,"directory"))J.kR(l,k.name) +s=12 +break +case 13:n.push(11) +s=10 +break +case 9:n=[2] +case 10:p=2 +s=15 +return A.c(j.u(),$async$eL) +case 15:s=n.pop() +break +case 11:q=l +s=1 +break +case 1:return A.h(q,r) +case 2:return A.f(o.at(-1),r)}}) +return A.i($async$eL,r)}, +z8(a,b){var s=new A.l($.n,b.h("l<0>")),r=new A.M(s,b.h("M<0>")),q=t.m +A.aF(a,"success",new A.lr(r,a,b),!1,q) +A.aF(a,"error",new A.ls(r,a),!1,q) +return s}, +z9(a,b){var s=new A.l($.n,b.h("l<0>")),r=new A.M(s,b.h("M<0>")),q=t.m +A.aF(a,"success",new A.lv(r,a,b),!1,q) +A.aF(a,"error",new A.lw(r,a),!1,q) +A.aF(a,"blocked",new A.lx(r,a),!1,q) +return s}, +tz:function tz(a,b){this.a=a +this.b=b}, +lr:function lr(a,b,c){this.a=a +this.b=b +this.c=c}, +ls:function ls(a,b){this.a=a +this.b=b}, +lv:function lv(a,b,c){this.a=a +this.b=b +this.c=c}, +lw:function lw(a,b){this.a=a +this.b=b}, +lx:function lx(a,b){this.a=a +this.b=b}, +f2:function f2(a,b){this.a=a +this.b=b}, +cr:function cr(a,b){this.a=a +this.b=b}, +cU:function cU(a,b){this.a=a +this.b=b}, +bu:function bu(a,b){this.a=a +this.b=b}, +BT(a){var s=a.gnM() +return new A.bG(new A.t6(),s,A.q(s).h("bG"))}, +wI(a,b){var s=A.v([],t.W),r=b==null?a.b:b +return new A.eb(a,r,new A.hq(),new A.hq(),new A.hq(),s)}, +AS(a,b,c){var s=t.S +s=new A.ea(c,A.v([],t.ba),a,A.P(s,t.br),A.P(s,t.m)) +s.hx(a) +s.kw(a,b,c) +return s}, +xr(a){var s +switch(a.a){case 0:s="/database" +break +case 1:s="/database-journal" +break +default:s=null}return s}, +dx(){var s=0,r=A.j(t.kO),q,p=2,o=[],n=[],m,l,k,j,i,h,g,f,e,d,c,b +var $async$dx=A.e(function(a,a0){if(a===1){o.push(a0) +s=p}for(;;)switch(s){case 0:c=A.kI() +if(c==null){q=B.L +s=1 +break}m=null +l=null +k=null +j=!1 +p=4 +e=t.m +s=7 +return A.c(A.ac(c.getDirectory(),e),$async$dx) +case 7:m=a0 +s=8 +return A.c(A.ac(m.getFileHandle("_drift_feature_detection",{create:!0}),e),$async$dx) +case 8:l=a0 +s=9 +return A.c(A.hF(l),$async$dx) +case 9:i=a0 +h=null +g=null +h=i.a +g=i.b +j=h +k=g +f=A.iq(k,"getSize",null,null,null,null) +s=typeof f==="object"?10:11 +break +case 10:s=12 +return A.c(A.ac(A.a4(f),t.X),$async$dx) +case 12:q=B.L +n=[1] +s=5 +break +case 11:h=j +q=new A.hk(!0,h) +n=[1] +s=5 +break +n.push(6) +s=5 +break +case 4:p=3 +b=o.pop() +q=B.L +n=[1] +s=5 +break +n.push(6) +s=5 +break +case 3:n=[2] +case 5:p=2 +if(k!=null)k.close() +s=m!=null&&l!=null?13:14 +break +case 13:s=15 +return A.c(A.ul(m,"_drift_feature_detection"),$async$dx) +case 15:case 14:s=n.pop() +break +case 6:case 1:return A.h(q,r) +case 2:return A.f(o.at(-1),r)}}) +return A.i($async$dx,r)}, +hF(a){return A.CD(a)}, +CD(a){var s=0,r=A.j(t.mk),q,p=2,o=[],n,m,l,k,j,i +var $async$hF=A.e(function(b,c){if(b===1){o.push(c) +s=p}for(;;)switch(s){case 0:j=null +p=4 +l=t.m +s=7 +return A.c(A.ac(a.createSyncAccessHandle({mode:"readwrite-unsafe"}),l),$async$hF) +case 7:j=c +s=8 +return A.c(A.ac(a.createSyncAccessHandle({mode:"readwrite-unsafe"}),l),$async$hF) +case 8:n=c +n.close() +l=j +q=new A.au(!0,l) +s=1 +break +p=2 +s=6 +break +case 4:p=3 +i=o.pop() +l=j +if(l!=null)l.close() +s=9 +return A.c(A.ac(a.createSyncAccessHandle(),t.m),$async$hF) +case 9:m=c +q=new A.au(!1,m) +s=1 +break +s=6 +break +case 3:s=2 +break +case 6:case 1:return A.h(q,r) +case 2:return A.f(o.at(-1),r)}}) +return A.i($async$hF,r)}, +t6:function t6(){}, +hq:function hq(){this.a=null}, +eb:function eb(a,b,c,d,e,f){var _=this +_.a=a +_.b=b +_.c=c +_.d=d +_.e=e +_.f=null +_.r=1 +_.w=f}, +qj:function qj(a){this.a=a}, +qn:function qn(a,b){this.a=a +this.b=b}, +qk:function qk(a,b){this.a=a +this.b=b}, +ql:function ql(a){this.a=a}, +qm:function qm(a,b){this.a=a +this.b=b}, +ea:function ea(a,b,c,d,e){var _=this +_.e=a +_.f=b +_.a=c +_.b=0 +_.c=d +_.d=e}, +q7:function q7(a){this.a=a}, +qa:function qa(a,b,c){this.a=a +this.b=b +this.c=c}, +qb:function qb(a,b){this.a=a +this.b=b}, +qe:function qe(a,b){this.a=a +this.b=b}, +q9:function q9(a,b){this.a=a +this.b=b}, +q8:function q8(a,b){this.a=a +this.b=b}, +qd:function qd(a,b){this.a=a +this.b=b}, +qc:function qc(a,b){this.a=a +this.b=b}, +qg:function qg(a,b){this.a=a +this.b=b}, +qf:function qf(a,b){this.a=a +this.b=b}, +q6:function q6(a){this.a=a}, +i6:function i6(a,b,c,d,e,f){var _=this +_.a=a +_.b=b +_.c=c +_.d=d +_.e=e +_.f=f +_.r=1 +_.z=_.y=_.x=_.w=null}, +mc:function mc(a){this.a=a}, +mb:function mb(a){this.a=a}, +ma:function ma(a,b){this.a=a +this.b=b}, +px:function px(a,b,c,d,e,f){var _=this +_.a=a +_.b=b +_.c=c +_.d=0 +_.e=d +_.f=0 +_.w=_.r=null +_.x=e +_.y=f +_.Q=$}, +py:function py(a,b){this.a=a +this.b=b}, +pz:function pz(a,b){this.a=a +this.b=b}, +pA:function pA(a){this.a=a}, +qx:function qx(a){this.a=a}, +rR:function rR(){}, +qv:function qv(a){this.a=a}, +Be(){return new A.rg(A.jS(new A.rh(),t.z))}, +iB:function iB(a){this.a=a}, +rg:function rg(a){this.a=null +this.b=a}, +rh:function rh(){}, +rl:function rl(a,b,c,d){var _=this +_.a=a +_.b=b +_.c=c +_.d=d}, +ri:function ri(a,b){this.a=a +this.b=b}, +rj:function rj(a){this.a=a}, +rm:function rm(a,b){this.a=a +this.b=b}, +rk:function rk(a){this.a=a}, +j8:function j8(){}, +j9:function j9(){}, +dD:function dD(a){this.a=a}, +nW(a,b,c){return A.Ae(a,b,c,c)}, +Ae(a,b,c,d){var s=0,r=A.j(d),q,p=2,o=[],n=[],m,l +var $async$nW=A.e(function(e,f){if(e===1){o.push(f) +s=p}for(;;)switch(s){case 0:l=new A.fy(a) +p=3 +s=6 +return A.c(b.$1(l),$async$nW) +case 6:m=f +q=m +n=[1] +s=4 +break +n.push(5) +s=4 +break +case 3:n=[2] +case 4:p=2 +l.c=!0 +s=n.pop() +break +case 5:case 1:return A.h(q,r) +case 2:return A.f(o.at(-1),r)}}) +return A.i($async$nW,r)}, +Af(a){var s +A:{if(0===a){s=B.bM +break A}s=""+a +s=new A.hm("SAVEPOINT s"+s,"RELEASE s"+s,"ROLLBACK TO s"+s) +break A}return s}, +fA(a,b,c){return A.Ag(a,b,c,c)}, +Ag(a,b,c,d){var s=0,r=A.j(d),q,p=2,o=[],n=[],m,l +var $async$fA=A.e(function(e,f){if(e===1){o.push(f) +s=p}for(;;)switch(s){case 0:l=new A.fz(0,a) +p=3 +s=6 +return A.c(b.$1(l),$async$fA) +case 6:m=f +s=7 +return A.c(a.ea(),$async$fA) +case 7:q=m +n=[1] +s=4 +break +n.push(5) +s=4 +break +case 3:n=[2] +case 4:p=2 +l.c=!0 +s=n.pop() +break +case 5:case 1:return A.h(q,r) +case 2:return A.f(o.at(-1),r)}}) +return A.i($async$fA,r)}, +jm:function jm(){}, +fy:function fy(a){this.a=a +this.c=this.b=!1}, +fz:function fz(a,b){var _=this +_.d=a +_.a=b +_.c=_.b=!1}, +j7:function j7(){}, +o3:function o3(a,b){this.a=a +this.b=b}, +o4:function o4(a,b){this.a=a +this.b=b}, +At(a,b,c){return A.CC(new A.oZ(),c,a,!0,b,t.en)}, +As(a){var s,r=A.bK(t.N) +for(s=0;s<1;++s)r.q(0,a[s].toLowerCase()) +return new A.kn(new A.oY(r))}, +CC(a,b,c,d,e,f){return new A.bH(!1,new A.to(e,a,c,b,!0,f),f.h("bH<0>"))}, +ad:function ad(a){this.a=a}, +oZ:function oZ(){}, +oY:function oY(a){this.a=a}, +oX:function oX(a){this.a=a}, +to:function to(a,b,c,d,e,f){var _=this +_.a=a +_.b=b +_.c=c +_.d=d +_.e=e +_.f=f}, +tp:function tp(a,b){this.a=a +this.b=b}, +tq:function tq(a,b,c,d){var _=this +_.a=a +_.b=b +_.c=c +_.d=d}, +tk:function tk(a,b,c){this.a=a +this.b=b +this.c=c}, +tj:function tj(a,b){this.a=a +this.b=b}, +tr:function tr(a,b,c,d){var _=this +_.a=a +_.b=b +_.c=c +_.d=d}, +tt:function tt(a,b){this.a=a +this.b=b}, +ts:function ts(a,b){this.a=a +this.b=b}, +tl:function tl(a){this.a=a}, +tm:function tm(a,b,c){this.a=a +this.b=b +this.c=c}, +tn:function tn(a,b){this.a=a +this.b=b}, +wr(a,b,c,d,e,f){var s +if(a==null)return c.$0() +s=A.DA(b,d,e) +a.pe(s.a,s.b) +return A.dO(c,f).O(new A.oN(a))}, +DA(a,b,c){var s,r,q,p,o,n=t.z +n=A.P(n,n) +n.m(0,"sql",c) +s=[] +for(r=b.length,q=t.j,p=0;p") +else s.push(o)}n.m(0,"parameters",s) +return new A.au("sqlite_async:"+a+" "+c,n)}, +oN:function oN(a){this.a=a}, +Ar(a){var s={},r=A.v([],t.jI),q=A.bK(t.N) +s.a=A.v([],t.bO) +return new A.bH(!0,new A.oK(new A.oF(s,r,a,new A.oL(q),new A.oI(r,q),new A.oJ(q)),new A.oM(s,r)),t.lX)}, +oL:function oL(a){this.a=a}, +oI:function oI(a,b){this.a=a +this.b=b}, +oJ:function oJ(a){this.a=a}, +oF:function oF(a,b,c,d,e,f){var _=this +_.a=a +_.b=b +_.c=c +_.d=d +_.e=e +_.f=f}, +oG:function oG(a){this.a=a}, +oH:function oH(a){this.a=a}, +oM:function oM(a,b){this.a=a +this.b=b}, +oK:function oK(a,b){this.a=a +this.b=b}, +oE:function oE(a,b){this.a=a +this.b=b}, +dm:function dm(a,b){this.a=a +this.b=b}, +kK(a,b){return A.DO(a,b,b)}, +DO(a,b,c){var s=0,r=A.j(c),q,p=2,o=[],n,m,l,k,j,i,h +var $async$kK=A.e(function(d,e){if(d===1){o.push(e) +s=p}for(;;)switch(s){case 0:p=4 +s=7 +return A.c(a.$0(),$async$kK) +case 7:j=e +q=j +s=1 +break +p=2 +s=6 +break +case 4:p=3 +h=o.pop() +j=A.H(h) +if(j instanceof A.cU){n=j +m=n.b +l=null +if(m!=null){l=m +throw A.a(l)}if(B.a.T(n.a,"Database is not in a transaction"))throw A.a(A.ja(null,null,0,"Transaction rolled back by earlier statement. Cannot execute.",null,null,null)) +if(B.a.T("Remote error: "+n.a,"SqliteException")){k=A.ar("SqliteException\\((\\d+)\\)",!0) +j=k.j2(n.a) +j=j==null?null:j.jY(1) +throw A.a(A.ja(null,null,A.xZ(j==null?"0":j),n.a,null,null,null))}throw h}else throw h +s=6 +break +case 3:s=2 +break +case 6:case 1:return A.h(q,r) +case 2:return A.f(o.at(-1),r)}}) +return A.i($async$kK,r)}, +BU(a,b,c){return A.mn(a,new A.t7(b),c,t.fN)}, +jv:function jv(a,b,c,d){var _=this +_.a=a +_.b=b +_.c=c +_.d=d}, +pj:function pj(a,b){this.a=a +this.b=b}, +pm:function pm(a,b){this.a=a +this.b=b}, +pl:function pl(a,b){this.a=a +this.b=b}, +pk:function pk(a,b){this.a=a +this.b=b}, +ph:function ph(a,b,c,d){var _=this +_.a=a +_.b=b +_.c=c +_.d=d}, +pi:function pi(a,b,c,d){var _=this +_.a=a +_.b=b +_.c=c +_.d=d}, +cc:function cc(a,b,c){var _=this +_.a=a +_.b=b +_.c=c +_.d=!1}, +rL:function rL(a,b,c){this.a=a +this.b=b +this.c=c}, +rK:function rK(a,b,c){this.a=a +this.b=b +this.c=c}, +rJ:function rJ(a,b,c){this.a=a +this.b=b +this.c=c}, +rI:function rI(a,b,c){this.a=a +this.b=b +this.c=c}, +t7:function t7(a){this.a=a}, +ug(a,b,c){var s=A.uH(c) +return{rawKind:a.b,rawSql:b,rawParameters:s.a,typeInfo:s.b}}, +ch:function ch(a,b){this.a=a +this.b=b}, +jn:function jn(a){this.a=0 +this.b=a}, +oU:function oU(){}, +oV:function oV(a,b){this.a=a +this.b=b}, +oW:function oW(a,b,c){this.a=a +this.b=b +this.c=c}, +uJ(a){var s=A.Be() +return new A.pq(s,a)}, +pq:function pq(a,b){this.a=a +this.b=b}, +pr:function pr(a,b){this.a=a +this.b=b}, +pt:function pt(a){this.a=a}, +ps:function ps(){}, +f8:function f8(a){this.a=a}, +AU(){return new A.ec()}, +kZ:function kZ(){}, +hS:function hS(a,b,c){this.a=a +this.b=b +this.c=c}, +l_:function l_(a){this.a=a}, +l0:function l0(a,b){this.a=a +this.b=b}, +l1:function l1(a,b,c){this.a=a +this.b=b +this.c=c}, +ec:function ec(){this.a=!1 +this.b=null}, +vS(a,b,c,d){var s,r={} +r.a=a +s=new A.f6(d.h("f6<0>")) +s.ko(b,!0,r,d) +return s}, +f6:function f6(a){var _=this +_.b=_.a=$ +_.c=null +_.d=!1 +_.$ti=a}, +mB:function mB(a,b){this.a=a +this.b=b}, +mA:function mA(a){this.a=a}, +h9:function h9(a,b,c,d){var _=this +_.a=a +_.b=b +_.c=c +_.e=_.d=!1 +_.r=_.f=null +_.w=d}, +jb:function jb(a){this.b=this.a=$ +this.$ti=a}, +fF:function fF(){}, +jf:function jf(a,b,c){this.c=a +this.a=b +this.b=c}, +ow:function ow(a,b){var _=this +_.a=a +_.b=b +_.c=0 +_.e=_.d=null}, +e5:function e5(){}, +jX:function jX(){}, +bE:function bE(a,b){this.a=a +this.b=b}, +aF(a,b,c,d,e){var s +if(c==null)s=null +else{s=A.xO(new A.qC(c),t.m) +s=s==null?null:A.bV(s)}s=new A.eh(a,b,s,!1,e.h("eh<0>")) +s.fH() +return s}, +xO(a,b){var s=$.n +if(s===B.e)return a +return s.fR(a,b)}, +ui:function ui(a,b){this.a=a +this.$ti=b}, +eg:function eg(a,b,c,d){var _=this +_.a=a +_.b=b +_.c=c +_.$ti=d}, +eh:function eh(a,b,c,d,e){var _=this +_.a=0 +_.b=a +_.c=b +_.d=c +_.e=d +_.$ti=e}, +qC:function qC(a){this.a=a}, +qD:function qD(a){this.a=a}, +pu(a){var s=0,r=A.j(t.m1),q,p,o,n,m +var $async$pu=A.e(function(b,c){if(b===1)return A.f(c,r) +for(;;)switch(s){case 0:o=new A.jn(A.P(t.N,t.ao)) +s=3 +return A.c(A.zf(B.bf,A.fS(),B.bc,o.gnF()).fS(new A.au(a.b,a.a)),$async$pu) +case 3:n=c +m=a.c +A:{p=null +if(m!=null){p=A.uJ(m) +break A}break A}q=new A.jv(n,p,!1,o.os(n)) +s=1 +break +case 1:return A.h(q,r)}}) +return A.i($async$pu,r)}, +vk(a){if(typeof dartPrint=="function"){dartPrint(a) +return}if(typeof console=="object"&&typeof console.log!="undefined"){console.log(a) +return}if(typeof print=="function"){print(a) +return}throw"Unable to print message: "+String(a)}, +zF(a,b){return b in a}, +iq(a,b,c,d,e,f){var s +if(c==null)return a[b]() +else if(d==null)return a[b](c) +else if(e==null)return a[b](c,d) +else{s=a[b](c,d,e) +return s}}, +zE(a,b){return b in a}, +Dh(a,b,c,d){var s,r,q,p,o,n=A.P(d,c.h("t<0>")) +for(s=c.h("A<0>"),r=0;r<1;++r){q=a[r] +p=b.$1(q) +o=n.i(0,p) +if(o==null){o=A.v([],s) +n.m(0,p,o) +p=o}else p=o +J.kR(p,q)}return n}, +zy(a,b){var s,r,q +for(s=a.length,r=0;r")),s=s.y[1],q=0;r.l();){p=r.a +q+=p==null?s.a(p):p}return q}, +vW(a,b){var s,r,q=A.bK(b) +for(s=a.a,s=new A.by(s,s.r,s.e);s.l();)for(r=J.U(s.d);r.l();)q.q(0,r.gp()) +return q}, +xX(a){var s,r=a.c.a.i(0,"charset") +if(a.a==="application"&&a.b==="json"&&r==null)return B.i +if(r!=null){s=A.vP(r) +if(s==null)s=B.m}else s=B.m +return s}, +yc(a){return a}, +DK(a){return new A.dF(a)}, +DN(a,b,c){var s,r,q,p +try{q=c.$0() +return q}catch(p){q=A.H(p) +if(q instanceof A.e2){s=q +throw A.a(A.Ak("Invalid "+a+": "+s.a,s.b,s.gdF()))}else if(t.lW.b(q)){r=q +throw A.a(A.ai("Invalid "+a+' "'+b+'": '+r.gji(),r.gdF(),r.ga5()))}else throw p}}, +xU(){var s,r,q,p,o=null +try{o=A.fS()}catch(s){if(t.L.b(A.H(s))){r=$.t5 +if(r!=null)return r +throw s}else throw s}if(J.y(o,$.xo)){r=$.t5 +r.toString +return r}$.xo=o +if($.vo()===$.dB())r=$.t5=o.ey(".").j(0) +else{q=o.ho() +p=q.length-1 +r=$.t5=p===0?q:B.a.t(q,0,p)}return r}, +y_(a){var s +if(!(a>=65&&a<=90))s=a>=97&&a<=122 +else s=!0 +return s}, +xW(a,b){var s,r,q=null,p=a.length,o=b+2 +if(p")),q=q.h("W.E");r.l();){p=r.d +if(!J.y(p==null?q.a(p):p,s))return!1}return!0}, +DB(a,b){var s=B.d.cr(a,null) +if(s<0)throw A.a(A.K(A.o(a)+" contains no null elements.",null)) +a[s]=b}, +y8(a,b){var s=B.d.cr(a,b) +if(s<0)throw A.a(A.K(A.o(a)+" contains no elements matching "+b.j(0)+".",null)) +a[s]=null}, +D4(a,b){var s,r,q,p +for(s=new A.bv(a),r=t.V,s=new A.aq(s,s.gk(0),r.h("aq")),r=r.h("C.E"),q=0;s.l();){p=s.d +if((p==null?r.a(p):p)===b)++q}return q}, +tG(a,b,c){var s,r,q +if(b.length===0)for(s=0;;){r=B.a.bj(a,"\n",s) +if(r===-1)return a.length-s>=c?s:null +if(r-s>=c)return s +s=r+1}r=B.a.cr(a,b) +while(r!==-1){q=r===0?0:B.a.en(a,"\n",r-1)+1 +if(c===r-q)return q +r=B.a.bj(a,b,r+1)}return null}, +vd(a,b,c,d,e,f){var s,r=b.a,q=b.b,p=r.d,o=p.sqlite3_extended_errcode(q),n=p.sqlite3_error_offset(q) +A:{if(n<0){n=null +break A}break A}s=a.a +return new A.cX(A.d7(r.b,p.sqlite3_errmsg(q)),A.d7(s.b,s.d.sqlite3_errstr(o))+" (code "+A.o(o)+")",c,n,d,e,f)}, +kJ(a,b,c,d,e){throw A.a(A.vd(a.a,a.b,b,c,d,e))}, +uo(a,b){var s,r +for(s=b,r=0;r<16;++r)s+=A.aQ("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ012346789".charCodeAt(a.er(61))) +return s.charCodeAt(0)==0?s:s}, +nG(a){var s=0,r=A.j(t.lo),q +var $async$nG=A.e(function(b,c){if(b===1)return A.f(c,r) +for(;;)switch(s){case 0:s=3 +return A.c(A.ac(a.arrayBuffer(),t.a),$async$nG) +case 3:q=c +s=1 +break +case 1:return A.h(q,r)}}) +return A.i($async$nG,r)}, +wl(a,b,c){var s=v.G.DataView,r=[a] +r.push(b) +r.push(c) +return t.eq.a(A.dw(s,r))}, +uC(a,b,c){var s=v.G.Uint8Array,r=[a] +r.push(b) +r.push(c) +return t.Z.a(A.dw(s,r))}, +yZ(a,b){v.G.Atomics.notify(a,b,1/0)}},B={} +var w=[A,J,B] +var $={} +A.uu.prototype={} +J.ik.prototype={ +H(a,b){return a===b}, +gB(a){return A.fv(a)}, +j(a){return"Instance of '"+A.iO(a)+"'"}, +ga0(a){return A.bp(A.v7(this))}} +J.io.prototype={ +j(a){return String(a)}, +gB(a){return a?519018:218159}, +ga0(a){return A.bp(t.y)}, +$iY:1, +$iI:1} +J.dP.prototype={ +H(a,b){return null==b}, +j(a){return"null"}, +gB(a){return 0}, +$iY:1, +$iJ:1} +J.aj.prototype={$iw:1} +J.cm.prototype={ +gB(a){return 0}, +ga0(a){return B.bX}, +j(a){return String(a)}} +J.iN.prototype={} +J.d1.prototype={} +J.b0.prototype={ +j(a){var s=a[$.dA()] +if(s==null)return this.kf(a) +return"JavaScript function for "+J.aZ(s)}} +J.aO.prototype={ +gB(a){return 0}, +j(a){return String(a)}} +J.dR.prototype={ +gB(a){return 0}, +j(a){return String(a)}} +J.A.prototype={ +d7(a,b){return new A.al(a,A.a1(a).h("@<1>").J(b).h("al<1,2>"))}, +q(a,b){a.$flags&1&&A.D(a,29) +a.push(b)}, +ew(a,b){var s +a.$flags&1&&A.D(a,"removeAt",1) +s=a.length +if(b>=s)throw A.a(A.nF(b,null)) +return a.splice(b,1)[0]}, +nO(a,b,c){var s +a.$flags&1&&A.D(a,"insert",2) +s=a.length +if(b>s)throw A.a(A.nF(b,null)) +a.splice(b,0,c)}, +h8(a,b,c){var s,r +a.$flags&1&&A.D(a,"insertAll",2) +A.wf(b,0,a.length,"index") +if(!t.O.b(c))c=J.yW(c) +s=J.ay(c) +a.length=a.length+s +r=b+s +this.L(a,r,a.length,a,b) +this.al(a,b,r,c)}, +jr(a){a.$flags&1&&A.D(a,"removeLast",1) +if(a.length===0)throw A.a(A.eJ(a,-1)) +return a.pop()}, +E(a,b){var s +a.$flags&1&&A.D(a,"remove",1) +for(s=0;s").J(c).h("a8<1,2>"))}, +bF(a,b){var s,r=A.aW(a.length,"",!1,t.N) +for(s=0;ss)throw A.a(A.a0(b,0,s,"start",null)) +if(cs)throw A.a(A.a0(c,b,s,"end",null)) +if(b===c)return A.v([],A.a1(a)) +return A.v(a.slice(b,c),A.a1(a))}, +gai(a){if(a.length>0)return a[0] +throw A.a(A.ck())}, +gaS(a){var s=a.length +if(s>0)return a[s-1] +throw A.a(A.ck())}, +L(a,b,c,d,e){var s,r,q,p,o +a.$flags&2&&A.D(a,5) +A.aL(b,c,a.length) +s=c-b +if(s===0)return +A.aI(e,"skipCount") +if(t.j.b(d)){r=d +q=e}else{r=J.kT(d,e).bp(0,!1) +q=0}p=J.a2(r) +if(q+s>p.gk(r))throw A.a(A.vU()) +if(q=0;--o)a[b+o]=p.i(r,q+o) +else for(o=0;o0){a[0]=q +a[1]=r}return}p=0 +if(A.a1(a).c.b(null))for(o=0;o0)this.lP(a,p)}, +k8(a){return this.cN(a,null)}, +lP(a,b){var s,r=a.length +for(;s=r-1,r>0;r=s)if(a[s]===null){a[s]=void 0;--b +if(b===0)break}}, +cr(a,b){var s,r=a.length +if(0>=r)return-1 +for(s=0;s=0;--s)if(J.y(a[s],b))return s +return-1}, +T(a,b){var s +for(s=0;s"))}, +gB(a){return A.fv(a)}, +gk(a){return a.length}, +sk(a,b){a.$flags&1&&A.D(a,"set length","change the length of") +if(b<0)throw A.a(A.a0(b,0,null,"newLength",null)) +if(b>a.length)A.a1(a).c.a(null) +a.length=b}, +i(a,b){if(!(b>=0&&b=0&&b=a.length)return-1 +for(s=0;s=p){r.d=null +return!1}r.d=q[s] +r.c=s+1 +return!0}} +J.dQ.prototype={ +S(a,b){var s +if(ab)return 1 +else if(a===b){if(a===0){s=this.ghb(b) +if(this.ghb(a)===s)return 0 +if(this.ghb(a))return-1 +return 1}return 0}else if(isNaN(a)){if(isNaN(b))return 0 +return 1}else return-1}, +ghb(a){return a===0?1/a<0:a<0}, +mD(a){var s,r +if(a>=0){if(a<=2147483647){s=a|0 +return a===s?s:s+1}}else if(a>=-2147483648)return a|0 +r=Math.ceil(a) +if(isFinite(r))return r +throw A.a(A.R(""+a+".ceil()"))}, +mF(a,b,c){if(B.b.S(b,c)>0)throw A.a(A.dv(b)) +if(this.S(a,b)<0)return b +if(this.S(a,c)>0)return c +return a}, +op(a,b){var s,r,q,p +if(b<2||b>36)throw A.a(A.a0(b,2,36,"radix",null)) +s=a.toString(b) +if(s.charCodeAt(s.length-1)!==41)return s +r=/^([\da-z]+)(?:\.([\da-z]+))?\(e\+(\d+)\)$/.exec(s) +if(r==null)A.p(A.R("Unexpected toString result: "+s)) +s=r[1] +q=+r[3] +p=r[2] +if(p!=null){s+=p +q-=p.length}return s+B.a.aK("0",q)}, +j(a){if(a===0&&1/a<0)return"-0.0" +else return""+a}, +gB(a){var s,r,q,p,o=a|0 +if(a===o)return o&536870911 +s=Math.abs(a) +r=Math.log(s)/0.6931471805599453|0 +q=Math.pow(2,r) +p=s<1?s/q:q/s +return((p*9007199254740992|0)+(p*3542243181176521|0))*599197+r*1259&536870911}, +dB(a,b){return a+b}, +aU(a,b){var s=a%b +if(s===0)return 0 +if(s>0)return s +return s+b}, +hv(a,b){if((a|0)===a)if(b>=1||b<-1)return a/b|0 +return this.iB(a,b)}, +M(a,b){return(a|0)===a?a/b|0:this.iB(a,b)}, +iB(a,b){var s=a/b +if(s>=-2147483648&&s<=2147483647)return s|0 +if(s>0){if(s!==1/0)return Math.floor(s)}else if(s>-1/0)return Math.ceil(s) +throw A.a(A.R("Result of truncating division is "+A.o(s)+": "+A.o(a)+" ~/ "+b))}, +cL(a,b){if(b<0)throw A.a(A.dv(b)) +return b>31?0:a<>>0}, +cM(a,b){var s +if(b<0)throw A.a(A.dv(b)) +if(a>0)s=this.fF(a,b) +else{s=b>31?31:b +s=a>>s>>>0}return s}, +Y(a,b){var s +if(a>0)s=this.fF(a,b) +else{s=b>31?31:b +s=a>>s>>>0}return s}, +m_(a,b){if(0>b)throw A.a(A.dv(b)) +return this.fF(a,b)}, +fF(a,b){return b>31?0:a>>>b}, +jZ(a,b){return a>b}, +ga0(a){return A.bp(t.r)}, +$ia7:1, +$ia5:1} +J.fc.prototype={ +giR(a){var s,r=a<0?-a-1:a,q=r +for(s=32;q>=4294967296;){q=this.M(q,4294967296) +s+=32}return s-Math.clz32(q)}, +ga0(a){return A.bp(t.S)}, +$iY:1, +$ib:1} +J.ip.prototype={ +ga0(a){return A.bp(t.i)}, +$iY:1} +J.cl.prototype={ +mG(a,b){if(b<0)throw A.a(A.eJ(a,b)) +if(b>=a.length)A.p(A.eJ(a,b)) +return a.charCodeAt(b)}, +fO(a,b,c){var s=b.length +if(c>s)throw A.a(A.a0(c,0,s,null,null)) +return new A.kp(b,a,c)}, +e6(a,b){return this.fO(a,b,0)}, +cz(a,b,c){var s,r,q=null +if(c<0||c>b.length)throw A.a(A.a0(c,0,b.length,q,q)) +s=a.length +if(c+s>b.length)return q +for(r=0;rr)return!1 +return b===this.X(a,r-s)}, +c2(a,b,c,d){var s=A.aL(b,c,a.length) +return A.yb(a,b,s,d)}, +P(a,b,c){var s +if(c<0||c>a.length)throw A.a(A.a0(c,0,a.length,null,null)) +s=c+b.length +if(s>a.length)return!1 +return b===a.substring(c,s)}, +I(a,b){return this.P(a,b,0)}, +t(a,b,c){return a.substring(b,A.aL(b,c,a.length))}, +X(a,b){return this.t(a,b,null)}, +aK(a,b){var s,r +if(0>=b)return"" +if(b===1||a.length===0)return a +if(b!==b>>>0)throw A.a(B.b6) +for(s=a,r="";;){if((b&1)===1)r=s+r +b=b>>>1 +if(b===0)break +s+=s}return r}, +ob(a,b,c){var s=b-a.length +if(s<=0)return a +return this.aK(c,s)+a}, +oc(a,b){var s=b-a.length +if(s<=0)return a +return a+this.aK(" ",s)}, +bj(a,b,c){var s +if(c<0||c>a.length)throw A.a(A.a0(c,0,a.length,null,null)) +s=a.indexOf(b,c) +return s}, +cr(a,b){return this.bj(a,b,0)}, +en(a,b,c){var s,r +if(c==null)c=a.length +else if(c<0||c>a.length)throw A.a(A.a0(c,0,a.length,null,null)) +s=b.length +r=a.length +if(c+s>r)c=r-s +return a.lastIndexOf(b,c)}, +cu(a,b){return this.en(a,b,null)}, +T(a,b){return A.DG(a,b,0)}, +S(a,b){var s +if(a===b)s=0 +else s=a>6}r=r+((r&67108863)<<3)&536870911 +r^=r>>11 +return r+((r&16383)<<15)&536870911}, +ga0(a){return A.bp(t.N)}, +gk(a){return a.length}, +i(a,b){if(!(b>=0&&b")) +s.bH(r.glp()) +r.bH(a) +r.dq(d) +return r}, +Z(a){return this.A(a,null,null,null)}, +aj(a,b,c){return this.A(a,null,b,c)}, +bk(a,b,c){return this.A(a,b,c,null)}} +A.dG.prototype={ +u(){return this.a.u()}, +bH(a){this.c=a==null?null:this.b.bo(a,t.z,this.$ti.y[1])}, +dq(a){var s=this +s.a.dq(a) +if(a==null)s.d=null +else if(t.v.b(a))s.d=s.b.cD(a,t.z,t.K,t.l) +else if(t.i6.b(a))s.d=s.b.bo(a,t.z,t.K) +else throw A.a(A.K(u.y,null))}, +lq(a){var s,r,q,p,o,n,m=this,l=m.c +if(l==null)return +s=null +try{s=m.$ti.y[1].a(a)}catch(o){r=A.H(o) +q=A.N(o) +p=m.d +if(p==null)m.b.cq(r,q) +else{l=t.K +n=m.b +if(t.v.b(p))n.hn(p,r,q,l,t.l) +else n.c4(t.i6.a(p),r,l)}return}m.b.c4(l,s,m.$ti.y[1])}, +aJ(a){this.a.aJ(a)}, +ak(){return this.aJ(null)}, +ar(){this.a.ar()}, +$iak:1} +A.cw.prototype={ +gv(a){return new A.i_(J.U(this.gb5()),A.q(this).h("i_<1,2>"))}, +gk(a){return J.ay(this.gb5())}, +gG(a){return J.kS(this.gb5())}, +gaQ(a){return J.yR(this.gb5())}, +aV(a,b){var s=A.q(this) +return A.ln(J.kT(this.gb5(),b),s.c,s.y[1])}, +bK(a,b){var s=A.q(this) +return A.ln(J.vz(this.gb5(),b),s.c,s.y[1])}, +U(a,b){return A.q(this).y[1].a(J.hJ(this.gb5(),b))}, +T(a,b){return J.vw(this.gb5(),b)}, +j(a){return J.aZ(this.gb5())}} +A.i_.prototype={ +l(){return this.a.l()}, +gp(){return this.$ti.y[1].a(this.a.gp())}} +A.cJ.prototype={ +gb5(){return this.a}} +A.h6.prototype={$ix:1} +A.h2.prototype={ +i(a,b){return this.$ti.y[1].a(J.kP(this.a,b))}, +m(a,b,c){J.kQ(this.a,b,this.$ti.c.a(c))}, +sk(a,b){J.yT(this.a,b)}, +q(a,b){J.kR(this.a,this.$ti.c.a(b))}, +cN(a,b){var s=b==null?null:new A.q4(this,b) +J.vy(this.a,s)}, +L(a,b,c,d,e){var s=this.$ti +J.yU(this.a,b,c,A.ln(d,s.y[1],s.c),e)}, +al(a,b,c,d){return this.L(0,b,c,d,0)}, +$ix:1, +$it:1} +A.q4.prototype={ +$2(a,b){var s=this.a.$ti.y[1] +return this.b.$2(s.a(a),s.a(b))}, +$S(){return this.a.$ti.h("b(1,1)")}} +A.al.prototype={ +d7(a,b){return new A.al(this.a,this.$ti.h("@<1>").J(b).h("al<1,2>"))}, +gb5(){return this.a}} +A.cQ.prototype={ +j(a){return"LateInitializationError: "+this.a}} +A.bv.prototype={ +gk(a){return this.a.length}, +i(a,b){return this.a.charCodeAt(b)}} +A.u1.prototype={ +$0(){return A.mu(null,t.H)}, +$S:3} +A.nX.prototype={} +A.x.prototype={} +A.W.prototype={ +gv(a){var s=this +return new A.aq(s,s.gk(s),A.q(s).h("aq"))}, +gG(a){return this.gk(this)===0}, +gai(a){if(this.gk(this)===0)throw A.a(A.ck()) +return this.U(0,0)}, +T(a,b){var s,r=this,q=r.gk(r) +for(s=0;s").J(c).h("a8<1,2>"))}, +og(a,b){var s,r,q=this,p=q.gk(q) +if(p===0)throw A.a(A.ck()) +s=q.U(0,0) +for(r=1;rs)throw A.a(A.a0(r,0,s,"start",null))}}, +gkX(){var s=J.ay(this.a),r=this.c +if(r==null||r>s)return s +return r}, +gm1(){var s=J.ay(this.a),r=this.b +if(r>s)return s +return r}, +gk(a){var s,r=J.ay(this.a),q=this.b +if(q>=r)return 0 +s=this.c +if(s==null||s>=r)return r-q +return s-q}, +U(a,b){var s=this,r=s.gm1()+b +if(b<0||r>=s.gkX())throw A.a(A.ih(b,s.gk(0),s,null,"index")) +return J.hJ(s.a,r)}, +aV(a,b){var s,r,q=this +A.aI(b,"count") +s=q.b+b +r=q.c +if(r!=null&&s>=r)return new A.cN(q.$ti.h("cN<1>")) +return A.bS(q.a,s,r,q.$ti.c)}, +bK(a,b){var s,r,q,p=this +A.aI(b,"count") +s=p.c +r=p.b +if(s==null)return A.bS(p.a,r,B.b.dB(r,b),p.$ti.c) +else{q=B.b.dB(r,b) +if(s=o){r.d=null +return!1}r.d=p.U(q,s);++r.c +return!0}} +A.bZ.prototype={ +gv(a){return new A.bL(J.U(this.a),this.b,A.q(this).h("bL<1,2>"))}, +gk(a){return J.ay(this.a)}, +gG(a){return J.kS(this.a)}, +U(a,b){return this.b.$1(J.hJ(this.a,b))}} +A.cM.prototype={$ix:1} +A.bL.prototype={ +l(){var s=this,r=s.b +if(r.l()){s.a=s.c.$1(r.gp()) +return!0}s.a=null +return!1}, +gp(){var s=this.a +return s==null?this.$ti.y[1].a(s):s}} +A.a8.prototype={ +gk(a){return J.ay(this.a)}, +U(a,b){return this.b.$1(J.hJ(this.a,b))}} +A.d5.prototype={ +gv(a){return new A.fV(J.U(this.a),this.b)}, +bm(a,b,c){return new A.bZ(this,b,this.$ti.h("@<1>").J(c).h("bZ<1,2>"))}} +A.fV.prototype={ +l(){var s,r +for(s=this.a,r=this.b;s.l();)if(r.$1(s.gp()))return!0 +return!1}, +gp(){return this.a.gp()}} +A.f0.prototype={ +gv(a){return new A.ic(J.U(this.a),this.b,B.Z,this.$ti.h("ic<1,2>"))}} +A.ic.prototype={ +gp(){var s=this.d +return s==null?this.$ti.y[1].a(s):s}, +l(){var s,r,q=this,p=q.c +if(p==null)return!1 +for(s=q.a,r=q.b;!p.l();){q.d=null +if(s.l()){q.c=null +p=J.U(r.$1(s.gp())) +q.c=p}else return!1}q.d=q.c.gp() +return!0}} +A.d0.prototype={ +gv(a){var s=this.a +return new A.jh(s.gv(s),this.b,A.q(this).h("jh<1>"))}} +A.eZ.prototype={ +gk(a){var s=this.a,r=s.gk(s) +s=this.b +if(B.b.jZ(r,s))return s +return r}, +$ix:1} +A.jh.prototype={ +l(){if(--this.b>=0)return this.a.l() +this.b=-1 +return!1}, +gp(){if(this.b<0){this.$ti.c.a(null) +return null}return this.a.gp()}} +A.c2.prototype={ +aV(a,b){A.hM(b,"count") +A.aI(b,"count") +return new A.c2(this.a,this.b+b,A.q(this).h("c2<1>"))}, +gv(a){var s=this.a +return new A.j0(s.gv(s),this.b)}} +A.dL.prototype={ +gk(a){var s=this.a,r=s.gk(s)-this.b +if(r>=0)return r +return 0}, +aV(a,b){A.hM(b,"count") +A.aI(b,"count") +return new A.dL(this.a,this.b+b,this.$ti)}, +$ix:1} +A.j0.prototype={ +l(){var s,r +for(s=this.a,r=0;r"))}, +aV(a,b){A.aI(b,"count") +return this}, +bK(a,b){A.aI(b,"count") +return this}, +bp(a,b){var s=this.$ti.c +return b?J.us(0,s):J.ur(0,s)}} +A.i9.prototype={ +l(){return!1}, +gp(){throw A.a(A.ck())}} +A.fW.prototype={ +gv(a){return new A.jw(J.U(this.a),this.$ti.h("jw<1>"))}} +A.jw.prototype={ +l(){var s,r +for(s=this.a,r=this.$ti.c;s.l();)if(r.b(s.gp()))return!0 +return!1}, +gp(){return this.$ti.c.a(this.a.gp())}} +A.fs.prototype={ +ghY(){var s,r,q +for(s=this.a,r=A.q(s),s=new A.bL(J.U(s.a),s.b,r.h("bL<1,2>")),r=r.y[1];s.l();){q=s.a +if(q==null)q=r.a(q) +if(q!=null)return q}return null}, +gG(a){return this.ghY()==null}, +gaQ(a){return this.ghY()!=null}, +gv(a){var s=this.a +return new A.iI(new A.bL(J.U(s.a),s.b,A.q(s).h("bL<1,2>")))}} +A.iI.prototype={ +l(){var s,r,q +this.b=null +for(s=this.a,r=s.$ti.y[1];s.l();){q=s.a +if(q==null)q=r.a(q) +if(q!=null){this.b=q +return!0}}return!1}, +gp(){var s=this.b +return s==null?A.p(A.ck()):s}} +A.f3.prototype={ +sk(a,b){throw A.a(A.R(u.O))}, +q(a,b){throw A.a(A.R("Cannot add to a fixed-length list"))}} +A.jk.prototype={ +m(a,b,c){throw A.a(A.R("Cannot modify an unmodifiable list"))}, +sk(a,b){throw A.a(A.R("Cannot change the length of an unmodifiable list"))}, +q(a,b){throw A.a(A.R("Cannot add to an unmodifiable list"))}, +cN(a,b){throw A.a(A.R("Cannot modify an unmodifiable list"))}, +L(a,b,c,d,e){throw A.a(A.R("Cannot modify an unmodifiable list"))}, +al(a,b,c,d){return this.L(0,b,c,d,0)}} +A.e6.prototype={} +A.cV.prototype={ +gk(a){return J.ay(this.a)}, +U(a,b){var s=this.a,r=J.a2(s) +return r.U(s,r.gk(s)-1-b)}} +A.hB.prototype={} +A.hj.prototype={$r:"+immediateRestart(1)",$s:1} +A.au.prototype={$r:"+(1,2)",$s:2} +A.hk.prototype={$r:"+basicSupport,supportsReadWriteUnsafe(1,2)",$s:3} +A.hl.prototype={$r:"+controller,sync(1,2)",$s:4} +A.k8.prototype={$r:"+downloaded,total(1,2)",$s:5} +A.dj.prototype={$r:"+file,outFlags(1,2)",$s:6} +A.k9.prototype={$r:"+name,parameters(1,2)",$s:7} +A.ka.prototype={$r:"+result,resultCode(1,2)",$s:8} +A.hm.prototype={$r:"+(1,2,3)",$s:9} +A.kb.prototype={$r:"+autocommit,lastInsertRowid,result(1,2,3)",$s:10} +A.kc.prototype={$r:"+connectName,connectPort,lockName(1,2,3)",$s:11} +A.kd.prototype={$r:"+hasSynced,lastSyncedAt,priority(1,2,3)",$s:12} +A.ke.prototype={$r:"+atLast,priority,sinceLast,targetCount(1,2,3,4)",$s:13} +A.eS.prototype={ +gG(a){return this.gk(this)===0}, +j(a){return A.nj(this)}, +gbZ(){return new A.ex(this.nf(),A.q(this).h("ex>"))}, +nf(){var s=this +return function(){var r=0,q=1,p=[],o,n,m +return function $async$gbZ(a,b,c){if(b===1){p.push(c) +r=q}for(;;)switch(r){case 0:o=s.ga6(),o=o.gv(o),n=A.q(s).h("Q<1,2>") +case 2:if(!o.l()){r=3 +break}m=o.gp() +r=4 +return a.b=new A.Q(m,s.i(0,m),n),1 +case 4:r=2 +break +case 3:return 0 +case 1:return a.c=p.at(-1),3}}}}, +cw(a,b,c,d){var s=A.P(c,d) +this.a4(0,new A.lB(this,b,s)) +return s}, +$ia_:1} +A.lB.prototype={ +$2(a,b){var s=this.b.$2(a,b) +this.c.m(0,s.a,s.b)}, +$S(){return A.q(this.a).h("~(1,2)")}} +A.bw.prototype={ +gk(a){return this.b.length}, +gi7(){var s=this.$keys +if(s==null){s=Object.keys(this.a) +this.$keys=s}return s}, +F(a){if(typeof a!="string")return!1 +if("__proto__"===a)return!1 +return this.a.hasOwnProperty(a)}, +i(a,b){if(!this.F(b))return null +return this.b[this.a[b]]}, +a4(a,b){var s,r,q=this.gi7(),p=this.b +for(s=q.length,r=0;r"))}} +A.hc.prototype={ +gk(a){return this.a.length}, +gG(a){return 0===this.a.length}, +gaQ(a){return 0!==this.a.length}, +gv(a){var s=this.a +return new A.ek(s,s.length,this.$ti.h("ek<1>"))}} +A.ek.prototype={ +gp(){var s=this.d +return s==null?this.$ti.c.a(s):s}, +l(){var s=this,r=s.c +if(r>=s.b){s.d=null +return!1}s.d=s.a[r] +s.c=r+1 +return!0}} +A.eT.prototype={ +q(a,b){A.zb()}} +A.eU.prototype={ +gk(a){return this.b}, +gG(a){return this.b===0}, +gaQ(a){return this.b!==0}, +gv(a){var s,r=this,q=r.$keys +if(q==null){q=Object.keys(r.a) +r.$keys=q}s=q +return new A.ek(s,s.length,r.$ti.h("ek<1>"))}, +T(a,b){if("__proto__"===b)return!1 +return this.a.hasOwnProperty(b)}, +eC(a){return A.zJ(this,this.$ti.c)}} +A.n1.prototype={ +H(a,b){if(b==null)return!1 +return b instanceof A.fb&&this.a.H(0,b.a)&&A.vf(this)===A.vf(b)}, +gB(a){return A.bN(this.a,A.vf(this),B.c,B.c,B.c,B.c,B.c,B.c,B.c,B.c)}, +j(a){var s=B.d.bF([A.bp(this.$ti.c)],", ") +return this.a.j(0)+" with "+("<"+s+">")}} +A.fb.prototype={ +$1(a){return this.a.$1$1(a,this.$ti.y[0])}, +$2(a,b){return this.a.$1$2(a,b,this.$ti.y[0])}, +$4(a,b,c,d){return this.a.$1$4(a,b,c,d,this.$ti.y[0])}, +$S(){return A.Do(A.kG(this.a),this.$ti)}} +A.fx.prototype={} +A.oP.prototype={ +b7(a){var s,r,q=this,p=new RegExp(q.a).exec(a) +if(p==null)return null +s=Object.create(null) +r=q.b +if(r!==-1)s.arguments=p[r+1] +r=q.c +if(r!==-1)s.argumentsExpr=p[r+1] +r=q.d +if(r!==-1)s.expr=p[r+1] +r=q.e +if(r!==-1)s.method=p[r+1] +r=q.f +if(r!==-1)s.receiver=p[r+1] +return s}} +A.ft.prototype={ +j(a){return"Null check operator used on a null value"}} +A.ir.prototype={ +j(a){var s,r=this,q="NoSuchMethodError: method not found: '",p=r.b +if(p==null)return"NoSuchMethodError: "+r.a +s=r.c +if(s==null)return q+p+"' ("+r.a+")" +return q+p+"' on '"+s+"' ("+r.a+")"}} +A.jj.prototype={ +j(a){var s=this.a +return s.length===0?"Error":"Error: "+s}} +A.iK.prototype={ +j(a){return"Throw of null ('"+(this.a===null?"null":"undefined")+"' from JavaScript)"}, +$iV:1} +A.f_.prototype={} +A.hp.prototype={ +j(a){var s,r=this.b +if(r!=null)return r +r=this.a +s=r!==null&&typeof r==="object"?r.stack:null +return this.b=s==null?"":s}, +$iae:1} +A.cK.prototype={ +j(a){var s=this.constructor,r=s==null?null:s.name +return"Closure '"+A.yd(r==null?"unknown":r)+"'"}, +ga0(a){var s=A.kG(this) +return A.bp(s==null?A.br(this):s)}, +gpd(){return this}, +$C:"$1", +$R:1, +$D:null} +A.lo.prototype={$C:"$0",$R:0} +A.lp.prototype={$C:"$2",$R:2} +A.oD.prototype={} +A.o6.prototype={ +j(a){var s=this.$static_name +if(s==null)return"Closure of unknown static method" +return"Closure '"+A.yd(s)+"'"}} +A.eO.prototype={ +H(a,b){if(b==null)return!1 +if(this===b)return!0 +if(!(b instanceof A.eO))return!1 +return this.$_target===b.$_target&&this.a===b.a}, +gB(a){return(A.kH(this.a)^A.fv(this.$_target))>>>0}, +j(a){return"Closure '"+this.$_name+"' of "+("Instance of '"+A.iO(this.a)+"'")}} +A.iW.prototype={ +j(a){return"RuntimeError: "+this.a}} +A.b2.prototype={ +gk(a){return this.a}, +gG(a){return this.a===0}, +ga6(){return new A.bx(this,A.q(this).h("bx<1>"))}, +gbZ(){return new A.az(this,A.q(this).h("az<1,2>"))}, +F(a){var s,r +if(typeof a=="string"){s=this.b +if(s==null)return!1 +return s[a]!=null}else if(typeof a=="number"&&(a&0x3fffffff)===a){r=this.c +if(r==null)return!1 +return r[a]!=null}else return this.jc(a)}, +jc(a){var s=this.d +if(s==null)return!1 +return this.ct(s[this.cs(a)],a)>=0}, +a8(a,b){b.a4(0,new A.na(this))}, +i(a,b){var s,r,q,p,o=null +if(typeof b=="string"){s=this.b +if(s==null)return o +r=s[b] +q=r==null?o:r.b +return q}else if(typeof b=="number"&&(b&0x3fffffff)===b){p=this.c +if(p==null)return o +r=p[b] +q=r==null?o:r.b +return q}else return this.jd(b)}, +jd(a){var s,r,q=this.d +if(q==null)return null +s=q[this.cs(a)] +r=this.ct(s,a) +if(r<0)return null +return s[r].b}, +m(a,b,c){var s,r,q=this +if(typeof b=="string"){s=q.b +q.hy(s==null?q.b=q.fz():s,b,c)}else if(typeof b=="number"&&(b&0x3fffffff)===b){r=q.c +q.hy(r==null?q.c=q.fz():r,b,c)}else q.jf(b,c)}, +jf(a,b){var s,r,q,p=this,o=p.d +if(o==null)o=p.d=p.fz() +s=p.cs(a) +r=o[s] +if(r==null)o[s]=[p.eU(a,b)] +else{q=p.ct(r,a) +if(q>=0)r[q].b=b +else r.push(p.eU(a,b))}}, +cB(a,b){var s,r,q=this +if(q.F(a)){s=q.i(0,a) +return s==null?A.q(q).y[1].a(s):s}r=b.$0() +q.m(0,a,r) +return r}, +E(a,b){var s=this +if(typeof b=="string")return s.iq(s.b,b) +else if(typeof b=="number"&&(b&0x3fffffff)===b)return s.iq(s.c,b) +else return s.je(b)}, +je(a){var s,r,q,p,o=this,n=o.d +if(n==null)return null +s=o.cs(a) +r=n[s] +q=o.ct(r,a) +if(q<0)return null +p=r.splice(q,1)[0] +o.iG(p) +if(r.length===0)delete n[s] +return p.b}, +bA(a){var s=this +if(s.a>0){s.b=s.c=s.d=s.e=s.f=null +s.a=0 +s.fw()}}, +a4(a,b){var s=this,r=s.e,q=s.r +while(r!=null){b.$2(r.a,r.b) +if(q!==s.r)throw A.a(A.am(s)) +r=r.c}}, +hy(a,b,c){var s=a[b] +if(s==null)a[b]=this.eU(b,c) +else s.b=c}, +iq(a,b){var s +if(a==null)return null +s=a[b] +if(s==null)return null +this.iG(s) +delete a[b] +return s.b}, +fw(){this.r=this.r+1&1073741823}, +eU(a,b){var s,r=this,q=new A.ne(a,b) +if(r.e==null)r.e=r.f=q +else{s=r.f +s.toString +q.d=s +r.f=s.c=q}++r.a +r.fw() +return q}, +iG(a){var s=this,r=a.d,q=a.c +if(r==null)s.e=q +else r.c=q +if(q==null)s.f=r +else q.d=r;--s.a +s.fw()}, +cs(a){return J.z(a)&1073741823}, +ct(a,b){var s,r +if(a==null)return-1 +s=a.length +for(r=0;r"]=s +delete s[""] +return s}} +A.na.prototype={ +$2(a,b){this.a.m(0,a,b)}, +$S(){return A.q(this.a).h("~(1,2)")}} +A.ne.prototype={} +A.bx.prototype={ +gk(a){return this.a.a}, +gG(a){return this.a.a===0}, +gv(a){var s=this.a +return new A.fg(s,s.r,s.e)}, +T(a,b){return this.a.F(b)}} +A.fg.prototype={ +gp(){return this.d}, +l(){var s,r=this,q=r.a +if(r.b!==q.r)throw A.a(A.am(q)) +s=r.c +if(s==null){r.d=null +return!1}else{r.d=s.a +r.c=s.c +return!0}}} +A.bf.prototype={ +gk(a){return this.a.a}, +gG(a){return this.a.a===0}, +gv(a){var s=this.a +return new A.by(s,s.r,s.e)}} +A.by.prototype={ +gp(){return this.d}, +l(){var s,r=this,q=r.a +if(r.b!==q.r)throw A.a(A.am(q)) +s=r.c +if(s==null){r.d=null +return!1}else{r.d=s.b +r.c=s.c +return!0}}} +A.az.prototype={ +gk(a){return this.a.a}, +gG(a){return this.a.a===0}, +gv(a){var s=this.a +return new A.iy(s,s.r,s.e,this.$ti.h("iy<1,2>"))}} +A.iy.prototype={ +gp(){var s=this.d +s.toString +return s}, +l(){var s,r=this,q=r.a +if(r.b!==q.r)throw A.a(A.am(q)) +s=r.c +if(s==null){r.d=null +return!1}else{r.d=new A.Q(s.a,s.b,r.$ti.h("Q<1,2>")) +r.c=s.c +return!0}}} +A.fe.prototype={ +cs(a){return A.kH(a)&1073741823}, +ct(a,b){var s,r,q +if(a==null)return-1 +s=a.length +for(r=0;r0;){--q;--s +k[q]=r[s]}}return A.iA(k,t.K)}} +A.k5.prototype={ +cR(){return[this.a,this.b]}, +H(a,b){if(b==null)return!1 +return b instanceof A.k5&&this.$s===b.$s&&J.y(this.a,b.a)&&J.y(this.b,b.b)}, +gB(a){return A.bN(this.$s,this.a,this.b,B.c,B.c,B.c,B.c,B.c,B.c,B.c)}} +A.k4.prototype={ +cR(){return[this.a]}, +H(a,b){if(b==null)return!1 +return b instanceof A.k4&&this.$s===b.$s&&J.y(this.a,b.a)}, +gB(a){return A.bN(this.$s,this.a,B.c,B.c,B.c,B.c,B.c,B.c,B.c,B.c)}} +A.k6.prototype={ +cR(){return[this.a,this.b,this.c]}, +H(a,b){var s=this +if(b==null)return!1 +return b instanceof A.k6&&s.$s===b.$s&&J.y(s.a,b.a)&&J.y(s.b,b.b)&&J.y(s.c,b.c)}, +gB(a){var s=this +return A.bN(s.$s,s.a,s.b,s.c,B.c,B.c,B.c,B.c,B.c,B.c)}} +A.k7.prototype={ +cR(){return this.a}, +H(a,b){if(b==null)return!1 +return b instanceof A.k7&&this.$s===b.$s&&A.Bd(this.a,b.a)}, +gB(a){return A.bN(this.$s,A.zX(this.a),B.c,B.c,B.c,B.c,B.c,B.c,B.c,B.c)}} +A.fd.prototype={ +j(a){return"RegExp/"+this.a+"/"+this.b.flags}, +gll(){var s=this,r=s.c +if(r!=null)return r +r=s.b +return s.c=A.ut(s.a,r.multiline,!r.ignoreCase,r.unicode,r.dotAll,"g")}, +glk(){var s=this,r=s.d +if(r!=null)return r +r=s.b +return s.d=A.ut(s.a,r.multiline,!r.ignoreCase,r.unicode,r.dotAll,"y")}, +j2(a){var s=this.b.exec(a) +if(s==null)return null +return new A.en(s)}, +fO(a,b,c){var s=b.length +if(c>s)throw A.a(A.a0(c,0,s,null,null)) +return new A.jA(this,b,c)}, +e6(a,b){return this.fO(0,b,0)}, +l_(a,b){var s,r=this.gll() +r.lastIndex=b +s=r.exec(a) +if(s==null)return null +return new A.en(s)}, +kZ(a,b){var s,r=this.glk() +r.lastIndex=b +s=r.exec(a) +if(s==null)return null +return new A.en(s)}, +cz(a,b,c){if(c<0||c>b.length)throw A.a(A.a0(c,0,b.length,null,null)) +return this.kZ(b,c)}} +A.en.prototype={ +gC(){var s=this.b +return s.index+s[0].length}, +jY(a){return this.b[a]}, +i(a,b){return this.b[b]}, +$icR:1, +$iiR:1} +A.jA.prototype={ +gv(a){return new A.jB(this.a,this.b,this.c)}} +A.jB.prototype={ +gp(){var s=this.d +return s==null?t.lu.a(s):s}, +l(){var s,r,q,p,o,n,m=this,l=m.b +if(l==null)return!1 +s=m.c +r=l.length +if(s<=r){q=m.a +p=q.l_(l,s) +if(p!=null){m.d=p +o=p.gC() +if(p.b.index===o){s=!1 +if(q.b.unicode){q=m.c +n=q+1 +if(n=55296&&r<=56319){s=l.charCodeAt(n) +s=s>=56320&&s<=57343}}}o=(s?o+1:o)+1}m.c=o +return!0}}m.b=m.d=null +return!1}} +A.fI.prototype={ +gC(){return this.a+this.c.length}, +i(a,b){if(b!==0)A.p(A.nF(b,null)) +return this.c}, +$icR:1} +A.kp.prototype={ +gv(a){return new A.rs(this.a,this.b,this.c)}} +A.rs.prototype={ +l(){var s,r,q=this,p=q.c,o=q.b,n=o.length,m=q.a,l=m.length +if(p+n>l){q.d=null +return!1}s=m.indexOf(o,p) +if(s<0){q.c=l+1 +q.d=null +return!1}r=s+n +q.d=new A.fI(s,o) +q.c=r===q.c?r+1:r +return!0}, +gp(){var s=this.d +s.toString +return s}} +A.jK.prototype={ +cX(){var s=this.b +if(s===this)throw A.a(new A.cQ("Local '"+this.a+"' has not been initialized.")) +return s}, +aW(){var s=this.b +if(s===this)throw A.a(A.vZ(this.a)) +return s}} +A.dX.prototype={ +ga0(a){return B.bQ}, +e7(a,b,c){A.kD(a,b,c) +return c==null?new Uint8Array(a,b):new Uint8Array(a,b,c)}, +iO(a){return this.e7(a,0,null)}, +$iY:1, +$ieP:1} +A.dW.prototype={$idW:1} +A.fp.prototype={ +gaG(a){if(((a.$flags|0)&2)!==0)return new A.kx(a.buffer) +else return a.buffer}, +lc(a,b,c,d){var s=A.a0(b,0,c,d,null) +throw A.a(s)}, +hG(a,b,c,d){if(b>>>0!==b||b>c)this.lc(a,b,c,d)}} +A.kx.prototype={ +e7(a,b,c){var s=A.bg(this.a,b,c) +s.$flags=3 +return s}, +iO(a){return this.e7(0,0,null)}, +$ieP:1} +A.cS.prototype={ +ga0(a){return B.bR}, +$iY:1, +$icS:1, +$iuf:1} +A.dZ.prototype={ +gk(a){return a.length}, +ix(a,b,c,d,e){var s,r,q=a.length +this.hG(a,b,q,"start") +this.hG(a,c,q,"end") +if(b>c)throw A.a(A.a0(b,0,c,null,null)) +s=c-b +if(e<0)throw A.a(A.K(e,null)) +r=d.length +if(r-e0){s=Date.now()-r.c +if(s>(p+1)*o)p=B.b.hv(s,o)}q.c=p +r.d.$1(q)}, +$S:1} +A.h_.prototype={ +W(a){var s,r=this +if(a==null)a=r.$ti.c.a(a) +if(!r.b)r.a.aB(a) +else{s=r.a +if(r.$ti.h("r<1>").b(a))s.hF(a) +else s.bS(a)}}, +b6(a,b){var s +if(b==null)b=A.cI(a) +s=this.a +if(this.b)s.a7(new A.a6(a,b)) +else s.R(new A.a6(a,b))}, +ao(a){return this.b6(a,null)}, +$idI:1} +A.rV.prototype={ +$1(a){return this.a.$2(0,a)}, +$S:11} +A.rW.prototype={ +$2(a,b){this.a.$2(1,new A.f_(a,b))}, +$S:79} +A.tv.prototype={ +$2(a,b){this.a(a,b)}, +$S:92} +A.rT.prototype={ +$0(){var s,r=this.a,q=r.a +q===$&&A.B() +s=q.b +if((s&1)!==0?(q.gan().e&4)!==0:(s&2)===0){r.b=!0 +return}r=r.c!=null?2:0 +this.b.$2(r,null)}, +$S:0} +A.rU.prototype={ +$1(a){var s=this.a.c!=null?2:0 +this.b.$2(s,null)}, +$S:8} +A.jD.prototype={ +kv(a,b){var s=new A.pQ(a) +this.a=A.bi(new A.pS(this,a),new A.pT(s),null,new A.pU(this,s),!1,b)}} +A.pQ.prototype={ +$0(){A.eM(new A.pR(this.a))}, +$S:1} +A.pR.prototype={ +$0(){this.a.$2(0,null)}, +$S:0} +A.pT.prototype={ +$0(){this.a.$0()}, +$S:0} +A.pU.prototype={ +$0(){var s=this.a +if(s.b){s.b=!1 +this.b.$0()}}, +$S:0} +A.pS.prototype={ +$0(){var s=this.a,r=s.a +r===$&&A.B() +if((r.b&4)===0){s.c=new A.l($.n,t._) +if(s.b){s.b=!1 +A.eM(new A.pP(this.b))}return s.c}}, +$S:94} +A.pP.prototype={ +$0(){this.a.$2(2,null)}, +$S:0} +A.hb.prototype={ +j(a){return"IterationMarker("+this.b+", "+A.o(this.a)+")"}} +A.kr.prototype={ +gp(){return this.b}, +lS(a,b){var s,r,q +a=a +b=b +s=this.a +for(;;)try{r=s(this,a,b) +return r}catch(q){b=q +a=1}}, +l(){var s,r,q,p,o=this,n=null,m=0 +for(;;){s=o.d +if(s!=null)try{if(s.l()){o.b=s.gp() +return!0}else o.d=null}catch(r){n=r +m=1 +o.d=null}q=o.lS(m,n) +if(1===q)return!0 +if(0===q){o.b=null +p=o.e +if(p==null||p.length===0){o.a=A.wV +return!1}o.a=p.pop() +m=0 +n=null +continue}if(2===q){m=0 +n=null +continue}if(3===q){n=o.c +o.c=null +p=o.e +if(p==null||p.length===0){o.b=null +o.a=A.wV +throw n +return!1}o.a=p.pop() +m=1 +continue}throw A.a(A.u("sync*"))}return!1}, +pf(a){var s,r,q=this +if(a instanceof A.ex){s=a.a() +r=q.e +if(r==null)r=q.e=[] +r.push(q.a) +q.a=s +return 2}else{q.d=J.U(a) +return 2}}} +A.ex.prototype={ +gv(a){return new A.kr(this.a())}} +A.a6.prototype={ +j(a){return A.o(this.a)}, +$iZ:1, +gcf(){return this.b}} +A.aJ.prototype={ +gaq(){return!0}} +A.d8.prototype={ +b3(){}, +b4(){}} +A.c8.prototype={ +sjl(a){throw A.a(A.R(u.t))}, +sjm(a){throw A.a(A.R(u.t))}, +gbs(){return new A.aJ(this,A.q(this).h("aJ<1>"))}, +gbx(){return this.c<4}, +dN(){var s=this.r +return s==null?this.r=new A.l($.n,t.D):s}, +ir(a){var s=a.CW,r=a.ch +if(s==null)this.d=r +else s.ch=r +if(r==null)this.e=s +else r.CW=s +a.CW=a +a.ch=a}, +fG(a,b,c,d){var s,r,q,p,o,n,m,l,k,j=this +if((j.c&4)!==0)return A.wJ(c,A.q(j).c) +s=A.q(j) +r=$.n +q=d?1:0 +p=b!=null?32:0 +o=A.jG(r,a,s.c) +n=A.jH(r,b) +m=c==null?A.tw():c +l=new A.d8(j,o,n,r.b0(m,t.H),r,q|p,s.h("d8<1>")) +l.CW=l +l.ch=l +l.ay=j.c&1 +k=j.e +j.e=l +l.ch=null +l.CW=k +if(k==null)j.d=l +else k.ch=l +if(j.d===l)A.kE(j.a) +return l}, +ij(a){var s,r=this +A.q(r).h("d8<1>").a(a) +if(a.ch===a)return null +s=a.ay +if((s&2)!==0)a.ay=s|4 +else{r.ir(a) +if((r.c&2)===0&&r.d==null)r.eY()}return null}, +ik(a){}, +il(a){}, +bu(){if((this.c&4)!==0)return new A.b7("Cannot add new events after calling close") +return new A.b7("Cannot add new events while doing an addStream")}, +q(a,b){if(!this.gbx())throw A.a(this.bu()) +this.aE(b)}, +a2(a,b){var s +if(!this.gbx())throw A.a(this.bu()) +s=A.aw(a,b) +this.bg(s.a,s.b)}, +n(){var s,r,q=this +if((q.c&4)!==0){s=q.r +s.toString +return s}if(!q.gbx())throw A.a(q.bu()) +q.c|=4 +r=q.dN() +q.bz() +return r}, +e5(a,b){var s,r=this +if(!r.gbx())throw A.a(r.bu()) +r.c|=8 +s=A.AD(r,a,!1) +r.f=s +return s.a}, +iN(a){return this.e5(a,null)}, +af(a){this.aE(a)}, +au(a,b){this.bg(a,b)}, +b2(){var s=this.f +s.toString +this.f=null +this.c&=4294967287 +s.a.aB(null)}, +fg(a){var s,r,q,p=this,o=p.c +if((o&2)!==0)throw A.a(A.u(u.c)) +s=p.d +if(s==null)return +r=o&1 +p.c=o^3 +while(s!=null){o=s.ay +if((o&1)===r){s.ay=o|2 +a.$1(s) +o=s.ay^=1 +q=s.ch +if((o&4)!==0)p.ir(s) +s.ay&=4294967293 +s=q}else s=s.ch}p.c&=4294967293 +if(p.d==null)p.eY()}, +eY(){if((this.c&4)!==0){var s=this.r +if((s.a&30)===0)s.aB(null)}A.kE(this.b)}, +$iaa:1, +$ibQ:1, +sjk(a){return this.a=a}, +sjj(a){return this.b=a}} +A.dl.prototype={ +gbx(){return A.c8.prototype.gbx.call(this)&&(this.c&2)===0}, +bu(){if((this.c&2)!==0)return new A.b7(u.c) +return this.kj()}, +aE(a){var s=this,r=s.d +if(r==null)return +if(r===s.e){s.c|=2 +r.af(a) +s.c&=4294967293 +if(s.d==null)s.eY() +return}s.fg(new A.ru(s,a))}, +bg(a,b){if(this.d==null)return +this.fg(new A.rw(this,a,b))}, +bz(){var s=this +if(s.d!=null)s.fg(new A.rv(s)) +else s.r.aB(null)}} +A.ru.prototype={ +$1(a){a.af(this.b)}, +$S(){return this.a.$ti.h("~(at<1>)")}} +A.rw.prototype={ +$1(a){a.au(this.b,this.c)}, +$S(){return this.a.$ti.h("~(at<1>)")}} +A.rv.prototype={ +$1(a){a.b2()}, +$S(){return this.a.$ti.h("~(at<1>)")}} +A.h0.prototype={ +aE(a){var s +for(s=this.d;s!=null;s=s.ch)s.bc(new A.c9(a))}, +bg(a,b){var s +for(s=this.d;s!=null;s=s.ch)s.bc(new A.ed(a,b))}, +bz(){var s=this.d +if(s!=null)for(;s!=null;s=s.ch)s.bc(B.A) +else this.r.aB(null)}} +A.mv.prototype={ +$0(){var s,r,q,p,o,n,m=null +try{m=this.a.$0()}catch(q){s=A.H(q) +r=A.N(q) +p=s +o=r +n=A.ds(p,o) +if(n==null)p=new A.a6(p,o) +else p=n +this.b.a7(p) +return}this.b.bd(m)}, +$S:0} +A.mt.prototype={ +$0(){this.c.a(null) +this.b.bd(null)}, +$S:0} +A.mz.prototype={ +$2(a,b){var s=this,r=s.a,q=--r.b +if(r.a!=null){r.a=null +r.d=a +r.c=b +if(q===0||s.c)s.d.a7(new A.a6(a,b))}else if(q===0&&!s.c){q=r.d +q.toString +r=r.c +r.toString +s.d.a7(new A.a6(q,r))}}, +$S:4} +A.my.prototype={ +$1(a){var s,r,q,p,o,n,m=this,l=m.a,k=--l.b,j=l.a +if(j!=null){J.kQ(j,m.b,a) +if(J.y(k,0)){l=m.d +s=A.v([],l.h("A<0>")) +for(q=j,p=q.length,o=0;o")) +r=b==null?1:3 +this.cj(new A.bl(s,r,a,b,this.$ti.h("@<1>").J(c).h("bl<1,2>"))) +return s}, +b8(a,b){return this.b9(a,null,b)}, +iD(a,b,c){var s=new A.l($.n,c.h("l<0>")) +this.cj(new A.bl(s,19,a,b,this.$ti.h("@<1>").J(c).h("bl<1,2>"))) +return s}, +l9(){var s,r +if(((this.a|=1)&4)!==0){s=this +do s=s.c +while(r=s.a,(r&4)!==0) +s.a=r|1}}, +iS(a){var s=this.$ti,r=$.n,q=new A.l(r,s) +if(r!==B.e)a=A.xB(a,r) +this.cj(new A.bl(q,2,null,a,s.h("bl<1,1>"))) +return q}, +O(a){var s=this.$ti,r=$.n,q=new A.l(r,s) +if(r!==B.e)a=r.b0(a,t.z) +this.cj(new A.bl(q,8,a,null,s.h("bl<1,1>"))) +return q}, +lY(a){this.a=this.a&1|16 +this.c=a}, +dK(a){this.a=a.a&30|this.a&1 +this.c=a.c}, +cj(a){var s=this,r=s.a +if(r<=3){a.a=s.c +s.c=a}else{if((r&4)!==0){r=s.c +if((r.a&24)===0){r.cj(a) +return}s.dK(r)}s.b.bN(new A.qG(s,a))}}, +ig(a){var s,r,q,p,o,n=this,m={} +m.a=a +if(a==null)return +s=n.a +if(s<=3){r=n.c +n.c=a +if(r!=null){q=a.a +for(p=a;q!=null;p=q,q=o)o=q.a +p.a=r}}else{if((s&4)!==0){s=n.c +if((s.a&24)===0){s.ig(a) +return}n.dK(s)}m.a=n.dP(a) +n.b.bN(new A.qL(m,n))}}, +cY(){var s=this.c +this.c=null +return this.dP(s)}, +dP(a){var s,r,q +for(s=a,r=null;s!=null;r=s,s=q){q=s.a +s.a=r}return r}, +bd(a){var s,r=this +if(r.$ti.h("r<1>").b(a))A.qJ(a,r,!0) +else{s=r.cY() +r.a=8 +r.c=a +A.dg(r,s)}}, +bS(a){var s=this,r=s.cY() +s.a=8 +s.c=a +A.dg(s,r)}, +kQ(a){var s,r,q,p=this +if((a.a&16)!==0){s=p.b +r=a.b +s=!(s===r||s.gbi()===r.gbi())}else s=!1 +if(s)return +q=p.cY() +p.dK(a) +A.dg(p,q)}, +a7(a){var s=this.cY() +this.lY(a) +A.dg(this,s)}, +kP(a,b){this.a7(new A.a6(a,b))}, +aB(a){if(this.$ti.h("r<1>").b(a)){this.hF(a) +return}this.hE(a)}, +hE(a){this.a^=2 +this.b.bN(new A.qI(this,a))}, +hF(a){A.qJ(a,this,!1) +return}, +R(a){this.a^=2 +this.b.bN(new A.qH(this,a))}, +oo(a,b){var s,r,q,p=this,o={} +if((p.a&24)!==0){o=new A.l($.n,p.$ti) +o.aB(p) +return o}s=p.$ti +r=$.n +q=new A.l(r,s) +o.a=null +o.a=A.oO(a,new A.qR(p,q,r,r.b0(b,s.h("1/")))) +p.b9(new A.qS(o,p,q),new A.qT(o,q),t.P) +return q}, +$ir:1} +A.qG.prototype={ +$0(){A.dg(this.a,this.b)}, +$S:0} +A.qL.prototype={ +$0(){A.dg(this.b,this.a.a)}, +$S:0} +A.qK.prototype={ +$0(){A.qJ(this.a.a,this.b,!0)}, +$S:0} +A.qI.prototype={ +$0(){this.a.bS(this.b)}, +$S:0} +A.qH.prototype={ +$0(){this.a.a7(this.b)}, +$S:0} +A.qO.prototype={ +$0(){var s,r,q,p,o,n,m,l,k=this,j=null +try{q=k.a.a +j=q.b.b.bJ(q.d,t.z)}catch(p){s=A.H(p) +r=A.N(p) +if(k.c&&k.b.a.c.a===s){q=k.a +q.c=k.b.a.c}else{q=s +o=r +if(o==null)o=A.cI(q) +n=k.a +n.c=new A.a6(q,o) +q=n}q.b=!0 +return}if(j instanceof A.l&&(j.a&24)!==0){if((j.a&16)!==0){q=k.a +q.c=j.c +q.b=!0}return}if(j instanceof A.l){m=k.b.a +l=new A.l(m.b,m.$ti) +j.b9(new A.qP(l,m),new A.qQ(l),t.H) +q=k.a +q.c=l +q.b=!1}}, +$S:0} +A.qP.prototype={ +$1(a){this.a.kQ(this.b)}, +$S:8} +A.qQ.prototype={ +$2(a,b){this.a.a7(new A.a6(a,b))}, +$S:7} +A.qN.prototype={ +$0(){var s,r,q,p,o,n +try{q=this.a +p=q.a +o=p.$ti +q.c=p.b.b.c3(p.d,this.b,o.h("2/"),o.c)}catch(n){s=A.H(n) +r=A.N(n) +q=s +p=r +if(p==null)p=A.cI(q) +o=this.a +o.c=new A.a6(q,p) +o.b=!0}}, +$S:0} +A.qM.prototype={ +$0(){var s,r,q,p,o,n,m,l=this +try{s=l.a.a.c +p=l.b +if(p.a.o6(s)&&p.a.e!=null){p.c=p.a.ny(s) +p.b=!1}}catch(o){r=A.H(o) +q=A.N(o) +p=l.a.a.c +if(p.a===r){n=l.b +n.c=p +p=n}else{p=r +n=q +if(n==null)n=A.cI(p) +m=l.b +m.c=new A.a6(p,n) +p=m}p.b=!0}}, +$S:0} +A.qR.prototype={ +$0(){var s,r,q,p,o,n=this +try{n.b.bd(n.c.bJ(n.d,n.a.$ti.h("1/")))}catch(q){s=A.H(q) +r=A.N(q) +p=s +o=r +if(o==null)o=A.cI(p) +n.b.a7(new A.a6(p,o))}}, +$S:0} +A.qS.prototype={ +$1(a){var s=this.a.a +if(s.b!=null){s.u() +this.c.bS(a)}}, +$S(){return this.b.$ti.h("J(1)")}} +A.qT.prototype={ +$2(a,b){var s=this.a.a +if(s.b!=null){s.u() +this.b.a7(new A.a6(a,b))}}, +$S:7} +A.jC.prototype={} +A.G.prototype={ +gaq(){return!1}, +mB(a,b){var s,r=null,q={} +q.a=null +s=this.gaq()?q.a=new A.dl(r,r,b.h("dl<0>")):q.a=new A.cB(r,r,r,r,b.h("cB<0>")) +s.sjk(new A.od(q,this,a)) +return q.a.gbs()}, +nr(a,b,c,d){var s,r={},q=new A.l($.n,d.h("l<0>")) +r.a=b +s=this.A(null,!0,new A.oi(r,q),q.gf9()) +s.bH(new A.oj(r,this,c,s,q,d)) +return q}, +gk(a){var s={},r=new A.l($.n,t.hy) +s.a=0 +this.A(new A.ok(s,this),!0,new A.ol(s,r),r.gf9()) +return r}, +gai(a){var s=new A.l($.n,A.q(this).h("l")),r=this.A(null,!0,new A.oe(s),s.gf9()) +r.bH(new A.of(this,r,s)) +return s}} +A.od.prototype={ +$0(){var s=this.b,r=this.a,q=r.a.gdI(),p=s.aj(null,r.a.gag(),q) +p.bH(new A.oc(r,s,this.c,p)) +r.a.sjj(p.ge9()) +if(!s.gaq()){s=r.a +s.sjl(p.geu()) +s.sjm(p.gbI())}}, +$S:0} +A.oc.prototype={ +$1(a){var s,r,q,p,o,n,m,l=this,k=null +try{k=l.c.$1(a)}catch(p){s=A.H(p) +r=A.N(p) +o=s +n=r +m=A.ds(o,n) +if(m==null)m=new A.a6(o,n==null?A.cI(o):n) +q=m +l.a.a.a2(q.a,q.b) +return}if(k!=null){o=l.d +o.ak() +l.a.a.iN(k).O(o.gbI())}}, +$S(){return A.q(this.b).h("~(G.T)")}} +A.oi.prototype={ +$0(){this.b.bd(this.a.a)}, +$S:0} +A.oj.prototype={ +$1(a){var s=this,r=s.a,q=s.f +A.Ct(new A.og(r,s.c,a,q),new A.oh(r,q),A.BM(s.d,s.e))}, +$S(){return A.q(this.b).h("~(G.T)")}} +A.og.prototype={ +$0(){return this.b.$2(this.a.a,this.c)}, +$S(){return this.d.h("0()")}} +A.oh.prototype={ +$1(a){this.a.a=a}, +$S(){return this.b.h("J(0)")}} +A.ok.prototype={ +$1(a){++this.a.a}, +$S(){return A.q(this.b).h("~(G.T)")}} +A.ol.prototype={ +$0(){this.b.bd(this.a.a)}, +$S:0} +A.oe.prototype={ +$0(){var s,r=A.fD(),q=new A.b7("No element") +A.iP(q,r) +s=A.ds(q,r) +if(s==null)s=new A.a6(q,r) +this.a.a7(s)}, +$S:0} +A.of.prototype={ +$1(a){A.BN(this.b,this.c,a)}, +$S(){return A.q(this.a).h("~(G.T)")}} +A.fH.prototype={ +gaq(){return this.a.gaq()}, +A(a,b,c,d){return this.a.A(a,b,c,d)}, +Z(a){return this.A(a,null,null,null)}, +aj(a,b,c){return this.A(a,null,b,c)}, +bk(a,b,c){return this.A(a,b,c,null)}} +A.jc.prototype={} +A.cz.prototype={ +gbs(){return new A.O(this,A.q(this).h("O<1>"))}, +glB(){if((this.b&8)===0)return this.a +return this.a.c}, +cQ(){var s,r,q=this +if((q.b&8)===0){s=q.a +return s==null?q.a=new A.er():s}r=q.a +s=r.c +return s==null?r.c=new A.er():s}, +gan(){var s=this.a +return(this.b&8)!==0?s.c:s}, +aL(){if((this.b&4)!==0)return new A.b7("Cannot add event after closing") +return new A.b7("Cannot add event while adding a stream")}, +e5(a,b){var s,r,q,p=this,o=p.b +if(o>=4)throw A.a(p.aL()) +if((o&2)!==0){o=new A.l($.n,t._) +o.aB(null) +return o}o=p.a +s=b===!0 +r=new A.l($.n,t._) +q=s?A.AE(p):p.gdI() +q=a.A(p.geW(),s,p.gf2(),q) +s=p.b +if((s&1)!==0?(p.gan().e&4)!==0:(s&2)===0)q.ak() +p.a=new A.ko(o,r,q) +p.b|=8 +return r}, +iN(a){return this.e5(a,null)}, +dN(){var s=this.c +if(s==null)s=this.c=(this.b&2)!==0?$.cH():new A.l($.n,t.D) +return s}, +q(a,b){if(this.b>=4)throw A.a(this.aL()) +this.af(b)}, +a2(a,b){var s +if(this.b>=4)throw A.a(this.aL()) +s=A.aw(a,b) +this.au(s.a,s.b)}, +mu(a){return this.a2(a,null)}, +n(){var s=this,r=s.b +if((r&4)!==0)return s.dN() +if(r>=4)throw A.a(s.aL()) +s.hH() +return s.dN()}, +hH(){var s=this.b|=4 +if((s&1)!==0)this.bz() +else if((s&3)===0)this.cQ().q(0,B.A)}, +af(a){var s=this.b +if((s&1)!==0)this.aE(a) +else if((s&3)===0)this.cQ().q(0,new A.c9(a))}, +au(a,b){var s=this.b +if((s&1)!==0)this.bg(a,b) +else if((s&3)===0)this.cQ().q(0,new A.ed(a,b))}, +b2(){var s=this.a +this.a=s.c +this.b&=4294967287 +s.a.aB(null)}, +fG(a,b,c,d){var s,r,q,p=this +if((p.b&3)!==0)throw A.a(A.u("Stream has already been listened to.")) +s=A.AV(p,a,b,c,d,A.q(p).c) +r=p.glB() +if(((p.b|=1)&8)!==0){q=p.a +q.c=s +q.b.ar()}else p.a=s +s.lZ(r) +s.fi(new A.ro(p)) +return s}, +ij(a){var s,r,q,p,o,n,m,l=this,k=null +if((l.b&8)!==0)k=l.a.u() +l.a=null +l.b=l.b&4294967286|2 +s=l.r +if(s!=null)if(k==null)try{r=s.$0() +if(r instanceof A.l)k=r}catch(o){q=A.H(o) +p=A.N(o) +n=new A.l($.n,t.D) +n.R(new A.a6(q,p)) +k=n}else k=k.O(s) +m=new A.rn(l) +if(k!=null)k=k.O(m) +else m.$0() +return k}, +ik(a){if((this.b&8)!==0)this.a.b.ak() +A.kE(this.e)}, +il(a){if((this.b&8)!==0)this.a.b.ar() +A.kE(this.f)}, +$iaa:1, +$ibQ:1, +sjk(a){return this.d=a}, +sjl(a){return this.e=a}, +sjm(a){return this.f=a}, +sjj(a){return this.r=a}} +A.ro.prototype={ +$0(){A.kE(this.a.d)}, +$S:0} +A.rn.prototype={ +$0(){var s=this.a.c +if(s!=null&&(s.a&30)===0)s.aB(null)}, +$S:0} +A.ks.prototype={ +aE(a){this.gan().af(a)}, +bg(a,b){this.gan().au(a,b)}, +bz(){this.gan().b2()}} +A.jE.prototype={ +aE(a){this.gan().bc(new A.c9(a))}, +bg(a,b){this.gan().bc(new A.ed(a,b))}, +bz(){this.gan().bc(B.A)}} +A.bT.prototype={} +A.cB.prototype={} +A.O.prototype={ +gB(a){return(A.fv(this.a)^892482866)>>>0}, +H(a,b){if(b==null)return!1 +if(this===b)return!0 +return b instanceof A.O&&b.a===this.a}} +A.cx.prototype={ +dJ(){return this.w.ij(this)}, +b3(){this.w.ik(this)}, +b4(){this.w.il(this)}} +A.ev.prototype={ +q(a,b){this.a.q(0,b)}, +a2(a,b){this.a.a2(a,b)}, +n(){return this.a.n()}, +$iaa:1} +A.fZ.prototype={ +u(){var s=this.b.u() +return s.O(new A.pI(this))}} +A.pJ.prototype={ +$2(a,b){var s=this.a +s.au(a,b) +s.b2()}, +$S:7} +A.pI.prototype={ +$0(){this.a.a.aB(null)}, +$S:1} +A.ko.prototype={} +A.at.prototype={ +lZ(a){var s=this +if(a==null)return +s.r=a +if(a.c!=null){s.e=(s.e|128)>>>0 +a.dE(s)}}, +bH(a){this.a=A.jG(this.d,a,A.q(this).h("at.T"))}, +dq(a){var s=this,r=s.e +if(a==null)s.e=(r&4294967263)>>>0 +else s.e=(r|32)>>>0 +s.b=A.jH(s.d,a)}, +aJ(a){var s,r=this,q=r.e +if((q&8)!==0)return +r.e=(q+256|4)>>>0 +if(a!=null)a.O(r.gbI()) +if(q<256){s=r.r +if(s!=null)if(s.a===1)s.a=3}if((q&4)===0&&(r.e&64)===0)r.fi(r.gcT())}, +ak(){return this.aJ(null)}, +ar(){var s=this,r=s.e +if((r&8)!==0)return +if(r>=256){r=s.e=r-256 +if(r<256)if((r&128)!==0&&s.r.c!=null)s.r.dE(s) +else{r=(r&4294967291)>>>0 +s.e=r +if((r&64)===0)s.fi(s.gcU())}}}, +u(){var s=this,r=(s.e&4294967279)>>>0 +s.e=r +if((r&8)===0)s.eZ() +r=s.f +return r==null?$.cH():r}, +eZ(){var s,r=this,q=r.e=(r.e|8)>>>0 +if((q&128)!==0){s=r.r +if(s.a===1)s.a=3}if((q&64)===0)r.r=null +r.f=r.dJ()}, +af(a){var s=this.e +if((s&8)!==0)return +if(s<64)this.aE(a) +else this.bc(new A.c9(a))}, +au(a,b){var s +if(t.C.b(a))A.iP(a,b) +s=this.e +if((s&8)!==0)return +if(s<64)this.bg(a,b) +else this.bc(new A.ed(a,b))}, +b2(){var s=this,r=s.e +if((r&8)!==0)return +r=(r|2)>>>0 +s.e=r +if(r<64)s.bz() +else s.bc(B.A)}, +b3(){}, +b4(){}, +dJ(){return null}, +bc(a){var s,r=this,q=r.r +if(q==null)q=r.r=new A.er() +q.q(0,a) +s=r.e +if((s&128)===0){s=(s|128)>>>0 +r.e=s +if(s<256)q.dE(r)}}, +aE(a){var s=this,r=s.e +s.e=(r|64)>>>0 +s.d.c4(s.a,a,A.q(s).h("at.T")) +s.e=(s.e&4294967231)>>>0 +s.f1((r&4)!==0)}, +bg(a,b){var s,r=this,q=r.e,p=new A.q2(r,a,b) +if((q&1)!==0){r.e=(q|16)>>>0 +r.eZ() +s=r.f +if(s!=null&&s!==$.cH())s.O(p) +else p.$0()}else{p.$0() +r.f1((q&4)!==0)}}, +bz(){var s,r=this,q=new A.q1(r) +r.eZ() +r.e=(r.e|16)>>>0 +s=r.f +if(s!=null&&s!==$.cH())s.O(q) +else q.$0()}, +fi(a){var s=this,r=s.e +s.e=(r|64)>>>0 +a.$0() +s.e=(s.e&4294967231)>>>0 +s.f1((r&4)!==0)}, +f1(a){var s,r,q=this,p=q.e +if((p&128)!==0&&q.r.c==null){p=q.e=(p&4294967167)>>>0 +s=!1 +if((p&4)!==0)if(p<256){s=q.r +s=s==null?null:s.c==null +s=s!==!1}if(s){p=(p&4294967291)>>>0 +q.e=p}}for(;;a=r){if((p&8)!==0){q.r=null +return}r=(p&4)!==0 +if(a===r)break +q.e=(p^64)>>>0 +if(r)q.b3() +else q.b4() +p=(q.e&4294967231)>>>0 +q.e=p}if((p&128)!==0&&p<256)q.r.dE(q)}, +$iak:1} +A.q2.prototype={ +$0(){var s,r,q,p=this.a,o=p.e +if((o&8)!==0&&(o&16)===0)return +p.e=(o|64)>>>0 +s=p.b +o=this.b +r=t.K +q=p.d +if(t.v.b(s))q.hn(s,o,this.c,r,t.l) +else q.c4(s,o,r) +p.e=(p.e&4294967231)>>>0}, +$S:0} +A.q1.prototype={ +$0(){var s=this.a,r=s.e +if((r&16)===0)return +s.e=(r|74)>>>0 +s.d.dv(s.c) +s.e=(s.e&4294967231)>>>0}, +$S:0} +A.eu.prototype={ +A(a,b,c,d){return this.a.fG(a,d,c,b===!0)}, +Z(a){return this.A(a,null,null,null)}, +aj(a,b,c){return this.A(a,null,b,c)}, +bk(a,b,c){return this.A(a,b,c,null)}, +nW(a,b){return this.A(a,null,null,b)}, +nV(a,b){return this.A(a,null,b,null)}} +A.jO.prototype={ +gc1(){return this.a}, +sc1(a){return this.a=a}} +A.c9.prototype={ +hh(a){a.aE(this.b)}} +A.ed.prototype={ +hh(a){a.bg(this.b,this.c)}} +A.qy.prototype={ +hh(a){a.bz()}, +gc1(){return null}, +sc1(a){throw A.a(A.u("No events after a done."))}} +A.er.prototype={ +dE(a){var s=this,r=s.a +if(r===1)return +if(r>=1){s.a=1 +return}A.eM(new A.r8(s,a)) +s.a=1}, +q(a,b){var s=this,r=s.c +if(r==null)s.b=s.c=b +else{r.sc1(b) +s.c=b}}} +A.r8.prototype={ +$0(){var s,r,q=this.a,p=q.a +q.a=0 +if(p===3)return +s=q.b +r=s.gc1() +q.b=r +if(r==null)q.c=null +s.hh(this.b)}, +$S:0} +A.ef.prototype={ +bH(a){}, +dq(a){}, +aJ(a){var s=this.a +if(s>=0){this.a=s+2 +if(a!=null)a.O(this.gbI())}}, +ak(){return this.aJ(null)}, +ar(){var s=this,r=s.a-2 +if(r<0)return +if(r===0){s.a=1 +A.eM(s.gic())}else s.a=r}, +u(){this.a=-1 +this.c=null +return $.cH()}, +lx(){var s,r=this,q=r.a-1 +if(q===0){r.a=-1 +s=r.c +if(s!=null){r.c=null +r.b.dv(s)}}else r.a=q}, +$iak:1} +A.bU.prototype={ +gp(){if(this.c)return this.b +return null}, +l(){var s,r=this,q=r.a +if(q!=null){if(r.c){s=new A.l($.n,t.x) +r.b=s +r.c=!1 +q.ar() +return s}throw A.a(A.u("Already waiting for next."))}return r.la()}, +la(){var s,r,q=this,p=q.b +if(p!=null){s=new A.l($.n,t.x) +q.b=s +r=p.A(q.gkE(),!0,q.glr(),q.glt()) +if(q.b!=null)q.a=r +return s}return $.ye()}, +u(){var s=this,r=s.a,q=s.b +s.b=null +if(r!=null){s.a=null +if(!s.c)q.aB(!1) +else s.c=!1 +return r.u()}return $.cH()}, +kF(a){var s,r,q=this +if(q.a==null)return +s=q.b +q.b=a +q.c=!0 +s.bd(!0) +if(q.c){r=q.a +if(r!=null)r.ak()}}, +lu(a,b){var s=this,r=s.a,q=s.b +s.b=s.a=null +if(r!=null)q.a7(new A.a6(a,b)) +else q.R(new A.a6(a,b))}, +ls(){var s=this,r=s.a,q=s.b +s.b=s.a=null +if(r!=null)q.bS(!1) +else q.hE(!1)}} +A.de.prototype={ +A(a,b,c,d){return A.wJ(c,this.$ti.c)}, +Z(a){return this.A(a,null,null,null)}, +aj(a,b,c){return this.A(a,null,b,c)}, +bk(a,b,c){return this.A(a,b,c,null)}, +gaq(){return!0}} +A.bH.prototype={ +A(a,b,c,d){var s=null,r=new A.he(s,s,s,s,this.$ti.h("he<1>")) +r.d=new A.r7(this,r) +return r.fG(a,d,c,b===!0)}, +Z(a){return this.A(a,null,null,null)}, +aj(a,b,c){return this.A(a,null,b,c)}, +bk(a,b,c){return this.A(a,b,c,null)}, +gaq(){return this.a}} +A.r7.prototype={ +$0(){this.a.b.$1(this.b)}, +$S:0} +A.he.prototype={ +my(a){var s=this.b +if(s>=4)throw A.a(this.aL()) +if((s&1)!==0)this.gan().af(a)}, +mv(a,b){var s=this.b +if(s>=4)throw A.a(this.aL()) +if((s&1)!==0){s=this.gan() +s.au(a,b==null?B.r:b)}}, +iU(){var s=this,r=s.b +if((r&4)!==0)return +if(r>=4)throw A.a(s.aL()) +r|=4 +s.b=r +if((r&1)!==0)s.gan().b2()}, +$ic0:1} +A.rZ.prototype={ +$0(){return this.a.a7(this.b)}, +$S:0} +A.rY.prototype={ +$2(a,b){A.BL(this.a,this.b,new A.a6(a,b))}, +$S:4} +A.t_.prototype={ +$0(){return this.a.bd(this.b)}, +$S:0} +A.b9.prototype={ +gaq(){return this.a.gaq()}, +A(a,b,c,d){var s=A.q(this),r=$.n,q=b===!0?1:0,p=d!=null?32:0,o=A.jG(r,a,s.h("b9.T")),n=A.jH(r,d),m=c==null?A.tw():c +s=new A.ej(this,o,n,r.b0(m,t.H),r,q|p,s.h("ej")) +s.x=this.a.aj(s.gfj(),s.gfl(),s.gfn()) +return s}, +Z(a){return this.A(a,null,null,null)}, +aj(a,b,c){return this.A(a,null,b,c)}, +bk(a,b,c){return this.A(a,b,c,null)}} +A.ej.prototype={ +af(a){if((this.e&2)!==0)return +this.ad(a)}, +au(a,b){if((this.e&2)!==0)return +this.bR(a,b)}, +b3(){var s=this.x +if(s!=null)s.ak()}, +b4(){var s=this.x +if(s!=null)s.ar()}, +dJ(){var s=this.x +if(s!=null){this.x=null +return s.u()}return null}, +fk(a){this.w.i4(a,this)}, +fo(a,b){this.au(a,b)}, +fm(){this.b2()}} +A.dq.prototype={ +i4(a,b){var s,r,q,p=null +try{p=this.b.$1(a)}catch(q){s=A.H(q) +r=A.N(q) +A.xe(b,s,r) +return}if(p)b.af(a)}} +A.bG.prototype={ +i4(a,b){var s,r,q,p=null +try{p=this.b.$1(a)}catch(q){s=A.H(q) +r=A.N(q) +A.xe(b,s,r) +return}b.af(p)}} +A.h7.prototype={ +q(a,b){var s=this.a +if((s.e&2)!==0)A.p(A.u("Stream is already closed")) +s.ad(b)}, +a2(a,b){var s=this.a +if((s.e&2)!==0)A.p(A.u("Stream is already closed")) +s.bR(a,b)}, +n(){var s=this.a +if((s.e&2)!==0)A.p(A.u("Stream is already closed")) +s.aA()}, +$iaa:1} +A.es.prototype={ +b3(){var s=this.x +if(s!=null)s.ak()}, +b4(){var s=this.x +if(s!=null)s.ar()}, +dJ(){var s=this.x +if(s!=null){this.x=null +return s.u()}return null}, +fk(a){var s,r,q,p +try{q=this.w +q===$&&A.B() +q.q(0,a)}catch(p){s=A.H(p) +r=A.N(p) +if((this.e&2)!==0)A.p(A.u("Stream is already closed")) +this.bR(s,r)}}, +fo(a,b){var s,r,q,p,o=this,n="Stream is already closed" +try{q=o.w +q===$&&A.B() +q.a2(a,b)}catch(p){s=A.H(p) +r=A.N(p) +if(s===a){if((o.e&2)!==0)A.p(A.u(n)) +o.bR(a,b)}else{if((o.e&2)!==0)A.p(A.u(n)) +o.bR(s,r)}}}, +fm(){var s,r,q,p,o=this +try{o.x=null +q=o.w +q===$&&A.B() +q.n()}catch(p){s=A.H(p) +r=A.N(p) +if((o.e&2)!==0)A.p(A.u("Stream is already closed")) +o.bR(s,r)}}} +A.c7.prototype={ +gaq(){return this.b.gaq()}, +A(a,b,c,d){var s=this.$ti,r=$.n,q=b===!0?1:0,p=d!=null?32:0,o=A.jG(r,a,s.y[1]),n=A.jH(r,d),m=c==null?A.tw():c,l=new A.es(o,n,r.b0(m,t.H),r,q|p,s.h("es<1,2>")) +l.w=this.a.$1(new A.h7(l)) +l.x=this.b.aj(l.gfj(),l.gfl(),l.gfn()) +return l}, +Z(a){return this.A(a,null,null,null)}, +aj(a,b,c){return this.A(a,null,b,c)}, +bk(a,b,c){return this.A(a,b,c,null)}} +A.kn.prototype={ +aY(a){return this.a.$1(a)}} +A.aN.prototype={} +A.kA.prototype={ +cV(a,b,c){var s,r,q,p,o,n,m,l,k=this.gfq(),j=k.a +if(j===B.e){A.hE(b,c) +return}s=k.b +r=j.gaC() +m=j.gjn() +m.toString +q=m +p=$.n +try{$.n=q +s.$5(j,r,a,b,c) +$.n=p}catch(l){o=A.H(l) +n=A.N(l) +$.n=p +m=b===o?c:n +q.cV(j,o,m)}}, +$iE:1} +A.jM.prototype={ +ghQ(){var s=this.at +return s==null?this.at=new A.eA():s}, +gaC(){return this.ax.ghQ()}, +gbi(){return this.as.a}, +dv(a){var s,r,q +try{this.bJ(a,t.H)}catch(q){s=A.H(q) +r=A.N(q) +this.cV(this,s,r)}}, +c4(a,b,c){var s,r,q +try{this.c3(a,b,t.H,c)}catch(q){s=A.H(q) +r=A.N(q) +this.cV(this,s,r)}}, +hn(a,b,c,d,e){var s,r,q +try{this.hm(a,b,c,t.H,d,e)}catch(q){s=A.H(q) +r=A.N(q) +this.cV(this,s,r)}}, +fQ(a,b){return new A.qs(this,this.b0(a,b),b)}, +iQ(a,b,c){return new A.qu(this,this.bo(a,b,c),c,b)}, +e8(a){return new A.qr(this,this.b0(a,t.H))}, +fR(a,b){return new A.qt(this,this.bo(a,t.H,b),b)}, +i(a,b){var s,r=this.ay,q=r.i(0,b) +if(q!=null||r.F(b))return q +s=this.ax.i(0,b) +if(s!=null)r.m(0,b,s) +return s}, +cq(a,b){this.cV(this,a,b)}, +j3(a){var s=this.Q,r=s.a +return s.b.$5(r,r.gaC(),this,null,a)}, +bJ(a){var s=this.a,r=s.a +return s.b.$4(r,r.gaC(),this,a)}, +c3(a,b){var s=this.b,r=s.a +return s.b.$5(r,r.gaC(),this,a,b)}, +hm(a,b,c){var s=this.c,r=s.a +return s.b.$6(r,r.gaC(),this,a,b,c)}, +b0(a){var s=this.d,r=s.a +return s.b.$4(r,r.gaC(),this,a)}, +bo(a){var s=this.e,r=s.a +return s.b.$4(r,r.gaC(),this,a)}, +cD(a){var s=this.f,r=s.a +return s.b.$4(r,r.gaC(),this,a)}, +j_(a,b){var s=this.r,r=s.a +if(r===B.e)return null +return s.b.$5(r,r.gaC(),this,a,b)}, +bN(a){var s=this.w,r=s.a +return s.b.$4(r,r.gaC(),this,a)}, +fV(a,b){var s=this.x,r=s.a +return s.b.$5(r,r.gaC(),this,a,b)}, +jp(a){var s=this.z,r=s.a +return s.b.$4(r,r.gaC(),this,a)}, +git(){return this.a}, +giv(){return this.b}, +giu(){return this.c}, +gio(){return this.d}, +gip(){return this.e}, +gim(){return this.f}, +ghU(){return this.r}, +gfE(){return this.w}, +ghO(){return this.x}, +ghN(){return this.y}, +gih(){return this.z}, +ghZ(){return this.Q}, +gfq(){return this.as}, +gjn(){return this.ax}, +gi9(){return this.ay}} +A.qs.prototype={ +$0(){return this.a.bJ(this.b,this.c)}, +$S(){return this.c.h("0()")}} +A.qu.prototype={ +$1(a){var s=this +return s.a.c3(s.b,a,s.d,s.c)}, +$S(){return this.d.h("@<0>").J(this.c).h("1(2)")}} +A.qr.prototype={ +$0(){return this.a.dv(this.b)}, +$S:0} +A.qt.prototype={ +$1(a){return this.a.c4(this.b,a,this.c)}, +$S(){return this.c.h("~(0)")}} +A.kj.prototype={ +git(){return B.ck}, +giv(){return B.cm}, +giu(){return B.cl}, +gio(){return B.cj}, +gip(){return B.ce}, +gim(){return B.co}, +ghU(){return B.cg}, +gfE(){return B.cn}, +ghO(){return B.cf}, +ghN(){return B.cd}, +gih(){return B.ci}, +ghZ(){return B.ch}, +gfq(){return B.cc}, +gjn(){return null}, +gi9(){return $.yu()}, +ghQ(){var s=$.ra +return s==null?$.ra=new A.eA():s}, +gaC(){var s=$.ra +return s==null?$.ra=new A.eA():s}, +gbi(){return this}, +dv(a){var s,r,q +try{if(B.e===$.n){a.$0() +return}A.tf(null,null,this,a)}catch(q){s=A.H(q) +r=A.N(q) +A.hE(s,r)}}, +c4(a,b){var s,r,q +try{if(B.e===$.n){a.$1(b) +return}A.th(null,null,this,a,b)}catch(q){s=A.H(q) +r=A.N(q) +A.hE(s,r)}}, +hn(a,b,c){var s,r,q +try{if(B.e===$.n){a.$2(b,c) +return}A.tg(null,null,this,a,b,c)}catch(q){s=A.H(q) +r=A.N(q) +A.hE(s,r)}}, +fQ(a,b){return new A.rc(this,a,b)}, +iQ(a,b,c){return new A.re(this,a,c,b)}, +e8(a){return new A.rb(this,a)}, +fR(a,b){return new A.rd(this,a,b)}, +i(a,b){return null}, +cq(a,b){A.hE(a,b)}, +j3(a){return A.xD(null,null,this,null,a)}, +bJ(a){if($.n===B.e)return a.$0() +return A.tf(null,null,this,a)}, +c3(a,b){if($.n===B.e)return a.$1(b) +return A.th(null,null,this,a,b)}, +hm(a,b,c){if($.n===B.e)return a.$2(b,c) +return A.tg(null,null,this,a,b,c)}, +b0(a){return a}, +bo(a){return a}, +cD(a){return a}, +j_(a,b){return null}, +bN(a){A.ti(null,null,this,a)}, +fV(a,b){return A.uF(a,b)}, +jp(a){A.vk(a)}} +A.rc.prototype={ +$0(){return this.a.bJ(this.b,this.c)}, +$S(){return this.c.h("0()")}} +A.re.prototype={ +$1(a){var s=this +return s.a.c3(s.b,a,s.d,s.c)}, +$S(){return this.d.h("@<0>").J(this.c).h("1(2)")}} +A.rb.prototype={ +$0(){return this.a.dv(this.b)}, +$S:0} +A.rd.prototype={ +$1(a){return this.a.c4(this.b,a,this.c)}, +$S(){return this.c.h("~(0)")}} +A.eA.prototype={$iaf:1} +A.te.prototype={ +$0(){A.uh(this.a,this.b)}, +$S:0} +A.ca.prototype={ +gk(a){return this.a}, +gG(a){return this.a===0}, +ga6(){return new A.ha(this,A.q(this).h("ha<1>"))}, +F(a){var s,r +if(typeof a=="string"&&a!=="__proto__"){s=this.b +return s==null?!1:s[a]!=null}else if(typeof a=="number"&&(a&1073741823)===a){r=this.c +return r==null?!1:r[a]!=null}else return this.hL(a)}, +hL(a){var s=this.d +if(s==null)return!1 +return this.be(this.i1(s,a),a)>=0}, +i(a,b){var s,r,q +if(typeof b=="string"&&b!=="__proto__"){s=this.b +r=s==null?null:A.wL(s,b) +return r}else if(typeof b=="number"&&(b&1073741823)===b){q=this.c +r=q==null?null:A.wL(q,b) +return r}else return this.i0(b)}, +i0(a){var s,r,q=this.d +if(q==null)return null +s=this.i1(q,a) +r=this.be(s,a) +return r<0?null:s[r+1]}, +m(a,b,c){var s,r,q=this +if(typeof b=="string"&&b!=="__proto__"){s=q.b +q.hC(s==null?q.b=A.uU():s,b,c)}else if(typeof b=="number"&&(b&1073741823)===b){r=q.c +q.hC(r==null?q.c=A.uU():r,b,c)}else q.iw(b,c)}, +iw(a,b){var s,r,q,p=this,o=p.d +if(o==null)o=p.d=A.uU() +s=p.bv(a) +r=o[s] +if(r==null){A.uV(o,s,[a,b]);++p.a +p.e=null}else{q=p.be(r,a) +if(q>=0)r[q+1]=b +else{r.push(a,b);++p.a +p.e=null}}}, +a4(a,b){var s,r,q,p,o,n=this,m=n.hK() +for(s=m.length,r=A.q(n).y[1],q=0;q"))}, +T(a,b){return this.a.F(b)}} +A.jU.prototype={ +gp(){var s=this.d +return s==null?this.$ti.c.a(s):s}, +l(){var s=this,r=s.b,q=s.c,p=s.a +if(r!==p.e)throw A.a(A.am(p)) +else if(q>=r.length){s.d=null +return!1}else{s.d=r[q] +s.c=q+1 +return!0}}} +A.hd.prototype={ +i(a,b){if(!this.y.$1(b))return null +return this.kc(b)}, +m(a,b,c){this.ke(b,c)}, +F(a){if(!this.y.$1(a))return!1 +return this.kb(a)}, +E(a,b){if(!this.y.$1(b))return null +return this.kd(b)}, +cs(a){return this.x.$1(a)&1073741823}, +ct(a,b){var s,r,q +if(a==null)return-1 +s=a.length +for(r=this.w,q=0;q"))}, +gv(a){var s=this,r=new A.k0(s,s.r,A.q(s).h("k0<1>")) +r.c=s.e +return r}, +gk(a){return this.a}, +gG(a){return this.a===0}, +gaQ(a){return this.a!==0}, +T(a,b){var s,r +if(b!=="__proto__"){s=this.b +if(s==null)return!1 +return s[b]!=null}else{r=this.kT(b) +return r}}, +kT(a){var s=this.d +if(s==null)return!1 +return this.be(s[this.bv(a)],a)>=0}, +q(a,b){var s,r,q=this +if(typeof b=="string"&&b!=="__proto__"){s=q.b +return q.hB(s==null?q.b=A.uW():s,b)}else if(typeof b=="number"&&(b&1073741823)===b){r=q.c +return q.hB(r==null?q.c=A.uW():r,b)}else return q.f6(b)}, +f6(a){var s,r,q=this,p=q.d +if(p==null)p=q.d=A.uW() +s=q.bv(a) +r=p[s] +if(r==null)p[s]=[q.fA(a)] +else{if(q.be(r,a)>=0)return!1 +r.push(q.fA(a))}return!0}, +E(a,b){var s=this +if(typeof b=="string"&&b!=="__proto__")return s.hI(s.b,b) +else if(typeof b=="number"&&(b&1073741823)===b)return s.hI(s.c,b) +else return s.fD(b)}, +fD(a){var s,r,q,p,o=this,n=o.d +if(n==null)return!1 +s=o.bv(a) +r=n[s] +q=o.be(r,a) +if(q<0)return!1 +p=r.splice(q,1)[0] +if(0===r.length)delete n[s] +o.hJ(p) +return!0}, +bA(a){var s=this +if(s.a>0){s.b=s.c=s.d=s.e=s.f=null +s.a=0 +s.f7()}}, +hB(a,b){if(a[b]!=null)return!1 +a[b]=this.fA(b) +return!0}, +hI(a,b){var s +if(a==null)return!1 +s=a[b] +if(s==null)return!1 +this.hJ(s) +delete a[b] +return!0}, +f7(){this.r=this.r+1&1073741823}, +fA(a){var s,r=this,q=new A.r6(a) +if(r.e==null)r.e=r.f=q +else{s=r.f +s.toString +q.c=s +r.f=s.b=q}++r.a +r.f7() +return q}, +hJ(a){var s=this,r=a.c,q=a.b +if(r==null)s.e=q +else r.b=q +if(q==null)s.f=r +else q.c=r;--s.a +s.f7()}, +bv(a){return J.z(a)&1073741823}, +be(a,b){var s,r +if(a==null)return-1 +s=a.length +for(r=0;r"))}, +gk(a){return J.ay(this.a)}, +i(a,b){return J.hJ(this.a,b)}} +A.mD.prototype={ +$2(a,b){this.a.m(0,this.b.a(a),this.c.a(b))}, +$S:30} +A.nf.prototype={ +$2(a,b){this.a.m(0,this.b.a(a),this.c.a(b))}, +$S:30} +A.fh.prototype={ +E(a,b){if(b.a!==this)return!1 +this.fI(b) +return!0}, +T(a,b){return!1}, +gv(a){var s=this +return new A.k1(s,s.a,s.c,s.$ti.h("k1<1>"))}, +gk(a){return this.b}, +gai(a){var s +if(this.b===0)throw A.a(A.u("No such element")) +s=this.c +s.toString +return s}, +gaS(a){var s +if(this.b===0)throw A.a(A.u("No such element")) +s=this.c.c +s.toString +return s}, +gG(a){return this.b===0}, +fs(a,b,c){var s,r,q=this +if(b.a!=null)throw A.a(A.u("LinkedListEntry is already in a LinkedList"));++q.a +b.a=q +s=q.b +if(s===0){b.b=b +q.c=b.c=b +q.b=s+1 +return}r=a.c +r.toString +b.c=r +b.b=a +a.c=r.b=b +q.b=s+1}, +fI(a){var s,r,q=this;++q.a +s=a.b +s.c=a.c +a.c.b=s +r=--q.b +a.a=a.b=a.c=null +if(r===0)q.c=null +else if(a===q.c)q.c=s}} +A.k1.prototype={ +gp(){var s=this.c +return s==null?this.$ti.c.a(s):s}, +l(){var s=this,r=s.a +if(s.b!==r.a)throw A.a(A.am(s)) +if(r.b!==0)r=s.e&&s.d===r.gai(0) +else r=!0 +if(r){s.c=null +return!1}s.e=!0 +r=s.d +s.c=r +s.d=r.b +return!0}} +A.aV.prototype={ +gds(){var s=this.a +if(s==null||this===s.gai(0))return null +return this.c}} +A.C.prototype={ +gv(a){return new A.aq(a,this.gk(a),A.br(a).h("aq"))}, +U(a,b){return this.i(a,b)}, +gG(a){return this.gk(a)===0}, +gaQ(a){return!this.gG(a)}, +gai(a){if(this.gk(a)===0)throw A.a(A.ck()) +return this.i(a,0)}, +T(a,b){var s,r=this.gk(a) +for(s=0;s").J(c).h("a8<1,2>"))}, +aV(a,b){return A.bS(a,b,null,A.br(a).h("C.E"))}, +bK(a,b){return A.bS(a,0,A.bd(b,"count",t.S),A.br(a).h("C.E"))}, +q(a,b){var s=this.gk(a) +this.sk(a,s+1) +this.m(a,s,b)}, +d7(a,b){return new A.al(a,A.br(a).h("@").J(b).h("al<1,2>"))}, +cN(a,b){var s=b==null?A.D0():b +A.j1(a,0,this.gk(a)-1,s)}, +jW(a,b,c){A.aL(b,c,this.gk(a)) +return A.bS(a,b,c,A.br(a).h("C.E"))}, +h1(a,b,c,d){var s +A.aL(b,c,this.gk(a)) +for(s=b;sp.gk(q))throw A.a(A.vU()) +if(r=0;--o)this.m(a,b+o,p.i(q,r+o)) +else for(o=0;o"))}, +cw(a,b,c,d){var s,r,q,p,o,n=A.P(c,d) +for(s=J.U(this.ga6()),r=A.q(this).h("L.V");s.l();){q=s.gp() +p=this.i(0,q) +o=b.$2(q,p==null?r.a(p):p) +n.m(0,o.a,o.b)}return n}, +F(a){return J.vw(this.ga6(),a)}, +gk(a){return J.ay(this.ga6())}, +gG(a){return J.kS(this.ga6())}, +j(a){return A.nj(this)}, +$ia_:1} +A.ni.prototype={ +$1(a){var s=this.a,r=s.i(0,a) +if(r==null)r=A.q(s).h("L.V").a(r) +return new A.Q(a,r,A.q(s).h("Q"))}, +$S(){return A.q(this.a).h("Q(L.K)")}} +A.nk.prototype={ +$2(a,b){var s,r=this.a +if(!r.a)this.b.a+=", " +r.a=!1 +r=this.b +s=A.o(a) +r.a=(r.a+=s)+": " +s=A.o(b) +r.a+=s}, +$S:27} +A.kw.prototype={} +A.fk.prototype={ +i(a,b){return this.a.i(0,b)}, +F(a){return this.a.F(a)}, +a4(a,b){this.a.a4(0,b)}, +gG(a){var s=this.a +return s.gG(s)}, +gk(a){var s=this.a +return s.gk(s)}, +ga6(){return this.a.ga6()}, +j(a){return this.a.j(0)}, +gbZ(){return this.a.gbZ()}, +cw(a,b,c,d){return this.a.cw(0,b,c,d)}, +$ia_:1} +A.fN.prototype={} +A.fi.prototype={ +gv(a){var s=this +return new A.k2(s,s.c,s.d,s.b,s.$ti.h("k2<1>"))}, +gG(a){return this.b===this.c}, +gk(a){return(this.c-this.b&this.a.length-1)>>>0}, +U(a,b){var s,r=this +A.zu(b,r.gk(0),r,null,null) +s=r.a +s=s[(r.b+b&s.length-1)>>>0] +return s==null?r.$ti.c.a(s):s}, +E(a,b){var s,r=this +for(s=r.b;s!==r.c;s=(s+1&r.a.length-1)>>>0)if(J.y(r.a[s],b)){r.fD(s);++r.d +return!0}return!1}, +j(a){return A.n8(this,"{","}")}, +ol(){var s,r,q=this,p=q.b +if(p===q.c)throw A.a(A.ck());++q.d +s=q.a +r=s[p] +if(r==null)r=q.$ti.c.a(r) +s[p]=null +q.b=(p+1&s.length-1)>>>0 +return r}, +f6(a){var s,r,q=this,p=q.a,o=q.c +p[o]=a +p=p.length +o=(o+1&p-1)>>>0 +q.c=o +if(q.b===o){s=A.aW(p*2,null,!1,q.$ti.h("1?")) +p=q.a +o=q.b +r=p.length-o +B.d.L(s,0,r,p,o) +B.d.L(s,r,r+q.b,q.a,0) +q.b=0 +q.c=q.a.length +q.a=s}++q.d}, +fD(a){var s,r,q,p=this,o=p.a,n=o.length-1,m=p.b,l=p.c +if((a-m&n)>>>0<(l-a&n)>>>0){for(s=a;s!==m;s=r){r=(s-1&n)>>>0 +o[s]=o[r]}o[m]=null +p.b=(m+1&n)>>>0 +return(a+1&n)>>>0}else{m=p.c=(l-1&n)>>>0 +for(s=a;s!==m;s=q){q=(s+1&n)>>>0 +o[s]=o[q]}o[m]=null +return a}}} +A.k2.prototype={ +gp(){var s=this.e +return s==null?this.$ti.c.a(s):s}, +l(){var s,r=this,q=r.a +if(r.c!==q.d)A.p(A.am(q)) +s=r.d +if(s===r.b){r.e=null +return!1}q=q.a +r.e=q[s] +r.d=(s+1&q.length-1)>>>0 +return!0}} +A.cq.prototype={ +gG(a){return this.gk(this)===0}, +gaQ(a){return this.gk(this)!==0}, +a8(a,b){var s +for(s=J.U(b);s.l();)this.q(0,s.gp())}, +cG(a){var s=this.eC(0) +s.a8(0,a) +return s}, +bp(a,b){var s=A.an(this,A.q(this).c) +return s}, +eB(a){return this.bp(0,!0)}, +bm(a,b,c){return new A.cM(this,b,A.q(this).h("@<1>").J(c).h("cM<1,2>"))}, +j(a){return A.n8(this,"{","}")}, +bK(a,b){return A.wq(this,b,A.q(this).c)}, +aV(a,b){return A.wm(this,b,A.q(this).c)}, +U(a,b){var s,r +A.aI(b,"index") +s=this.gv(this) +for(r=b;s.l();){if(r===0)return s.gp();--r}throw A.a(A.ih(b,b-r,this,null,"index"))}, +$ix:1, +$im:1, +$ibB:1} +A.ho.prototype={ +eC(a){var s=this.ln() +s.a8(0,this) +return s}} +A.hx.prototype={} +A.jY.prototype={ +i(a,b){var s,r=this.b +if(r==null)return this.c.i(0,b) +else if(typeof b!="string")return null +else{s=r[b] +return typeof s=="undefined"?this.lE(b):s}}, +gk(a){return this.b==null?this.c.a:this.dL().length}, +gG(a){return this.gk(0)===0}, +ga6(){if(this.b==null){var s=this.c +return new A.bx(s,A.q(s).h("bx<1>"))}return new A.jZ(this)}, +F(a){if(this.b==null)return this.c.F(a) +return Object.prototype.hasOwnProperty.call(this.a,a)}, +a4(a,b){var s,r,q,p,o=this +if(o.b==null)return o.c.a4(0,b) +s=o.dL() +for(r=0;r"))}return s}, +T(a,b){return this.a.F(b)}} +A.qZ.prototype={ +n(){var s,r,q,p=this,o="Stream is already closed" +p.kn() +s=p.a +r=s.a +s.a="" +q=A.xy(r.charCodeAt(0)==0?r:r,p.b) +r=p.c.a +if((r.e&2)!==0)A.p(A.u(o)) +r.ad(q) +if((r.e&2)!==0)A.p(A.u(o)) +r.aA()}} +A.rO.prototype={ +$0(){var s,r +try{s=new TextDecoder("utf-8",{fatal:true}) +return s}catch(r){}return null}, +$S:42} +A.rN.prototype={ +$0(){var s,r +try{s=new TextDecoder("utf-8",{fatal:false}) +return s}catch(r){}return null}, +$S:42} +A.hN.prototype={ +gbG(){return"us-ascii"}, +bB(a){return B.aT.ap(a)}, +aO(a){var s=B.Y.ap(a) +return s}, +gd9(){return B.Y}} +A.kv.prototype={ +ap(a){var s,r,q,p=A.aL(0,null,a.length),o=new Uint8Array(p) +for(s=~this.a,r=0;r>>0!==0){if(r>b)s.aa(a,b,r,!1) +s.aa(B.by,0,3,!1) +b=r+1}if(b>>0!==0)throw A.a(A.ai("Source contains non-ASCII bytes.",null,null)) +s=A.bR(b,0,null) +q=this.a.a.a +if((q.e&2)!==0)A.p(A.u("Stream is already closed")) +q.ad(s)}} +A.l7.prototype={ +o7(a0,a1,a2){var s,r,q,p,o,n,m,l,k,j,i,h,g,f,e,d,c,b,a="Invalid base64 encoding length " +a2=A.aL(a1,a2,a0.length) +s=$.yr() +for(r=a1,q=r,p=null,o=-1,n=-1,m=0;r=0){g=u.U.charCodeAt(f) +if(g===k)continue +k=g}else{if(f===-1){if(o<0){e=p==null?null:p.a.length +if(e==null)e=0 +o=e+(r-q) +n=r}++m +if(k===61)continue}k=g}if(f!==-2){if(p==null){p=new A.X("") +e=p}else e=p +e.a+=B.a.t(a0,q,r) +d=A.aQ(k) +e.a+=d +q=l +continue}}throw A.a(A.ai("Invalid base64 data",a0,r))}if(p!=null){e=B.a.t(a0,q,a2) +e=p.a+=e +d=e.length +if(o>=0)A.vA(a0,n,a2,o,m,d) +else{c=B.b.aU(d-1,4)+1 +if(c===1)throw A.a(A.ai(a,a0,a2)) +while(c<4){e+="=" +p.a=e;++c}}e=p.a +return B.a.c2(a0,a1,a2,e.charCodeAt(0)==0?e:e)}b=a2-a1 +if(o>=0)A.vA(a0,n,a2,o,m,b) +else{c=B.b.aU(b,4) +if(c===1)throw A.a(A.ai(a,a0,a2)) +if(c>1)a0=B.a.c2(a0,a2,a2,c===2?"==":"=")}return a0}} +A.hU.prototype={ +ba(a){return new A.pK(a,new A.q0(u.U))}} +A.pV.prototype={ +iW(a){return new Uint8Array(a)}, +nd(a,b,c,d){var s,r=this,q=(r.a&3)+(c-b),p=B.b.M(q,3),o=p*4 +if(d&&q-p*3>0)o+=4 +s=r.iW(o) +r.a=A.AK(r.b,a,b,c,d,s,0,r.a) +if(o>0)return s +return null}} +A.q0.prototype={ +iW(a){var s=this.c +if(s==null||s.lengthp.length-o){p=q.b +s=n.gk(b)+p.length-1 +s|=B.b.Y(s,1) +s|=s>>>2 +s|=s>>>4 +s|=s>>>8 +r=new Uint8Array((((s|s>>>16)>>>0)+1)*2) +p=q.b +B.f.al(r,0,p.length,p) +q.b=r}p=q.b +o=q.c +B.f.al(p,o,o+n.gk(b),b) +q.c=q.c+n.gk(b)}, +n(){this.a.$1(B.f.bb(this.b,0,this.c))}} +A.i0.prototype={} +A.db.prototype={ +q(a,b){this.b.q(0,b)}, +a2(a,b){A.bd(a,"error",t.K) +this.a.a2(a,b)}, +n(){this.b.n()}, +$iaa:1} +A.i1.prototype={} +A.ah.prototype={ +ba(a){throw A.a(A.R("This converter does not support chunked conversions: "+this.j(0)))}, +aY(a){return new A.c7(new A.lE(this),a,t.fM.J(A.q(this).h("ah.T")).h("c7<1,2>"))}} +A.lE.prototype={ +$1(a){return new A.db(a,this.a.ba(a))}, +$S:128} +A.cO.prototype={ +mN(a){return this.gd9().aY(a).nr(0,new A.X(""),new A.mg(),t.of).b8(new A.mh(),t.N)}} +A.mg.prototype={ +$2(a,b){a.a+=b +return a}, +$S:134} +A.mh.prototype={ +$1(a){var s=a.a +return s.charCodeAt(0)==0?s:s}, +$S:57} +A.ff.prototype={ +j(a){var s=A.ib(this.a) +return(this.b!=null?"Converting object to an encodable object failed:":"Converting object did not return an encodable object:")+" "+s}} +A.is.prototype={ +j(a){return"Cyclic error in JSON stringify"}} +A.nb.prototype={ +cn(a,b){var s=A.xy(a,this.gd9().a) +return s}, +aO(a){return this.cn(a,null)}, +iZ(a,b){var s=A.B3(a,this.gne().b,null) +return s}, +bB(a){return this.iZ(a,null)}, +gne(){return B.bw}, +gd9(){return B.bv}} +A.iu.prototype={ +ba(a){return new A.r_(null,this.b,new A.dk(a))}} +A.r_.prototype={ +q(a,b){var s,r,q,p=this +if(p.d)throw A.a(A.u("Only one call to add allowed")) +p.d=!0 +s=p.c +r=new A.X("") +q=new A.rt(r,s) +A.wO(b,q,p.b,p.a) +if(r.a.length!==0)q.ff() +s.n()}, +n(){}} +A.it.prototype={ +ba(a){return new A.qZ(this.a,a,new A.X(""))}} +A.r1.prototype={ +jC(a){var s,r,q,p,o,n=this,m=a.length +for(s=0,r=0;r92){if(q>=55296){p=q&64512 +if(p===55296){o=r+1 +o=!(o=0&&(a.charCodeAt(p)&64512)===55296)}else p=!1 +else p=!0 +if(p){if(r>s)n.eG(a,s,r) +s=r+1 +n.a1(92) +n.a1(117) +n.a1(100) +p=q>>>8&15 +n.a1(p<10?48+p:87+p) +p=q>>>4&15 +n.a1(p<10?48+p:87+p) +p=q&15 +n.a1(p<10?48+p:87+p)}}continue}if(q<32){if(r>s)n.eG(a,s,r) +s=r+1 +n.a1(92) +switch(q){case 8:n.a1(98) +break +case 9:n.a1(116) +break +case 10:n.a1(110) +break +case 12:n.a1(102) +break +case 13:n.a1(114) +break +default:n.a1(117) +n.a1(48) +n.a1(48) +p=q>>>4&15 +n.a1(p<10?48+p:87+p) +p=q&15 +n.a1(p<10?48+p:87+p) +break}}else if(q===34||q===92){if(r>s)n.eG(a,s,r) +s=r+1 +n.a1(92) +n.a1(q)}}if(s===0)n.aw(a) +else if(s255||r<0){if(s>b){q=this.a +q.toString +p=A.bR(a,b,s) +q=q.a.a +if((q.e&2)!==0)A.p(A.u(o)) +q.ad(p)}q=this.a +q.toString +p=A.bR(B.bz,0,1) +q=q.a.a +if((q.e&2)!==0)A.p(A.u(o)) +q.ad(p) +b=s+1}}if(b16)this.ff()}, +c8(a){if(this.a.a.length!==0)this.ff() +this.b.q(0,a)}, +ff(){var s=this.a,r=s.a +s.a="" +this.b.q(0,r.charCodeAt(0)==0?r:r)}} +A.hr.prototype={ +n(){}, +aa(a,b,c,d){var s,r,q +if(b!==0||c!==a.length)for(s=this.a,r=b;r>>18|240 +q=o.b=p+1 +r[p]=s>>>12&63|128 +p=o.b=q+1 +r[q]=s>>>6&63|128 +o.b=p+1 +r[p]=s&63|128 +return!0}else{o.dU() +return!1}}, +hX(a,b,c){var s,r,q,p,o,n,m,l,k=this +if(b!==c&&(a.charCodeAt(c-1)&64512)===55296)--c +for(s=k.c,r=s.$flags|0,q=s.length,p=b;p=q)break +k.b=n+1 +r&2&&A.D(s) +s[n]=o}else{n=o&64512 +if(n===55296){if(k.b+4>q)break +m=p+1 +if(k.iM(o,a.charCodeAt(m)))p=m}else if(n===56320){if(k.b+3>q)break +k.dU()}else if(o<=2047){n=k.b +l=n+1 +if(l>=q)break +k.b=l +r&2&&A.D(s) +s[n]=o>>>6|192 +k.b=l+1 +s[l]=o&63|128}else{n=k.b +if(n+2>=q)break +l=k.b=n+1 +r&2&&A.D(s) +s[n]=o>>>12|224 +n=k.b=l+1 +s[l]=o>>>6&63|128 +k.b=n+1 +s[n]=o&63|128}}}return p}} +A.rP.prototype={ +n(){if(this.a!==0){this.aa("",0,0,!0) +return}var s=this.d.a.a +if((s.e&2)!==0)A.p(A.u("Stream is already closed")) +s.aA()}, +aa(a,b,c,d){var s,r,q,p,o,n=this +n.b=0 +s=b===c +if(s&&!d)return +r=n.a +if(r!==0){if(n.iM(r,!s?a.charCodeAt(b):0))++b +n.a=0}s=n.d +r=n.c +q=c-1 +p=r.length-3 +do{b=n.hX(a,b,c) +o=d&&b===c +if(b===q&&(a.charCodeAt(b)&64512)===55296){if(d&&n.b=15){p=m.a +o=A.By(p,r,b,l) +if(o!=null){if(!p)return o +if(o.indexOf("\ufffd")<0)return o}}o=m.fc(r,b,l,d) +p=m.b +if((p&1)!==0){n=A.xc(p) +m.b=0 +throw A.a(A.ai(n,a,q+m.c))}return o}, +fc(a,b,c,d){var s,r,q=this +if(c-b>1000){s=B.b.M(b+c,2) +r=q.fc(a,b,s,!1) +if((q.b&1)!==0)return r +return r+q.fc(a,s,c,d)}return q.mM(a,b,c,d)}, +nq(a){var s,r=this.b +this.b=0 +if(r<=32)return +if(this.a){s=A.aQ(65533) +a.a+=s}else throw A.a(A.ai(A.xc(77),null,null))}, +mM(a,b,c,d){var s,r,q,p,o,n,m,l=this,k=65533,j=l.b,i=l.c,h=new A.X(""),g=b+1,f=a[b] +A:for(s=l.a;;){for(;;g=p){r="AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFFFFFFFFFFFFFFFFGGGGGGGGGGGGGGGGHHHHHHHHHHHHHHHHHHHHHHHHHHHIHHHJEEBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBKCCCCCCCCCCCCDCLONNNMEEEEEEEEEEE".charCodeAt(f)&31 +i=j<=32?f&61694>>>r:(f&63|i<<6)>>>0 +j=" \x000:XECCCCCN:lDb \x000:XECCCCCNvlDb \x000:XECCCCCN:lDb AAAAA\x00\x00\x00\x00\x00AAAAA00000AAAAA:::::AAAAAGG000AAAAA00KKKAAAAAG::::AAAAA:IIIIAAAAA000\x800AAAAA\x00\x00\x00\x00 AAAAA".charCodeAt(j+r) +if(j===0){q=A.aQ(i) +h.a+=q +if(g===c)break A +break}else if((j&1)!==0){if(s)switch(j){case 69:case 67:q=A.aQ(k) +h.a+=q +break +case 65:q=A.aQ(k) +h.a+=q;--g +break +default:q=A.aQ(k) +h.a=(h.a+=q)+q +break}else{l.b=j +l.c=g-1 +return""}j=0}if(g===c)break A +p=g+1 +f=a[g]}p=g+1 +f=a[g] +if(f<128){for(;;){if(!(p=128){o=n-1 +p=n +break}p=n}if(o-g<20)for(m=g;m32)if(s){s=A.aQ(k) +h.a+=s}else{l.b=77 +l.c=c +return""}l.b=j +l.c=i +s=h.a +return s.charCodeAt(0)==0?s:s}} +A.kB.prototype={} +A.aC.prototype={ +br(a){var s,r,q=this,p=q.c +if(p===0)return q +s=!q.a +r=q.b +p=A.bk(p,r) +return new A.aC(p===0?!1:s,r,p)}, +kW(a){var s,r,q,p,o,n,m,l=this,k=l.c +if(k===0)return $.cf() +s=k-a +if(s<=0)return l.a?$.vs():$.cf() +r=l.b +q=new Uint16Array(s) +for(p=a;p>>0!==0)return l.eT(0,$.kM()) +for(k=0;k=0)return q.dH(b,r) +return b.dH(q,!r)}, +eT(a,b){var s,r,q=this,p=q.c +if(p===0)return b.br(0) +s=b.c +if(s===0)return q +r=q.a +if(r!==b.a)return q.eV(b,r) +if(A.pY(q.b,p,b.b,s)>=0)return q.dH(b,r) +return b.dH(q,!r)}, +aK(a,b){var s,r,q,p,o,n,m,l=this.c,k=b.c +if(l===0||k===0)return $.cf() +s=l+k +r=this.b +q=b.b +p=new Uint16Array(s) +for(o=0;o0?p.br(0):p}, +lN(a){var s,r,q,p=this +if(p.c0)q=q.cM(0,$.uQ.aW()) +return p.a&&q.c>0?q.br(0):q}, +hR(a){var s,r,q,p,o,n,m,l,k,j,i,h,g,f,e,d,c=this,b=c.c +if(b===$.wC&&a.c===$.wE&&c.b===$.wB&&a.b===$.wD)return +s=a.b +r=a.c +q=16-B.b.giR(s[r-1]) +if(q>0){p=new Uint16Array(r+5) +o=A.wA(s,r,q,p) +n=new Uint16Array(b+5) +m=A.wA(c.b,b,q,n)}else{n=A.uR(c.b,0,b,b+2) +o=r +p=s +m=b}l=p[o-1] +k=m-o +j=new Uint16Array(m) +i=A.uS(p,o,k,j) +h=m+1 +g=n.$flags|0 +if(A.pY(n,m,j,i)>=0){g&2&&A.D(n) +n[m]=1 +A.jF(n,h,j,i,n)}else{g&2&&A.D(n) +n[m]=0}f=new Uint16Array(o+2) +f[o]=1 +A.jF(f,o+1,p,o,f) +e=m-1 +while(k>0){d=A.AM(l,n,e);--k +A.wF(d,f,0,n,k,o) +if(n[e]1){q=$.vr() +if(q.c===0)A.p(B.aZ) +p=r.lN(q).j(0) +s.push(p) +o=p.length +if(o===1)s.push("000") +if(o===2)s.push("00") +if(o===3)s.push("0") +r=r.kV(q)}s.push(B.b.j(r.b[0])) +if(m)s.push("-") +return new A.cV(s,t.hF).nR(0)}, +$ia7:1} +A.pZ.prototype={ +$2(a,b){a=a+b&536870911 +a=a+((a&524287)<<10)&536870911 +return a^a>>>6}, +$S:170} +A.q_.prototype={ +$1(a){a=a+((a&67108863)<<3)&536870911 +a^=a>>>11 +return a+((a&16383)<<15)&536870911}, +$S:67} +A.jR.prototype={ +iP(a,b,c){var s=this.a +if(s!=null)s.register(a,b,c)}, +iY(a){var s=this.a +if(s!=null)s.unregister(a)}} +A.aK.prototype={ +H(a,b){if(b==null)return!1 +return b instanceof A.aK&&this.a===b.a&&this.b===b.b&&this.c===b.c}, +gB(a){return A.bN(this.a,this.b,B.c,B.c,B.c,B.c,B.c,B.c,B.c,B.c)}, +S(a,b){var s=B.b.S(this.a,b.a) +if(s!==0)return s +return B.b.S(this.b,b.b)}, +j(a){var s=this,r=A.zg(A.wc(s)),q=A.i7(A.wa(s)),p=A.i7(A.w7(s)),o=A.i7(A.w8(s)),n=A.i7(A.w9(s)),m=A.i7(A.wb(s)),l=A.vO(A.A0(s)),k=s.b,j=k===0?"":A.vO(k) +k=r+"-"+q +if(s.c)return k+"-"+p+" "+o+":"+n+":"+m+"."+l+j+"Z" +else return k+"-"+p+" "+o+":"+n+":"+m+"."+l+j}, +$ia7:1} +A.b_.prototype={ +H(a,b){if(b==null)return!1 +return b instanceof A.b_&&this.a===b.a}, +gB(a){return B.b.gB(this.a)}, +S(a,b){return B.b.S(this.a,b.a)}, +j(a){var s,r,q,p,o,n=this.a,m=B.b.M(n,36e8),l=n%36e8 +if(n<0){m=0-m +n=0-l +s="-"}else{n=l +s=""}r=B.b.M(n,6e7) +n%=6e7 +q=r<10?"0":"" +p=B.b.M(n,1e6) +o=p<10?"0":"" +return s+m+":"+q+r+":"+o+p+"."+B.a.ob(B.b.j(n%1e6),6,"0")}, +$ia7:1} +A.qz.prototype={ +j(a){return this.av()}} +A.Z.prototype={ +gcf(){return A.A_(this)}} +A.hQ.prototype={ +j(a){var s=this.a +if(s!=null)return"Assertion failed: "+A.ib(s) +return"Assertion failed"}} +A.c5.prototype={} +A.a3.prototype={ +gfe(){return"Invalid argument"+(!this.a?"(s)":"")}, +gfd(){return""}, +j(a){var s=this,r=s.c,q=r==null?"":" ("+r+")",p=s.d,o=p==null?"":": "+A.o(p),n=s.gfe()+q+o +if(!s.a)return n +return n+s.gfd()+": "+A.ib(s.gha())}, +gha(){return this.b}} +A.e0.prototype={ +gha(){return this.b}, +gfe(){return"RangeError"}, +gfd(){var s,r=this.e,q=this.f +if(r==null)s=q!=null?": Not less than or equal to "+A.o(q):"" +else if(q==null)s=": Not greater than or equal to "+A.o(r) +else if(q>r)s=": Not in inclusive range "+A.o(r)+".."+A.o(q) +else s=qe.length +else s=!1 +if(s)f=null +if(f==null){if(e.length>78)e=B.a.t(e,0,75)+"..." +return g+"\n"+e}for(r=1,q=0,p=!1,o=0;o1?g+(" (at line "+r+", character "+(f-q+1)+")\n"):g+(" (at character "+(f+1)+")\n") +m=e.length +for(o=f;o78){k="..." +if(f-q<75){j=q+75 +i=q}else{if(m-f<75){i=m-75 +j=m +k=""}else{i=f-36 +j=f+36}l="..."}}else{j=m +i=q +k=""}return g+l+B.a.t(e,i,j)+k+"\n"+B.a.aK(" ",f-i+l.length)+"^\n"}else return f!=null?g+(" (at offset "+A.o(f)+")"):g}, +$iV:1, +gji(){return this.a}, +gdF(){return this.b}, +ga5(){return this.c}} +A.ij.prototype={ +gcf(){return null}, +j(a){return"IntegerDivisionByZeroException"}, +$iZ:1, +$iV:1} +A.m.prototype={ +d7(a,b){return A.ln(this,A.q(this).h("m.E"),b)}, +bm(a,b,c){return A.fl(this,b,A.q(this).h("m.E"),c)}, +T(a,b){var s +for(s=this.gv(this);s.l();)if(J.y(s.gp(),b))return!0 +return!1}, +bp(a,b){var s=A.q(this).h("m.E") +if(b)s=A.an(this,s) +else{s=A.an(this,s) +s.$flags=1 +s=s}return s}, +eB(a){return this.bp(0,!0)}, +gk(a){var s,r=this.gv(this) +for(s=0;r.l();)++s +return s}, +gG(a){return!this.gv(this).l()}, +gaQ(a){return!this.gG(this)}, +bK(a,b){return A.wq(this,b,A.q(this).h("m.E"))}, +aV(a,b){return A.wm(this,b,A.q(this).h("m.E"))}, +U(a,b){var s,r +A.aI(b,"index") +s=this.gv(this) +for(r=b;s.l();){if(r===0)return s.gp();--r}throw A.a(A.ih(b,b-r,this,null,"index"))}, +j(a){return A.zA(this,"(",")")}} +A.Q.prototype={ +j(a){return"MapEntry("+A.o(this.a)+": "+A.o(this.b)+")"}} +A.J.prototype={ +gB(a){return A.k.prototype.gB.call(this,0)}, +j(a){return"null"}} +A.k.prototype={$ik:1, +H(a,b){return this===b}, +gB(a){return A.fv(this)}, +j(a){return"Instance of '"+A.iO(this)+"'"}, +ga0(a){return A.tK(this)}, +toString(){return this.j(this)}} +A.kq.prototype={ +j(a){return""}, +$iae:1} +A.X.prototype={ +gk(a){return this.a.length}, +c8(a){var s=A.o(a) +this.a+=s}, +a1(a){var s=A.aQ(a) +this.a+=s}, +j(a){var s=this.a +return s.charCodeAt(0)==0?s:s}} +A.p1.prototype={ +$2(a,b){throw A.a(A.ai("Illegal IPv6 address, "+a,this.a,b))}, +$S:70} +A.hy.prototype={ +giC(){var s,r,q,p,o=this,n=o.w +if(n===$){s=o.a +r=s.length!==0?s+":":"" +q=o.c +p=q==null +if(!p||s==="file"){s=r+"//" +r=o.b +if(r.length!==0)s=s+r+"@" +if(!p)s+=q +r=o.d +if(r!=null)s=s+":"+A.o(r)}else s=r +s+=o.e +r=o.f +if(r!=null)s=s+"?"+r +r=o.r +if(r!=null)s=s+"#"+r +n=o.w=s.charCodeAt(0)==0?s:s}return n}, +god(){var s,r,q=this,p=q.x +if(p===$){s=q.e +if(s.length!==0&&s.charCodeAt(0)===47)s=B.a.X(s,1) +r=s.length===0?B.H:A.iA(new A.a8(A.v(s.split("/"),t.s),A.D3(),t.iZ),t.N) +q.x!==$&&A.vm() +p=q.x=r}return p}, +gB(a){var s,r=this,q=r.y +if(q===$){s=B.a.gB(r.giC()) +r.y!==$&&A.vm() +r.y=s +q=s}return q}, +ghq(){return this.b}, +gbE(){var s=this.c +if(s==null)return"" +if(B.a.I(s,"[")&&!B.a.P(s,"v",1))return B.a.t(s,1,s.length-1) +return s}, +gdr(){var s=this.d +return s==null?A.x0(this.a):s}, +gdt(){var s=this.f +return s==null?"":s}, +gei(){var s=this.r +return s==null?"":s}, +em(a){var s=this.a +if(a.length!==s.length)return!1 +return A.xk(a,s,0)>=0}, +jt(a){var s,r,q,p,o,n,m,l=this +a=A.v_(a,0,a.length) +s=a==="file" +r=l.b +q=l.d +if(a!==l.a)q=A.rM(q,a) +p=l.c +if(!(p!=null))p=r.length!==0||q!=null||s?"":null +o=l.e +if(!s)n=p!=null&&o.length!==0 +else n=!0 +if(n&&!B.a.I(o,"/"))o="/"+o +m=o +return A.hz(a,r,p,q,m,l.f,l.r)}, +gjg(){if(this.a!==""){var s=this.r +s=(s==null?"":s)===""}else s=!1 +return s}, +ia(a,b){var s,r,q,p,o,n,m +for(s=0,r=0;B.a.P(b,"../",r);){r+=3;++s}q=B.a.cu(a,"/") +for(;;){if(!(q>0&&s>0))break +p=B.a.en(a,"/",q-1) +if(p<0)break +o=q-p +n=o!==2 +m=!1 +if(!n||o===3)if(a.charCodeAt(p+1)===46)n=!n||a.charCodeAt(p+2)===46 +else n=m +else n=m +if(n)break;--s +q=p}return B.a.c2(a,q+1,null,B.a.X(b,r-3*s))}, +ey(a){return this.du(A.d3(a))}, +du(a){var s,r,q,p,o,n,m,l,k,j,i,h=this +if(a.gaz().length!==0)return a +else{s=h.a +if(a.gh5()){r=a.jt(s) +return r}else{q=h.b +p=h.c +o=h.d +n=h.e +if(a.gja())m=a.gek()?a.gdt():h.f +else{l=A.Bx(h,n) +if(l>0){k=B.a.t(n,0,l) +n=a.gh4()?k+A.dn(a.gaT()):k+A.dn(h.ia(B.a.X(n,k.length),a.gaT()))}else if(a.gh4())n=A.dn(a.gaT()) +else if(n.length===0)if(p==null)n=s.length===0?a.gaT():A.dn(a.gaT()) +else n=A.dn("/"+a.gaT()) +else{j=h.ia(n,a.gaT()) +r=s.length===0 +if(!r||p!=null||B.a.I(n,"/"))n=A.dn(j) +else n=A.v1(j,!r||p!=null)}m=a.gek()?a.gdt():null}}}i=a.gh6()?a.gei():null +return A.hz(s,q,p,o,n,m,i)}, +gh5(){return this.c!=null}, +gek(){return this.f!=null}, +gh6(){return this.r!=null}, +gja(){return this.e.length===0}, +gh4(){return B.a.I(this.e,"/")}, +ho(){var s,r=this,q=r.a +if(q!==""&&q!=="file")throw A.a(A.R("Cannot extract a file path from a "+q+" URI")) +q=r.f +if((q==null?"":q)!=="")throw A.a(A.R(u.z)) +q=r.r +if((q==null?"":q)!=="")throw A.a(A.R(u.A)) +if(r.c!=null&&r.gbE()!=="")A.p(A.R(u.Q)) +s=r.god() +A.Bs(s,!1) +q=A.uE(B.a.I(r.e,"/")?"/":"",s,"/") +q=q.charCodeAt(0)==0?q:q +return q}, +j(a){return this.giC()}, +H(a,b){var s,r,q,p=this +if(b==null)return!1 +if(p===b)return!0 +s=!1 +if(t.w.b(b))if(p.a===b.gaz())if(p.c!=null===b.gh5())if(p.b===b.ghq())if(p.gbE()===b.gbE())if(p.gdr()===b.gdr())if(p.e===b.gaT()){r=p.f +q=r==null +if(!q===b.gek()){if(q)r="" +if(r===b.gdt()){r=p.r +q=r==null +if(!q===b.gh6()){s=q?"":r +s=s===b.gei()}}}}return s}, +$ijo:1, +gaz(){return this.a}, +gaT(){return this.e}} +A.p0.prototype={ +gjy(){var s,r,q,p,o=this,n=null,m=o.c +if(m==null){m=o.a +s=o.b[0]+1 +r=B.a.bj(m,"?",s) +q=m.length +if(r>=0){p=A.hA(m,r+1,q,256,!1,!1) +q=r}else p=n +m=o.c=new A.jN("data","",n,n,A.hA(m,s,q,128,!1,!1),p,n)}return m}, +j(a){var s=this.a +return this.b[0]===-1?"data:"+s:s}} +A.bn.prototype={ +gh5(){return this.c>0}, +gh7(){return this.c>0&&this.d+10&&this.r>=this.a.length}, +em(a){var s=a.length +if(s===0)return this.b<0 +if(s!==this.b)return!1 +return A.xk(a,this.a,0)>=0}, +gaz(){var s=this.w +return s==null?this.w=this.kS():s}, +kS(){var s,r=this,q=r.b +if(q<=0)return"" +s=q===4 +if(s&&B.a.I(r.a,"http"))return"http" +if(q===5&&B.a.I(r.a,"https"))return"https" +if(s&&B.a.I(r.a,"file"))return"file" +if(q===7&&B.a.I(r.a,"package"))return"package" +return B.a.t(r.a,0,q)}, +ghq(){var s=this.c,r=this.b+3 +return s>r?B.a.t(this.a,r,s-1):""}, +gbE(){var s=this.c +return s>0?B.a.t(this.a,s,this.d):""}, +gdr(){var s,r=this +if(r.gh7())return A.xZ(B.a.t(r.a,r.d+1,r.e)) +s=r.b +if(s===4&&B.a.I(r.a,"http"))return 80 +if(s===5&&B.a.I(r.a,"https"))return 443 +return 0}, +gaT(){return B.a.t(this.a,this.e,this.f)}, +gdt(){var s=this.f,r=this.r +return s=q.length)return s +return new A.bn(B.a.t(q,0,r),s.b,s.c,s.d,s.e,s.f,r,s.w)}, +jt(a){var s,r,q,p,o,n,m,l,k,j,i,h=this,g=null +a=A.v_(a,0,a.length) +s=!(h.b===a.length&&B.a.I(h.a,a)) +r=a==="file" +q=h.c +p=q>0?B.a.t(h.a,h.b+3,q):"" +o=h.gh7()?h.gdr():g +if(s)o=A.rM(o,a) +q=h.c +if(q>0)n=B.a.t(h.a,q,h.d) +else n=p.length!==0||o!=null||r?"":g +q=h.a +m=h.f +l=B.a.t(q,h.e,m) +if(!r)k=n!=null&&l.length!==0 +else k=!0 +if(k&&!B.a.I(l,"/"))l="/"+l +k=h.r +j=m0)return b +s=b.c +if(s>0){r=a.b +if(r<=0)return b +q=r===4 +if(q&&B.a.I(a.a,"file"))p=b.e!==b.f +else if(q&&B.a.I(a.a,"http"))p=!b.i6("80") +else p=!(r===5&&B.a.I(a.a,"https"))||!b.i6("443") +if(p){o=r+1 +return new A.bn(B.a.t(a.a,0,o)+B.a.X(b.a,c+1),r,s+o,b.d+o,b.e+o,b.f+o,b.r+o,a.w)}else return this.iE().du(b)}n=b.e +c=b.f +if(n===c){s=b.r +if(c0?l:m +o=k-n +return new A.bn(B.a.t(a.a,0,k)+B.a.X(s,n),a.b,a.c,a.d,m,c+o,b.r+o,a.w)}j=a.e +i=a.f +if(j===i&&a.c>0){while(B.a.P(s,"../",n))n+=3 +o=j-n+1 +return new A.bn(B.a.t(a.a,0,j)+"/"+B.a.X(s,n),a.b,a.c,a.d,j,c+o,b.r+o,a.w)}h=a.a +l=A.wU(this) +if(l>=0)g=l +else for(g=j;B.a.P(h,"../",g);)g+=3 +f=0 +for(;;){e=n+3 +if(!(e<=c&&B.a.P(s,"../",n)))break;++f +n=e}for(d="";i>g;){--i +if(h.charCodeAt(i)===47){if(f===0){d="/" +break}--f +d="/"}}if(i===g&&a.b<=0&&!B.a.P(h,"/",j)){n-=f*3 +d=""}o=i-n+d.length +return new A.bn(B.a.t(h,0,i)+d+B.a.X(s,n),a.b,a.c,a.d,j,c+o,b.r+o,a.w)}, +ho(){var s,r=this,q=r.b +if(q>=0){s=!(q===4&&B.a.I(r.a,"file")) +q=s}else q=!1 +if(q)throw A.a(A.R("Cannot extract a file path from a "+r.gaz()+" URI")) +q=r.f +s=r.a +if(q0?s.gbE():r,n=s.gh7()?s.gdr():r,m=s.a,l=s.f,k=B.a.t(m,s.e,l),j=s.r +l=l4294967296)throw A.a(A.aA(u.E+a)) +return Math.random()*a>>>0}} +A.qX.prototype={ +ky(){var s=self.crypto +if(s!=null)if(s.getRandomValues!=null)return +throw A.a(A.R("No source of cryptographically secure random numbers available."))}, +er(a){var s,r,q,p,o,n,m,l +if(a<=0||a>4294967296)throw A.a(A.aA(u.E+a)) +if(a>255)if(a>65535)s=a>16777215?4:3 +else s=2 +else s=1 +r=this.a +r.$flags&2&&A.D(r,11) +r.setUint32(0,0,!1) +q=4-s +p=A.S(Math.pow(256,s)) +for(o=a-1,n=(a&o)===0;;){crypto.getRandomValues(J.cg(B.ad.gaG(r),q,s)) +m=r.getUint32(0,!1) +if(n)return(m&o)>>>0 +l=m%a +if(m-l+a"))}} +A.nZ.prototype={ +$0(){return this.a.cX().u()}, +$S:3} +A.o_.prototype={ +$1(a){var s,r,q,p +try{this.b.q(0,this.a.$ti.y[1].a(a))}catch(q){p=A.H(q) +if(t.do.b(p)){s=p +r=A.N(q) +this.b.a2(s,r)}else throw q}}, +$S(){return this.a.$ti.h("~(1)")}} +A.fG.prototype={ +q(a,b){var s,r=this +if(r.b)throw A.a(A.u("Can't add a Stream to a closed StreamGroup.")) +s=r.c +if(s===B.aQ)r.e.cB(b,new A.oa()) +else if(s===B.aP)return b.Z(null).u() +else r.e.cB(b,new A.ob(r,b)) +return null}, +lw(){var s,r,q,p,o,n,m,l=this +l.c=B.aR +r=l.e +q=A.an(new A.az(r,A.q(r).h("az<1,2>")),l.$ti.h("Q,ak<1>?>")) +p=q.length +o=0 +for(;o") +q=t.bC +p=A.an(new A.fs(A.fl(new A.az(s,r),new A.o8(this),r.h("m.E"),t.m2),q),q.h("m.E")) +s.bA(0) +return p.length===0?null:A.f5(p,t.H)}, +i8(a){var s,r=this.a +r===$&&A.B() +s=a.aj(r.gd4(r),new A.o7(this,a),r.gd5()) +if(this.c===B.aS)s.ak() +return s}} +A.oa.prototype={ +$0(){return null}, +$S:1} +A.ob.prototype={ +$0(){return this.a.i8(this.b)}, +$S(){return this.a.$ti.h("ak<1>()")}} +A.o9.prototype={ +$1(a){}, +$S:8} +A.o8.prototype={ +$1(a){var s,r,q=a.b +try{if(q!=null){s=q.u() +return s}s=a.a.Z(null).u() +return s}catch(r){return null}}, +$S(){return this.a.$ti.h("r<~>?(Q,ak<1>?>)")}} +A.o7.prototype={ +$0(){var s=this.a,r=s.e,q=r.E(0,this.b),p=q==null?null:q.u() +if(r.a===0)if(s.b){s=s.a +s===$&&A.B() +A.eM(s.gag())}return p}, +$S:0} +A.et.prototype={ +j(a){return this.a}} +A.T.prototype={ +i(a,b){var s,r=this +if(!r.fu(b))return null +s=r.c.i(0,r.a.$1(r.$ti.h("T.K").a(b))) +return s==null?null:s.b}, +m(a,b,c){var s=this +if(!s.fu(b))return +s.c.m(0,s.a.$1(b),new A.Q(b,c,s.$ti.h("Q")))}, +a8(a,b){b.a4(0,new A.li(this))}, +F(a){var s=this +if(!s.fu(a))return!1 +return s.c.F(s.a.$1(s.$ti.h("T.K").a(a)))}, +gbZ(){var s=this.c,r=A.q(s).h("az<1,2>") +return A.fl(new A.az(s,r),new A.lj(this),r.h("m.E"),this.$ti.h("Q"))}, +a4(a,b){this.c.a4(0,new A.lk(this,b))}, +gG(a){return this.c.a===0}, +ga6(){var s=this.c,r=A.q(s).h("bf<2>") +return A.fl(new A.bf(s,r),new A.ll(this),r.h("m.E"),this.$ti.h("T.K"))}, +gk(a){return this.c.a}, +cw(a,b,c,d){return this.c.cw(0,new A.lm(this,b,c,d),c,d)}, +j(a){return A.nj(this)}, +fu(a){return this.$ti.h("T.K").b(a)}, +$ia_:1} +A.li.prototype={ +$2(a,b){this.a.m(0,a,b) +return b}, +$S(){return this.a.$ti.h("~(T.K,T.V)")}} +A.lj.prototype={ +$1(a){var s=a.b +return new A.Q(s.a,s.b,this.a.$ti.h("Q"))}, +$S(){return this.a.$ti.h("Q(Q>)")}} +A.lk.prototype={ +$2(a,b){return this.b.$2(b.a,b.b)}, +$S(){return this.a.$ti.h("~(T.C,Q)")}} +A.ll.prototype={ +$1(a){return a.a}, +$S(){return this.a.$ti.h("T.K(Q)")}} +A.lm.prototype={ +$2(a,b){return this.b.$2(b.a,b.b)}, +$S(){return this.a.$ti.J(this.c).J(this.d).h("Q<1,2>(T.C,Q)")}} +A.eX.prototype={ +aP(a,b){return J.y(a,b)}, +c_(a){return J.z(a)}, +nQ(a){return!0}} +A.iz.prototype={ +aP(a,b){var s,r,q,p +if(a==null?b==null:a===b)return!0 +if(a==null||b==null)return!1 +s=J.a2(a) +r=s.gk(a) +q=J.a2(b) +if(r!==q.gk(b))return!1 +for(p=0;p>>0)&2147483647 +r^=r>>>6}r=r+(r<<3>>>0)&2147483647 +r^=r>>>11 +return r+(r<<15>>>0)&2147483647}} +A.ey.prototype={ +aP(a,b){var s,r,q,p,o +if(a===b)return!0 +s=A.mC(B.D.gng(),B.D.gnJ(),B.D.gnP(),this.$ti.h("ey.E"),t.S) +for(r=a.gv(a),q=0;r.l();){p=r.gp() +o=s.i(0,p) +s.m(0,p,(o==null?0:o)+1);++q}for(r=b.gv(b);r.l();){p=r.gp() +o=s.i(0,p) +if(o==null||o===0)return!1 +s.m(0,p,o-1);--q}return q===0}} +A.cW.prototype={} +A.em.prototype={ +gB(a){return 3*J.z(this.b)+7*J.z(this.c)&2147483647}, +H(a,b){if(b==null)return!1 +return b instanceof A.em&&J.y(this.b,b.b)&&J.y(this.c,b.c)}} +A.dU.prototype={ +aP(a,b){var s,r,q,p,o +if(a==b)return!0 +if(a==null||b==null)return!1 +if(a.gk(a)!==b.gk(b))return!1 +s=A.mC(null,null,null,t.fA,t.S) +for(r=J.U(a.ga6());r.l();){q=r.gp() +p=new A.em(this,q,a.i(0,q)) +o=s.i(0,p) +s.m(0,p,(o==null?0:o)+1)}for(r=J.U(b.ga6());r.l();){q=r.gp() +p=new A.em(this,q,b.i(0,q)) +o=s.i(0,p) +if(o==null||o===0)return!1 +s.m(0,p,o-1)}return!0}, +c_(a){var s,r,q,p,o,n +if(a==null)return B.a5.gB(null) +for(s=J.U(a.ga6()),r=this.$ti.y[1],q=0;s.l();){p=s.gp() +o=J.z(p) +n=a.i(0,p) +q=q+3*o+7*J.z(n==null?r.a(n):n)&2147483647}q=q+(q<<3>>>0)&2147483647 +q^=q>>>11 +return q+(q<<15>>>0)&2147483647}} +A.iH.prototype={ +sk(a,b){A.w3()}, +q(a,b){return A.w3()}} +A.jl.prototype={} +A.kV.prototype={} +A.fw.prototype={} +A.l8.prototype={ +dR(a,b,c){return this.lX(a,b,c)}, +lX(a,b,c){var s=0,r=A.j(t.cD),q,p=this,o,n +var $async$dR=A.e(function(d,e){if(d===1)return A.f(e,r) +for(;;)switch(s){case 0:o=A.A7(a,b) +o.r.a8(0,c) +n=A +s=3 +return A.c(p.cd(o),$async$dR) +case 3:q=n.nT(e) +s=1 +break +case 1:return A.h(q,r)}}) +return A.i($async$dR,r)}} +A.hV.prototype={ +nn(){if(this.w)throw A.a(A.u("Can't finalize a finalized Request.")) +this.w=!0 +return B.aU}, +j(a){return this.a+" "+this.b.j(0)}} +A.hW.prototype={ +$2(a,b){return a.toLowerCase()===b.toLowerCase()}, +$S:96} +A.hX.prototype={ +$1(a){return B.a.gB(a.toLowerCase())}, +$S:98} +A.l9.prototype={ +hw(a,b,c,d,e,f,g){var s=this.b +if(s<100)throw A.a(A.K("Invalid status code "+s+".",null)) +else{s=this.d +if(s!=null&&s<0)throw A.a(A.K("Invalid content length "+A.o(s)+".",null))}}} +A.la.prototype={ +cd(a){return this.k6(a)}, +k6(b6){var s=0,r=A.j(t.hL),q,p=2,o=[],n=[],m=this,l,k,j,i,h,g,f,e,d,c,b,a,a0,a1,a2,a3,a4,a5,a6,a7,a8,a9,b0,b1,b2,b3,b4,b5 +var $async$cd=A.e(function(b7,b8){if(b7===1){o.push(b8) +s=p}for(;;)switch(s){case 0:if(m.b)throw A.a(A.vJ("HTTP request failed. Client is already closed.",b6.b)) +a4=v.G +l=new a4.AbortController() +a5=m.c +a5.push(l) +b6.ka() +a6=t.oU +a7=new A.bT(null,null,null,null,a6) +a7.af(b6.y) +a7.hH() +s=3 +return A.c(new A.dF(new A.O(a7,a6.h("O<1>"))).jw(),$async$cd) +case 3:k=b8 +p=5 +j=b6 +i=null +h=!1 +g=null +if(j instanceof A.hL){if(h)a6=i +else{h=!0 +a8=j.cx +i=a8 +a6=a8}a6=a6!=null}else a6=!1 +if(a6){if(h){a6=i +a9=a6}else{h=!0 +a8=j.cx +i=a8 +a9=a8}g=a9==null?t.p8.a(a9):a9 +g.O(new A.lb(l))}a6=b6.b +b0=a6.j(0) +a7=!J.kS(k)?k:null +b1=t.N +f=A.P(b1,t.K) +e=b6.y.length +d=null +if(e!=null){d=e +J.kQ(f,"content-length",d)}for(b2=b6.r,b2=new A.az(b2,A.q(b2).h("az<1,2>")).gv(0);b2.l();){b3=b2.d +b3.toString +c=b3 +J.kQ(f,c.a,c.b)}f=A.vi(f) +f.toString +A.a4(f) +b2=l.signal +s=8 +return A.c(A.ac(a4.fetch(b0,{method:b6.a,headers:f,body:a7,credentials:"same-origin",redirect:"follow",signal:b2}),t.m),$async$cd) +case 8:b=b8 +a=b.headers.get("content-length") +a0=a!=null?A.uz(a,null):null +if(a0==null&&a!=null){f=A.vJ("Invalid content-length header ["+a+"].",a6) +throw A.a(f)}a1=A.P(b1,b1) +b.headers.forEach(A.t8(new A.lc(a1))) +f=A.BE(b6,b) +a4=b.status +a6=a1 +a7=a0 +A.d3(b.url) +b1=b.statusText +f=new A.jd(A.DK(f),b6,a4,b1,a7,a6,!1,!0) +f.hw(a4,a7,a6,!1,!0,b1,b6) +q=f +n=[1] +s=6 +break +n.push(7) +s=6 +break +case 5:p=4 +b5=o.pop() +a2=A.H(b5) +a3=A.N(b5) +A.xC(a2,a3,b6) +n.push(7) +s=6 +break +case 4:n=[2] +case 6:p=2 +B.d.E(a5,l) +s=n.pop() +break +case 7:case 1:return A.h(q,r) +case 2:return A.f(o.at(-1),r)}}) +return A.i($async$cd,r)}, +n(){var s,r,q +for(s=this.c,r=s.length,q=0;q=q.gnT().b){if((d==null||d===B.r)&&p>=2000){d=A.fD() +if(c==null)c="autogenerated stack trace for "+a.j(0)+" "+b}p=q.gj4() +s=Date.now() +$.w0=$.w0+1 +r=new A.dS(a,b,p,new A.aK(s,0,!1),c,d) +if(q.b==null)q.ii(r) +else $.ud().ii(r)}}, +o3(a,b){return this.a_(a,b,null,null)}, +fh(){if(this.b==null){var s=this.f +if(s==null)s=this.f=A.cY(!0,t.ag) +return new A.aJ(s,A.q(s).h("aJ<1>"))}else return $.ud().fh()}, +ii(a){var s=this.f +return s==null?null:s.q(0,a)}} +A.nh.prototype={ +$0(){var s,r,q=this.a +if(B.a.I(q,"."))A.p(A.K("name shouldn't start with a '.'",null)) +if(B.a.bC(q,"."))A.p(A.K("name shouldn't end with a '.'",null)) +s=B.a.cu(q,".") +if(s===-1)r=q!==""?A.uy(""):null +else{r=A.uy(B.a.t(q,0,s)) +q=B.a.X(q,s+1)}return A.w1(q,r,A.P(t.N,t.Y))}, +$S:123} +A.i3.prototype={ +bh(a){var s,r,q=t.mf +A.xN("absolute",A.v([a,null,null,null,null,null,null,null,null,null,null,null,null,null,null],q)) +s=this.a +s=s.a9(a)>0&&!s.aR(a) +if(s)return a +s=this.b +r=A.v([s==null?A.xU():s,a,null,null,null,null,null,null,null,null,null,null,null,null,null,null],q) +A.xN("join",r) +return this.nS(new A.fW(r,t.lS))}, +nS(a){var s,r,q,p,o,n,m,l,k +for(s=a.gv(0),r=new A.fV(s,new A.lC()),q=this.a,p=!1,o=!1,n="";r.l();){m=s.gp() +if(q.aR(m)&&o){l=A.iM(m,q) +k=n.charCodeAt(0)==0?n:n +n=B.a.t(k,0,q.cF(k,!0)) +l.b=n +if(q.dn(n))l.e[0]=q.gce() +n=l.j(0)}else if(q.a9(m)>0){o=!q.aR(m) +n=m}else{if(!(m.length!==0&&q.fU(m[0])))if(p)n+=q.gce() +n+=m}p=q.dn(m)}return n.charCodeAt(0)==0?n:n}, +cO(a,b){var s=A.iM(b,this.a),r=s.d,q=A.a1(r).h("d5<1>") +r=A.an(new A.d5(r,new A.lD(),q),q.h("m.E")) +s.d=r +q=s.b +if(q!=null)B.d.nO(r,0,q) +return s.d}, +cA(a){var s +if(!this.lm(a))return a +s=A.iM(a,this.a) +s.hd() +return s.j(0)}, +lm(a){var s,r,q,p,o,n,m,l=this.a,k=l.a9(a) +if(k!==0){if(l===$.kL())for(s=0;s0)return o.cA(a) +if(m.a9(a)<=0||m.aR(a))a=o.bh(a) +if(m.a9(a)<=0&&m.a9(b)>0)throw A.a(A.w4(n+a+'" from "'+b+'".')) +s=A.iM(b,m) +s.hd() +r=A.iM(a,m) +r.hd() +q=s.d +if(q.length!==0&&q[0]===".")return r.j(0) +q=s.b +p=r.b +if(q!=p)q=q==null||p==null||!m.hg(q,p) +else q=!1 +if(q)return r.j(0) +for(;;){q=s.d +if(q.length!==0){p=r.d +q=p.length!==0&&m.hg(q[0],p[0])}else q=!1 +if(!q)break +B.d.ew(s.d,0) +B.d.ew(s.e,1) +B.d.ew(r.d,0) +B.d.ew(r.e,1)}q=s.d +p=q.length +if(p!==0&&q[0]==="..")throw A.a(A.w4(n+a+'" from "'+b+'".')) +q=t.N +B.d.h8(r.d,0,A.aW(p,"..",!1,q)) +p=r.e +p[0]="" +B.d.h8(p,1,A.aW(s.d.length,m.gce(),!1,q)) +m=r.d +q=m.length +if(q===0)return"." +if(q>1&&B.d.gaS(m)==="."){B.d.jr(r.d) +m=r.e +m.pop() +m.pop() +m.push("")}r.b="" +r.js() +return r.j(0)}, +oh(a){return this.hk(a,null)}, +lg(a,b){var s,r,q,p,o,n,m,l,k=this +a=a +b=b +r=k.a +q=r.a9(a)>0 +p=r.a9(b)>0 +if(q&&!p){b=k.bh(b) +if(r.aR(a))a=k.bh(a)}else if(p&&!q){a=k.bh(a) +if(r.aR(b))b=k.bh(b)}else if(p&&q){o=r.aR(b) +n=r.aR(a) +if(o&&!n)b=k.bh(b) +else if(n&&!o)a=k.bh(a)}m=k.lh(a,b) +if(m!==B.t)return m +s=null +try{s=k.hk(b,a)}catch(l){if(A.H(l) instanceof A.fu)return B.q +else throw l}if(r.a9(s)>0)return B.q +if(J.y(s,"."))return B.W +if(J.y(s,".."))return B.q +return J.ay(s)>=3&&J.yV(s,"..")&&r.N(J.yP(s,2))?B.q:B.X}, +lh(a,b){var s,r,q,p,o,n,m,l,k,j,i,h,g,f,e=this +if(a===".")a="" +s=e.a +r=s.a9(a) +q=s.a9(b) +if(r!==q)return B.q +for(p=0;pq.cO(0,s).length?s:r}} +A.lC.prototype={ +$1(a){return a!==""}, +$S:53} +A.lD.prototype={ +$1(a){return a.length!==0}, +$S:53} +A.tu.prototype={ +$1(a){return a==null?"null":'"'+a+'"'}, +$S:129} +A.ep.prototype={ +j(a){return this.a}} +A.eq.prototype={ +j(a){return this.a}} +A.n5.prototype={ +jX(a){var s=this.a9(a) +if(s>0)return B.a.t(a,0,s) +return this.aR(a)?a[0]:null}, +ec(a,b){return a===b}, +hg(a,b){return a===b}} +A.nu.prototype={ +js(){var s,r,q=this +for(;;){s=q.d +if(!(s.length!==0&&B.d.gaS(s)===""))break +B.d.jr(q.d) +q.e.pop()}s=q.e +r=s.length +if(r!==0)s[r-1]=""}, +hd(){var s,r,q,p,o,n=this,m=A.v([],t.s) +for(s=n.d,r=s.length,q=0,p=0;p0){s=B.a.bj(a,"\\",s+1) +if(s>0)return s}return r}if(r<3)return 0 +if(!A.y_(a.charCodeAt(0)))return 0 +if(a.charCodeAt(1)!==58)return 0 +r=a.charCodeAt(2) +if(!(r===47||r===92))return 0 +return 3}, +a9(a){return this.cF(a,!1)}, +aR(a){return this.a9(a)===1}, +hf(a){var s,r +if(a.gaz()!==""&&a.gaz()!=="file")throw A.a(A.K("Uri "+a.j(0)+" must have scheme 'file:'.",null)) +s=a.gaT() +if(a.gbE()===""){r=s.length +if(r>=3&&B.a.I(s,"/")&&A.xW(s,1)!=null){A.wf(0,0,r,"startIndex") +s=A.DI(s,"/","",0)}}else s="\\\\"+a.gbE()+s +r=A.hG(s,"/","\\") +return A.v2(r,0,r.length,B.i,!1)}, +ec(a,b){var s +if(a===b)return!0 +if(a===47)return b===92 +if(a===92)return b===47 +if((a^b)!==32)return!1 +s=a|32 +return s>=97&&s<=122}, +hg(a,b){var s,r +if(a===b)return!0 +s=a.length +if(s!==b.length)return!1 +for(r=0;r"}} +A.eW.prototype={ +eA(){var s=this +return A.bJ(["op_id",s.a,"op",s.c.c,"type",s.d,"id",s.e,"tx_id",s.b,"data",s.r,"metadata",s.f,"old",s.w],t.N,t.z)}, +j(a){var s=this +return"CrudEntry<"+s.b+"/"+s.a+" "+s.c.c+" "+s.d+"/"+s.e+" "+A.o(s.r)+">"}, +H(a,b){var s=this +if(b==null)return!1 +return b instanceof A.eW&&b.b===s.b&&b.a===s.a&&b.c===s.c&&b.d===s.d&&b.e===s.e&&B.z.aP(b.r,s.r)}, +gB(a){var s=this +return A.bN(s.b,s.a,s.c.c,s.d,s.e,B.z.c_(s.r),B.c,B.c,B.c,B.c)}} +A.fQ.prototype={ +av(){return"UpdateType."+this.b}, +eA(){return this.c}} +A.u3.prototype={ +$1(a){return new A.bh(A.v5(a.a))}, +$S:131} +A.u2.prototype={ +$1(a){var s=a.a +return s.gaQ(s)}, +$S:133} +A.eV.prototype={ +j(a){return"CredentialsException: "+this.a}, +$iV:1} +A.e_.prototype={ +j(a){return"SyncProtocolException: "+this.a}, +$iV:1} +A.d_.prototype={ +j(a){return"SyncResponseException: "+this.a+" "+this.b}, +$iV:1} +A.ta.prototype={ +$1(a){var s +A.u4("["+a.d+"] "+a.a.a+": "+a.e.j(0)+": "+a.b) +s=a.r +if(s!=null)A.u4(s) +s=a.w +if(s!=null)A.u4(s)}, +$S:33} +A.bh.prototype={ +cG(a){var s=this.a +if(a instanceof A.bh)return new A.bh(s.cG(a.a)) +else return new A.bh(s.cG(A.v5(a.a)))}, +fT(a){return this.ki(A.v5(a))}} +A.ld.prototype={ +cc(a){return this.k0(a)}, +k0(a){var s=0,r=A.j(t.G),q,p=this +var $async$cc=A.e(function(b,c){if(b===1)return A.f(c,r) +for(;;)switch(s){case 0:s=3 +return A.c(p.a.ab(a,B.w),$async$cc) +case 3:q=c +s=1 +break +case 1:return A.h(q,r)}}) +return A.i($async$cc,r)}, +dC(){var s=0,r=A.j(t.N),q,p=this,o +var $async$dC=A.e(function(a,b){if(a===1)return A.f(b,r) +for(;;)switch(s){case 0:s=3 +return A.c(p.cc("SELECT powersync_client_id() as client_id"),$async$dC) +case 3:o=b +q=A.av(o.gai(o).i(0,"client_id")) +s=1 +break +case 1:return A.h(q,r)}}) +return A.i($async$dC,r)}, +c6(a){var s=0,r=A.j(t.y),q,p=this,o,n,m +var $async$c6=A.e(function(b,c){if(b===1)return A.f(c,r) +for(;;)switch(s){case 0:s=3 +return A.c(p.cc("SELECT CAST(target_op AS TEXT) FROM ps_buckets WHERE name = '$local' AND target_op = 9223372036854775807"),$async$c6) +case 3:if(c.gk(0)===0){q=!1 +s=1 +break}s=4 +return A.c(p.cc(u.B),$async$c6) +case 4:o=c +if(o.gk(0)===0){q=!1 +s=1 +break}n=A +m=A.S(o.gai(o).i(0,"seq")) +s=6 +return A.c(a.$0(),$async$c6) +case 6:s=5 +return A.c(p.eH(new n.lf(m,c),!0,t.y),$async$c6) +case 5:q=c +s=1 +break +case 1:return A.h(q,r)}}) +return A.i($async$c6,r)}, +eq(){var s=0,r=A.j(t.d_),q,p=this,o,n,m,l,k,j,i,h,g,f +var $async$eq=A.e(function(a,b){if(a===1)return A.f(b,r) +for(;;)switch(s){case 0:s=3 +return A.c(p.a.jT("SELECT * FROM ps_crud ORDER BY id ASC LIMIT 1"),$async$eq) +case 3:f=b +if(f==null)o=null +else{n=B.h.cn(A.av(f.i(0,"data")),null) +o=A.S(f.i(0,"id")) +m=J.a2(n) +l=A.Au(A.av(m.i(n,"op"))) +l.toString +k=A.av(m.i(n,"type")) +j=A.av(m.i(n,"id")) +i=A.S(f.i(0,"tx_id")) +h=t.h9 +g=h.a(m.i(n,"data")) +h=h.a(m.i(n,"old")) +h=new A.eW(o,i,l,k,j,A.xi(m.i(n,"metadata")),g,h) +o=h}q=o +s=1 +break +case 1:return A.h(q,r)}}) +return A.i($async$eq,r)}, +ed(a,b){return this.mI(a,b)}, +mI(a,b){var s=0,r=A.j(t.N),q,p=this +var $async$ed=A.e(function(c,d){if(c===1)return A.f(d,r) +for(;;)switch(s){case 0:s=3 +return A.c(p.eH(new A.le(a,b),!1,t.N),$async$ed) +case 3:q=d +s=1 +break +case 1:return A.h(q,r)}}) +return A.i($async$ed,r)}} +A.lf.prototype={ +$1(a){return this.jF(a)}, +jF(a){var s=0,r=A.j(t.y),q,p=this,o,n +var $async$$1=A.e(function(b,c){if(b===1)return A.f(c,r) +for(;;)switch(s){case 0:s=3 +return A.c(a.j0("SELECT 1 FROM ps_crud LIMIT 1"),$async$$1) +case 3:n=c +if(!n.gG(n)){q=!1 +s=1 +break}s=4 +return A.c(a.j0(u.B),$async$$1) +case 4:o=c +if(A.S(o.gai(o).i(0,"seq"))!==p.a){q=!1 +s=1 +break}s=5 +return A.c(a.ab("UPDATE ps_buckets SET target_op = CAST(? as INTEGER) WHERE name='$local'",[p.b]),$async$$1) +case 5:q=!0 +s=1 +break +case 1:return A.h(q,r)}}) +return A.i($async$$1,r)}, +$S:144} +A.le.prototype={ +$1(a){return this.jE(a)}, +jE(a){var s=0,r=A.j(t.N),q,p=this,o,n,m,l +var $async$$1=A.e(function(b,c){if(b===1)return A.f(c,r) +for(;;)switch(s){case 0:s=3 +return A.c(a.ab("SELECT powersync_control(?, ?)",[p.a,p.b]),$async$$1) +case 3:o=c +n=o.d +m=n.length===1 +l=m?new A.aX(o,A.iA(n[0],t.X)):null +if(!m)throw A.a(A.u("Pattern matching error")) +q=A.av(l.b[0]) +s=1 +break +case 1:return A.h(q,r)}}) +return A.i($async$$1,r)}, +$S:148} +A.fj.prototype={$iaD:1,$ibz:1} +A.dM.prototype={$iaD:1} +A.fP.prototype={$iaD:1,$ibz:1} +A.lF.prototype={} +A.lG.prototype={ +$1(a){return A.zc(t.f.a(a))}, +$S:55} +A.md.prototype={ +eA(){var s,r,q,p,o=t.N,n=A.P(o,t.dV) +for(s=this.a,s=new A.az(s,A.q(s).h("az<1,2>")).gv(0),r=t.S;s.l();){q=s.d +p=q.a +q=q.b.a +n.m(0,p,A.bJ(["priority",q[1],"at_last",q[0],"since_last",q[2],"target_count",q[3]],o,r))}return A.bJ(["buckets",n],o,t.X)}} +A.me.prototype={ +$2(a,b){var s +t.f.a(b) +s=A.S(b.i(0,"priority")) +return new A.Q(a,new A.ke([A.S(b.i(0,"at_last")),s,A.S(b.i(0,"since_last")),A.S(b.i(0,"target_count"))]),t.lx)}, +$S:56} +A.f1.prototype={$iaD:1,$ibz:1} +A.dH.prototype={$iaD:1} +A.f4.prototype={$iaD:1,$ibz:1} +A.eY.prototype={$iaD:1,$ibz:1} +A.fM.prototype={$iaD:1,$ibz:1} +A.q3.prototype={} +A.fo.prototype={ +mA(a){var s,r,q,p=this +p.a=a.a +p.b=a.b +s=a.d +r=s==null +p.c=!r +q=a.c +p.f=q +A:{if(r){s=null +break A}s=A.zx(s.a) +break A}p.e=s +q=A.zy(q,new A.np()) +p.w=q==null?null:q.b +p.r=a.e}} +A.np.prototype={ +$1(a){return a.c===2147483647}, +$S:54} +A.oz.prototype={ +c7(a){var s,r,q,p,o,n,m,l,k,j=this,i=j.a +a.$1(i) +s=j.c +if((s.c&4)!==0)return +r=i.a +q=i.b +p=i.c +o=i.d +n=i.e +if(n==null)n=null +m=i.f +l=i.w +k=new A.ct(r,q,p,n,o,l,null,i.x,i.y,new A.d2(m,t.ph),i.r) +if(!k.H(0,j.b)){s.q(0,k) +j.b=k}}} +A.fJ.prototype={} +A.jg.prototype={ +av(){return"SyncClientImplementation."+this.b}} +A.dK.prototype={ +eA(){var s,r,q,p,o=this,n=o.d,m=t.N +n=A.bJ(["total",n.b,"downloaded",n.a],m,t.S) +s=o.w +A:{if(s==null){r=null +break A}r=s.a/1000 +break A}q=o.x +B:{if(q==null){p=null +break B}p=q.a/1000 +break B}return A.bJ(["name",o.a,"parameters",o.b,"priority",o.c,"progress",n,"active",o.e,"is_default",o.f,"has_explicit_subscription",o.r,"expires_at",r,"last_synced_at",p],m,t.X)}} +A.tY.prototype={ +$0(){var s=this,r=s.b,q=s.a,p=s.d,o=A.a1(r).h("@<1>").J(p.h("ak<0>")).h("a8<1,2>"),n=A.an(new A.a8(r,new A.tX(q,s.c,p),o),o.h("W.E")) +q.a=n}, +$S:0} +A.tX.prototype={ +$1(a){var s=this.b +return a.aj(new A.tV(s,this.c),new A.tW(this.a,s),s.gd5())}, +$S(){return this.c.h("ak<0>(G<0>)")}} +A.tV.prototype={ +$1(a){return this.a.q(0,a)}, +$S(){return this.b.h("~(0)")}} +A.tW.prototype={ +$0(){var s=0,r=A.j(t.H),q=1,p=[],o=[],n=this,m,l,k,j,i +var $async$$0=A.e(function(a,b){if(a===1){p.push(b) +s=q}for(;;)switch(s){case 0:j=n.a +s=!j.b?2:3 +break +case 2:j.b=!0 +q=5 +j=j.a +j.toString +s=8 +return A.c(A.kF(j),$async$$0) +case 8:o.push(7) +s=6 +break +case 5:q=4 +i=p.pop() +m=A.H(i) +l=A.N(i) +n.b.a2(m,l) +o.push(7) +s=6 +break +case 4:o=[1] +case 6:q=1 +n.b.n() +s=o.pop() +break +case 7:case 3:return A.h(null,r) +case 1:return A.f(p.at(-1),r)}}) +return A.i($async$$0,r)}, +$S:3} +A.tZ.prototype={ +$0(){var s=this.a,r=s.a +if(r!=null&&!s.b)return A.kF(r)}, +$S:29} +A.u_.prototype={ +$0(){var s=this.a.a +if(s!=null)return A.Dy(s)}, +$S:0} +A.u0.prototype={ +$0(){var s=this.a.a +if(s!=null)return A.DC(s)}, +$S:0} +A.tx.prototype={ +$1(a){return a.u()}, +$S:58} +A.u8.prototype={ +$1(a){var s=this.a +s.q(0,a) +s.n()}, +$S(){return this.b.h("J(0)")}} +A.u9.prototype={ +$2(a,b){var s +if(this.a.a)throw A.a(a) +else{s=this.b +s.a2(a,b) +s.n()}}, +$S:7} +A.u7.prototype={ +$0(){var s=0,r=A.j(t.H),q=this +var $async$$0=A.e(function(a,b){if(a===1)return A.f(b,r) +for(;;)switch(s){case 0:q.a.a=!0 +s=2 +return A.c(q.b,$async$$0) +case 2:return A.h(null,r)}}) +return A.i($async$$0,r)}, +$S:3} +A.e9.prototype={ +q(a,b){var s,r,q,p,o,n,m,l,k,j,i,h=this,g=null,f="Stream is already closed" +for(s=J.a2(b),r=h.b,q=h.a.a,p=0;pk)A.p(A.a0(k,p,g,"end",g)) +n.hA(b,p,k) +if((h.c-=l)===0){m=B.f.gaG(n.a) +j=n.a +j=J.cg(m,j.byteOffset,n.b*j.BYTES_PER_ELEMENT) +if((q.e&2)!==0)A.p(A.u(f)) +q.ad(j) +h.d=null +h.c=4}p=k}else{l=Math.min(o,m) +i=J.yO(B.ad.gaG(r)) +m=4-h.c +B.f.L(i,m,m+l,b,p) +p+=l +if((h.c-=l)===0){m=h.c=r.getInt32(0,!0)-4 +if(m<5){j=A.fD() +if((q.e&2)!==0)A.p(A.u(f)) +q.bR(new A.e_("Invalid length for bson: "+m),j)}m=new A.bE(new Uint8Array(0),0) +m.hA(i,0,g) +h.d=m}}}}, +a2(a,b){this.a.a2(a,b)}, +n(){var s,r=this +if(r.d!=null||r.c!==4)r.a.a2(new A.e_("Pending data when stream was closed"),A.fD()) +s=r.a.a +if((s.e&2)!==0)A.p(A.u("Stream is already closed")) +s.aA()}, +$iaa:1, +gk(a){return this.b}} +A.om.prototype={ +aF(){var s=0,r=A.j(t.H),q=this,p,o,n,m +var $async$aF=A.e(function(a,b){if(a===1)return A.f(b,r) +for(;;)switch(s){case 0:m=q.z +s=m!=null?2:3 +break +case 2:p=m.aF() +q.w.n() +s=4 +return A.c(q.ax.n(),$async$aF) +case 4:o=A.v([p],t.M) +n=q.at +if(n!=null)o.push(n.a) +s=5 +return A.c(A.f5(o,t.H),$async$aF) +case 5:q.x.n() +q.y.c.n() +case 3:return A.h(null,r)}}) +return A.i($async$aF,r)}, +ge4(){var s=this.z +s=s==null?null:s.a +return s===!0}, +cg(){var s=0,r=A.j(t.H),q,p=2,o=[],n=[],m=this,l,k,j,i,h,g,f,e,d,c,b,a,a0,a1,a2,a3,a4 +var $async$cg=A.e(function(a5,a6){if(a5===1){o.push(a6) +s=p}for(;;)switch(s){case 0:a0=$.n +a1=t.D +a2=t.h +a3=new A.kU(new A.as(new A.l(a0,a1),a2),new A.as(new A.l(a0,a1),a2)) +m.z=a3 +l=a3 +p=3 +s=6 +return A.c(m.b.dC(),$async$cg) +case 6:m.ch=a6 +m.bT() +a0=m.f +a1=m.y +a2=t.H +e=t.U +d=m.Q +c=m.d.d +case 7:b=m.z +b=b==null?null:b.a +if(!(b!==!0)){s=8 +break}k=!1 +p=10 +j=null +s=13 +return A.c(d.c0(new A.ou(m,l),A.ms(c==null?B.u:c,a2),e),$async$cg) +case 13:i=a6 +j=i.a +k=!j +p=3 +s=12 +break +case 10:p=9 +a4=o.pop() +h=A.H(a4) +g=A.N(a4) +b=m.z +b=b==null?null:b.a +if(b===!0&&h instanceof A.bY){n=[1] +s=4 +break}k=!0 +f=A.CB(h) +a0.a_(B.o,"Sync error: "+A.o(f),h,g) +a1.c7(new A.ov(h)) +s=12 +break +case 9:s=3 +break +case 12:b=m.z +b=b==null?null:b.a +s=b!==!0&&k?14:15 +break +case 14:s=16 +return A.c(m.cP(),$async$cg) +case 16:case 15:s=7 +break +case 8:n.push(5) +s=4 +break +case 3:n=[2] +case 4:p=2 +a0=l.c +if((a0.a.a&30)===0)a0.ah() +s=n.pop() +break +case 5:case 1:return A.h(q,r) +case 2:return A.f(o.at(-1),r)}}) +return A.i($async$cg,r)}, +bT(){var s=0,r=A.j(t.H),q=1,p=[],o=[],n=this,m +var $async$bT=A.e(function(a,b){if(a===1){p.push(b) +s=q}for(;;)switch(s){case 0:s=2 +return A.c(n.iI(),$async$bT) +case 2:m=n.w +m=new A.bU(A.bd(A.y2(A.v([n.r,new A.aJ(m,A.q(m).h("aJ<1>"))],t.i3),t.H),"stream",t.K)) +q=3 +case 6:s=8 +return A.c(m.l(),$async$bT) +case 8:if(!b){s=7 +break}m.gp() +s=9 +return A.c(n.iI(),$async$bT) +case 9:s=6 +break +case 7:o.push(5) +s=4 +break +case 3:o=[1] +case 4:q=1 +s=10 +return A.c(m.u(),$async$bT) +case 10:s=o.pop() +break +case 5:return A.h(null,r) +case 1:return A.f(p.at(-1),r)}}) +return A.i($async$bT,r)}, +iI(){var s,r=this,q=new A.as(new A.l($.n,t.D),t.h) +r.at=q +s=r.d.d +if(s==null)s=B.u +return r.as.c0(new A.os(r),A.ms(s,t.H),t.P).O(new A.ot(r,q))}, +cb(){var s=0,r=A.j(t.N),q,p=this,o,n,m,l,k +var $async$cb=A.e(function(a,b){if(a===1)return A.f(b,r) +for(;;)switch(s){case 0:l=p.c +s=3 +return A.c(l.a.$0(),$async$cb) +case 3:k=b +if(k==null)throw A.a(A.vM("Not logged in")) +o=p.ch +n=A.d3(k.a).ey("write-checkpoint2.json?client_id="+A.o(o)) +o=t.N +o=A.P(o,o) +o.m(0,"Content-Type","application/json") +o.m(0,"Authorization","Token "+k.b) +o.a8(0,p.ay) +s=4 +return A.c(p.x.dR("GET",n,o),$async$cb) +case 4:m=b +o=m.b +s=o===401?5:6 +break +case 5:s=7 +return A.c(l.b.$1$invalidate(!0),$async$cb) +case 7:case 6:if(o!==200)throw A.a(A.Ap(m)) +q=A.av(J.kP(J.kP(B.h.cn(A.xX(A.xm(m.e)).aO(m.w),null),"data"),"write_checkpoint")) +s=1 +break +case 1:return A.h(q,r)}}) +return A.i($async$cb,r)}, +dQ(a){return this.lV(a)}, +lV(a){var s=0,r=A.j(t.U),q,p=this,o,n +var $async$dQ=A.e(function(b,c){if(b===1)return A.f(c,r) +for(;;)switch(s){case 0:n=p.f +n.a_(B.j,"Starting Rust sync iteration",null,null) +s=3 +return A.c(new A.pB(p,a).bt(),$async$dQ) +case 3:o=c +n.a_(B.j,"Ending Rust sync iteration. Immediate restart: "+o.a,null,null) +q=o +s=1 +break +case 1:return A.h(q,r)}}) +return A.i($async$dQ,r)}, +bW(a,b){return this.lC(a,b)}, +lC(a,b){var s=0,r=A.j(t.cn),q,p=this,o,n,m,l,k,j,i +var $async$bW=A.e(function(c,d){if(c===1)return A.f(d,r) +for(;;)switch(s){case 0:k=p.c +s=3 +return A.c(k.a.$0(),$async$bW) +case 3:j=d +if(j==null)throw A.a(A.vM("Not logged in")) +o=A.d3(j.a).ey("sync/stream") +n=A.yX("POST",o,b) +m=n.r +m.m(0,"Content-Type","application/json") +m.m(0,"Authorization","Token "+j.b) +m.m(0,"Accept","application/vnd.powersync.bson-stream;q=0.9,application/x-ndjson;q=0.8") +m.a8(0,p.ay) +n.smC(B.h.iZ(a,null)) +s=4 +return A.c(p.x.cd(n),$async$bW) +case 4:l=d +if(p.ge4()){q=null +s=1 +break}m=l.b +s=m===401?5:6 +break +case 5:s=7 +return A.c(k.b.$1$invalidate(!0),$async$bW) +case 7:case 6:s=m!==200?8:9 +break +case 8:i=A +s=10 +return A.c(A.oy(l),$async$bW) +case 10:throw i.a(d) +case 9:q=l +s=1 +break +case 1:return A.h(q,r)}}) +return A.i($async$bW,r)}, +cP(){var s=0,r=A.j(t.H),q=this,p,o +var $async$cP=A.e(function(a,b){if(a===1)return A.f(b,r) +for(;;)switch(s){case 0:o=q.d.d +if(o==null)o=B.u +p=t.H +s=2 +return A.c(A.zp(A.v([A.ms(o,p),q.z.b.a],t.M),p),$async$cP) +case 2:return A.h(null,r)}}) +return A.i($async$cP,r)}} +A.ou.prototype={ +$0(){return this.a.dQ(this.b)}, +$S:59} +A.ov.prototype={ +$1(a){a.c=a.b=a.a=!1 +a.e=null +a.y=this.a +return null}, +$S:6} +A.os.prototype={ +$0(){var s=0,r=A.j(t.P),q=1,p=[],o=[],n=this,m,l,k,j,i,h,g,f,e,d,c,b,a,a0 +var $async$$0=A.e(function(a1,a2){if(a1===1){p.push(a2) +s=q}for(;;)switch(s){case 0:a=null +j=n.a,i=j.y,h=i.a,g=j.f,f=j.c.c,e=j.b +case 2:q=5 +d=j.z +d=d==null?null:d.a +if(d===!0){o=[3] +s=6 +break}s=8 +return A.c(e.eq(),$async$$0) +case 8:m=a2 +s=m!=null?9:11 +break +case 9:i.c7(new A.on()) +d=m.a +c=a +if(d===(c==null?null:c.a)){g.a_(B.o,"Potentially previously uploaded CRUD entries are still present in the upload queue. \n Make sure to handle uploads and complete CRUD transactions or batches by calling and awaiting their [.complete()] method.\n The next upload iteration will be delayed.",null,null) +d=A.uj("Delaying due to previously encountered CRUD item.") +throw A.a(d)}a=m +s=12 +return A.c(f.$0(),$async$$0) +case 12:i.c7(new A.oo()) +s=10 +break +case 11:s=13 +return A.c(e.c6(new A.op(j)),$async$$0) +case 13:o=[3] +s=6 +break +case 10:o.push(7) +s=6 +break +case 5:q=4 +a0=p.pop() +l=A.H(a0) +k=A.N(a0) +a=null +g.a_(B.o,"Data upload error",l,k) +i.c7(new A.oq(l)) +s=14 +return A.c(j.cP(),$async$$0) +case 14:if(!h.a){o=[3] +s=6 +break}g.a_(B.o,"Caught exception when uploading. Upload will retry after a delay",l,k) +o.push(7) +s=6 +break +case 4:o=[1] +case 6:q=1 +i.c7(new A.or()) +s=o.pop() +break +case 7:s=2 +break +case 3:return A.h(null,r) +case 1:return A.f(p.at(-1),r)}}) +return A.i($async$$0,r)}, +$S:28} +A.on.prototype={ +$1(a){return a.d=!0}, +$S:6} +A.oo.prototype={ +$1(a){return a.x=null}, +$S:6} +A.op.prototype={ +$0(){return this.a.cb()}, +$S:62} +A.oq.prototype={ +$1(a){a.d=!1 +a.x=this.a +return null}, +$S:6} +A.or.prototype={ +$1(a){return a.d=!1}, +$S:6} +A.ot.prototype={ +$0(){var s=this.a +if(!s.ge4())s.ax.q(0,B.ba) +s.at=null +this.b.ah()}, +$S:1} +A.pB.prototype={ +hS(a){var s=this.a.e,r=A.a1(s).h("a8<1,a_>") +s=A.an(new A.a8(s,new A.pC(),r),r.h("W.E")) +return s}, +bt(){var s=0,r=A.j(t.U),q,p=2,o=[],n=[],m=this,l,k,j,i,h,g,f,e,d,c,b +var $async$bt=A.e(function(a,a0){if(a===1){o.push(a0) +s=p}for(;;)switch(s){case 0:c=null +b=J +s=3 +return A.c(m.dS(),$async$bt) +case 3:l=b.U(a0),k=t.b,j=m.a.ax,i=A.q(j).h("aJ<1>"),h=t.k,g=t.fu +case 4:if(!l.l()){s=5 +break}f=l.gp() +e=f instanceof A.dM +d=e?f.a:null +if(e){c=A.y2(A.v([m.lI(d),new A.aJ(j,i)],g),h) +s=4 +break}if(f instanceof A.dH){q=B.af +s=1 +break}e=k.b(f) +f=e?f:null +s=e?6:7 +break +case 6:s=8 +return A.c(m.bU(f),$async$bt) +case 8:case 7:s=4 +break +case 5:if(c==null){q=B.af +s=1 +break}p=9 +s=12 +return A.c(m.aN(c),$async$bt) +case 12:l=a0 +q=l +n=[1] +s=10 +break +n.push(11) +s=10 +break +case 9:n=[2] +case 10:p=2 +l=A.h8(null,t.H) +s=13 +return A.c(l,$async$bt) +case 13:s=14 +return A.c(m.d_(),$async$bt) +case 14:s=n.pop() +break +case 11:case 1:return A.h(q,r) +case 2:return A.f(o.at(-1),r)}}) +return A.i($async$bt,r)}, +dS(){var s=0,r=A.j(t.ks),q,p=this,o,n,m,l,k +var $async$dS=A.e(function(a,b){if(a===1)return A.f(b,r) +for(;;)switch(s){case 0:o=p.a +n=o.d +m=A.A8(n) +l=A.A9(n) +k=B.h.aO(o.a) +s=3 +return A.c(p.bf("start",B.h.bB(A.bJ(["app_metadata",m,"parameters",l,"schema",k,"include_defaults",n.f!==!1,"active_streams",p.hS(o.e)],t.N,t.z))),$async$dS) +case 3:q=b +s=1 +break +case 1:return A.h(q,r)}}) +return A.i($async$dS,r)}, +lI(a){return A.DD(this.a.bW(a,this.b.b.a),t.cn).mB(new A.pH(),t.k)}, +aN(a){return this.l8(a)}, +l8(b2){var s=0,r=A.j(t.U),q,p=2,o=[],n=[],m=this,l,k,j,i,h,g,f,e,d,c,b,a,a0,a1,a2,a3,a4,a5,a6,a7,a8,a9,b0,b1 +var $async$aN=A.e(function(b3,b4){if(b3===1){o.push(b4) +s=p}for(;;)switch(s){case 0:b0=!1 +p=4 +a0=new A.bU(A.bd(b2,"stream",t.K)) +p=7 +a1=t.b,a2=m.a,a3=a2.f,a4=t.p,a5=a2.w +case 11:s=13 +return A.c(a0.l(),$async$aN) +case 13:if(!b4){s=12 +break}l=a0.gp() +a6=a2.z +a6=a6==null?null:a6.a +if(a6===!0){s=10 +break}k=null +j=l +i=null +h=!1 +s=j instanceof A.dJ?15:16 +break +case 15:s=17 +return A.c(m.bf("connection",l.b),$async$aN) +case 17:k=b4 +s=14 +break +case 16:g=null +if(j instanceof A.cp){if(h)a6=i +else{h=!0 +a7=j.a +i=a7 +a6=a7}a6=a4.b(a6) +if(a6){if(h)a8=i +else{h=!0 +a7=j.a +i=a7 +a8=a7}g=a4.a(a8)}}else a6=!1 +s=a6?18:19 +break +case 18:if(!m.c){if(!a5.gbx())A.p(a5.bu()) +a5.aE(null) +m.c=!0}s=20 +return A.c(m.bf("line_binary",g),$async$aN) +case 20:k=b4 +s=14 +break +case 19:f=null +a6=j instanceof A.cp +if(a6){if(h)a8=i +else{h=!0 +a7=j.a +i=a7 +a8=a7}A.av(a8) +if(h)a8=i +else{h=!0 +a7=j.a +i=a7 +a8=a7}f=A.av(a8)}s=a6?21:22 +break +case 21:if(!m.c){if(!a5.gbx())A.p(a5.bu()) +a5.aE(null) +m.c=!0}s=23 +return A.c(m.bf("line_text",f),$async$aN) +case 23:k=b4 +s=14 +break +case 22:s=j instanceof A.fR?24:25 +break +case 24:s=26 +return A.c(m.ft("completed_upload"),$async$aN) +case 26:k=b4 +s=14 +break +case 25:s=j instanceof A.fL?27:28 +break +case 27:s=29 +return A.c(m.ft("refreshed_token"),$async$aN) +case 29:k=b4 +s=14 +break +case 28:e=null +a6=j instanceof A.f7 +if(a6)e=j.a +s=a6?30:31 +break +case 30:s=32 +return A.c(m.bf("update_subscriptions",B.h.bB(m.hS(e))),$async$aN) +case 32:k=b4 +case 31:case 14:a6=J.U(k) +case 33:if(!a6.l()){s=34 +break}d=a6.gp() +c=d +if(c instanceof A.dM){a3.a_(B.o,"Received EstablishSyncStream connection while already connected.",null,null) +s=33 +break}b=null +a8=c instanceof A.dH +if(a8)b=c.a +if(a8){b0=b +s=10 +break}a=null +a8=a1.b(c) +if(a8)a=c +s=a8?35:36 +break +case 35:s=37 +return A.c(m.bU(a),$async$aN) +case 37:case 36:s=33 +break +case 34:s=11 +break +case 12:case 10:n.push(9) +s=8 +break +case 7:n=[4] +case 8:p=4 +s=38 +return A.c(a0.u(),$async$aN) +case 38:s=n.pop() +break +case 9:p=2 +s=6 +break +case 4:p=3 +b1=o.pop() +if(A.H(b1) instanceof A.fw){if(!m.a.ge4())throw b1}else throw b1 +s=6 +break +case 3:s=2 +break +case 6:q=new A.hj(b0) +s=1 +break +case 1:return A.h(q,r) +case 2:return A.f(o.at(-1),r)}}) +return A.i($async$aN,r)}, +d_(){var s=0,r=A.j(t.H),q=this,p,o,n,m +var $async$d_=A.e(function(a,b){if(a===1)return A.f(b,r) +for(;;)switch(s){case 0:m=J +s=2 +return A.c(q.ft("stop"),$async$d_) +case 2:p=m.U(b),o=t.b +case 3:if(!p.l()){s=4 +break}n=p.gp() +s=o.b(n)?5:6 +break +case 5:s=7 +return A.c(q.bU(n),$async$d_) +case 7:case 6:s=3 +break +case 4:return A.h(null,r)}}) +return A.i($async$d_,r)}, +bf(a,b){return this.ld(a,b)}, +ft(a){return this.bf(a,null)}, +ld(a,b){var s=0,r=A.j(t.ks),q,p=this,o,n,m,l +var $async$bf=A.e(function(c,d){if(c===1)return A.f(d,r) +for(;;)switch(s){case 0:n=J +m=t.j +l=B.h +s=3 +return A.c(p.a.b.ed(a,b),$async$bf) +case 3:o=n.vu(m.a(l.aO(d)),t.f) +q=new A.a8(o,A.Dp(),A.q(o).h("a8")) +s=1 +break +case 1:return A.h(q,r)}}) +return A.i($async$bf,r)}, +bU(a){return this.l7(a)}, +l7(a){var s=0,r=A.j(t.H),q=this,p,o,n,m,l,k +var $async$bU=A.e(function(b,c){if(b===1)return A.f(c,r) +for(;;)switch(s){case 0:p=a instanceof A.fj +if(p){o=a.a +n=a.b}else{o=null +n=null}if(p){A:{if("DEBUG"===o){p=B.v +break A}if("INFO"===o){p=B.j +break A}p=B.o +break A}q.a.f.o3(p,n) +s=2 +break}p={} +p.a=null +m=a instanceof A.fP +if(m)p.a=a.a +if(m){q.a.y.c7(new A.pD(p)) +s=2 +break}p=a instanceof A.f1 +l=p?a.a:null +s=p?3:4 +break +case 3:p=q.a.c +s=l?5:7 +break +case 5:s=8 +return A.c(p.b.$1$invalidate(!0),$async$bU) +case 8:s=6 +break +case 7:p.b.$1$invalidate(!1).b9(new A.pE(q),new A.pF(q),t.P) +case 6:s=2 +break +case 4:s=a instanceof A.f4?9:10 +break +case 9:s=11 +return A.c(q.a.b.b.aI(),$async$bU) +case 11:s=2 +break +case 10:if(a instanceof A.eY){q.a.y.c7(new A.pG()) +s=2 +break}p=a instanceof A.fM +k=p?a.a:null +if(p)q.a.f.a_(B.o,"Unknown instruction: "+A.o(k),null,null) +case 2:return A.h(null,r)}}) +return A.i($async$bU,r)}} +A.pC.prototype={ +$1(a){return A.bJ(["name",a.a,"params",B.h.aO(a.b)],t.N,t.z)}, +$S:63} +A.pH.prototype={ +$1(a){return this.jO(a)}, +jO(a){var $async$$1=A.e(function(b,c){switch(b){case 2:n=q +s=n.pop() +break +case 1:o.push(c) +s=p}for(;;)switch(s){case 0:s=a==null?3:5 +break +case 3:s=1 +break +s=4 +break +case 5:s=6 +q=[1] +return A.kC(A.wN(B.bg),$async$$1,r) +case 6:m=a.e.i(0,"content-type") +l=a.w +if(m==="application/vnd.powersync.bson-stream")l=new A.c7(A.DE(),l,t.jB) +else l=B.b5.aY(B.az.aY(l)) +s=7 +q=[1] +return A.kC(A.B1(new A.bG(A.DF(),l,l.$ti.h("bG"))),$async$$1,r) +case 7:s=8 +q=[1] +return A.kC(A.wN(B.bh),$async$$1,r) +case 8:case 4:case 1:return A.kC(null,0,r) +case 2:return A.kC(o.at(-1),1,r)}}) +var s=0,r=A.Ce($async$$1,t.k),q,p=2,o=[],n=[],m,l +return A.Cy(r)}, +$S:64} +A.pD.prototype={ +$1(a){return a.mA(this.a.a)}, +$S:6} +A.pE.prototype={ +$1(a){var s=this.a.a +if(!s.ge4())s.ax.q(0,B.b9)}, +$S:65} +A.pF.prototype={ +$2(a,b){this.a.a.f.a_(B.o,"Could not prefetch credentials",a,b)}, +$S:7} +A.pG.prototype={ +$1(a){return a.y=null}, +$S:6} +A.dJ.prototype={ +av(){return"ConnectionEvent."+this.b}, +$ib8:1} +A.cp.prototype={$ib8:1} +A.fR.prototype={$ib8:1} +A.fL.prototype={$ib8:1} +A.f7.prototype={$ib8:1} +A.ct.prototype={ +H(a,b){var s=this +if(b==null)return!1 +return b instanceof A.ct&&b.a===s.a&&b.c===s.c&&b.e===s.e&&b.b===s.b&&J.y(b.x,s.x)&&J.y(b.w,s.w)&&J.y(b.f,s.f)&&b.r==s.r&&B.y.aP(b.y,s.y)&&B.y.aP(b.z,s.z)&&J.y(b.d,s.d)}, +gB(a){var s=this +return A.bN(s.a,s.c,s.e,s.b,s.w,s.x,s.f,B.y.c_(s.y),s.d,B.y.c_(s.z))}, +j(a){var s,r,q,p,o=this,n="connected",m={},l=new A.X("SyncStatus<") +m.a=!0 +m=new A.oA(m,l) +if(o.a)m.$2(n,!0) +else if(o.b)m.$2(n,"connecting") +else m.$2(n,"offline (not connecting)") +m.$2("downloading",""+o.c+" (progress: "+A.o(o.d)+")") +m.$2("uploading",o.e) +m.$2("lastSyncedAt",o.f) +m.$2("hasSynced",o.r) +s=o.x +r=s==null +if(!r)m.$2("downloadError",s) +q=o.w +p=q==null +if(!p)m.$2("uploadError",q) +if(r&&p)m.$2("error",null) +m=l.a+=">" +return m.charCodeAt(0)==0?m:m}} +A.oA.prototype={ +$2(a,b){var s,r,q=this.a +if(!q.a)this.b.a+=" " +s=this.b +r=a+": "+A.o(b) +s.a+=r +q.a=!1}, +$S:66} +A.il.prototype={ +gB(a){return B.a1.c_(this.c)}, +H(a,b){if(b==null)return!1 +return b instanceof A.il&&this.a===b.a&&this.b===b.b&&B.a1.aP(this.c,b.c)}, +j(a){return"for total: "+this.b+" / "+this.a}} +A.n6.prototype={ +$1(a){var s=a.a +return s[3]-s[0]}, +$S:26} +A.n7.prototype={ +$1(a){return a.a[2]}, +$S:26} +A.ny.prototype={} +A.oB.prototype={ +lJ(a,b,c,d,e){var s=this.a.cB(a,new A.oC(a)) +s.e.q(0,new A.fY(e,b,c,d)) +return s}} +A.oC.prototype={ +$0(){return A.Bf(this.a)}, +$S:68} +A.da.prototype={ +kx(a,b){var s=this +s.a=A.AA(a,new A.qh(s)) +s.d=$.dC().fh().Z(new A.qi(s))}, +jh(){var s=this,r=s.d +if(r!=null)r.u() +r=s.c +if(r!=null)r.e.q(0,new A.hn(s)) +s.c=null}} +A.qh.prototype={ +$2(a,b){return this.jP(a,b)}, +jP(a,b){var s=0,r=A.j(t.iS),q,p=this,o,n,m,l,k,j,i,h,g,f,e,d,c +var $async$$2=A.e(function(a0,a1){if(a0===1)return A.f(a1,r) +for(;;)A:switch(s){case 0:switch(a.a){case 1:A.a4(b) +o=A.mf(0,b.crudThrottleTimeMs) +n=b.retryDelayMs +B:{if(n==null){m=null +break B}m=A.mf(0,n) +break B}l=b.syncParamsEncoded +C:{if(l==null){k=null +break C}k=t.f.a(B.h.cn(l,null)) +break C}j=b.implementationName +D:{if(j==null){i=B.M +break D}i=A.ia(B.bA,j) +break D}h=b.appMetadataEncoded +E:{if(h==null){g=null +break E}g=t.N +g=A.w_(t.ea.a(B.h.cn(h,null)),g,g) +break E}f=p.a +e=b.databaseName +d=b.schemaJson +c=b.subscriptions +c=c==null?null:A.wu(c) +if(c==null)c=B.bD +f.c=f.b.lJ(e,new A.fJ(g,k,o,m,i,null),d,c,f) +q=new A.au({},null) +s=1 +break A +case 3:o=p.a +m=o.c +if(m!=null)m.e.q(0,new A.h5(o)) +o.c=null +q=new A.au({},null) +s=1 +break A +case 2:o=p.a +m=o.c +if(m!=null){k=A.wu(A.a4(b)) +m.e.q(0,new A.h3(o,k))}q=new A.au({},null) +s=1 +break A +default:throw A.a(A.u("Unexpected message type "+a.j(0)))}case 1:return A.h(q,r)}}) +return A.i($async$$2,r)}, +$S:69} +A.qi.prototype={ +$1(a){var s="["+a.d+"] "+a.a.a+": "+a.e.j(0)+": "+a.b,r=a.r +if(r!=null)s=s+"\n"+A.o(r) +r=a.w +if(r!=null)s=s+"\n"+r.j(0) +r=this.a.a +r===$&&A.B() +r.f.postMessage({type:"logEvent",payload:s.charCodeAt(0)==0?s:s})}, +$S:33} +A.ew.prototype={ +kz(a){var s=this.e +this.d.q(0,new A.O(s,A.q(s).h("O<1>"))) +A.un(new A.rC(this),t.P)}, +jq(){var s,r,q=this,p=q.y,o=A.zI(p,A.a1(p).c) +p=q.x +s=A.vW(new A.bf(p,A.q(p).h("bf<2>")),t.E) +if(!B.b7.aP(o,s)){$.dC().a_(B.j,"Subscriptions across tabs have changed, checking whether a reconnect is necessary",null,null) +p=A.an(s,A.q(s).c) +q.y=p +r=q.f +if(r!=null){r.e=p +r=r.ax +if(r.d!=null)r.q(0,new A.f7(p))}}}, +f5(){return this.kM()}, +kM(){var s=0,r=A.j(t.gh),q,p=this,o,n,m,l,k,j,i,h,g +var $async$f5=A.e(function(a,b){if(a===1)return A.f(b,r) +for(;;)switch(s){case 0:j={} +i=p.x +h=A.q(i).h("bx<1>") +g=A.an(new A.bx(i,h),h.h("m.E")) +i=g.length +if(i===0){q=null +s=1 +break}h=new A.l($.n,t.mK) +o=new A.as(h,t.k5) +j.a=i +for(n=t.P,m=0;m")),t.E) +l=A.an(l,A.q(l).c) +q.y=l +l=q.c +i=a1.a +h=q.b +g=A.v([],t.W) +f=q.a +e=q.y +p=A.cY(!1,p) +d=A.cY(!1,t.gs) +c=A.cY(!1,t.k) +b=A.uJ("sync-"+f) +f=A.uJ("crud-"+f) +a=t.N +a=A.bJ(["X-User-Agent","powersync-dart-core/2.1.0 Dart (flutter-web)"],a,a) +q.f=new A.om(l,new A.pg(n,n),new A.q3(i.gmL(),new A.rA(a1),i.got()),h,e,a0,j,p,new A.la(g),new A.oz(new A.fo(B.ab),B.bP,d),b,f,c,a) +new A.aJ(d,A.q(d).h("aJ<1>")).Z(new A.rB(q)) +q.f.cg() +return A.h(null,r)}}) +return A.i($async$bX,r)}} +A.rC.prototype={ +$0(){var s=0,r=A.j(t.P),q=1,p=[],o=[],n=this,m,l,k,j,i,h,g,f,e,d,c,b,a,a0,a1,a2,a3,a4,a5,a6,a7,a8,a9,b0,b1,b2,b3,b4,b5,b6,b7,b8,b9,c0,c1,c2,c3,c4,c5,c6,c7 +var $async$$0=A.e(function(c8,c9){if(c8===1){p.push(c9) +s=q}for(;;)switch(s){case 0:c5=n.a +c6=c5.d.a +c6===$&&A.B() +c6=new A.bU(A.bd(new A.O(c6,A.q(c6).h("O<1>")),"stream",t.K)) +q=2 +a9=c5.x,b0=t.D +case 5:s=7 +return A.c(c6.l(),$async$$0) +case 7:if(!c9){s=6 +break}m=c6.gp() +q=9 +l=m +k=null +j=!1 +i=null +h=!1 +g=null +f=null +e=null +d=null +b1=l instanceof A.fY +if(b1){if(j)b2=k +else{j=!0 +b3=l.a +k=b3 +b2=b3}g=b2 +f=l.b +e=l.c +if(h)b4=i +else{h=!0 +b5=l.d +i=b5 +b4=b5}d=b4}s=b1?13:14 +break +case 13:a9.m(0,g,d) +c=null +b=null +b1=c5.b +b6=f +b7=b6.c +if(b7==null){b7=b1.c +if(b7==null)b7=B.G}b8=b6.d +if(b8==null){b8=b1.d +if(b8==null)b8=B.u}b9=b6.b +if(b9==null){b9=b1.b +if(b9==null)b9=B.I}c0=b6.e +c1=b6.f +if(c1==null)c1=b1.f!==!1 +b6=b6.a +if(b6==null){b6=b1.a +if(b6==null)b6=B.J}c2=b1.b +c3=!0 +if(B.z.aP(b9,c2==null?B.I:c2)){c2=b1.c +if(b7.H(0,c2==null?B.G:c2)){c2=b1.d +if(b8.H(0,c2==null?B.u:c2))if(c0===b1.e)if(c1===(b1.f!==!1)){b1=b1.a +b1=!B.z.aP(b6,b1==null?B.J:b1)}else b1=c3 +else b1=c3 +else b1=c3 +c3=b1}}a=new A.au(new A.fJ(b6,b9,b7,b8,c0,c1),c3) +c=a.a +b=a.b +c5.b=c +c5.c=e +b1=c5.f +s=b1==null?15:17 +break +case 15:s=18 +return A.c(c5.bX(g),$async$$0) +case 18:s=16 +break +case 17:s=b?19:21 +break +case 19:b1.aF() +c5.f=null +s=22 +return A.c(c5.bX(g),$async$$0) +case 22:s=20 +break +case 21:c5.jq() +case 20:case 16:a0=c5.r +a1=null +if(a0!=null){a1=a0 +b1=g +b6=A.wk(a1) +b1=b1.a +b1===$&&A.B() +b1.f.postMessage({type:"notifySyncStatus",payload:b6})}s=12 +break +case 14:a2=null +b1=l instanceof A.hn +if(b1){if(j)b2=k +else{j=!0 +b3=l.a +k=b3 +b2=b3}a2=b2}s=b1?23:24 +break +case 23:a9.E(0,a2) +s=a9.a===0?25:26 +break +case 25:b1=c5.f +b1=b1==null?null:b1.aF() +if(!(b1 instanceof A.l)){b6=new A.l($.n,b0) +b6.a=8 +b6.c=b1 +b1=b6}s=27 +return A.c(b1,$async$$0) +case 27:c5.f=null +case 26:s=12 +break +case 24:a3=null +b1=l instanceof A.h5 +if(b1){if(j)b2=k +else{j=!0 +b3=l.a +k=b3 +b2=b3}a3=b2}s=b1?28:29 +break +case 28:a9.E(0,a3) +b1=c5.f +b1=b1==null?null:b1.aF() +if(!(b1 instanceof A.l)){b6=new A.l($.n,b0) +b6.a=8 +b6.c=b1 +b1=b6}s=30 +return A.c(b1,$async$$0) +case 30:c5.f=null +s=12 +break +case 29:s=l instanceof A.fX?31:32 +break +case 31:b1=$.dC() +b1.a_(B.j,"Remote database closed, finding a new client",null,null) +b6=c5.f +if(b6!=null)b6.aF() +c5.f=null +s=33 +return A.c(c5.f5(),$async$$0) +case 33:a4=c9 +s=a4==null?34:36 +break +case 34:b1.a_(B.j,"No client remains",null,null) +s=35 +break +case 36:s=37 +return A.c(c5.bX(a4),$async$$0) +case 37:case 35:s=12 +break +case 32:a5=null +a6=null +b1=l instanceof A.h3 +if(b1){if(j)b2=k +else{j=!0 +b3=l.a +k=b3 +b2=b3}a5=b2 +if(h)b4=i +else{h=!0 +b5=l.b +i=b5 +b4=b5}a6=b4}if(b1){a9.m(0,a5,a6) +c5.jq()}case 12:q=2 +s=11 +break +case 9:q=8 +c7=p.pop() +a7=A.H(c7) +a8=A.N(c7) +b1=$.dC() +b6=A.o(m) +b1.a_(B.o,"Error handling "+b6,a7,a8) +s=11 +break +case 8:s=2 +break +case 11:s=5 +break +case 6:o.push(4) +s=3 +break +case 2:o=[1] +case 3:q=1 +s=38 +return A.c(c6.u(),$async$$0) +case 38:s=o.pop() +break +case 4:return A.h(null,r) +case 1:return A.f(p.at(-1),r)}}) +return A.i($async$$0,r)}, +$S:28} +A.rx.prototype={ +$1(a){var s;--this.a.a +s=this.b +if((s.a.a&30)===0)s.W(this.c)}, +$S:9} +A.ry.prototype={ +$0(){var s=this,r=s.a;--r.a +s.b.jh() +if(r.a===0&&(s.c.a.a&30)===0)s.c.W(null)}, +$S:1} +A.rz.prototype={ +$1(a){var s,r,q=null,p=$.dC() +p.a_(B.v,"Detected closed client",q,q) +s=this.b +s.jh() +r=this.a +if(s===r.w){p.a_(B.j,"Tab providing sync database has gone down, reconnecting...",q,q) +r.e.q(0,B.bb)}}, +$S:9} +A.rA.prototype={ +$1$invalidate(a){return this.jR(a)}, +jR(a){var s=0,r=A.j(t.B),q,p=this,o +var $async$$1$invalidate=A.e(function(b,c){if(b===1)return A.f(c,r) +for(;;)switch(s){case 0:o=p.a.a +o===$&&A.B() +s=3 +return A.c(o.el(),$async$$1$invalidate) +case 3:q=c +s=1 +break +case 1:return A.h(q,r)}}) +return A.i($async$$1$invalidate,r)}, +$S:71} +A.rB.prototype={ +$1(a){var s,r,q +$.dC().a_(B.v,"Broadcasting sync event: "+a.j(0),null,null) +s=this.a +s.r=a +r=A.wk(a) +for(s=s.x,s=new A.fg(s,s.r,s.e);s.l();){q=s.d.a +q===$&&A.B() +q.f.postMessage({type:"notifySyncStatus",payload:r})}}, +$S:72} +A.fY.prototype={$ibm:1} +A.hn.prototype={$ibm:1} +A.h5.prototype={$ibm:1} +A.h3.prototype={$ibm:1} +A.fX.prototype={$ibm:1} +A.aE.prototype={ +av(){return"SyncWorkerMessageType."+this.b}} +A.p_.prototype={ +$1(a){var s,r,q,p,o +t.c.a(a) +s=t.o.b(a)?a:new A.al(a,A.a1(a).h("al<1,d>")) +r=J.a2(s) +q=r.gk(s)===2 +if(q){p=r.i(s,0) +o=r.i(s,1)}else{p=null +o=null}if(!q)throw A.a(A.u("Pattern matching error")) +return new A.k9(p,o)}, +$S:73} +A.jx.prototype={ +ku(a,b,c,d){var s=this.f +s.start() +A.aF(s,"message",new A.pw(this),!1,t.m)}, +cS(a){var s,r,q=this +if(q.c)A.p(A.u("Channel has error, cannot send new requests")) +s=q.b++ +r=new A.l($.n,t.ny) +q.a.m(0,s,new A.M(r,t.gW)) +q.f.postMessage({type:a.b,payload:s}) +return r}, +ev(){var s=0,r=A.j(t.H),q=this +var $async$ev=A.e(function(a,b){if(a===1)return A.f(b,r) +for(;;)switch(s){case 0:s=2 +return A.c(q.cS(B.N),$async$ev) +case 2:return A.h(null,r)}}) +return A.i($async$ev,r)}, +ex(){var s=0,r=A.j(t.m),q,p=this,o +var $async$ex=A.e(function(a,b){if(a===1)return A.f(b,r) +for(;;)switch(s){case 0:o=A +s=3 +return A.c(p.cS(B.O),$async$ex) +case 3:q=o.a4(b) +s=1 +break +case 1:return A.h(q,r)}}) +return A.i($async$ex,r)}, +ef(){var s=0,r=A.j(t.B),q,p=this,o,n +var $async$ef=A.e(function(a,b){if(a===1)return A.f(b,r) +for(;;)switch(s){case 0:n=A +s=3 +return A.c(p.cS(B.R),$async$ef) +case 3:o=n.rS(b) +q=o==null?null:A.wj(o) +s=1 +break +case 1:return A.h(q,r)}}) +return A.i($async$ef,r)}, +el(){var s=0,r=A.j(t.B),q,p=this,o,n +var $async$el=A.e(function(a,b){if(a===1)return A.f(b,r) +for(;;)switch(s){case 0:n=A +s=3 +return A.c(p.cS(B.Q),$async$el) +case 3:o=n.rS(b) +q=o==null?null:A.wj(o) +s=1 +break +case 1:return A.h(q,r)}}) +return A.i($async$el,r)}, +eD(){var s=0,r=A.j(t.H),q=this +var $async$eD=A.e(function(a,b){if(a===1)return A.f(b,r) +for(;;)switch(s){case 0:s=2 +return A.c(q.cS(B.P),$async$eD) +case 2:return A.h(null,r)}}) +return A.i($async$eD,r)}} +A.pw.prototype={ +$1(a){return this.jN(a)}, +jN(a0){var s=0,r=A.j(t.H),q,p=2,o=[],n=this,m,l,k,j,i,h,g,f,e,d,c,b,a +var $async$$1=A.e(function(a1,a2){if(a1===1){o.push(a2) +s=p}for(;;)A:switch(s){case 0:e=A.a4(a0.data) +d=A.ia(B.bC,e.type) +c=n.a +b=c.x +b.a_(B.v,"[in] "+A.o(d),null,null) +m=null +switch(d){case B.N:m=A.S(A.cD(e.payload)) +c.f.postMessage({type:"okResponse",payload:{requestId:m,payload:null}}) +s=1 +break A +case B.al:m=A.a4(e.payload).requestId +break +case B.ao:m=A.a4(e.payload).requestId +break +case B.O:case B.ap:case B.R:case B.Q:case B.P:m=A.S(A.cD(e.payload)) +break +case B.am:g=A.a4(e.payload) +c.a.E(0,g.requestId).W(g.payload) +s=1 +break A +case B.an:g=A.a4(e.payload) +c.a.E(0,g.requestId).ao(g.errorMessage) +s=1 +break A +case B.aq:c.w.q(0,new A.au(d,e.payload)) +s=1 +break A +case B.ar:b.a_(B.j,"[Sync Worker]: "+A.av(e.payload),null,null) +s=1 +break A}p=4 +l=null +k=null +b=c.r.$2(d,e.payload) +s=7 +return A.c(t.nK.b(b)?b:A.h8(b,t.iu),$async$$1) +case 7:j=a2 +l=j.a +k=j.b +i={type:"okResponse",payload:{requestId:m,payload:l}} +b=c.f +if(k!=null)b.postMessage(i,k) +else b.postMessage(i) +p=2 +s=6 +break +case 4:p=3 +a=o.pop() +h=A.H(a) +c.f.postMessage({type:"errorResponse",payload:{requestId:m,errorMessage:J.aZ(h)}}) +s=6 +break +case 3:s=2 +break +case 6:case 1:return A.h(q,r) +case 2:return A.f(o.at(-1),r)}}) +return A.i($async$$1,r)}, +$S:75} +A.pg.prototype={ +eH(a,b,c){return this.oC(a,b,c,c)}, +oC(a,b,c,d){var s=0,r=A.j(d),q,p=this +var $async$eH=A.e(function(e,f){if(e===1)return A.f(f,r) +for(;;)switch(s){case 0:q=p.b.oA(a,b,null,c) +s=1 +break +case 1:return A.h(q,r)}}) +return A.i($async$eH,r)}} +A.tS.prototype={ +$1(a){var s=A.a4(a.data) +if(s.isForSyncWorker)A.AT(A.a4(s.message),this.a) +else this.b.q(0,new v.G.MessageEvent("message",{data:s.message}))}, +$S:2} +A.tT.prototype={ +$1(a){a.start() +A.aF(a,"message",this.a,!1,t.m)}, +$S:2} +A.tR.prototype={ +$1(a){var s,r=a.ports +r=J.U(t.ip.b(r)?r:new A.al(r,A.a1(r).h("al<1,w>"))) +s=this.a +while(r.l())s.$1(r.gp())}, +$S:2} +A.qA.prototype={ +giV(){return this.a}, +gnM(){return this.b}} +A.nw.prototype={} +A.nx.prototype={ +dG(){return this.a.dG()}} +A.o0.prototype={ +gk(a){return this.c.length}, +gnU(){return this.b.length}, +kq(a,b){var s,r,q,p,o,n,m,l,k +for(s=this.c,r=s.length,q=a.a,p=s.$flags|0,o=q.length,n=this.b,m=0;m=o||q.charCodeAt(k)!==10)l=10}if(l===10)n.push(m+1)}}, +cJ(a){var s,r=this +if(a<0)throw A.a(A.aA("Offset may not be negative, was "+a+".")) +else if(a>r.c.length)throw A.a(A.aA("Offset "+a+u.D+r.gk(0)+".")) +s=r.b +if(a=B.d.gaS(s))return s.length-1 +if(r.le(a)){s=r.d +s.toString +return s}return r.d=r.kG(a)-1}, +le(a){var s,r,q=this.d +if(q==null)return!1 +s=this.b +if(a=r-1||a=r-2||aa)p=r +else s=r+1}return p}, +eR(a){var s,r,q=this +if(a<0)throw A.a(A.aA("Offset may not be negative, was "+a+".")) +else if(a>q.c.length)throw A.a(A.aA("Offset "+a+" must be not be greater than the number of characters in the file, "+q.gk(0)+".")) +s=q.cJ(a) +r=q.b[s] +if(r>a)throw A.a(A.aA("Line "+s+" comes after offset "+a+".")) +return a-r}, +dD(a){var s,r,q,p +if(a<0)throw A.a(A.aA("Line may not be negative, was "+a+".")) +else{s=this.b +r=s.length +if(a>=r)throw A.a(A.aA("Line "+a+" must be less than the number of lines in the file, "+this.gnU()+"."))}q=s[a] +if(q<=this.c.length){p=a+1 +s=p=s[p]}else s=!0 +if(s)throw A.a(A.aA("Line "+a+" doesn't have 0 columns.")) +return q}} +A.ie.prototype={ +gK(){return this.a.a}, +gV(){return this.a.cJ(this.b)}, +ga3(){return this.a.eR(this.b)}, +ga5(){return this.b}} +A.ei.prototype={ +gK(){return this.a.a}, +gk(a){return this.c-this.b}, +gD(){return A.uk(this.a,this.b)}, +gC(){return A.uk(this.a,this.c)}, +gae(){return A.bR(B.K.bb(this.a.c,this.b,this.c),0,null)}, +gaH(){var s=this,r=s.a,q=s.c,p=r.cJ(q) +if(r.eR(q)===0&&p!==0){if(q-s.b===0)return p===r.b.length-1?"":A.bR(B.K.bb(r.c,r.dD(p),r.dD(p+1)),0,null)}else q=p===r.b.length-1?r.c.length:r.dD(p+1) +return A.bR(B.K.bb(r.c,r.dD(r.cJ(s.b)),q),0,null)}, +S(a,b){var s +if(!(b instanceof A.ei))return this.kh(0,b) +s=B.b.S(this.b,b.b) +return s===0?B.b.S(this.c,b.c):s}, +H(a,b){var s=this +if(b==null)return!1 +if(!(b instanceof A.ei))return s.kg(0,b) +return s.b===b.b&&s.c===b.c&&J.y(s.a.a,b.a.a)}, +gB(a){return A.bN(this.b,this.c,this.a.a,B.c,B.c,B.c,B.c,B.c,B.c,B.c)}, +$ic3:1} +A.mE.prototype={ +nK(){var s,r,q,p,o,n,m,l,k,j,i,h,g,f,e,d,c,b,a=this,a0=null,a1=a.a +a.iK(B.d.gai(a1).c) +s=a.e +r=A.aW(s,a0,!1,t.dd) +for(q=a.r,s=s!==0,p=a.b,o=0;o0){m=a1[o-1] +l=n.c +if(!J.y(m.c,l)){a.dV("\u2575") +q.a+="\n" +a.iK(l)}else if(m.b+1!==n.b){a.mg("...") +q.a+="\n"}}for(l=n.d,k=A.a1(l).h("cV<1>"),j=new A.cV(l,k),j=new A.aq(j,j.gk(0),k.h("aq")),k=k.h("W.E"),i=n.b,h=n.a;j.l();){g=j.d +if(g==null)g=k.a(g) +f=g.a +if(f.gD().gV()!==f.gC().gV()&&f.gD().gV()===i&&a.lf(B.a.t(h,0,f.gD().ga3()))){e=B.d.cr(r,a0) +if(e<0)A.p(A.K(A.o(r)+" contains no null elements.",a0)) +r[e]=g}}a.mf(i) +q.a+=" " +a.me(n,r) +if(s)q.a+=" " +d=B.d.nN(l,new A.mZ()) +c=d===-1?a0:l[d] +k=c!=null +if(k){j=c.a +g=j.gD().gV()===i?j.gD().ga3():0 +a.mc(h,g,j.gC().gV()===i?j.gC().ga3():h.length,p)}else a.dX(h) +q.a+="\n" +if(k)a.md(n,c,r) +for(l=l.length,b=0;b")),q=this.r,r=r.h("C.E");s.l();){p=s.d +if(p==null)p=r.a(p) +if(p===9)q.a+=B.a.aK(" ",4) +else{p=A.aQ(p) +q.a+=p}}}, +dW(a,b,c){var s={} +s.a=c +if(b!=null)s.a=B.b.j(b+1) +this.aM(new A.mX(s,this,a),"\x1b[34m")}, +dV(a){return this.dW(a,null,null)}, +mg(a){return this.dW(null,null,a)}, +mf(a){return this.dW(null,a,null)}, +fL(){return this.dW(null,null,null)}, +fa(a){var s,r,q,p +for(s=new A.bv(a),r=t.V,s=new A.aq(s,s.gk(0),r.h("aq")),r=r.h("C.E"),q=0;s.l();){p=s.d +if((p==null?r.a(p):p)===9)++q}return q}, +lf(a){var s,r,q +for(s=new A.bv(a),r=t.V,s=new A.aq(s,s.gk(0),r.h("aq")),r=r.h("C.E");s.l();){q=s.d +if(q==null)q=r.a(q) +if(q!==32&&q!==9)return!1}return!0}, +kN(a,b){var s,r=this.b!=null +if(r&&b!=null)this.r.a+=b +s=a.$0() +if(r&&b!=null)this.r.a+="\x1b[0m" +return s}, +aM(a,b){return this.kN(a,b,t.z)}} +A.mY.prototype={ +$0(){return this.a}, +$S:77} +A.mG.prototype={ +$1(a){var s=a.d +return new A.d5(s,new A.mF(),A.a1(s).h("d5<1>")).gk(0)}, +$S:78} +A.mF.prototype={ +$1(a){var s=a.a +return s.gD().gV()!==s.gC().gV()}, +$S:24} +A.mH.prototype={ +$1(a){return a.c}, +$S:80} +A.mJ.prototype={ +$1(a){var s=a.a.gK() +return s==null?new A.k():s}, +$S:81} +A.mK.prototype={ +$2(a,b){return a.a.S(0,b.a)}, +$S:82} +A.mL.prototype={ +$1(a){var s,r,q,p,o,n,m,l,k,j,i,h,g,f,e,d=a.a,c=a.b,b=A.v([],t.dg) +for(s=J.bq(c),r=s.gv(c),q=t.g7;r.l();){p=r.gp().a +o=p.gaH() +n=A.tG(o,p.gae(),p.gD().ga3()) +n.toString +m=B.a.e6("\n",B.a.t(o,0,n)).gk(0) +l=p.gD().gV()-m +for(p=o.split("\n"),n=p.length,k=0;kB.d.gaS(b).b)b.push(new A.bF(j,l,d,A.v([],q)));++l}}i=A.v([],q) +for(r=b.length,h=i.$flags|0,g=0,k=0;k")),n=j.b,p=p.h("W.E");q.l();){e=q.d +if(e==null)e=p.a(e) +if(e.a.gD().gV()>n)break +i.push(e)}g+=i.length-f +B.d.a8(j.d,i)}return b}, +$S:83} +A.mI.prototype={ +$1(a){return a.a.gC().gV()" +return null}, +$S:0} +A.mT.prototype={ +$0(){var s=this.a.r,r=this.b===this.c.b?"\u250c":"\u2514" +s.a+=r}, +$S:1} +A.mU.prototype={ +$0(){var s=this.a.r,r=this.b==null?"\u2500":"\u253c" +s.a+=r}, +$S:1} +A.mV.prototype={ +$0(){this.a.r.a+="\u2500" +return null}, +$S:0} +A.mW.prototype={ +$0(){var s,r,q=this,p=q.a,o=p.a?"\u253c":"\u2502" +if(q.c!=null)q.b.r.a+=o +else{s=q.e +r=s.b +if(q.d===r){s=q.b +s.aM(new A.mR(p,s),p.b) +p.a=!0 +if(p.b==null)p.b=s.b}else{s=q.r===r&&q.f.a.gC().ga3()===s.a.length +r=q.b +if(s)r.r.a+="\u2514" +else r.aM(new A.mS(r,o),p.b)}}}, +$S:1} +A.mR.prototype={ +$0(){var s=this.b.r,r=this.a.a?"\u252c":"\u250c" +s.a+=r}, +$S:1} +A.mS.prototype={ +$0(){this.a.r.a+=this.b}, +$S:1} +A.mN.prototype={ +$0(){var s=this +return s.a.dX(B.a.t(s.b,s.c,s.d))}, +$S:0} +A.mO.prototype={ +$0(){var s,r,q=this.a,p=q.r,o=p.a,n=this.c.a,m=n.gD().ga3(),l=n.gC().ga3() +n=this.b.a +s=q.fa(B.a.t(n,0,m)) +r=q.fa(B.a.t(n,m,l)) +m+=s*3 +n=(p.a+=B.a.aK(" ",m))+B.a.aK("^",Math.max(l+(s+r)*3-m,1)) +p.a=n +return n.length-o.length}, +$S:25} +A.mP.prototype={ +$0(){return this.a.mb(this.b,this.c.a.gD().ga3())}, +$S:0} +A.mQ.prototype={ +$0(){var s=this,r=s.a,q=r.r,p=q.a +if(s.b)q.a=p+B.a.aK("\u2500",3) +else r.iJ(s.c,Math.max(s.d.a.gC().ga3()-1,0),!1) +return q.a.length-p.length}, +$S:25} +A.mX.prototype={ +$0(){var s=this.b,r=s.r,q=this.a.a +if(q==null)q="" +s=B.a.oc(q,s.d) +s=r.a+=s +q=this.c +r.a=s+(q==null?"\u2502":q)}, +$S:1} +A.aM.prototype={ +j(a){var s=this.a +s="primary "+(""+s.gD().gV()+":"+s.gD().ga3()+"-"+s.gC().gV()+":"+s.gC().ga3()) +return s.charCodeAt(0)==0?s:s}} +A.qU.prototype={ +$0(){var s,r,q,p,o=this.a +if(!(t.ol.b(o)&&A.tG(o.gaH(),o.gae(),o.gD().ga3())!=null)){s=A.j2(o.gD().ga5(),0,0,o.gK()) +r=o.gC().ga5() +q=o.gK() +p=A.D4(o.gae(),10) +o=A.o1(s,A.j2(r,A.wM(o.gae()),p,q),o.gae(),o.gae())}return A.AZ(A.B0(A.B_(o)))}, +$S:85} +A.bF.prototype={ +j(a){return""+this.b+': "'+this.a+'" ('+B.d.bF(this.d,", ")+")"}} +A.bC.prototype={ +h_(a){var s=this.a +if(!J.y(s,a.gK()))throw A.a(A.K('Source URLs "'+A.o(s)+'" and "'+A.o(a.gK())+"\" don't match.",null)) +return Math.abs(this.b-a.ga5())}, +S(a,b){var s=this.a +if(!J.y(s,b.gK()))throw A.a(A.K('Source URLs "'+A.o(s)+'" and "'+A.o(b.gK())+"\" don't match.",null)) +return this.b-b.ga5()}, +H(a,b){if(b==null)return!1 +return t.hq.b(b)&&J.y(this.a,b.gK())&&this.b===b.ga5()}, +gB(a){var s=this.a +s=s==null?null:s.gB(s) +if(s==null)s=0 +return s+this.b}, +j(a){var s=this,r=A.tK(s).j(0),q=s.a +return"<"+r+": "+s.b+" "+(A.o(q==null?"unknown source":q)+":"+(s.c+1)+":"+(s.d+1))+">"}, +$ia7:1, +gK(){return this.a}, +ga5(){return this.b}, +gV(){return this.c}, +ga3(){return this.d}} +A.j3.prototype={ +h_(a){if(!J.y(this.a.a,a.gK()))throw A.a(A.K('Source URLs "'+A.o(this.gK())+'" and "'+A.o(a.gK())+"\" don't match.",null)) +return Math.abs(this.b-a.ga5())}, +S(a,b){if(!J.y(this.a.a,b.gK()))throw A.a(A.K('Source URLs "'+A.o(this.gK())+'" and "'+A.o(b.gK())+"\" don't match.",null)) +return this.b-b.ga5()}, +H(a,b){if(b==null)return!1 +return t.hq.b(b)&&J.y(this.a.a,b.gK())&&this.b===b.ga5()}, +gB(a){var s=this.a.a +s=s==null?null:s.gB(s) +if(s==null)s=0 +return s+this.b}, +j(a){var s=A.tK(this).j(0),r=this.b,q=this.a,p=q.a +return"<"+s+": "+r+" "+(A.o(p==null?"unknown source":p)+":"+(q.cJ(r)+1)+":"+(q.eR(r)+1))+">"}, +$ia7:1, +$ibC:1} +A.j5.prototype={ +kr(a,b,c){var s,r=this.b,q=this.a +if(!J.y(r.gK(),q.gK()))throw A.a(A.K('Source URLs "'+A.o(q.gK())+'" and "'+A.o(r.gK())+"\" don't match.",null)) +else if(r.ga5()'}, +$ia7:1} +A.c3.prototype={ +gaH(){return this.d}} +A.e4.prototype={ +av(){return"SqliteUpdateKind."+this.b}} +A.b6.prototype={ +gB(a){return A.bN(this.a,this.b,this.c,B.c,B.c,B.c,B.c,B.c,B.c,B.c)}, +H(a,b){if(b==null)return!1 +return b instanceof A.b6&&b.a===this.a&&b.b===this.b&&b.c===this.c}, +j(a){return"SqliteUpdate: "+this.a.j(0)+" on "+this.b+", rowid = "+this.c}} +A.cX.prototype={ +j(a){var s,r,q=this,p=q.e +p=p==null?"":"while "+p+", " +p="SqliteException("+q.c+"): "+p+q.a +s=q.b +if(s!=null)p=p+", "+s +s=q.f +if(s!=null){r=q.d +r=r!=null?" (at position "+A.o(r)+"): ":": " +s=p+"\n Causing statement"+r+s +p=q.r +p=p!=null?s+(", parameters: "+new A.a8(p,new A.o5(),A.a1(p).h("a8<1,d>")).bF(0,", ")):s}return p.charCodeAt(0)==0?p:p}, +$iV:1} +A.o5.prototype={ +$1(a){if(t.p.b(a))return"blob ("+a.length+" bytes)" +else return J.aZ(a)}, +$S:34} +A.lZ.prototype={ +iH(){var s=this,r=s.d +return r==null?s.d=new A.cA(s,A.v([],t.fU),new A.m7(s),new A.m8(s),t.eZ):r}, +lT(){var s=this,r=s.e +return r==null?s.e=new A.cA(s,A.v([],t.lw),new A.m4(s),new A.m5(s),t.lU):r}, +f8(){var s=this,r=s.f +return r==null?s.f=new A.cA(s,A.v([],t.lw),new A.m0(s),new A.m1(s),t.af):r}, +n(){var s,r,q,p,o,n=this,m=null +if(n.r)return +n.r=!0 +s=n.d +if(s!=null)s.n() +s=n.f +if(s!=null)s.n() +s=n.e +if(s!=null)s.n() +s=n.b +r=s.a +q=s.b +r.fY(q,m) +r.fW(q,m) +r.fX(q,m) +p=s.hr() +o=p!==0?A.vd(n.a,s,p,"closing database",m,m):m +if(o!=null)throw A.a(o)}, +ab(a,b){var s,r,q,p=this +if(b.length===0){if(p.r)A.p(A.u("This database has already been closed")) +r=p.b +q=r.a +s=q.d6(B.n.ap(a),1) +q=q.d +r=A.xR(q,"sqlite3_exec",[r.b,s,0,0,0]) +q.dart_sqlite3_free(s) +if(r!==0)A.kJ(p,r,"executing",a,b)}else{s=p.hi(a,!0) +try{s.nk(new A.fa(b))}finally{s.n()}}}, +lD(a,b,c,d,a0){var s,r,q,p,o,n,m,l,k,j,i,h,g,f,e=this +if(e.r)A.p(A.u("This database has already been closed")) +s=B.n.ap(a) +r=e.b +q=r.a +p=q.fP(s) +o=q.d +n=o.dart_sqlite3_malloc(4) +o=o.dart_sqlite3_malloc(4) +m=new A.pf(r,p,n,o) +l=A.v([],t.lE) +k=new A.m2(m,l) +for(r=s.length,q=q.b,j=0;j"))}, +fZ(a){var s,r,q,p,o,n,m,l +for(s=this.b,r=s.length,q=0;q=4)A.p(o.aL()) +if((n&1)!==0){m=o.a;((n&8)!==0?m.c:m).af(a)}}else{n=o.b +if(n>=4)A.p(o.aL()) +if((n&1)!==0)o.aE(a) +else if((n&3)===0){o=o.cQ() +n=new A.c9(a) +l=o.c +if(l==null)o.b=o.c=n +else{l.sc1(n) +o.c=n}}}}}, +n(){var s,r,q +for(s=this.b,r=s.length,q=0;q)")}} +A.rq.prototype={ +$0(){var s=this.a,r=s.b,q=r.length +r.push(new A.hl(this.b,this.c)) +if(q===0)s.d.$0()}, +$S:0} +A.rr.prototype={ +$0(){var s=this.a,r=s.b +B.d.E(r,new A.hl(this.b,this.c)) +r=r.length +if(r===0&&!s.a.r)s.e.$0()}, +$S:0} +A.o2.prototype={ +jb(){var s=null,r=this.a.a.d.sqlite3_initialize() +if(r!==0)throw A.a(A.ja(s,s,r,"Error returned by sqlite3_initialize",s,s,s))}, +o9(a,b){var s,r,q,p,o,n,m,l,k,j +this.jb() +switch(2){case 2:break}s=this.a +r=s.a +q=r.d6(B.n.ap(a),1) +p=r.d +o=p.dart_sqlite3_malloc(4) +n=r.d6(B.n.ap(b),1) +m=p.sqlite3_open_v2(q,o,6,n) +l=A.c1(r.b.buffer,0,null)[B.b.Y(o,2)] +p.dart_sqlite3_free(q) +p.dart_sqlite3_free(n) +p.dart_sqlite3_free(n) +o=new A.k() +k=new A.p8(r,l,o) +r=r.r +if(r!=null)r.iP(k,l,o) +if(m!==0){j=A.vd(s,k,m,"opening the database",null,null) +k.hr() +throw A.a(j)}p.sqlite3_extended_result_codes(l,1) +return new A.lZ(s,k,!1)}} +A.fE.prototype={ +gkO(){var s,r,q,p,o,n,m,l=this.a,k=l.c +l=l.b +s=k.d +r=s.sqlite3_column_count(l) +q=A.v([],t.s) +for(k=k.b,p=0;p0)A.p(A.uj("BigInt value exceeds the range of 64 bits")) +s=s.c.d.sqlite3_bind_int64(s.b,b,v.G.BigInt(a.j(0))) +break A}if(A.dt(a)){s=o.a +r=a?1:0 +s=s.c.d.sqlite3_bind_int64(s.b,b,v.G.BigInt(r)) +break A}if(typeof a=="number"){s=o.a +s=s.c.d.sqlite3_bind_double(s.b,b,a) +break A}if(typeof a=="string"){s=o.a +q=B.n.ap(a) +p=s.c +p=p.d.dart_sqlite3_bind_text(s.b,b,p.fP(q),q.length) +s=p +break A}if(t.f4.b(a)){s=o.a +p=s.c +p=p.d.dart_sqlite3_bind_blob(s.b,b,p.fP(a),J.ay(a)) +s=p +break A}s=o.kH(a,b) +break A}if(s!==0)A.kJ(o.b,s,"binding parameter",o.d,o.e)}, +kH(a,b){throw A.a(A.aH(a,"params["+b+"]","Allowed parameters must either be null or bool, int, num, String or List."))}, +eX(a){A:{this.kI(a.a) +break A}}, +hl(){if(!this.f){var s=this.a +s.c.d.sqlite3_reset(s.b) +this.f=!0}}, +n(){var s,r,q=this +if(!q.r){q.r=!0 +q.hl() +s=q.a +r=s.c +r.d.sqlite3_finalize(s.b) +r=r.w +if(r!=null)r.iY(s.d)}}, +nk(a){var s=this +s.hT() +s.hl() +s.eX(a) +s.hV()}} +A.ig.prototype={ +dw(a,b){return this.d.F(a)?1:0}, +eJ(a,b){this.d.E(0,a)}, +eK(a){return $.hI().cA("/"+a)}, +bM(a,b){var s,r=a.a +if(r==null)r=A.uo(this.b,"/") +s=this.d +if(!s.F(r))if((b&4)!==0)s.m(0,r,new A.bE(new Uint8Array(0),0)) +else throw A.a(A.cu(14)) +return new A.dj(new A.jV(this,r,(b&8)!==0),0)}, +eN(a){}} +A.jV.prototype={ +hj(a,b){var s,r=this.a.d.i(0,this.b) +if(r==null||r.b<=b)return 0 +s=Math.min(a.length,r.b-b) +B.f.L(a,0,s,J.cg(B.f.gaG(r.a),0,r.b),b) +return s}, +eI(){return this.d>=2?1:0}, +dz(){if(this.c)this.a.d.E(0,this.b)}, +cH(){return this.a.d.i(0,this.b).b}, +eL(a){this.d=a}, +eO(a){}, +cI(a){var s=this.a.d,r=this.b,q=s.i(0,r) +if(q==null){s.m(0,r,new A.bE(new Uint8Array(0),0)) +s.i(0,r).sk(0,a)}else q.sk(0,a)}, +eP(a){this.d=a}, +ca(a,b){var s,r=this.a.d,q=this.b,p=r.i(0,q) +if(p==null){p=new A.bE(new Uint8Array(0),0) +r.m(0,q,p)}s=b+a.length +if(s>p.b)p.sk(0,s) +p.al(0,b,s,a)}} +A.lH.prototype={ +kK(){var s,r,q,p,o=A.P(t.N,t.S) +for(s=this.a,r=s.length,q=0;qq.d)throw A.a(A.cu(14)) +s=q.a.b +s===$&&A.B() +s=A.bg(s.buffer,0,null) +r=q.e +B.f.bQ(s,r,p) +s.$flags&2&&A.D(s) +s[r+o]=0}, +$S:0} +A.lQ.prototype={ +$0(){var s,r=this,q=r.a.b +q===$&&A.B() +s=A.bg(q.buffer,r.b,r.c) +q=r.d +if(q!=null)A.vB(s,q.b) +else return A.vB(s,null)}, +$S:0} +A.lS.prototype={ +$0(){this.a.eN(A.mf(this.b,0))}, +$S:0} +A.lL.prototype={ +$0(){return this.a.dz()}, +$S:0} +A.lR.prototype={ +$0(){var s=this,r=s.a.b +r===$&&A.B() +s.b.eM(A.bg(r.buffer,s.c,s.d),A.S(v.G.Number(s.e)))}, +$S:0} +A.lW.prototype={ +$0(){var s=this,r=s.a.b +r===$&&A.B() +s.b.ca(A.bg(r.buffer,s.c,s.d),A.S(v.G.Number(s.e)))}, +$S:0} +A.lU.prototype={ +$0(){return this.a.cI(A.S(v.G.Number(this.b)))}, +$S:0} +A.lT.prototype={ +$0(){return this.a.eO(this.b)}, +$S:0} +A.lN.prototype={ +$0(){var s,r=this.b.cH(),q=this.a.b +q===$&&A.B() +q=A.c1(q.buffer,0,null) +s=B.b.Y(this.c,2) +q.$flags&2&&A.D(q) +q[s]=r}, +$S:0} +A.lP.prototype={ +$0(){return this.a.eL(this.b)}, +$S:0} +A.lV.prototype={ +$0(){return this.a.eP(this.b)}, +$S:0} +A.lK.prototype={ +$0(){var s,r=this.b.eI(),q=this.a.b +q===$&&A.B() +q=A.c1(q.buffer,0,null) +s=B.b.Y(this.c,2) +q.$flags&2&&A.D(q) +q[s]=r}, +$S:0} +A.eN.prototype={ +A(a,b,c,d){var s,r=null,q={},p=A.a4(A.iq(this.a,v.G.Symbol.asyncIterator,r,r,r,r)),o=A.bi(r,r,r,r,!0,this.$ti.c) +q.a=null +s=new A.kW(q,this,p,o) +o.d=s +o.f=new A.kX(q,o,s) +return new A.O(o,A.q(o).h("O<1>")).A(a,b,c,d)}, +Z(a){return this.A(a,null,null,null)}, +aj(a,b,c){return this.A(a,null,b,c)}, +bk(a,b,c){return this.A(a,b,c,null)}} +A.kW.prototype={ +$0(){var s,r=this,q=r.c.next(),p=r.a +p.a=q +s=r.d +A.ac(q,t.m).b9(new A.kY(p,r.b,s,r),s.gd5(),t.P)}, +$S:0} +A.kY.prototype={ +$1(a){var s,r,q=this,p=a.done +if(p==null)p=null +s=a.value +r=q.c +if(p===!0){r.n() +q.a.a=null}else{r.q(0,s==null?q.b.$ti.c.a(s):s) +q.a.a=null +p=r.b +if(!((p&1)!==0?(r.gan().e&4)!==0:(p&2)===0))q.d.$0()}}, +$S:10} +A.kX.prototype={ +$0(){var s,r +if(this.a.a==null){s=this.b +r=s.b +s=!((r&1)!==0?(s.gan().e&4)!==0:(r&2)===0)}else s=!1 +if(s)this.c.$0()}, +$S:0} +A.dd.prototype={ +u(){var s=0,r=A.j(t.H),q=this,p +var $async$u=A.e(function(a,b){if(a===1)return A.f(b,r) +for(;;)switch(s){case 0:p=q.b +if(p!=null)p.u() +p=q.c +if(p!=null)p.u() +q.c=q.b=null +return A.h(null,r)}}) +return A.i($async$u,r)}, +gp(){var s=this.a +return s==null?A.p(A.u("Await moveNext() first")):s}, +l(){var s,r,q,p=this,o=p.a +if(o!=null)o.continue() +o=new A.l($.n,t.x) +s=new A.M(o,t.ex) +r=p.d +q=t.m +p.b=A.aF(r,"success",new A.qo(p,s),!1,q) +p.c=A.aF(r,"error",new A.qp(p,s),!1,q) +return o}} +A.qo.prototype={ +$1(a){var s,r=this.a +r.u() +s=r.$ti.h("1?").a(r.d.result) +r.a=s +this.b.W(s!=null)}, +$S:2} +A.qp.prototype={ +$1(a){var s=this.a +s.u() +s=s.d.error +if(s==null)s=a +this.b.ao(s)}, +$S:2} +A.lt.prototype={ +$1(a){this.a.W(this.c.a(this.b.result))}, +$S:2} +A.lu.prototype={ +$1(a){var s=this.b.error +if(s==null)s=a +this.a.ao(s)}, +$S:2} +A.ly.prototype={ +$1(a){this.a.W(this.c.a(this.b.result))}, +$S:2} +A.lz.prototype={ +$1(a){var s=this.b.error +if(s==null)s=a +this.a.ao(s)}, +$S:2} +A.lA.prototype={ +$1(a){var s=this.b.error +if(s==null)s=a +this.a.ao(s)}, +$S:2} +A.mj.prototype={ +$1(a){return A.a4(a[1])}, +$S:109} +A.p9.prototype={ +mK(){var s={} +s.dart=new A.pa(this).$0() +return s}, +ep(a){return this.nX(a)}, +nX(a){var s=0,r=A.j(t.m),q,p=this,o,n +var $async$ep=A.e(function(b,c){if(b===1)return A.f(c,r) +for(;;)switch(s){case 0:s=3 +return A.c(A.ac(v.G.WebAssembly.instantiateStreaming(a,p.mK()),t.m),$async$ep) +case 3:o=c +n=o.instance.exports +if("_initialize" in n)t.g.a(n._initialize).call() +q=o.instance +s=1 +break +case 1:return A.h(q,r)}}) +return A.i($async$ep,r)}} +A.pa.prototype={ +$0(){var s=this.a.a,r=A.a4(v.G.Object),q=A.a4(r.create.apply(r,[null])) +q.error_log=A.bV(s.go4()) +q.localtime=A.ba(s.go_()) +q.xOpen=A.v6(s.goU()) +q.xDelete=A.t8(s.goL()) +q.xAccess=A.eC(s.goD()) +q.xFullPathname=A.eC(s.goQ()) +q.xRandomness=A.t8(s.goW()) +q.xSleep=A.ba(s.gp_()) +q.xCurrentTimeInt64=A.ba(s.goJ()) +q.xClose=A.bV(s.goH()) +q.xRead=A.eC(s.goY()) +q.xWrite=A.eC(s.gpb()) +q.xTruncate=A.ba(s.gp7()) +q.xSync=A.ba(s.gp5()) +q.xFileSize=A.ba(s.goO()) +q.xLock=A.ba(s.goS()) +q.xUnlock=A.ba(s.gp9()) +q.xCheckReservedLock=A.ba(s.goF()) +q.xDeviceCharacteristics=A.bV(s.gdA()) +q["dispatch_()v"]=A.bV(s.gn_()) +q["dispatch_()i"]=A.bV(s.gmV()) +q.dispatch_update=A.v6(s.gmY()) +q.dispatch_xFunc=A.eC(s.gn5()) +q.dispatch_xStep=A.eC(s.gn9()) +q.dispatch_xInverse=A.eC(s.gn7()) +q.dispatch_xValue=A.ba(s.gnb()) +q.dispatch_xFinal=A.ba(s.gn3()) +q.dispatch_compare=A.v6(s.gn1()) +q.dispatch_busy=A.ba(s.gmT()) +q.changeset_apply_filter=A.ba(s.gmR()) +q.changeset_apply_conflict=A.t8(s.gmP()) +return q}, +$S:22} +A.e7.prototype={} +A.fU.prototype={ +lU(a,b){var s,r,q=this.e +q.c8(b) +s=this.d.b +r=v.G +r.Atomics.store(s,1,-1) +r.Atomics.store(s,0,a.a) +A.yZ(s,0) +r.Atomics.wait(s,1,-1) +s=r.Atomics.load(s,1) +if(s!==0)throw A.a(A.cu(s)) +return a.d.$1(q)}, +aD(a,b){var s=t.jT +return this.lU(a,b,s,s)}, +dw(a,b){return this.aD(B.aC,new A.b3(a,b,0,0)).a}, +eJ(a,b){this.aD(B.aD,new A.b3(a,b,0,0))}, +eK(a){var s=this.r.bh(a) +if($.kO().lg("/",s)!==B.X)throw A.a(B.aA) +return s}, +bM(a,b){var s=a.a,r=this.aD(B.aO,new A.b3(s==null?A.uo(this.b,"/"):s,b,0,0)) +return new A.dj(new A.ju(this,r.b),r.a)}, +eN(a){this.aD(B.aI,new A.ab(B.b.M(a.a,1000),0,0))}, +n(){this.aD(B.aE,B.l)}} +A.ju.prototype={ +gdA(){return 2048}, +hj(a,b){var s,r,q,p,o,n,m,l,k,j,i=a.length +for(s=this.a,r=this.b,q=s.e.a,p=v.G,o=t.Z,n=0;i>0;){m=Math.min(65536,i) +i-=m +l=s.aD(B.aM,new A.ab(r,b+n,m)).a +k=p.Uint8Array +j=[q] +j.push(0) +j.push(l) +A.iq(a,"set",o.a(A.dw(k,j)),n,null,null) +n+=l +if(l0;){o=Math.min(65536,n) +A.iq(r,"set",o===n&&p===0?a:J.cg(B.f.gaG(a),a.byteOffset+p,o),0,null,null) +s.aD(B.aH,new A.ab(q,b+p,o)) +p+=o +n-=o}}} +A.nS.prototype={} +A.bM.prototype={ +c8(a){var s,r +if(!(a instanceof A.be))if(a instanceof A.ab){s=this.b +s.$flags&2&&A.D(s,8) +s.setInt32(0,a.a,!1) +s.setInt32(4,a.b,!1) +s.setInt32(8,a.c,!1) +if(a instanceof A.b3){r=B.n.ap(a.d) +s.setInt32(12,r.length,!1) +B.f.bQ(this.c,16,r)}}else throw A.a(A.R("Message "+a.j(0)))}} +A.ap.prototype={ +av(){return"WorkerOperation."+this.b}} +A.c_.prototype={} +A.be.prototype={} +A.ab.prototype={} +A.b3.prototype={} +A.kf.prototype={} +A.fT.prototype={ +cZ(a,b){return this.lR(a,b)}, +is(a){return this.cZ(a,!1)}, +lR(a,b){var s=0,r=A.j(t.i7),q,p=this,o,n,m,l,k,j,i,h,g +var $async$cZ=A.e(function(c,d){if(c===1)return A.f(d,r) +for(;;)switch(s){case 0:j=$.hI() +i=j.hk(a,"/") +h=j.cO(0,i) +g=h.length +j=g>=1 +o=null +if(j){n=g-1 +m=B.d.bb(h,0,n) +o=h[n]}else m=null +if(!j)throw A.a(A.u("Pattern matching error")) +l=p.c +j=m.length,n=t.m,k=0 +case 3:if(!(k") +l=A.an(new A.bx(j,m),m.h("m.E")) +B.d.k8(l) +s=3 +return A.c(A.f5(new A.a8(l,new A.l3(new A.l4(o,a),b),A.a1(l).h("a8<1,r<~>>")),t.H),$async$bY) +case 3:s=b.c!==n.length?4:5 +break +case 4:k=new A.dd(p.objectStore("files").openCursor(a),t.Q) +s=6 +return A.c(k.l(),$async$bY) +case 6:s=7 +return A.c(A.bI(k.gp().update({name:n.name,length:b.c}),t.X),$async$bY) +case 7:case 5:return A.h(null,r)}}) +return A.i($async$bY,r)}, +c5(a,b,c){return this.oq(0,b,c)}, +oq(a,b,c){var s=0,r=A.j(t.H),q=this,p,o,n,m,l,k +var $async$c5=A.e(function(d,e){if(d===1)return A.f(e,r) +for(;;)switch(s){case 0:k=q.a +k.toString +p=k.transaction($.ub(),"readwrite") +o=p.objectStore("files") +n=p.objectStore("blocks") +s=2 +return A.c(q.fC(p,b),$async$c5) +case 2:m=e +s=m.length>c?3:4 +break +case 3:s=5 +return A.c(A.bI(n.delete(q.lG(b,B.b.M(c,4096)*4096+1)),t.X),$async$c5) +case 5:case 4:l=new A.dd(o.openCursor(b),t.Q) +s=6 +return A.c(l.l(),$async$c5) +case 6:s=7 +return A.c(A.bI(l.gp().update({name:m.name,length:c}),t.X),$async$c5) +case 7:return A.h(null,r)}}) +return A.i($async$c5,r)}, +eg(a){return this.mO(a)}, +mO(a){var s=0,r=A.j(t.H),q=this,p,o,n +var $async$eg=A.e(function(b,c){if(b===1)return A.f(c,r) +for(;;)switch(s){case 0:n=q.a +n.toString +p=n.transaction(A.v(["files","blocks"],t.s),"readwrite") +o=q.fB(a,9007199254740992,0) +n=t.X +s=2 +return A.c(A.f5(A.v([A.bI(p.objectStore("blocks").delete(o),n),A.bI(p.objectStore("files").delete(a),n)],t.M),t.H),$async$eg) +case 2:return A.h(null,r)}}) +return A.i($async$eg,r)}} +A.l5.prototype={ +$1(a){var s=A.a4(this.a.result) +if(J.y(a.oldVersion,0)){s.createObjectStore("files",{autoIncrement:!0}).createIndex("fileName","name",{unique:!0}) +s.createObjectStore("blocks")}}, +$S:10} +A.l2.prototype={ +$1(a){if(a==null)throw A.a(A.aH(this.a,"fileId","File not found in database")) +else return a}, +$S:111} +A.l6.prototype={ +$0(){var s=0,r=A.j(t.H),q=this,p,o +var $async$$0=A.e(function(a,b){if(a===1)return A.f(b,r) +for(;;)switch(s){case 0:p=q.a +s=A.uq(p.value,"Blob")?2:4 +break +case 2:s=5 +return A.c(A.nG(A.a4(p.value)),$async$$0) +case 5:s=3 +break +case 4:b=t.a.a(p.value) +case 3:o=b +B.f.bQ(q.b,q.c,J.cg(o,0,q.d)) +return A.h(null,r)}}) +return A.i($async$$0,r)}, +$S:3} +A.l4.prototype={ +jD(a,b){var s=0,r=A.j(t.H),q=this,p,o,n,m,l,k +var $async$$2=A.e(function(c,d){if(c===1)return A.f(d,r) +for(;;)switch(s){case 0:p=q.a +o=q.b +n=t.gk +s=2 +return A.c(A.bI(p.openCursor(v.G.IDBKeyRange.only(A.v([o,a],n))),t.A),$async$$2) +case 2:m=d +l=t.a.a(B.f.gaG(b)) +k=t.X +s=m==null?3:5 +break +case 3:s=6 +return A.c(A.bI(p.put(l,A.v([o,a],n)),k),$async$$2) +case 6:s=4 +break +case 5:s=7 +return A.c(A.bI(m.update(l),k),$async$$2) +case 7:case 4:return A.h(null,r)}}) +return A.i($async$$2,r)}, +$2(a,b){return this.jD(a,b)}, +$S:112} +A.l3.prototype={ +$1(a){var s=this.b.b.i(0,a) +s.toString +return this.a.$2(a,s)}, +$S:142} +A.qE.prototype={ +m8(a,b,c){B.f.bQ(this.b.cB(a,new A.qF(this,a)),b,c)}, +mz(a,b){var s,r,q,p,o,n,m,l +for(s=b.length,r=0;rp)B.f.bQ(s,0,J.cg(B.f.gaG(r),r.byteOffset+p,Math.min(4096,q-p))) +return s}, +$S:114} +A.k3.prototype={} +A.cP.prototype={ +cm(a){var s=this +if(s.e||s.d.a==null)A.p(A.cu(10)) +if(a.h9(s.w)){s.iA() +return a.d.a}else return A.mu(null,t.H)}, +iA(){var s,r,q=this +if(q.f==null&&!q.w.gG(0)){s=q.w +r=q.f=s.gai(0) +s.E(0,r) +r.d.W(A.un(r.gez(),t.H).O(new A.n_(q)))}}, +n(){var s=0,r=A.j(t.H),q,p=this,o,n +var $async$n=A.e(function(a,b){if(a===1)return A.f(b,r) +for(;;)switch(s){case 0:if(!p.e){o=p.cm(new A.df(p.d.gag(),new A.M(new A.l($.n,t.D),t.F))) +p.e=!0 +q=o +s=1 +break}else{n=p.w +if(!n.gG(0)){q=n.gaS(0).d.a +s=1 +break}}case 1:return A.h(q,r)}}) +return A.i($async$n,r)}, +ck(a){return this.l1(a)}, +l1(a){var s=0,r=A.j(t.S),q,p=this,o,n +var $async$ck=A.e(function(b,c){if(b===1)return A.f(c,r) +for(;;)switch(s){case 0:n=p.y +s=n.F(a)?3:5 +break +case 3:n=n.i(0,a) +n.toString +q=n +s=1 +break +s=4 +break +case 5:s=6 +return A.c(p.d.eh(a),$async$ck) +case 6:o=c +o.toString +n.m(0,a,o) +q=o +s=1 +break +case 4:case 1:return A.h(q,r)}}) +return A.i($async$ck,r)}, +cW(){var s=0,r=A.j(t.H),q=this,p,o,n,m,l,k,j,i,h,g +var $async$cW=A.e(function(a,b){if(a===1)return A.f(b,r) +for(;;)switch(s){case 0:h=q.d +s=2 +return A.c(h.eo(),$async$cW) +case 2:g=b +q.y.a8(0,g) +p=g.gbZ(),p=p.gv(p),o=q.r.d +case 3:if(!p.l()){s=4 +break}n=p.gp() +m=n.a +l=n.b +k=new A.bE(new Uint8Array(0),0) +s=5 +return A.c(h.cC(l),$async$cW) +case 5:j=b +n=j.length +k.sk(0,n) +i=k.b +if(n>i)A.p(A.a0(n,0,i,null,null)) +B.f.L(k.a,0,n,j,0) +o.m(0,m,k) +s=3 +break +case 4:return A.h(null,r)}}) +return A.i($async$cW,r)}, +aI(){return this.cm(new A.df(new A.n0(),new A.M(new A.l($.n,t.D),t.F)))}, +dw(a,b){return this.r.d.F(a)?1:0}, +eJ(a,b){var s=this +s.r.d.E(0,a) +if(!s.x.E(0,a))s.cm(new A.ee(s,a,new A.M(new A.l($.n,t.D),t.F)))}, +eK(a){return $.hI().cA("/"+a)}, +bM(a,b){var s,r,q,p=this,o=a.a +if(o==null)o=A.uo(p.b,"/") +s=p.r +r=s.d.F(o)?1:0 +q=s.bM(new A.fB(o),b) +if(r===0)if((b&8)!==0)p.x.q(0,o) +else p.cm(new A.dc(p,o,new A.M(new A.l($.n,t.D),t.F))) +return new A.dj(new A.jW(p,q.a,o),0)}, +eN(a){}} +A.n_.prototype={ +$0(){var s=this.a +s.f=null +s.iA()}, +$S:1} +A.n0.prototype={ +$0(){}, +$S:1} +A.jW.prototype={ +eM(a,b){this.b.eM(a,b)}, +gdA(){return 0}, +eI(){return this.b.d>=2?1:0}, +dz(){}, +cH(){return this.b.cH()}, +eL(a){this.b.d=a +return null}, +eO(a){}, +cI(a){var s=this,r=s.a +if(r.e||r.d.a==null)A.p(A.cu(10)) +s.b.cI(a) +if(!r.x.T(0,s.c))r.cm(new A.df(new A.qV(s,a),new A.M(new A.l($.n,t.D),t.F)))}, +eP(a){this.b.d=a +return null}, +ca(a,b){var s,r,q,p,o,n,m=this,l=m.a +if(l.e||l.d.a==null)A.p(A.cu(10)) +s=m.c +if(l.x.T(0,s)){m.b.ca(a,b) +return}r=l.r.d.i(0,s) +if(r==null)r=new A.bE(new Uint8Array(0),0) +q=J.cg(B.f.gaG(r.a),0,r.b) +m.b.ca(a,b) +p=new Uint8Array(a.length) +B.f.bQ(p,0,a) +o=A.v([],t.o6) +n=$.n +o.push(new A.k3(b,p)) +l.cm(new A.dr(l,s,q,o,new A.M(new A.l(n,t.D),t.F)))}, +$iaS:1} +A.qV.prototype={ +$0(){var s=0,r=A.j(t.H),q,p=this,o,n,m +var $async$$0=A.e(function(a,b){if(a===1)return A.f(b,r) +for(;;)switch(s){case 0:o=p.a +n=o.a +m=n.d +s=3 +return A.c(n.ck(o.c),$async$$0) +case 3:q=m.c5(0,b,p.b) +s=1 +break +case 1:return A.h(q,r)}}) +return A.i($async$$0,r)}, +$S:3} +A.aG.prototype={ +h9(a){a.fs(a.c,this,!1) +return!0}} +A.df.prototype={ +ac(){return this.w.$0()}} +A.ee.prototype={ +h9(a){var s,r,q,p +if(!a.gG(0)){s=a.gaS(0) +for(r=this.x;s!=null;)if(s instanceof A.ee)if(s.x===r)return!1 +else s=s.gds() +else if(s instanceof A.dr){q=s.gds() +if(s.x===r){p=s.a +p.toString +p.fI(A.q(s).h("aV.E").a(s))}s=q}else if(s instanceof A.dc){if(s.x===r){r=s.a +r.toString +r.fI(A.q(s).h("aV.E").a(s)) +return!1}s=s.gds()}else break}a.fs(a.c,this,!1) +return!0}, +ac(){var s=0,r=A.j(t.H),q=this,p,o,n +var $async$ac=A.e(function(a,b){if(a===1)return A.f(b,r) +for(;;)switch(s){case 0:p=q.w +o=q.x +s=2 +return A.c(p.ck(o),$async$ac) +case 2:n=b +p.y.E(0,o) +s=3 +return A.c(p.d.eg(n),$async$ac) +case 3:return A.h(null,r)}}) +return A.i($async$ac,r)}} +A.dc.prototype={ +ac(){var s=0,r=A.j(t.H),q=this,p,o,n,m +var $async$ac=A.e(function(a,b){if(a===1)return A.f(b,r) +for(;;)switch(s){case 0:p=q.w +o=q.x +n=p.y +m=o +s=2 +return A.c(p.d.ee(o),$async$ac) +case 2:n.m(0,m,b) +return A.h(null,r)}}) +return A.i($async$ac,r)}} +A.dr.prototype={ +h9(a){var s,r=a.b===0?null:a.gaS(0) +for(s=this.x;r!=null;)if(r instanceof A.dr)if(r.x===s){B.d.a8(r.z,this.z) +return!1}else r=r.gds() +else if(r instanceof A.dc){if(r.x===s)break +r=r.gds()}else break +a.fs(a.c,this,!1) +return!0}, +ac(){var s=0,r=A.j(t.H),q=this,p,o,n,m,l,k +var $async$ac=A.e(function(a,b){if(a===1)return A.f(b,r) +for(;;)switch(s){case 0:m=q.y +l=new A.qE(m,A.P(t.S,t.p),m.length) +for(m=q.z,p=m.length,o=0;o=2?1:0}, +dz(){var s=this +s.c.flush() +if(s.d)s.a.fv(s.b,!1)}, +cH(){return this.c.getSize()}, +eL(a){this.e=a}, +eO(a){this.c.flush()}, +cI(a){this.c.truncate(a)}, +eP(a){this.e=a}, +ca(a,b){if(A.um(this.c,a,{at:b})")).nW(this.gl5(),new A.nD(this))}, +fp(a){return this.l6(a)}, +l6(a){var s=0,r=A.j(t.H),q=this +var $async$fp=A.e(function(b,c){if(b===1)return A.f(c,r) +for(;;)switch(s){case 0:A.D7(a,new A.nz(q),q.gj7(),new A.nA(q),new A.nB(q),new A.nC()) +return A.h(null,r)}}) +return A.i($async$fp,r)}, +bP(a,b,c,d){return this.k7(a,b,c,d,d)}, +bO(a,b,c){return this.bP(a,b,null,c)}, +k7(a,b,c,d,e){var s=0,r=A.j(e),q,p=this,o,n,m,l,k +var $async$bP=A.e(function(f,g){if(f===1)return A.f(g,r) +for(;;)switch(s){case 0:m={} +l=p.b++ +k=new A.l($.n,t.a7) +p.c.m(0,l,new A.M(k,t.h1)) +o=p.a.a +o===$&&A.B() +a.i=l +o.q(0,a) +m.a=!1 +if(c!=null)c.O(new A.nE(m,p,l)) +s=3 +return A.c(k,$async$bP) +case 3:n=g +m.a=!0 +if(J.y(n.t,b.b)){q=d.a(n) +s=1 +break}else throw A.a(A.Aa(n)) +case 1:return A.h(q,r)}}) +return A.i($async$bP,r)}, +eb(a){var s=0,r=A.j(t.H),q=this,p,o +var $async$eb=A.e(function(b,c){if(b===1)return A.f(c,r) +for(;;)switch(s){case 0:o=q.a.a +o===$&&A.B() +s=2 +return A.c(o.n(),$async$eb) +case 2:for(o=q.c,p=new A.by(o,o.r,o.e);p.l();)p.d.ao(new A.b7("Channel closed before receiving response: "+A.o(a))) +o.bA(0) +return A.h(null,r)}}) +return A.i($async$eb,r)}} +A.nD.prototype={ +$1(a){this.a.eb(a)}, +$S:8} +A.nB.prototype={ +$1(a){var s=this.a.c.E(0,a.i) +if(s!=null)s.W(a)}, +$S:10} +A.nA.prototype={ +$1(a){return this.jG(a)}, +jG(a1){var s=0,r=A.j(t.P),q=1,p=[],o=[],n=this,m,l,k,j,i,h,g,f,e,d,c,b,a,a0 +var $async$$1=A.e(function(a2,a3){if(a2===1){p.push(a3) +s=q}for(;;)switch(s){case 0:f=null +e=a1.i +d=n.a +c=d.d +b=v.G +a=new b.AbortController() +c.m(0,e,a) +m=a +q=3 +j=d.mX(a1,m.signal) +s=6 +return A.c(t.nW.b(j)?j:A.h8(j,t.m),$async$$1) +case 6:f=a3 +o.push(5) +s=4 +break +case 3:q=2 +a0=p.pop() +l=A.H(a0) +k=A.N(a0) +if(!(l instanceof A.bu)){b.console.error("Error in worker: "+J.aZ(l)) +b.console.error("Original trace: "+A.o(k))}b=l +if(b instanceof A.cX){h=A.zk(b) +g=0}else{g=b instanceof A.bu?1:null +h=null}f={e:J.aZ(b),s:g,r:h,i:e,t:"errorResponse"} +o.push(5) +s=4 +break +case 2:o=[1] +case 4:q=1 +c.E(0,e) +s=o.pop() +break +case 5:d=d.a.a +d===$&&A.B() +d.q(0,f) +return A.h(null,r) +case 1:return A.f(p.at(-1),r)}}) +return A.i($async$$1,r)}, +$S:117} +A.nz.prototype={ +$1(a){var s=this.a.d.E(0,a.i) +if(s!=null)s.abort()}, +$S:10} +A.nC.prototype={ +$1(a){return A.p(A.u("Should only be a top-level message"))}, +$S:118} +A.nE.prototype={ +$0(){if(!this.a.a){var s=this.b.a.a +s===$&&A.B() +s.q(0,{i:this.c,t:"abort"})}}, +$S:1} +A.jL.prototype={} +A.iT.prototype={ +kp(a,b){var s=this,r=s.a.a.a +r===$&&A.B() +r.c.a.b8(new A.nL(s),t.P) +r=s.e +r.a=new A.nM(s) +r.b=new A.nN(s) +s.iy(s.f,new A.nO(s),"notifyCommit") +s.iy(s.r,new A.nP(s),"notifyRollback")}, +iy(a,b,c){var s=a.b +s.a=new A.nJ(this,a,c,b) +s.b=new A.nK(this,a,b)}, +aZ(a){var s=0,r=A.j(t.X),q,p=this +var $async$aZ=A.e(function(b,c){if(b===1)return A.f(c,r) +for(;;)switch(s){case 0:s=3 +return A.c(p.a.bP({r:a,z:null,i:0,d:p.b,t:"custom"},B.p,null,t.m),$async$aZ) +case 3:q=c.r +s=1 +break +case 1:return A.h(q,r)}}) +return A.i($async$aZ,r)}, +cE(a,b,c){return this.on(a,b,c,c)}, +on(a,b,c,d){var s=0,r=A.j(d),q,p=2,o=[],n=[],m=this,l,k,j,i,h,g,f +var $async$cE=A.e(function(e,a0){if(e===1){o.push(a0) +s=p}for(;;)switch(s){case 0:k=m.a +j=m.b +i=t.m +g=A +f=A +s=3 +return A.c(k.bP({i:0,d:j,t:"exclusiveLock"},B.p,b,i),$async$cE) +case 3:h=g.S(f.cD(a0.r)) +p=4 +s=7 +return A.c(a.$1(h),$async$cE) +case 7:l=a0 +q=l +n=[1] +s=5 +break +n.push(6) +s=5 +break +case 4:n=[2] +case 5:p=2 +s=8 +return A.c(k.bO({z:h,i:0,d:j,t:"releaseLock"},B.p,i),$async$cE) +case 8:s=n.pop() +break +case 6:case 1:return A.h(q,r) +case 2:return A.f(o.at(-1),r)}}) +return A.i($async$cE,r)}, +cK(a,b,c,d){return this.k5(a,b,c,d)}, +k5(a,b,c,d){var s=0,r=A.j(t.ii),q,p=this,o,n,m,l,k +var $async$cK=A.e(function(e,f){if(e===1)return A.f(f,r) +for(;;)switch(s){case 0:m=A.uH(c) +l=d==null?null:d +s=3 +return A.c(p.a.bP({s:a,p:m.a,v:m.b,z:l,r:!0,c:b,i:0,d:p.b,t:"runQuery"},B.bK,null,t.m),$async$cK) +case 3:k=f +l=k.x +o=k.y +n=A.Ab(k) +n.toString +q=new A.kb(l,o,n) +s=1 +break +case 1:return A.h(q,r)}}) +return A.i($async$cK,r)}, +$ivN:1} +A.nL.prototype={ +$1(a){var s=this.a,r=s.c +if((r.a.a&30)===0){r.ah() +s.e.n() +s.r.b.n() +s.f.b.n()}}, +$S:9} +A.nM.prototype={ +$0(){var s,r=this.a +if(r.d==null){s=r.a.e +r.d=new A.aJ(s,A.q(s).h("aJ<1>")).Z(new A.nH(r))}if((r.c.a.a&30)===0)r.a.bO({a:!0,i:0,d:r.b,t:"updateRequest"},B.p,t.m)}, +$S:0} +A.nH.prototype={ +$1(a){var s +if(J.y(a.t,"notifyUpdate")){s=this.a +if(J.y(a.d,s.b))s.e.q(0,new A.b6(B.bB[a.k],a.u,a.r))}}, +$S:2} +A.nN.prototype={ +$0(){var s=this.a,r=s.d +if(r!=null)r.u() +s.d=null +if((s.c.a.a&30)===0)s.a.bO({a:!1,i:0,d:s.b,t:"updateRequest"},B.p,t.m)}, +$S:1} +A.nO.prototype={ +$1(a){return{a:a,i:0,d:this.a.b,t:"commitRequest"}}, +$S:45} +A.nP.prototype={ +$1(a){return{a:a,i:0,d:this.a.b,t:"rollbackRequest"}}, +$S:45} +A.nJ.prototype={ +$0(){var s,r,q=this,p=q.b +if(p.a==null){s=q.a +r=s.a.e +p.a=new A.aJ(r,A.q(r).h("aJ<1>")).Z(new A.nI(s,q.c,p))}p=q.a +if((p.c.a.a&30)===0)p.a.bO(q.d.$1(!0),B.p,t.m)}, +$S:0} +A.nI.prototype={ +$1(a){if(J.y(a.t,this.b)&&J.y(a.d,this.a.b))this.c.b.q(0,null)}, +$S:2} +A.nK.prototype={ +$0(){var s=this.b,r=s.a +if(r!=null)r.u() +s.a=null +s=this.a +if((s.c.a.a&30)===0)s.a.bO(this.c.$1(!1),B.p,t.m)}, +$S:1} +A.nQ.prototype={ +aI(){var s=0,r=A.j(t.H),q=this,p +var $async$aI=A.e(function(a,b){if(a===1)return A.f(b,r) +for(;;)switch(s){case 0:p=q.a +s=2 +return A.c(p.a.bO({i:0,d:p.b,t:"fileSystemFlush"},B.p,t.m),$async$aI) +case 2:return A.h(null,r)}}) +return A.i($async$aI,r)}} +A.jy.prototype={ +b_(a,b){return this.nv(a,b)}, +nv(a,b){var s=0,r=A.j(t.m),q,p=this +var $async$b_=A.e(function(c,d){if(c===1)return A.f(d,r) +for(;;)switch(s){case 0:s=3 +return A.c(p.f.$1(a.r),$async$b_) +case 3:q={r:d,i:a.i,t:"simpleSuccessResponse"} +s=1 +break +case 1:return A.h(q,r)}}) +return A.i($async$b_,r)}, +h2(a){this.e.q(0,a)}} +A.lX.prototype={ +fS(a){var s=0,r=A.j(t.kS),q,p=this,o +var $async$fS=A.e(function(b,c){if(b===1)return A.f(c,r) +for(;;)switch(s){case 0:o={port:a.a,lockName:a.b} +q=A.A6(A.AB(A.v4(o.port,o.lockName,null),p.d),0) +s=1 +break +case 1:return A.h(q,r)}}) +return A.i($async$fS,r)}} +A.lY.prototype={ +bl(a){return this.nY(a)}, +nY(a){var s=0,r=A.j(t.n),q +var $async$bl=A.e(function(b,c){if(b===1)return A.f(c,r) +for(;;)switch(s){case 0:q=A.pc(a,null) +s=1 +break +case 1:return A.h(q,r)}}) +return A.i($async$bl,r)}} +A.i4.prototype={} +A.lI.prototype={} +A.d6.prototype={} +A.qw.prototype={} +A.pn.prototype={ +jv(a,b){var s,r=new A.l($.n,t.nI),q=new A.M(r,t.aP),p={} +if(b!=null)p.signal=b +s=t.X +A.mn(A.ac(this.a.request(a,p,A.bV(new A.po(q))),s),new A.pp(q),s,t.K) +return r}, +ju(a){return this.jv(a,null)}} +A.po.prototype={ +$1(a){var s=new A.l($.n,t.D) +this.a.W(new A.cj(new A.M(s,t.F))) +return A.vR(s)}, +$S:46} +A.pp.prototype={ +$2(a,b){var s +A.a4(a) +s=this.a +if((s.a.a&30)===0)if(J.y(a.name,"AbortError"))s.b6(new A.bu("Operation was cancelled",null),b) +else s.b6(a,b) +return null}, +$S:121} +A.cj.prototype={ +oj(){return this.a.ah()}} +A.m9.prototype={ +cv(a,b,c){return this.o1(a,b,c,c)}, +o1(a,b,c,d){var s=0,r=A.j(d),q,p=this,o +var $async$cv=A.e(function(e,f){if(e===1)return A.f(f,r) +for(;;)switch(s){case 0:s=p.c?3:4 +break +case 3:s=5 +return A.c($.ue().jv(p.a,b),$async$cv) +case 5:o=f +q=A.un(a,c).O(o.goi()) +s=1 +break +case 4:q=p.b.eE(a,b,c) +s=1 +break +case 1:return A.h(q,r)}}) +return A.i($async$cv,r)}} +A.dV.prototype={ +eE(a,b,c){return this.ou(a,b,c,c)}, +jA(a,b){return this.eE(a,null,b)}, +ou(a,b,c,d){var s=0,r=A.j(d),q,p=this,o,n,m,l,k,j +var $async$eE=A.e(function(e,f){if(e===1)return A.f(f,r) +for(;;)switch(s){case 0:k={} +j=b==null +if(J.y(j?null:b.aborted,!0))throw A.a(B.C) +k.a=!1 +o=new A.ns(k,p) +if(!p.a){k.a=p.a=!0 +q=A.dO(a,c).O(o) +s=1 +break}else{n={} +m=new A.l($.n,c.h("l<0>")) +l=new A.M(m,c.h("M<0>")) +n.a=null +k=new A.nr(k,n,l,a,c) +if(!j)n.a=A.aF(b,"abort",new A.nq(n,p,l,k),!1,t.m) +p.b.f6(k) +q=m.O(o) +s=1 +break}case 1:return A.h(q,r)}}) +return A.i($async$eE,r)}} +A.ns.prototype={ +$0(){var s,r +if(!this.a.a)return +s=this.b +r=s.b +if(!r.gG(0))r.ol().$0() +else s.a=!1}, +$S:0} +A.nr.prototype={ +$0(){var s,r=this +r.a.a=!0 +s=r.b.a +if(s!=null)s.u() +r.c.W(A.dO(r.d,r.e))}, +$S:0} +A.nq.prototype={ +$1(a){var s,r=this +r.a.a.u() +s=r.c +if((s.a.a&30)===0){r.b.b.E(0,r.d) +s.ao(B.C)}}, +$S:2} +A.cL.prototype={ +gjx(){var s,r,q,p,o,n=this,m=t.s,l=A.v([],m) +for(s=n.a,r=s.length,q=0;q()")}} +A.ri.prototype={ +$1(a){var s +this.a.a=!0 +s=this.b +if((s.a.a&30)===0)s.ah()}, +$S:9} +A.rj.prototype={ +$0(){var s=this.a +if((s.a.a&30)===0)s.b6(new A.dD("lock"),A.fD())}, +$S:1} +A.rm.prototype={ +$0(){var s=this.a,r=this.b +if(s.a===r.a)s.a=null +r.ah()}, +$S:0} +A.rk.prototype={ +$1(a){this.a.$0()}, +$S:9} +A.j8.prototype={} +A.j9.prototype={} +A.dD.prototype={ +j(a){return"A call to "+this.a+" has been aborted"}, +$iV:1} +A.jm.prototype={ +b1(a,b){return this.jV(a,b)}, +jV(a,b){var s=0,r=A.j(t.J),q,p=this,o +var $async$b1=A.e(function(c,d){if(c===1)return A.f(d,r) +for(;;)switch(s){case 0:o=A +s=3 +return A.c(p.eQ(a,b),$async$b1) +case 3:q=o.zz(d) +s=1 +break +case 1:return A.h(q,r)}}) +return A.i($async$b1,r)}, +ea(){var s=0,r=A.j(t.H),q=this +var $async$ea=A.e(function(a,b){if(a===1)return A.f(b,r) +for(;;)switch(s){case 0:s=2 +return A.c(q.bq(),$async$ea) +case 2:if(!b)throw A.a(A.ja(null,null,0,"Dangling transaction detected. If you want to use BEGIN statements manually, COMMIT or ROLLBACK them before returning from writeLock.",null,null,null)) +return A.h(null,r)}}) +return A.i($async$ea,r)}, +$ib5:1} +A.fy.prototype={ +f0(){if(this.c)A.p(A.u("This context to a callback is no longer open. Make sure to await all statements on a database to avoid a context still being used after its callback has finished.")) +if(this.b)throw A.a(A.u("The context from the callback was locked, e.g. due to a nested transaction."))}, +b1(a,b){return this.jU(a,b)}, +jU(a,b){var s=0,r=A.j(t.J),q,p=this +var $async$b1=A.e(function(c,d){if(c===1)return A.f(d,r) +for(;;)switch(s){case 0:p.f0() +q=p.a.b1(a,b) +s=1 +break +case 1:return A.h(q,r)}}) +return A.i($async$b1,r)}, +$ib5:1} +A.fz.prototype={ +ab(a,b){return this.ni(a,b)}, +j0(a){return this.ab(a,B.w)}, +ni(a,b){var s=0,r=A.j(t.G),q,p=this +var $async$ab=A.e(function(c,d){if(c===1)return A.f(d,r) +for(;;)switch(s){case 0:p.f0() +s=3 +return A.c(p.a.ab(a,b),$async$ab) +case 3:q=d +s=1 +break +case 1:return A.h(q,r)}}) +return A.i($async$ab,r)}, +c9(a,b){return this.oB(a,b,b)}, +oB(a2,a3,a4){var s=0,r=A.j(a4),q,p=2,o=[],n=[],m=this,l,k,j,i,h,g,f,e,d,c,b,a,a0,a1 +var $async$c9=A.e(function(a5,a6){if(a5===1){o.push(a6) +s=p}for(;;)switch(s){case 0:m.f0() +l=null +k=null +j=null +f=m.d +e=A.Af(f) +l=e.a +k=e.b +j=e.c +i=null +d=m.a +if(f===0){c=new A.cc(d.a,d.b,null) +c.d=!0}else c=d +h=c +p=4 +m.b=!0 +s=7 +return A.c(d.ab(l,B.w),$async$c9) +case 7:i=new A.fz(f+1,h) +s=8 +return A.c(a2.$1(i),$async$c9) +case 8:g=a6 +s=9 +return A.c(h.ab(k,B.w),$async$c9) +case 9:q=g +n=[1] +s=5 +break +n.push(6) +s=5 +break +case 4:p=3 +a0=o.pop() +p=11 +s=14 +return A.c(h.ab(j,B.w),$async$c9) +case 14:p=3 +s=13 +break +case 11:p=10 +a1=o.pop() +s=13 +break +case 10:s=3 +break +case 13:throw a0 +n.push(6) +s=5 +break +case 3:n=[2] +case 5:p=2 +m.b=!1 +f=i +if(f!=null)f.c=!0 +s=n.pop() +break +case 6:case 1:return A.h(q,r) +case 2:return A.f(o.at(-1),r)}}) +return A.i($async$c9,r)}, +$iaY:1} +A.j7.prototype={ +ab(a,b){return this.nj(a,b)}, +nj(a,b){var s=0,r=A.j(t.G),q,p=this +var $async$ab=A.e(function(c,d){if(c===1)return A.f(d,r) +for(;;)switch(s){case 0:q=p.ow(new A.o3(a,b),"execute()",t.G) +s=1 +break +case 1:return A.h(q,r)}}) +return A.i($async$ab,r)}, +b1(a,b){return this.ms(new A.o4(a,b),null,"getOptional()",t.J)}, +jT(a){return this.b1(a,B.w)}, +$ib5:1, +$iaY:1} +A.o3.prototype={ +$1(a){return this.jI(a)}, +jI(a){var s=0,r=A.j(t.G),q,p=this +var $async$$1=A.e(function(b,c){if(b===1)return A.f(c,r) +for(;;)switch(s){case 0:q=a.ab(p.a,p.b) +s=1 +break +case 1:return A.h(q,r)}}) +return A.i($async$$1,r)}, +$S:137} +A.o4.prototype={ +$1(a){return this.jJ(a)}, +jJ(a){var s=0,r=A.j(t.J),q,p=this +var $async$$1=A.e(function(b,c){if(b===1)return A.f(c,r) +for(;;)switch(s){case 0:q=a.b1(p.a,p.b) +s=1 +break +case 1:return A.h(q,r)}}) +return A.i($async$$1,r)}, +$S:138} +A.ad.prototype={ +H(a,b){if(b==null)return!1 +return b instanceof A.ad&&B.b8.aP(b.a,this.a)}, +gB(a){return A.zY(this.a)}, +j(a){return"UpdateNotification<"+this.a.j(0)+">"}, +cG(a){return new A.ad(this.a.cG(a.a))}, +fT(a){var s +for(s=this.a,s=s.gv(s);s.l();)if(a.T(0,s.gp().toLowerCase()))return!0 +return!1}} +A.oZ.prototype={ +$2(a,b){return a.cG(b)}, +$S:139} +A.oY.prototype={ +$1(a){return new A.dq(new A.oX(this.a),a,A.q(a).h("dq"))}, +$S:140} +A.oX.prototype={ +$1(a){return a.fT(this.a)}, +$S:141} +A.to.prototype={ +$1(a){var s,r,q,p,o=this,n={} +n.a=n.b=null +n.c=!1 +s=new A.tp(n,a) +r=A.uT() +q=new A.tq(n,a,s,r) +r.b=new A.tk(n,o.a,q) +p=o.c.aj(new A.tr(n,o.b,q,o.f),new A.ts(s,a),new A.tt(s,a)) +a.e=new A.tl(n) +a.f=new A.tm(n,r,q) +a.r=new A.tn(n,p) +a.q(0,o.d) +r.cX().$0()}, +$S(){return this.f.h("~(c0<0>)")}} +A.tp.prototype={ +$0(){var s,r=this.a,q=r.b +if(q!=null){r.b=null +this.b.my(q) +s=r.a +if(s!=null)s.u() +r.a=null +return!0}else return!1}, +$S:52} +A.tq.prototype={ +$0(){var s,r,q=this,p=q.a +if(p.a==null){s=q.b +r=s.b +s=!((r&1)!==0?(s.gan().e&4)!==0:(r&2)===0)}else s=!1 +if(s)if(q.c.$0()){s=q.b +r=s.b +if((r&1)!==0?(s.gan().e&4)!==0:(r&2)===0)p.c=!0 +else q.d.cX().$0()}}, +$S:0} +A.tk.prototype={ +$0(){var s=this.a +s.a=A.oO(this.b,new A.tj(s,this.c))}, +$S:0} +A.tj.prototype={ +$0(){this.a.a=null +this.b.$0()}, +$S:0} +A.tr.prototype={ +$1(a){var s,r=this.a,q=r.b +A:{if(q==null){s=a +break A}s=this.b.$2(q,a) +break A}r.b=s +this.c.$0()}, +$S(){return this.d.h("~(0)")}} +A.tt.prototype={ +$2(a,b){this.a.$0() +this.b.mv(a,b)}, +$S:4} +A.ts.prototype={ +$0(){this.a.$0() +this.b.iU()}, +$S:0} +A.tl.prototype={ +$0(){var s=this.a,r=s.a,q=r==null +s.c=!q +if(!q)r.u() +s.a=null}, +$S:0} +A.tm.prototype={ +$0(){if(this.a.c)this.b.cX().$0() +else this.c.$0()}, +$S:0} +A.tn.prototype={ +$0(){var s=0,r=A.j(t.H),q,p=this,o +var $async$$0=A.e(function(a,b){if(a===1)return A.f(b,r) +for(;;)switch(s){case 0:o=p.a.a +if(o!=null)o.u() +q=p.b.u() +s=1 +break +case 1:return A.h(q,r)}}) +return A.i($async$$0,r)}, +$S:3} +A.oN.prototype={ +$0(){this.a.pj()}, +$S:1} +A.oL.prototype={ +$1(a){this.a.q(0,a.b)}, +$S:50} +A.oI.prototype={ +$0(){var s,r,q,p,o,n,m,l,k,j,i,h +for(s=this.a,r=s.length,q=this.b,p=t.N,o=0;o=4)A.p(m.aL()) +if(k)m.aE(i) +else if((l&3)===0){m=m.cQ() +i=new A.c9(i) +h=m.c +if(h==null)m.b=m.c=i +else{h.sc1(i) +m.c=i}}n.b=A.bK(p)}}}q.bA(0)}, +$S:0} +A.oJ.prototype={ +$0(){this.a.bA(0)}, +$S:0} +A.oF.prototype={ +$1(a){var s,r,q=this,p=q.b +p.push(a) +if(p.length===1){p=q.c +s=p.iH() +r=s.r +s=r==null?s.r=s.i_(!0):r +q.a.a=A.v([s.Z(q.d),p.f8().gbs().Z(new A.oG(q.e)),p.f8().gbs().Z(new A.oH(q.f))],t.bO)}}, +$S:44} +A.oG.prototype={ +$1(a){return this.a.$0()}, +$S:16} +A.oH.prototype={ +$1(a){return this.a.$0()}, +$S:16} +A.oM.prototype={ +$1(a){var s,r,q=this.b +B.d.E(q,a) +if(q.length===0)for(q=this.a.a,s=q.length,r=0;r(cc)")}} +A.pm.prototype={ +$1(a){var s=this.b +return A.fA(a,new A.pl(this.a,s),s)}, +$S(){return this.b.h("r<0>(cc)")}} +A.pl.prototype={ +$1(a){return this.jM(a,this.b)}, +jM(a,b){var s=0,r=A.j(b),q,p=this +var $async$$1=A.e(function(c,d){if(c===1)return A.f(d,r) +for(;;)switch(s){case 0:s=3 +return A.c(a.c9(p.a,p.b),$async$$1) +case 3:q=d +s=1 +break +case 1:return A.h(q,r)}}) +return A.i($async$$1,r)}, +$S(){return this.b.h("r<0>(aY)")}} +A.pk.prototype={ +$1(a){return A.fA(a,this.a,this.b)}, +$S(){return this.b.h("r<0>(cc)")}} +A.ph.prototype={ +$0(){return this.jL(this.d)}, +jL(a){var s=0,r=A.j(a),q,p=2,o=[],n=[],m=this,l,k,j +var $async$$0=A.e(function(b,c){if(b===1){o.push(c) +s=p}for(;;)switch(s){case 0:k=m.a +j=new A.cc(k,null,null) +p=3 +s=6 +return A.c(m.b.$1(j),$async$$0) +case 6:l=c +q=l +n=[1] +s=4 +break +n.push(5) +s=4 +break +case 3:n=[2] +case 4:p=2 +s=m.c?7:8 +break +case 7:s=9 +return A.c(k.aI(),$async$$0) +case 9:case 8:s=n.pop() +break +case 5:case 1:return A.h(q,r) +case 2:return A.f(o.at(-1),r)}}) +return A.i($async$$0,r)}, +$S(){return this.d.h("r<0>()")}} +A.pi.prototype={ +$1(a){return this.jK(a,this.d)}, +jK(a,b){var s=0,r=A.j(b),q,p=2,o=[],n=[],m=this,l,k,j +var $async$$1=A.e(function(c,d){if(c===1){o.push(d) +s=p}for(;;)switch(s){case 0:k=m.a +j=new A.cc(k,a,null) +p=3 +s=6 +return A.c(m.b.$1(j),$async$$1) +case 6:l=d +q=l +n=[1] +s=4 +break +n.push(5) +s=4 +break +case 3:n=[2] +case 4:p=2 +s=m.c?7:8 +break +case 7:s=9 +return A.c(k.aI(),$async$$1) +case 9:case 8:s=n.pop() +break +case 5:case 1:return A.h(q,r) +case 2:return A.f(o.at(-1),r)}}) +return A.i($async$$1,r)}, +$S(){return this.d.h("r<0>(b)")}} +A.cc.prototype={ +eQ(a,b){return this.jS(a,b)}, +jS(a,b){var s=0,r=A.j(t.G),q,p=this +var $async$eQ=A.e(function(c,d){if(c===1)return A.f(d,r) +for(;;)switch(s){case 0:q=A.wr(p.c,"getAll",new A.rL(p,a,b),b,a,t.G) +s=1 +break +case 1:return A.h(q,r)}}) +return A.i($async$eQ,r)}, +bq(){var s=0,r=A.j(t.y),q,p=this +var $async$bq=A.e(function(a,b){if(a===1)return A.f(b,r) +for(;;)switch(s){case 0:q=p.a.bq() +s=1 +break +case 1:return A.h(q,r)}}) +return A.i($async$bq,r)}, +ab(a,b){return A.wr(this.c,"execute",new A.rJ(this,a,b),b,a,t.G)}} +A.rL.prototype={ +$0(){var s=0,r=A.j(t.G),q,p=this +var $async$$0=A.e(function(a,b){if(a===1)return A.f(b,r) +for(;;)switch(s){case 0:s=3 +return A.c(A.kK(new A.rK(p.a,p.b,p.c),t.G),$async$$0) +case 3:q=b +s=1 +break +case 1:return A.h(q,r)}}) +return A.i($async$$0,r)}, +$S:15} +A.rK.prototype={ +$0(){var s=0,r=A.j(t.G),q,p=this,o +var $async$$0=A.e(function(a,b){if(a===1)return A.f(b,r) +for(;;)switch(s){case 0:o=p.a +s=3 +return A.c(o.a.a.cK(p.b,o.d,p.c,o.b),$async$$0) +case 3:q=b.c +s=1 +break +case 1:return A.h(q,r)}}) +return A.i($async$$0,r)}, +$S:15} +A.rJ.prototype={ +$0(){return A.kK(new A.rI(this.a,this.b,this.c),t.G)}, +$S:15} +A.rI.prototype={ +$0(){var s=0,r=A.j(t.G),q,p=this,o +var $async$$0=A.e(function(a,b){if(a===1)return A.f(b,r) +for(;;)switch(s){case 0:o=p.a +s=3 +return A.c(o.a.a.cK(p.b,o.d,p.c,o.b),$async$$0) +case 3:q=b.c +s=1 +break +case 1:return A.h(q,r)}}) +return A.i($async$$0,r)}, +$S:15} +A.t7.prototype={ +$2(a,b){return A.uh(new A.dD(this.a),b)}, +$S:145} +A.ch.prototype={ +av(){return"CustomDatabaseMessageKind."+this.b}} +A.jn.prototype={ +h3(a){var s=0,r=A.j(t.X),q,p=this,o,n +var $async$h3=A.e(function(b,c){if(b===1)return A.f(c,r) +for(;;)switch(s){case 0:A.a4(a) +if(A.ia(B.a9,a.rawKind)===B.F){o=a.rawParameters +o=B.d.bm(o,new A.oU(),t.N).eC(0) +n=p.b.i(0,a.rawSql) +if(n!=null)n.q(0,new A.ad(o))}q=null +s=1 +break +case 1:return A.h(q,r)}}) +return A.i($async$h3,r)}, +os(a){var s=null,r=B.b.j(this.a++),q=A.bi(s,s,s,s,!1,t.en) +this.b.m(0,r,q) +q.d=new A.oV(a,r) +q.r=new A.oW(this,a,r) +return new A.O(q,A.q(q).h("O<1>"))}} +A.oU.prototype={ +$1(a){return A.av(a)}, +$S:34} +A.oV.prototype={ +$0(){this.a.aZ(A.ug(B.E,this.b,[!0]))}, +$S:0} +A.oW.prototype={ +$0(){var s=this.c +this.b.aZ(A.ug(B.E,s,[!1])) +this.a.b.E(0,s)}, +$S:1} +A.pq.prototype={ +c0(a,b,c){if("locks" in v.G.navigator)return this.d0(a,b,c) +else return this.a.c0(a,b,c)}, +d0(a,b,c){return this.m9(a,b,c,c)}, +m9(a,b,c,d){var s=0,r=A.j(d),q,p=2,o=[],n=[],m=this,l,k +var $async$d0=A.e(function(e,f){if(e===1){o.push(f) +s=p}for(;;)switch(s){case 0:s=3 +return A.c(m.l2(b),$async$d0) +case 3:k=f +p=4 +s=7 +return A.c(a.$0(),$async$d0) +case 7:l=f +q=l +n=[1] +s=5 +break +n.push(6) +s=5 +break +case 4:n=[2] +case 5:p=2 +k.a.ah() +s=n.pop() +break +case 6:case 1:return A.h(q,r) +case 2:return A.f(o.at(-1),r)}}) +return A.i($async$d0,r)}, +l2(a){var s,r=new A.l($.n,t.fV),q=new A.M(r,t.l6),p=v.G,o=new p.AbortController() +if(a!=null)a.O(new A.pr(q,o)) +s={} +s.signal=o.signal +A.ac(p.navigator.locks.request(this.b,s,A.bV(new A.pt(q))),t.X).iS(new A.ps()) +return r}} +A.pr.prototype={ +$0(){var s=this.a +if((s.a.a&30)===0){s.ao(new A.dD("getWebLock")) +this.b.abort("aborted in Dart")}}, +$S:1} +A.pt.prototype={ +$1(a){var s=new A.l($.n,t.D),r=new A.M(s,t.F),q=this.a +if((q.a.a&30)===0)q.W(new A.f8(r)) +else r.ah() +return A.vR(s)}, +$S:46} +A.ps.prototype={ +$1(a){return null}, +$S:8} +A.f8.prototype={} +A.kZ.prototype={ +he(a,b,c,d){return this.oa(a,b,c,d)}, +oa(a,b,c,d){var s=0,r=A.j(t.u),q,p,o +var $async$he=A.e(function(e,f){if(e===1)return A.f(f,r) +for(;;)switch(s){case 0:p=d==null?null:A.a4(d) +o=a.o9(b,p!=null&&p.useMultipleCiphersVfs?"multipleciphers-"+c:c) +q=new A.hS(o,A.Ar(o),A.P(t.eg,t.fK)) +s=1 +break +case 1:return A.h(q,r)}}) +return A.i($async$he,r)}, +co(a,b){throw A.a(A.uI(null))}} +A.hS.prototype={ +lK(a,b){var s +if(!a.a){a.a=!0 +s=b.a.a +s===$&&A.B() +s.c.a.b8(new A.l_(a),t.P)}}, +co(a,b){return this.nx(a,b)}, +nx(a,b){var s=0,r=A.j(t.X),q,p=this,o,n,m,l,k +var $async$co=A.e(function(c,d){if(c===1)return A.f(d,r) +for(;;)switch(s){case 0:k=A.a4(b.a) +case 3:switch(A.ia(B.a9,k.rawKind).a){case 0:s=5 +break +case 4:s=6 +break +case 1:s=7 +break +case 2:s=8 +break +case 3:s=9 +break +default:s=4 +break}break +case 5:case 6:throw A.a(A.R("This is a response, not a request")) +case 7:o=p.a.b +q=o.a.d.sqlite3_get_autocommit(o.b)!==0 +s=1 +break +case 8:s=10 +return A.c(b.c.$1$1(new A.l0(p,k),t.P),$async$co) +case 10:s=4 +break +case 9:o=k.rawParameters +n=A.aT(o[0]) +o=k.rawSql +m=p.c.cB(a,A.DM()) +if(n){m.hp() +p.lK(m,a) +l=A.uT() +l.b=m.b=p.b.Z(new A.l1(l,a,o))}else m.hp() +s=4 +break +case 4:q={rawKind:"ok"} +s=1 +break +case 1:return A.h(q,r)}}) +return A.i($async$co,r)}, +gd8(){return this.a}} +A.l_.prototype={ +$1(a){this.a.hp()}, +$S:9} +A.l0.prototype={ +$0(){var s,r,q,p,o,n=null,m=this.b +if(m.requireTransaction){q=this.a.a.b +q=q.a.d.sqlite3_get_autocommit(q.b)!==0}else q=!1 +if(q)throw A.a(A.ja(A.zG(A.tJ(m,"rawSql")),n,0,"Transaction rolled back by earlier statement. Cannot execute",n,n,n)) +s=this.a.a.oe(m.rawSql) +try{m=m.parameters +m=J.U(t.ip.b(m)?m:new A.al(m,A.a1(m).h("al<1,w>"))) +while(m.l()){r=m.gp() +q=s +p=r +p=A.uG(p.parameters,p.parameterTypes) +if(q.r||q.b.r)A.p(A.u(u.f)) +if(!q.f){o=q.a +o.c.d.sqlite3_reset(o.b) +q.f=!0}q.eX(new A.fa(p)) +q.hV()}}finally{s.n()}}, +$S:1} +A.l1.prototype={ +$1(a){this.a.cX().aJ(this.b.aZ(A.ug(B.F,this.c,a.eB(0))))}, +$S:147} +A.ec.prototype={ +hp(){var s=this.b +if(s!=null){this.b=null +s.u()}}} +A.f6.prototype={ +ko(a,b,c,d){var s=this,r=$.n +s.a!==$&&A.ua() +s.a=new A.h9(a,s,new A.as(new A.l(r,t.D),t.h),!0) +if(c.a.gaq())c.a=new A.j_(d.h("@<0>").J(d).h("j_<1,2>")).aY(c.a) +r=A.bi(null,new A.mB(c,s),null,null,!0,d) +s.b!==$&&A.ua() +s.b=r}, +ly(){var s,r +this.d=!0 +s=this.c +if(s!=null)s.u() +r=this.b +r===$&&A.B() +r.n()}} +A.mB.prototype={ +$0(){var s,r,q=this.b +if(q.d)return +s=this.a.a +r=q.b +r===$&&A.B() +q.c=s.aj(r.gd4(r),new A.mA(q),r.gd5())}, +$S:0} +A.mA.prototype={ +$0(){var s=this.a,r=s.a +r===$&&A.B() +r.lz() +s=s.b +s===$&&A.B() +s.n()}, +$S:0} +A.h9.prototype={ +q(a,b){if(this.e)throw A.a(A.u("Cannot add event after closing.")) +if(this.d)return +this.a.a.q(0,b)}, +a2(a,b){if(this.e)throw A.a(A.u("Cannot add event after closing.")) +if(this.d)return +this.l3(a,b)}, +l3(a,b){this.a.a.a2(a,b) +return}, +n(){var s=this +if(s.e)return s.c.a +s.e=!0 +if(!s.d){s.b.ly() +s.c.W(s.a.a.n())}return s.c.a}, +lz(){this.d=!0 +var s=this.c +if((s.a.a&30)===0)s.ah() +return}, +$iaa:1} +A.jb.prototype={} +A.fF.prototype={$iuD:1} +A.jf.prototype={ +gdF(){return A.av(this.c)}} +A.ow.prototype={ +ghc(){var s=this +if(s.c!==s.e)s.d=null +return s.d}, +eS(a){var s,r=this,q=r.d=J.yS(a,r.b,r.c) +r.e=r.c +s=q!=null +if(s)r.e=r.c=q.gC() +return s}, +j1(a,b){var s +if(this.eS(a))return +if(b==null)if(a instanceof A.fd)b="/"+a.a+"/" +else{s=J.aZ(a) +s=A.hG(s,"\\","\\\\") +b='"'+A.hG(s,'"','\\"')+'"'}this.hW(b)}, +dc(a){return this.j1(a,null)}, +nl(){if(this.c===this.b.length)return +this.hW("no more input")}, +nh(a,b,c){var s,r,q,p,o,n=this.b +if(c<0)A.p(A.aA("position must be greater than or equal to 0.")) +else if(c>n.length)A.p(A.aA("position must be less than or equal to the string length.")) +s=c+b>n.length +if(s)A.p(A.aA("position plus length must not go beyond the end of the string.")) +s=this.a +r=A.v([0],t.t) +q=n.length +p=new A.o0(s,r,new Uint32Array(q)) +p.kq(new A.bv(n),s) +o=c+b +if(o>q)A.p(A.aA("End "+o+u.D+p.gk(0)+".")) +else if(c<0)A.p(A.aA("Start may not be negative, was "+c+".")) +throw A.a(new A.jf(n,a,new A.ei(p,c,o)))}, +hW(a){this.nh("expected "+a+".",0,this.c)}} +A.e5.prototype={ +gk(a){return this.b}, +i(a,b){if(b>=this.b)throw A.a(A.vT(b,this)) +return this.a[b]}, +m(a,b,c){var s +if(b>=this.b)throw A.a(A.vT(b,this)) +s=this.a +s.$flags&2&&A.D(s) +s[b]=c}, +sk(a,b){var s,r,q,p,o=this,n=o.b +if(bn){if(n===0)p=new Uint8Array(b) +else p=o.fb(b) +B.f.al(p,0,o.b,o.a) +o.a=p}}o.b=b}, +m7(a){var s,r=this,q=r.b +if(q===r.a.length)r.i3(q) +q=r.a +s=r.b++ +q.$flags&2&&A.D(q) +q[s]=a}, +q(a,b){var s,r=this,q=r.b +if(q===r.a.length)r.i3(q) +q=r.a +s=r.b++ +q.$flags&2&&A.D(q) +q[s]=b}, +hA(a,b,c){var s,r,q +if(t.j.b(a))c=c==null?J.ay(a):c +if(c!=null){this.lb(this.b,a,b,c) +return}for(s=J.U(a),r=0;s.l();){q=s.gp() +if(r>=b)this.m7(q);++r}if(rs.gk(b)||d>s.gk(b))throw A.a(A.u("Too few elements"))}r=d-c +q=o.b+r +o.kY(q) +s=o.a +p=a+r +B.f.L(s,p,o.b+r,s,a) +B.f.L(o.a,a,p,b,c) +o.b=q}, +kY(a){var s,r=this +if(a<=r.a.length)return +s=r.fb(a) +B.f.al(s,0,r.b,r.a) +r.a=s}, +fb(a){var s=this.a.length*2 +if(a!=null&&ss)throw A.a(A.a0(c,0,s,null,null)) +s=this.a +if(d instanceof A.bE)B.f.L(s,b,c,d.a,e) +else B.f.L(s,b,c,d,e)}, +al(a,b,c,d){return this.L(0,b,c,d,0)}} +A.jX.prototype={} +A.bE.prototype={} +A.ui.prototype={} +A.eg.prototype={ +gaq(){return!0}, +A(a,b,c,d){return A.aF(this.a,this.b,a,!1,this.$ti.c)}, +Z(a){return this.A(a,null,null,null)}, +aj(a,b,c){return this.A(a,null,b,c)}, +bk(a,b,c){return this.A(a,b,c,null)}} +A.eh.prototype={ +u(){var s=this,r=A.mu(null,t.H) +if(s.b==null)return r +s.fJ() +s.d=s.b=null +return r}, +bH(a){var s,r=this +if(r.b==null)throw A.a(A.u("Subscription has been canceled.")) +r.fJ() +s=A.xO(new A.qD(a),t.m) +s=s==null?null:A.bV(s) +r.d=s +r.fH()}, +dq(a){}, +aJ(a){var s=this +if(s.b==null)return;++s.a +s.fJ() +if(a!=null)a.O(s.gbI())}, +ak(){return this.aJ(null)}, +ar(){var s=this +if(s.b==null||s.a<=0)return;--s.a +s.fH()}, +fH(){var s=this,r=s.d +if(r!=null&&s.a<=0)s.b.addEventListener(s.c,r,!1)}, +fJ(){var s=this.d +if(s!=null)this.b.removeEventListener(this.c,s,!1)}, +$iak:1} +A.qC.prototype={ +$1(a){return this.a.$1(a)}, +$S:2} +A.qD.prototype={ +$1(a){return this.a.$1(a)}, +$S:2};(function aliases(){var s=J.cm.prototype +s.kf=s.j +s=A.b2.prototype +s.kb=s.jc +s.kc=s.jd +s.ke=s.jf +s.kd=s.je +s=A.c8.prototype +s.kj=s.bu +s=A.at.prototype +s.ad=s.af +s.bR=s.au +s.aA=s.b2 +s=A.ca.prototype +s.kk=s.hL +s.kl=s.i0 +s.km=s.iw +s=A.C.prototype +s.hu=s.L +s=A.ah.prototype +s.ht=s.aY +s=A.hr.prototype +s.kn=s.n +s=A.hV.prototype +s.ka=s.nn +s=A.e3.prototype +s.kh=s.S +s.kg=s.H +s=A.ad.prototype +s.ki=s.fT})();(function installTearOffs(){var s=hunkHelpers._static_2,r=hunkHelpers._instance_0u,q=hunkHelpers._instance_1u,p=hunkHelpers.installInstanceTearOff,o=hunkHelpers._static_1,n=hunkHelpers._static_0,m=hunkHelpers.installStaticTearOff,l=hunkHelpers._instance_2u,k=hunkHelpers._instance_1i +s(J,"C1","zC",41) +var j +r(j=A.dG.prototype,"ge9","u",17) +q(j,"glp","lq",5) +p(j,"geu",0,0,null,["$1","$0"],["aJ","ak"],49,0,0) +r(j,"gbI","ar",0) +o(A,"CG","AG",14) +o(A,"CH","AH",14) +o(A,"CI","AI",14) +n(A,"xQ","Cx",0) +o(A,"CJ","Ch",11) +s(A,"CK","Cj",4) +n(A,"tw","Ci",0) +m(A,"CQ",5,null,["$5"],["Cr"],149,0) +m(A,"CV",4,null,["$1$4","$4"],["tf",function(a,b,c,d){return A.tf(a,b,c,d,t.z)}],150,0) +m(A,"CX",5,null,["$2$5","$5"],["th",function(a,b,c,d,e){var i=t.z +return A.th(a,b,c,d,e,i,i)}],151,0) +m(A,"CW",6,null,["$3$6","$6"],["tg",function(a,b,c,d,e,f){var i=t.z +return A.tg(a,b,c,d,e,f,i,i,i)}],152,0) +m(A,"CT",4,null,["$1$4","$4"],["xF",function(a,b,c,d){return A.xF(a,b,c,d,t.z)}],153,0) +m(A,"CU",4,null,["$2$4","$4"],["xG",function(a,b,c,d){var i=t.z +return A.xG(a,b,c,d,i,i)}],154,0) +m(A,"CS",4,null,["$3$4","$4"],["xE",function(a,b,c,d){var i=t.z +return A.xE(a,b,c,d,i,i,i)}],155,0) +m(A,"CO",5,null,["$5"],["Cq"],156,0) +m(A,"CY",4,null,["$4"],["ti"],157,0) +m(A,"CN",5,null,["$5"],["Cp"],158,0) +m(A,"CM",5,null,["$5"],["Co"],159,0) +m(A,"CR",4,null,["$4"],["Cs"],160,0) +o(A,"CL","Ck",161) +m(A,"CP",5,null,["$5"],["xD"],162,0) +r(j=A.d8.prototype,"gcT","b3",0) +r(j,"gcU","b4",0) +r(j=A.c8.prototype,"gag","n",3) +q(j,"geW","af",5) +l(j,"gdI","au",4) +r(j,"gf2","b2",0) +p(A.d9.prototype,"gmH",0,1,null,["$2","$1"],["b6","ao"],40,0,0) +l(A.l.prototype,"gf9","kP",4) +k(j=A.cz.prototype,"gd4","q",5) +p(j,"gd5",0,1,null,["$2","$1"],["a2","mu"],40,0,0) +r(j,"gag","n",17) +q(j,"geW","af",5) +l(j,"gdI","au",4) +r(j,"gf2","b2",0) +r(j=A.cx.prototype,"gcT","b3",0) +r(j,"gcU","b4",0) +p(j=A.at.prototype,"geu",0,0,null,["$1","$0"],["aJ","ak"],35,0,0) +r(j,"gbI","ar",0) +r(j,"ge9","u",17) +r(j,"gcT","b3",0) +r(j,"gcU","b4",0) +p(j=A.ef.prototype,"geu",0,0,null,["$1","$0"],["aJ","ak"],35,0,0) +r(j,"gbI","ar",0) +r(j,"ge9","u",17) +r(j,"gic","lx",0) +q(j=A.bU.prototype,"gkE","kF",5) +l(j,"glt","lu",4) +r(j,"glr","ls",0) +r(j=A.ej.prototype,"gcT","b3",0) +r(j,"gcU","b4",0) +q(j,"gfj","fk",5) +l(j,"gfn","fo",88) +r(j,"gfl","fm",0) +r(j=A.es.prototype,"gcT","b3",0) +r(j,"gcU","b4",0) +q(j,"gfj","fk",5) +l(j,"gfn","fo",4) +r(j,"gfl","fm",0) +s(A,"vb","BP",19) +o(A,"vc","BQ",20) +s(A,"D0","zK",41) +o(A,"D2","BR",47) +k(j=A.jJ.prototype,"gd4","q",5) +r(j,"gag","n",0) +o(A,"xT","Dj",20) +s(A,"xS","Di",19) +o(A,"D3","Ay",21) +m(A,"Dw",2,null,["$1$2","$2"],["y1",function(a,b){return A.y1(a,b,t.r)}],163,0) +r(j=A.fG.prototype,"glv","lw",0) +r(j,"gm2","m3",0) +r(j,"gm4","m5",0) +r(j,"glo","ib",29) +l(j=A.eX.prototype,"gng","aP",19) +q(j,"gnJ","c_",20) +q(j,"gnP","nQ",23) +o(A,"CZ","z1",21) +o(A,"Dp","zw",164) +o(A,"DE","AR",165) +o(A,"DF","A5",166) +r(j=A.jx.prototype,"gmL","ef",74) +r(j,"got","eD",3) +q(j=A.i5.prototype,"go4","o5",13) +l(j,"go_","o0",89) +p(j,"goU",0,5,null,["$5"],["oV"],90,0,0) +p(j,"goL",0,3,null,["$3"],["oM"],91,0,0) +p(j,"goD",0,4,null,["$4"],["oE"],36,0,0) +p(j,"goQ",0,4,null,["$4"],["oR"],36,0,0) +p(j,"goW",0,3,null,["$3"],["oX"],93,0,0) +l(j,"gp_","p0",37) +l(j,"goJ","oK",37) +q(j,"goH","oI",38) +p(j,"goY",0,4,null,["$4"],["oZ"],39,0,0) +p(j,"gpb",0,4,null,["$4"],["pc"],39,0,0) +l(j,"gp7","p8",97) +l(j,"gp5","p6",12) +l(j,"goO","oP",12) +l(j,"goS","oT",12) +l(j,"gp9","pa",12) +l(j,"goF","oG",12) +q(j,"gdA","oN",38) +q(j,"gn_","n0",14) +q(j,"gmV","mW",100) +p(j,"gmY",0,5,null,["$5"],["mZ"],101,0,0) +p(j,"gn5",0,4,null,["$4"],["n6"],18,0,0) +p(j,"gn9",0,4,null,["$4"],["na"],18,0,0) +p(j,"gn7",0,4,null,["$4"],["n8"],18,0,0) +l(j,"gnb","nc",43) +l(j,"gn3","n4",43) +p(j,"gn1",0,5,null,["$5"],["n2"],104,0,0) +l(j,"gmT","mU",105) +l(j,"gmR","mS",106) +p(j,"gmP",0,3,null,["$3"],["mQ"],107,0,0) +r(A.fU.prototype,"gag","n",0) +o(A,"ce","zP",167) +o(A,"bs","zQ",168) +o(A,"vl","zR",169) +q(A.fT.prototype,"glL","lM",110) +r(A.hT.prototype,"gag","n",0) +r(A.cP.prototype,"gag","n",3) +r(A.df.prototype,"gez","ac",0) +r(A.ee.prototype,"gez","ac",3) +r(A.dc.prototype,"gez","ac",3) +r(A.dr.prototype,"gez","ac",3) +r(A.e1.prototype,"gag","n",0) +q(A.iQ.prototype,"gl5","fp",2) +q(A.jy.prototype,"gj7","h2",2) +r(A.cj.prototype,"goi","oj",0) +q(A.ea.prototype,"gj7","h2",2) +r(A.dm.prototype,"gmw","mx",0) +q(A.jn.prototype,"gnF","h3",146) +n(A,"DM","AU",113) +r(j=A.eh.prototype,"ge9","u",3) +p(j,"geu",0,0,null,["$1","$0"],["aJ","ak"],49,0,0) +r(j,"gbI","ar",0)})();(function inheritance(){var s=hunkHelpers.mixin,r=hunkHelpers.inherit,q=hunkHelpers.inheritMany +r(A.k,null) +q(A.k,[A.uu,J.ik,A.fx,J.dE,A.G,A.dG,A.m,A.i_,A.cK,A.Z,A.C,A.nX,A.aq,A.bL,A.fV,A.ic,A.jh,A.j0,A.i9,A.jw,A.iI,A.f3,A.jk,A.di,A.eS,A.ek,A.cq,A.oP,A.iK,A.f_,A.hp,A.L,A.ne,A.fg,A.by,A.iy,A.fd,A.en,A.jB,A.fI,A.rs,A.jK,A.kx,A.bA,A.jT,A.rF,A.kt,A.h_,A.jD,A.hb,A.kr,A.a6,A.at,A.c8,A.d9,A.bl,A.l,A.jC,A.jc,A.cz,A.ks,A.jE,A.ev,A.fZ,A.jO,A.qy,A.er,A.ef,A.bU,A.h7,A.aN,A.kA,A.eA,A.jU,A.r6,A.k0,A.k1,A.aV,A.kw,A.fk,A.k2,A.je,A.i1,A.ah,A.lg,A.pV,A.i0,A.db,A.r1,A.rt,A.kz,A.dp,A.aC,A.jR,A.aK,A.b_,A.qz,A.iL,A.fC,A.jQ,A.aU,A.ij,A.Q,A.J,A.kq,A.X,A.hy,A.p0,A.bn,A.id,A.uN,A.iJ,A.qW,A.qX,A.fG,A.et,A.T,A.eX,A.iz,A.ey,A.em,A.dU,A.iH,A.jl,A.kV,A.bY,A.l8,A.hV,A.l9,A.fm,A.cn,A.dS,A.dT,A.i3,A.ep,A.eq,A.ox,A.nu,A.fu,A.kU,A.bO,A.eW,A.eV,A.e_,A.d_,A.ad,A.ld,A.fj,A.dM,A.fP,A.lF,A.md,A.f1,A.dH,A.f4,A.eY,A.fM,A.q3,A.fo,A.oz,A.fJ,A.dK,A.e9,A.om,A.pB,A.cp,A.fR,A.fL,A.f7,A.ct,A.ny,A.oB,A.da,A.ew,A.fY,A.hn,A.h5,A.h3,A.fX,A.jx,A.qA,A.lY,A.nx,A.o0,A.j3,A.e3,A.mE,A.aM,A.bF,A.bC,A.j6,A.b6,A.cX,A.lZ,A.cA,A.o2,A.lq,A.aB,A.hY,A.lH,A.kk,A.kg,A.fa,A.aR,A.fB,A.pd,A.p8,A.pf,A.pe,A.d4,A.cv,A.i5,A.dd,A.p9,A.nS,A.bM,A.c_,A.kf,A.fT,A.eo,A.hT,A.qE,A.k3,A.jW,A.p3,A.nR,A.jL,A.iT,A.nQ,A.lX,A.i4,A.d6,A.pn,A.cj,A.m9,A.dV,A.cL,A.cU,A.hq,A.eb,A.i6,A.px,A.qx,A.rR,A.qv,A.rg,A.j7,A.dD,A.jm,A.fy,A.dm,A.jn,A.pq,A.f8,A.ec,A.fF,A.h9,A.jb,A.ow,A.ui,A.eh]) +q(J.ik,[J.io,J.dP,J.aj,J.aO,J.dR,J.dQ,J.cl]) +q(J.aj,[J.cm,J.A,A.dX,A.fp]) +q(J.cm,[J.iN,J.d1,J.b0]) +r(J.im,A.fx) +r(J.n9,J.A) +q(J.dQ,[J.fc,J.ip]) +q(A.G,[A.eR,A.eu,A.fH,A.de,A.bH,A.b9,A.c7,A.eN,A.eg]) +q(A.m,[A.cw,A.x,A.bZ,A.d5,A.f0,A.d0,A.c2,A.fW,A.fs,A.hc,A.jA,A.kp,A.ex,A.fh]) +q(A.cw,[A.cJ,A.hB]) +r(A.h6,A.cJ) +r(A.h2,A.hB) +q(A.cK,[A.lp,A.lo,A.n1,A.oD,A.tM,A.tO,A.pM,A.pL,A.rV,A.rU,A.ru,A.rw,A.rv,A.my,A.mx,A.qP,A.qS,A.oc,A.oj,A.oh,A.ok,A.of,A.qu,A.qt,A.re,A.rd,A.qq,A.r5,A.ni,A.lE,A.mh,A.nd,A.q_,A.mp,A.tQ,A.u5,A.u6,A.tC,A.o_,A.o9,A.o8,A.lj,A.ll,A.hX,A.lc,A.rX,A.lh,A.nn,A.tE,A.lC,A.lD,A.tu,A.u3,A.u2,A.ta,A.lf,A.le,A.lG,A.np,A.tX,A.tV,A.tx,A.u8,A.ov,A.on,A.oo,A.oq,A.or,A.pC,A.pH,A.pD,A.pE,A.pG,A.n6,A.n7,A.qi,A.rx,A.rz,A.rA,A.rB,A.p_,A.pw,A.tS,A.tT,A.tR,A.mG,A.mF,A.mH,A.mJ,A.mL,A.mI,A.mZ,A.o5,A.m6,A.rp,A.kY,A.qo,A.qp,A.lt,A.lu,A.ly,A.lz,A.lA,A.mj,A.l5,A.l2,A.l3,A.nY,A.p4,A.p5,A.p6,A.p7,A.t0,A.t1,A.t3,A.nD,A.nB,A.nA,A.nz,A.nC,A.nL,A.nH,A.nO,A.nP,A.nI,A.po,A.nq,A.mi,A.nU,A.nV,A.tz,A.lr,A.ls,A.lv,A.lw,A.lx,A.t6,A.qa,A.q8,A.qc,A.qf,A.q6,A.rh,A.ri,A.rk,A.o3,A.o4,A.oY,A.oX,A.to,A.tr,A.oL,A.oF,A.oG,A.oH,A.oM,A.oK,A.pj,A.pm,A.pl,A.pk,A.pi,A.oU,A.pt,A.ps,A.l_,A.l1,A.qC,A.qD]) +q(A.lp,[A.q4,A.lB,A.na,A.tN,A.rW,A.tv,A.mz,A.mw,A.mo,A.qQ,A.qT,A.pJ,A.rY,A.mD,A.nf,A.nk,A.mg,A.r2,A.pZ,A.p1,A.mr,A.mq,A.li,A.lk,A.lm,A.hW,A.no,A.me,A.u9,A.pF,A.oA,A.qh,A.mK,A.l4,A.pp,A.ql,A.pA,A.oZ,A.tt,A.t7]) +r(A.al,A.h2) +q(A.Z,[A.cQ,A.c5,A.ir,A.jj,A.iW,A.jP,A.ff,A.hQ,A.a3,A.fO,A.ji,A.b7,A.i2,A.iB]) +q(A.C,[A.e6,A.e8,A.e5]) +q(A.e6,[A.bv,A.d2]) +q(A.lo,[A.u1,A.pN,A.pO,A.rE,A.rD,A.rT,A.pQ,A.pR,A.pT,A.pU,A.pS,A.pP,A.mv,A.mt,A.qG,A.qL,A.qK,A.qI,A.qH,A.qO,A.qN,A.qM,A.qR,A.od,A.oi,A.og,A.ol,A.oe,A.ro,A.rn,A.pI,A.q2,A.q1,A.r8,A.r7,A.rZ,A.t_,A.qs,A.qr,A.rc,A.rb,A.te,A.rO,A.rN,A.tb,A.t9,A.nZ,A.oa,A.ob,A.o7,A.lb,A.tc,A.td,A.nm,A.nh,A.tY,A.tW,A.tZ,A.u_,A.u0,A.u7,A.ou,A.os,A.op,A.ot,A.oC,A.rC,A.ry,A.mY,A.mM,A.mT,A.mU,A.mV,A.mW,A.mR,A.mS,A.mN,A.mO,A.mP,A.mQ,A.mX,A.qU,A.m7,A.m8,A.m4,A.m3,A.m5,A.m0,A.m_,A.m1,A.m2,A.rq,A.rr,A.lM,A.lJ,A.lO,A.lQ,A.lS,A.lL,A.lR,A.lW,A.lU,A.lT,A.lN,A.lP,A.lV,A.lK,A.kW,A.kX,A.pa,A.l6,A.qF,A.n_,A.n0,A.qV,A.t2,A.nE,A.nM,A.nN,A.nJ,A.nK,A.ns,A.nr,A.qj,A.qn,A.qk,A.qm,A.q7,A.qb,A.qe,A.q9,A.qd,A.qg,A.mc,A.mb,A.ma,A.py,A.pz,A.rl,A.rj,A.rm,A.tp,A.tq,A.tk,A.tj,A.ts,A.tl,A.tm,A.tn,A.oN,A.oI,A.oJ,A.oE,A.ph,A.rL,A.rK,A.rJ,A.rI,A.oV,A.oW,A.pr,A.l0,A.mB,A.mA]) +q(A.x,[A.W,A.cN,A.bx,A.bf,A.az,A.ha]) +q(A.W,[A.cZ,A.a8,A.cV,A.fi,A.jZ]) +r(A.cM,A.bZ) +r(A.eZ,A.d0) +r(A.dL,A.c2) +q(A.di,[A.k4,A.k5,A.k6,A.k7]) +r(A.hj,A.k4) +q(A.k5,[A.au,A.hk,A.hl,A.k8,A.dj,A.k9,A.ka]) +q(A.k6,[A.hm,A.kb,A.kc,A.kd]) +r(A.ke,A.k7) +r(A.bw,A.eS) +q(A.cq,[A.eT,A.ho]) +r(A.eU,A.eT) +r(A.fb,A.n1) +r(A.ft,A.c5) +q(A.oD,[A.o6,A.eO]) +q(A.L,[A.b2,A.ca,A.jY]) +q(A.b2,[A.fe,A.hd]) +r(A.dW,A.dX) +q(A.fp,[A.cS,A.dZ]) +q(A.dZ,[A.hf,A.hh]) +r(A.hg,A.hf) +r(A.co,A.hg) +r(A.hi,A.hh) +r(A.b4,A.hi) +q(A.co,[A.iC,A.iD]) +q(A.b4,[A.iE,A.dY,A.iF,A.iG,A.fq,A.fr,A.cT]) +r(A.hs,A.jP) +r(A.O,A.eu) +r(A.aJ,A.O) +q(A.at,[A.cx,A.ej,A.es]) +r(A.d8,A.cx) +q(A.c8,[A.dl,A.h0]) +q(A.d9,[A.as,A.M]) +q(A.cz,[A.bT,A.cB]) +r(A.ko,A.fZ) +q(A.jO,[A.c9,A.ed]) +r(A.he,A.bT) +q(A.b9,[A.dq,A.bG]) +q(A.jc,[A.kn,A.nc,A.j_]) +q(A.kA,[A.jM,A.kj]) +q(A.ca,[A.cy,A.h4]) +r(A.cb,A.ho) +r(A.hx,A.fk) +r(A.fN,A.hx) +q(A.je,[A.hr,A.rG,A.r4,A.dk]) +r(A.qZ,A.hr) +q(A.i1,[A.cO,A.l7,A.nb]) +q(A.cO,[A.hN,A.iv,A.jq]) +q(A.ah,[A.kv,A.ku,A.hU,A.iu,A.it,A.js,A.jr]) +q(A.kv,[A.hP,A.ix]) +q(A.ku,[A.hO,A.iw]) +q(A.lg,[A.qB,A.rf,A.pW,A.jI,A.jJ,A.k_,A.ky]) +r(A.q0,A.pV) +r(A.pK,A.pW) +r(A.is,A.ff) +r(A.r_,A.i0) +r(A.r0,A.r1) +r(A.r3,A.k_) +r(A.el,A.r4) +r(A.kB,A.kz) +r(A.rP,A.kB) +q(A.a3,[A.e0,A.f9]) +r(A.jN,A.hy) +r(A.cW,A.ey) +r(A.fw,A.bY) +r(A.la,A.l8) +r(A.dF,A.fH) +r(A.iU,A.hV) +r(A.jz,A.iU) +r(A.hL,A.jz) +q(A.l9,[A.iV,A.cs]) +r(A.jd,A.cs) +r(A.eQ,A.T) +r(A.n5,A.ox) +q(A.n5,[A.nv,A.p2,A.pv]) +q(A.qz,[A.fQ,A.jg,A.dJ,A.aE,A.e4,A.nt,A.ap,A.dN,A.fn,A.ci,A.bD,A.f2,A.cr,A.ch]) +r(A.bh,A.ad) +r(A.il,A.ny) +r(A.pg,A.ld) +q(A.lY,[A.kZ,A.qw]) +r(A.nw,A.kZ) +r(A.ie,A.j3) +q(A.e3,[A.ei,A.j5]) +r(A.e2,A.j6) +r(A.c3,A.j5) +r(A.fE,A.lq) +r(A.hZ,A.aB) +q(A.hZ,[A.ig,A.fU,A.cP,A.e1]) +q(A.hY,[A.jV,A.ju,A.km]) +r(A.kh,A.lH) +r(A.ki,A.kh) +r(A.bP,A.ki) +r(A.kl,A.kk) +r(A.aX,A.kl) +r(A.e7,A.o2) +q(A.c_,[A.be,A.ab]) +r(A.b3,A.ab) +r(A.aG,A.aV) +q(A.aG,[A.df,A.ee,A.dc,A.dr]) +r(A.iQ,A.nR) +q(A.iQ,[A.jy,A.ea]) +r(A.lI,A.i4) +r(A.bu,A.cU) +r(A.j8,A.j7) +r(A.j9,A.j8) +r(A.fz,A.fy) +r(A.jv,A.j9) +r(A.cc,A.jm) +r(A.hS,A.d6) +r(A.f6,A.fF) +r(A.jf,A.e2) +r(A.jX,A.e5) +r(A.bE,A.jX) +s(A.e6,A.jk) +s(A.hB,A.C) +s(A.hf,A.C) +s(A.hg,A.f3) +s(A.hh,A.C) +s(A.hi,A.f3) +s(A.bT,A.jE) +s(A.cB,A.ks) +s(A.hx,A.kw) +s(A.kB,A.je) +s(A.jz,A.kV) +s(A.kh,A.C) +s(A.ki,A.iH) +s(A.kk,A.jl) +s(A.kl,A.L)})() +var v={G:typeof self!="undefined"?self:globalThis,typeUniverse:{eC:new Map(),tR:{},eT:{},tPV:{},sEA:[]},mangledGlobalNames:{b:"int",a5:"double",bW:"num",d:"String",I:"bool",J:"Null",t:"List",k:"Object",a_:"Map",w:"JSObject"},mangledNames:{},types:["~()","J()","~(w)","r<~>()","~(k,ae)","~(k?)","~(fo)","J(k,ae)","J(@)","J(~)","J(w)","~(@)","b(aS,b)","~(b)","~(~())","r()","~(~)","r<@>()","~(iS,b,b,b)","I(k?,k?)","b(k?)","d(d)","w()","I(k?)","I(aM)","b()","b(+atLast,priority,sinceLast,targetCount(b,b,b,b))","~(k?,k?)","r()","r<~>?()","~(@,@)","d(cR)","k?(k?)","~(dS)","d(k?)","~([r<~>?])","b(aB,b,b,b)","b(aB,b)","b(aS)","b(aS,b,b,aO)","~(k[ae?])","b(@,@)","@()","~(iS,b)","~(dm)","w(I)","w(k)","@(@)","r>()","~([r<@>?])","~(b6)","r()","I()","I(d)","I(+hasSynced,lastSyncedAt,priority(I?,aK?,b))","dK(k?)","Q(d,k?)","d(X)","r<~>(ak<~>)","r<+immediateRestart(I)>()","el(aa)","@(@,d)","r()","a_(+name,parameters(d,d))","G?(cs?)","J(bO?)","~(d,k?)","b(b)","ew()","r<+(w,J)>(aE,k)","0&(d,b?)","r({invalidate!I})","~(ct)","+name,parameters(d,d)(k?)","r()","r<~>(w)","w?()","d?()","b(bF)","J(@,ae)","k(bF)","k(aM)","b(aM,aM)","t(Q>)","J(b0,b0)","c3()","k?(~)","~(b,d,b)","~(@,ae)","~(aO,b)","aS?(aB,b,b,b,b)","b(aB,b,b)","~(b,@)","b(aB?,b,b)","l<@>?()","J(~())","I(d,d)","b(aS,aO)","b(d)","J(d,d[k?])","b(b())","~(~(b,d,b),b,b,b,aO)","~(c0>)","~(t)","b(iS,b,b,b,b)","b(b(b),b)","b(uB,b)","b(uB,b,b)","fm()","w(A)","~(eo)","w(w?)","r<~>(b,bj)","ec()","bj()","r(d)","J(cj)","r(w)","0&(w)","~(d,d)","@(d)","J(k?,ae)","d?(k?)","dT()","d?(d?)","w(w)","r<0^>(0^())","r()","db<@,@>(aa<@>)","d(d?)","r>()","bh(ad)","I(eb)","I(bh)","X(X,d)","r()","0&(k?,ae)","r(aY)","r(b5)","ad(ad,ad)","G(G)","I(ad)","r<~>(b)","~(c0>)","r(aY)","0&(bu,ae)","r(k?)","~(bB)","r(aY)","~(E?,af?,E,k,ae)","0^(E?,af?,E,0^())","0^(E?,af?,E,0^(1^),1^)","0^(E?,af?,E,0^(1^,2^),1^,2^)","0^()(E,af,E,0^())","0^(1^)(E,af,E,0^(1^))","0^(1^,2^)(E,af,E,0^(1^,2^))","a6?(E,af,E,k,ae?)","~(E?,af?,E,~())","fK(E,af,E,b_,~())","fK(E,af,E,b_,~(fK))","~(E,af,E,d)","~(d)","E(E?,af?,E,AC?,a_?)","0^(0^,0^)","aD(a_)","e9(aa)","cp(k)","be(bM)","ab(bM)","b3(bM)","b(b,b)"],interceptorsByTag:null,leafTags:null,arrayRti:Symbol("$ti"),rttc:{"1;immediateRestart":a=>b=>b instanceof A.hj&&a.b(b.a),"2;":(a,b)=>c=>c instanceof A.au&&a.b(c.a)&&b.b(c.b),"2;basicSupport,supportsReadWriteUnsafe":(a,b)=>c=>c instanceof A.hk&&a.b(c.a)&&b.b(c.b),"2;controller,sync":(a,b)=>c=>c instanceof A.hl&&a.b(c.a)&&b.b(c.b),"2;downloaded,total":(a,b)=>c=>c instanceof A.k8&&a.b(c.a)&&b.b(c.b),"2;file,outFlags":(a,b)=>c=>c instanceof A.dj&&a.b(c.a)&&b.b(c.b),"2;name,parameters":(a,b)=>c=>c instanceof A.k9&&a.b(c.a)&&b.b(c.b),"2;result,resultCode":(a,b)=>c=>c instanceof A.ka&&a.b(c.a)&&b.b(c.b),"3;":(a,b,c)=>d=>d instanceof A.hm&&a.b(d.a)&&b.b(d.b)&&c.b(d.c),"3;autocommit,lastInsertRowid,result":(a,b,c)=>d=>d instanceof A.kb&&a.b(d.a)&&b.b(d.b)&&c.b(d.c),"3;connectName,connectPort,lockName":(a,b,c)=>d=>d instanceof A.kc&&a.b(d.a)&&b.b(d.b)&&c.b(d.c),"3;hasSynced,lastSyncedAt,priority":(a,b,c)=>d=>d instanceof A.kd&&a.b(d.a)&&b.b(d.b)&&c.b(d.c),"4;atLast,priority,sinceLast,targetCount":a=>b=>b instanceof A.ke&&A.Dx(a,b.a)}} +A.Bo(v.typeUniverse,JSON.parse('{"b0":"cm","iN":"cm","d1":"cm","E_":"dX","A":{"t":["1"],"aj":[],"x":["1"],"w":[],"m":["1"]},"io":{"I":[],"Y":[]},"dP":{"J":[],"Y":[]},"aj":{"w":[]},"cm":{"aj":[],"w":[]},"im":{"fx":[]},"n9":{"A":["1"],"t":["1"],"aj":[],"x":["1"],"w":[],"m":["1"]},"dQ":{"a5":[],"a7":["bW"]},"fc":{"a5":[],"b":[],"a7":["bW"],"Y":[]},"ip":{"a5":[],"a7":["bW"],"Y":[]},"cl":{"d":[],"a7":["d"],"Y":[]},"eR":{"G":["2"],"G.T":"2"},"dG":{"ak":["2"]},"cw":{"m":["2"]},"cJ":{"cw":["1","2"],"m":["2"],"m.E":"2"},"h6":{"cJ":["1","2"],"cw":["1","2"],"x":["2"],"m":["2"],"m.E":"2"},"h2":{"C":["2"],"t":["2"],"cw":["1","2"],"x":["2"],"m":["2"]},"al":{"h2":["1","2"],"C":["2"],"t":["2"],"cw":["1","2"],"x":["2"],"m":["2"],"C.E":"2","m.E":"2"},"cQ":{"Z":[]},"bv":{"C":["b"],"t":["b"],"x":["b"],"m":["b"],"C.E":"b"},"x":{"m":["1"]},"W":{"x":["1"],"m":["1"]},"cZ":{"W":["1"],"x":["1"],"m":["1"],"W.E":"1","m.E":"1"},"bZ":{"m":["2"],"m.E":"2"},"cM":{"bZ":["1","2"],"x":["2"],"m":["2"],"m.E":"2"},"a8":{"W":["2"],"x":["2"],"m":["2"],"W.E":"2","m.E":"2"},"d5":{"m":["1"],"m.E":"1"},"f0":{"m":["2"],"m.E":"2"},"d0":{"m":["1"],"m.E":"1"},"eZ":{"d0":["1"],"x":["1"],"m":["1"],"m.E":"1"},"c2":{"m":["1"],"m.E":"1"},"dL":{"c2":["1"],"x":["1"],"m":["1"],"m.E":"1"},"cN":{"x":["1"],"m":["1"],"m.E":"1"},"fW":{"m":["1"],"m.E":"1"},"fs":{"m":["1"],"m.E":"1"},"e6":{"C":["1"],"t":["1"],"x":["1"],"m":["1"]},"cV":{"W":["1"],"x":["1"],"m":["1"],"W.E":"1","m.E":"1"},"eS":{"a_":["1","2"]},"bw":{"eS":["1","2"],"a_":["1","2"]},"hc":{"m":["1"],"m.E":"1"},"eT":{"cq":["1"],"bB":["1"],"x":["1"],"m":["1"]},"eU":{"cq":["1"],"bB":["1"],"x":["1"],"m":["1"]},"ft":{"c5":[],"Z":[]},"ir":{"Z":[]},"jj":{"Z":[]},"iK":{"V":[]},"hp":{"ae":[]},"iW":{"Z":[]},"b2":{"L":["1","2"],"a_":["1","2"],"L.V":"2","L.K":"1"},"bx":{"x":["1"],"m":["1"],"m.E":"1"},"bf":{"x":["1"],"m":["1"],"m.E":"1"},"az":{"x":["Q<1,2>"],"m":["Q<1,2>"],"m.E":"Q<1,2>"},"fe":{"b2":["1","2"],"L":["1","2"],"a_":["1","2"],"L.V":"2","L.K":"1"},"en":{"iR":[],"cR":[]},"jA":{"m":["iR"],"m.E":"iR"},"fI":{"cR":[]},"kp":{"m":["cR"],"m.E":"cR"},"dW":{"aj":[],"w":[],"eP":[],"Y":[]},"cS":{"aj":[],"uf":[],"w":[],"Y":[]},"dY":{"b4":[],"n3":[],"C":["b"],"t":["b"],"b1":["b"],"aj":[],"x":["b"],"w":[],"m":["b"],"Y":[],"C.E":"b"},"cT":{"b4":[],"bj":[],"C":["b"],"t":["b"],"b1":["b"],"aj":[],"x":["b"],"w":[],"m":["b"],"Y":[],"C.E":"b"},"dX":{"aj":[],"w":[],"eP":[],"Y":[]},"fp":{"aj":[],"w":[]},"kx":{"eP":[]},"dZ":{"b1":["1"],"aj":[],"w":[]},"co":{"C":["a5"],"t":["a5"],"b1":["a5"],"aj":[],"x":["a5"],"w":[],"m":["a5"]},"b4":{"C":["b"],"t":["b"],"b1":["b"],"aj":[],"x":["b"],"w":[],"m":["b"]},"iC":{"co":[],"ml":[],"C":["a5"],"t":["a5"],"b1":["a5"],"aj":[],"x":["a5"],"w":[],"m":["a5"],"Y":[],"C.E":"a5"},"iD":{"co":[],"mm":[],"C":["a5"],"t":["a5"],"b1":["a5"],"aj":[],"x":["a5"],"w":[],"m":["a5"],"Y":[],"C.E":"a5"},"iE":{"b4":[],"n2":[],"C":["b"],"t":["b"],"b1":["b"],"aj":[],"x":["b"],"w":[],"m":["b"],"Y":[],"C.E":"b"},"iF":{"b4":[],"n4":[],"C":["b"],"t":["b"],"b1":["b"],"aj":[],"x":["b"],"w":[],"m":["b"],"Y":[],"C.E":"b"},"iG":{"b4":[],"oR":[],"C":["b"],"t":["b"],"b1":["b"],"aj":[],"x":["b"],"w":[],"m":["b"],"Y":[],"C.E":"b"},"fq":{"b4":[],"oS":[],"C":["b"],"t":["b"],"b1":["b"],"aj":[],"x":["b"],"w":[],"m":["b"],"Y":[],"C.E":"b"},"fr":{"b4":[],"oT":[],"C":["b"],"t":["b"],"b1":["b"],"aj":[],"x":["b"],"w":[],"m":["b"],"Y":[],"C.E":"b"},"jP":{"Z":[]},"hs":{"c5":[],"Z":[]},"a6":{"Z":[]},"l":{"r":["1"]},"c0":{"bQ":["1"],"aa":["1"]},"bQ":{"aa":["1"]},"at":{"ak":["1"],"at.T":"1"},"h_":{"dI":["1"]},"ex":{"m":["1"],"m.E":"1"},"aJ":{"O":["1"],"eu":["1"],"G":["1"],"G.T":"1"},"d8":{"cx":["1"],"at":["1"],"ak":["1"],"at.T":"1"},"c8":{"bQ":["1"],"aa":["1"]},"dl":{"c8":["1"],"bQ":["1"],"aa":["1"]},"h0":{"c8":["1"],"bQ":["1"],"aa":["1"]},"d9":{"dI":["1"]},"as":{"d9":["1"],"dI":["1"]},"M":{"d9":["1"],"dI":["1"]},"fH":{"G":["1"]},"cz":{"bQ":["1"],"aa":["1"]},"bT":{"cz":["1"],"bQ":["1"],"aa":["1"]},"cB":{"cz":["1"],"bQ":["1"],"aa":["1"]},"O":{"eu":["1"],"G":["1"],"G.T":"1"},"cx":{"at":["1"],"ak":["1"],"at.T":"1"},"ev":{"aa":["1"]},"eu":{"G":["1"]},"ef":{"ak":["1"]},"de":{"G":["1"],"G.T":"1"},"bH":{"G":["1"],"G.T":"1"},"he":{"bT":["1"],"cz":["1"],"c0":["1"],"bQ":["1"],"aa":["1"]},"b9":{"G":["2"]},"ej":{"at":["2"],"ak":["2"],"at.T":"2"},"dq":{"b9":["1","1"],"G":["1"],"G.T":"1","b9.T":"1","b9.S":"1"},"bG":{"b9":["1","2"],"G":["2"],"G.T":"2","b9.T":"2","b9.S":"1"},"h7":{"aa":["1"]},"es":{"at":["2"],"ak":["2"],"at.T":"2"},"c7":{"G":["2"],"G.T":"2"},"kA":{"E":[]},"jM":{"E":[]},"kj":{"E":[]},"eA":{"af":[]},"ca":{"L":["1","2"],"a_":["1","2"],"L.V":"2","L.K":"1"},"cy":{"ca":["1","2"],"L":["1","2"],"a_":["1","2"],"L.V":"2","L.K":"1"},"h4":{"ca":["1","2"],"L":["1","2"],"a_":["1","2"],"L.V":"2","L.K":"1"},"ha":{"x":["1"],"m":["1"],"m.E":"1"},"hd":{"b2":["1","2"],"L":["1","2"],"a_":["1","2"],"L.V":"2","L.K":"1"},"cb":{"ho":["1"],"cq":["1"],"bB":["1"],"x":["1"],"m":["1"]},"d2":{"C":["1"],"t":["1"],"x":["1"],"m":["1"],"C.E":"1"},"fh":{"m":["1"],"m.E":"1"},"C":{"t":["1"],"x":["1"],"m":["1"]},"L":{"a_":["1","2"]},"fk":{"a_":["1","2"]},"fN":{"fk":["1","2"],"kw":["1","2"],"a_":["1","2"]},"fi":{"W":["1"],"x":["1"],"m":["1"],"W.E":"1","m.E":"1"},"cq":{"bB":["1"],"x":["1"],"m":["1"]},"ho":{"cq":["1"],"bB":["1"],"x":["1"],"m":["1"]},"db":{"aa":["1"]},"el":{"aa":["d"]},"jY":{"L":["d","@"],"a_":["d","@"],"L.V":"@","L.K":"d"},"jZ":{"W":["d"],"x":["d"],"m":["d"],"W.E":"d","m.E":"d"},"hN":{"cO":[]},"kv":{"ah":["d","t"]},"hP":{"ah":["d","t"],"ah.T":"t"},"ku":{"ah":["t","d"]},"hO":{"ah":["t","d"],"ah.T":"d"},"hU":{"ah":["t","d"],"ah.T":"d"},"ff":{"Z":[]},"is":{"Z":[]},"iu":{"ah":["k?","d"],"ah.T":"d"},"it":{"ah":["d","k?"],"ah.T":"k?"},"iv":{"cO":[]},"ix":{"ah":["d","t"],"ah.T":"t"},"iw":{"ah":["t","d"],"ah.T":"d"},"jq":{"cO":[]},"js":{"ah":["d","t"],"ah.T":"t"},"jr":{"ah":["t","d"],"ah.T":"d"},"vC":{"a7":["vC"]},"aK":{"a7":["aK"]},"a5":{"a7":["bW"]},"b_":{"a7":["b_"]},"b":{"a7":["bW"]},"t":{"x":["1"],"m":["1"]},"bW":{"a7":["bW"]},"iR":{"cR":[]},"bB":{"x":["1"],"m":["1"]},"d":{"a7":["d"]},"aC":{"a7":["vC"]},"hQ":{"Z":[]},"c5":{"Z":[]},"a3":{"Z":[]},"e0":{"Z":[]},"f9":{"Z":[]},"fO":{"Z":[]},"ji":{"Z":[]},"b7":{"Z":[]},"i2":{"Z":[]},"iL":{"Z":[]},"fC":{"Z":[]},"jQ":{"V":[]},"aU":{"V":[]},"ij":{"V":[],"Z":[]},"kq":{"ae":[]},"hy":{"jo":[]},"bn":{"jo":[]},"jN":{"jo":[]},"iJ":{"V":[]},"T":{"a_":["2","3"]},"cW":{"ey":["1","bB<1>"],"ey.E":"1"},"fw":{"V":[]},"dF":{"G":["t"],"G.T":"t"},"bY":{"V":[]},"jd":{"cs":[]},"eQ":{"T":["d","d","1"],"a_":["d","1"],"T.C":"d","T.K":"d","T.V":"1"},"cn":{"a7":["cn"]},"fu":{"V":[]},"d_":{"V":[]},"eV":{"V":[]},"e_":{"V":[]},"bh":{"ad":[]},"fj":{"bz":[],"aD":[]},"dM":{"aD":[]},"fP":{"bz":[],"aD":[]},"f1":{"bz":[],"aD":[]},"dH":{"aD":[]},"f4":{"bz":[],"aD":[]},"eY":{"bz":[],"aD":[]},"fM":{"bz":[],"aD":[]},"e9":{"aa":["t"]},"cp":{"b8":[]},"dJ":{"b8":[]},"fR":{"b8":[]},"fL":{"b8":[]},"f7":{"b8":[]},"fY":{"bm":[]},"hn":{"bm":[]},"h5":{"bm":[]},"h3":{"bm":[]},"fX":{"bm":[]},"ie":{"bC":[],"a7":["bC"]},"ei":{"c3":[],"a7":["j4"]},"bC":{"a7":["bC"]},"j3":{"bC":[],"a7":["bC"]},"j4":{"a7":["j4"]},"j5":{"a7":["j4"]},"j6":{"V":[]},"e2":{"aU":[],"V":[]},"e3":{"a7":["j4"]},"c3":{"a7":["j4"]},"cX":{"V":[]},"ig":{"aB":[]},"jV":{"aS":[]},"bP":{"C":["aX"],"t":["aX"],"x":["aX"],"m":["aX"],"C.E":"aX"},"aX":{"jl":["d","@"],"L":["d","@"],"a_":["d","@"],"L.V":"@","L.K":"d"},"aR":{"V":[]},"hZ":{"aB":[]},"hY":{"aS":[]},"e8":{"C":["cv"],"t":["cv"],"x":["cv"],"m":["cv"],"C.E":"cv"},"eN":{"G":["1"],"G.T":"1"},"fU":{"aB":[]},"ju":{"aS":[]},"be":{"c_":[]},"ab":{"c_":[]},"b3":{"ab":[],"c_":[]},"cP":{"aB":[]},"aG":{"aV":["aG"]},"jW":{"aS":[]},"df":{"aG":[],"aV":["aG"],"aV.E":"aG"},"ee":{"aG":[],"aV":["aG"],"aV.E":"aG"},"dc":{"aG":[],"aV":["aG"],"aV.E":"aG"},"dr":{"aG":[],"aV":["aG"],"aV.E":"aG"},"e1":{"aB":[]},"km":{"aS":[]},"iT":{"vN":[]},"bu":{"V":[]},"cU":{"V":[]},"ea":{"vI":[]},"iB":{"Z":[]},"j8":{"aY":[],"b5":[]},"j9":{"aY":[],"b5":[]},"dD":{"V":[]},"jm":{"b5":[]},"fy":{"b5":[]},"fz":{"aY":[],"b5":[]},"aY":{"b5":[]},"j7":{"aY":[],"b5":[]},"cc":{"b5":[]},"jv":{"uK":[],"aY":[],"b5":[]},"hS":{"d6":[]},"f6":{"uD":["1"]},"h9":{"aa":["1"]},"fF":{"uD":["1"]},"jf":{"aU":[],"V":[]},"bE":{"e5":["b"],"C":["b"],"t":["b"],"x":["b"],"m":["b"],"C.E":"b"},"e5":{"C":["1"],"t":["1"],"x":["1"],"m":["1"]},"jX":{"e5":["b"],"C":["b"],"t":["b"],"x":["b"],"m":["b"]},"eg":{"G":["1"],"G.T":"1"},"eh":{"ak":["1"]},"n4":{"t":["b"],"x":["b"],"m":["b"]},"bj":{"t":["b"],"x":["b"],"m":["b"]},"oT":{"t":["b"],"x":["b"],"m":["b"]},"n2":{"t":["b"],"x":["b"],"m":["b"]},"oR":{"t":["b"],"x":["b"],"m":["b"]},"n3":{"t":["b"],"x":["b"],"m":["b"]},"oS":{"t":["b"],"x":["b"],"m":["b"]},"ml":{"t":["a5"],"x":["a5"],"m":["a5"]},"mm":{"t":["a5"],"x":["a5"],"m":["a5"]},"uK":{"aY":[],"b5":[]}}')) +A.Bn(v.typeUniverse,JSON.parse('{"fV":1,"j0":1,"i9":1,"iI":1,"f3":1,"jk":1,"e6":1,"hB":2,"eT":1,"fg":1,"by":1,"dZ":1,"aa":1,"kr":1,"fH":1,"jc":2,"ks":1,"jE":1,"ev":1,"fZ":1,"ko":1,"jO":1,"c9":1,"er":1,"bU":1,"h7":1,"kn":2,"aN":1,"hx":2,"db":2,"i0":1,"i1":2,"hr":1,"id":1,"eX":1,"iH":1,"fn":1,"h9":1,"fF":1,"yY":1}')) +var u={S:"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\u03f6\x00\u0404\u03f4 \u03f4\u03f6\u01f6\u01f6\u03f6\u03fc\u01f4\u03ff\u03ff\u0584\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u05d4\u01f4\x00\u01f4\x00\u0504\u05c4\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u0400\x00\u0400\u0200\u03f7\u0200\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u0200\u0200\u0200\u03f7\x00",D:" must not be greater than the number of characters in the file, ",U:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",t:"Broadcast stream controllers do not support pause callbacks",O:"Cannot change the length of a fixed-length list",A:"Cannot extract a file path from a URI with a fragment component",z:"Cannot extract a file path from a URI with a query component",Q:"Cannot extract a non-Windows file path from a file URI with an authority",c:"Cannot fire new event. Controller is already firing an event",w:"Error handler must accept one Object or one Object and a StackTrace as arguments, and return a value of the returned future's type",B:"SELECT seq FROM main.sqlite_sequence WHERE name = 'ps_crud'",C:"Time including microseconds is outside valid range",f:"Tried to operate on a released prepared statement",y:"handleError callback must take either an Object (the error), or both an Object (the error) and a StackTrace.",E:"max must be in range 0 < max \u2264 2^32, was "} +var t=(function rtii(){var s=A.ag +return{fM:s("@<@>"),fN:s("bu"),ie:s("yY"),om:s("eN>"),lo:s("eP"),fW:s("uf"),kj:s("eQ"),eg:s("vI"),V:s("bv"),bP:s("a7<@>"),p6:s("cL"),br:s("dI"),kn:s("dI"),em:s("dK"),kS:s("vN"),lp:s("i6"),O:s("x<@>"),q:s("be"),C:s("Z"),L:s("V"),lF:s("dN"),I:s("ab"),pk:s("ml"),kI:s("mm"),lW:s("aU"),gY:s("DV"),nW:s("r"),nK:s("r<+(k?,A?)>"),jN:s("r"),p8:s("r<~>"),cF:s("cP"),m6:s("n2"),bW:s("n3"),jx:s("n4"),ks:s("m"),e7:s("m<@>"),M:s("A>"),bb:s("A>"),W:s("A"),dO:s("A>"),hf:s("A"),fU:s("A<+controller,sync(c0,I)>"),lw:s("A<+controller,sync(c0<~>,I)>"),kC:s("A<+(cr,d)>"),bN:s("A<+name,parameters(d,d)>"),cH:s("A<+hasSynced,lastSyncedAt,priority(I?,aK?,b)>"),lE:s("A"),bO:s("A>"),fu:s("A>"),i3:s("A>"),s:s("A"),az:s("A"),ba:s("A"),g7:s("A"),dg:s("A"),o6:s("A"),jI:s("A"),gk:s("A"),dG:s("A<@>"),t:s("A"),fT:s("A?>"),c:s("A"),mf:s("A"),T:s("dP"),m:s("w"),bJ:s("aO"),g:s("b0"),dX:s("b1<@>"),d9:s("aj"),p3:s("fh"),mu:s("t>"),ip:s("t"),eL:s("t<+name,parameters(d,d)>"),o:s("t"),j:s("t<@>"),f4:s("t"),ia:s("t"),fi:s("t"),ag:s("dS"),Y:s("dT"),gc:s("Q"),lx:s("Q"),ea:s("a_"),dV:s("a_"),av:s("a_<@,@>"),f:s("a_"),iZ:s("a8"),jT:s("c_"),jC:s("DZ"),kp:s("b3"),a:s("dW"),eq:s("cS"),jS:s("dY"),dQ:s("co"),aj:s("b4"),Z:s("cT"),b:s("bz"),bC:s("fs>"),P:s("J"),K:s("k"),lZ:s("E1"),aK:s("+()"),U:s("+immediateRestart(I)"),iS:s("+(w,J)"),jH:s("+(w,uD)"),cU:s("+(cr,d)"),E:s("+name,parameters(d,d)"),l4:s("+(aE,k)"),mk:s("+(I,w)"),kO:s("+basicSupport,supportsReadWriteUnsafe(I,I)"),mt:s("+(w?,w)"),iu:s("+(k?,A?)"),ii:s("+autocommit,lastInsertRowid,result(I,b,bP)"),cV:s("+atLast,priority,sinceLast,targetCount(b,b,b,b)"),lu:s("iR"),cD:s("iV"),G:s("bP"),hF:s("cV"),g_:s("e1"),hq:s("bC"),ol:s("c3"),e1:s("b6"),l:s("ae"),cB:s("jb"),ao:s("bQ"),a9:s("fG"),ha:s("ak"),ey:s("ak<~>"),ir:s("G"),hL:s("cs"),N:s("d"),of:s("X"),k:s("b8"),jM:s("d_"),gs:s("ct"),hU:s("fK"),aJ:s("Y"),do:s("c5"),hM:s("oR"),mC:s("oS"),nn:s("oT"),p:s("bj"),cx:s("d1"),ph:s("d2<+hasSynced,lastSyncedAt,priority(I?,aK?,b)>"),oP:s("fN"),en:s("ad"),w:s("jo"),a1:s("fT"),e6:s("aB"),n:s("e7"),m1:s("uK"),lS:s("fW"),u:s("d6"),R:s("ap"),l2:s("ap"),nY:s("ap"),iq:s("as"),ho:s("as"),mE:s("as"),k5:s("as"),h:s("as<~>"),oU:s("bT>"),it:s("c7<@,d>"),jB:s("c7<@,bj>"),eV:s("da"),fK:s("ec"),Q:s("dd"),hV:s("de"),d4:s("eg"),nI:s("l"),fV:s("l"),a7:s("l"),e:s("l<0&>"),jz:s("l"),x:s("l"),_:s("l<@>"),hy:s("l"),ny:s("l"),mK:s("l"),D:s("l<~>"),nf:s("aM"),mp:s("cy"),fA:s("em"),fb:s("bH>"),lX:s("bH>"),ei:s("eo"),i7:s("kf"),pp:s("bm"),eZ:s("cA"),af:s("cA<~,I()>"),lU:s("cA<~,~()>"),aP:s("M"),l6:s("M"),h1:s("M"),ex:s("M"),gW:s("M"),F:s("M<~>"),lG:s("ew"),y:s("I"),i:s("a5"),z:s("@"),mq:s("@(k)"),d:s("@(k,ae)"),S:s("b"),d_:s("eW?"),gK:s("r?"),m2:s("r<~>?"),A:s("w?"),h9:s("a_?"),X:s("k?"),B:s("bO?"),J:s("aX?"),mQ:s("ak?"),cn:s("cs?"),jv:s("d?"),a_:s("bE?"),he:s("e7?"),gh:s("da?"),dd:s("aM?"),o9:s("I?"),jX:s("a5?"),aV:s("b?"),jh:s("bW?"),r:s("bW"),H:s("~"),cj:s("~()"),i6:s("~(k)"),v:s("~(k,ae)")}})();(function constants(){var s=hunkHelpers.makeConstList +B.bs=J.ik.prototype +B.d=J.A.prototype +B.b=J.fc.prototype +B.a5=J.dP.prototype +B.a6=J.dQ.prototype +B.a=J.cl.prototype +B.bt=J.b0.prototype +B.bu=J.aj.prototype +B.ad=A.cS.prototype +B.K=A.fq.prototype +B.f=A.cT.prototype +B.ae=J.iN.prototype +B.S=J.d1.prototype +B.C=new A.bu("Operation was cancelled",null) +B.Y=new A.hO(!1,127) +B.aT=new A.hP(127) +B.bd=new A.de(A.ag("de>")) +B.aU=new A.dF(B.bd) +B.aV=new A.fb(A.Dw(),A.ag("fb")) +B.cp=new A.hU() +B.aW=new A.l7() +B.D=new A.eX() +B.aX=new A.eY() +B.Z=new A.i9() +B.l=new A.be() +B.aY=new A.f4() +B.aZ=new A.ij() +B.a_=function getTagFallback(o) { + var s = Object.prototype.toString.call(o); + return s.substring(8, s.length - 1); +} +B.b_=function() { + var toStringFunction = Object.prototype.toString; + function getTag(o) { + var s = toStringFunction.call(o); + return s.substring(8, s.length - 1); + } + function getUnknownTag(object, tag) { + if (/^HTML[A-Z].*Element$/.test(tag)) { + var name = toStringFunction.call(object); + if (name == "[object Object]") return null; + return "HTMLElement"; + } + } + function getUnknownTagGenericBrowser(object, tag) { + if (object instanceof HTMLElement) return "HTMLElement"; + return getUnknownTag(object, tag); + } + function prototypeForTag(tag) { + if (typeof window == "undefined") return null; + if (typeof window[tag] == "undefined") return null; + var constructor = window[tag]; + if (typeof constructor != "function") return null; + return constructor.prototype; + } + function discriminator(tag) { return null; } + var isBrowser = typeof HTMLElement == "function"; + return { + getTag: getTag, + getUnknownTag: isBrowser ? getUnknownTagGenericBrowser : getUnknownTag, + prototypeForTag: prototypeForTag, + discriminator: discriminator }; +} +B.b4=function(getTagFallback) { + return function(hooks) { + if (typeof navigator != "object") return hooks; + var userAgent = navigator.userAgent; + if (typeof userAgent != "string") return hooks; + if (userAgent.indexOf("DumpRenderTree") >= 0) return hooks; + if (userAgent.indexOf("Chrome") >= 0) { + function confirm(p) { + return typeof window == "object" && window[p] && window[p].name == p; + } + if (confirm("Window") && confirm("HTMLElement")) return hooks; + } + hooks.getTag = getTagFallback; + }; +} +B.b0=function(hooks) { + if (typeof dartExperimentalFixupGetTag != "function") return hooks; + hooks.getTag = dartExperimentalFixupGetTag(hooks.getTag); +} +B.b3=function(hooks) { + if (typeof navigator != "object") return hooks; + var userAgent = navigator.userAgent; + if (typeof userAgent != "string") return hooks; + if (userAgent.indexOf("Firefox") == -1) return hooks; + var getTag = hooks.getTag; + var quickMap = { + "BeforeUnloadEvent": "Event", + "DataTransfer": "Clipboard", + "GeoGeolocation": "Geolocation", + "Location": "!Location", + "WorkerMessageEvent": "MessageEvent", + "XMLDocument": "!Document"}; + function getTagFirefox(o) { + var tag = getTag(o); + return quickMap[tag] || tag; + } + hooks.getTag = getTagFirefox; +} +B.b2=function(hooks) { + if (typeof navigator != "object") return hooks; + var userAgent = navigator.userAgent; + if (typeof userAgent != "string") return hooks; + if (userAgent.indexOf("Trident/") == -1) return hooks; + var getTag = hooks.getTag; + var quickMap = { + "BeforeUnloadEvent": "Event", + "DataTransfer": "Clipboard", + "HTMLDDElement": "HTMLElement", + "HTMLDTElement": "HTMLElement", + "HTMLPhraseElement": "HTMLElement", + "Position": "Geoposition" + }; + function getTagIE(o) { + var tag = getTag(o); + var newTag = quickMap[tag]; + if (newTag) return newTag; + if (tag == "Object") { + if (window.DataView && (o instanceof window.DataView)) return "DataView"; + } + return tag; + } + function prototypeForTagIE(tag) { + var constructor = window[tag]; + if (constructor == null) return null; + return constructor.prototype; + } + hooks.getTag = getTagIE; + hooks.prototypeForTag = prototypeForTagIE; +} +B.b1=function(hooks) { + var getTag = hooks.getTag; + var prototypeForTag = hooks.prototypeForTag; + function getTagFixed(o) { + var tag = getTag(o); + if (tag == "Document") { + if (!!o.xmlVersion) return "!Document"; + return "!HTMLDocument"; + } + return tag; + } + function prototypeForTagFixed(tag) { + if (tag == "Document") return null; + return prototypeForTag(tag); + } + hooks.getTag = getTagFixed; + hooks.prototypeForTag = prototypeForTagFixed; +} +B.a0=function(hooks) { return hooks; } + +B.h=new A.nb() +B.m=new A.iv() +B.b5=new A.nc() +B.y=new A.iz(A.ag("iz")) +B.z=new A.dU(A.ag("dU")) +B.a1=new A.dU(A.ag("dU")) +B.b6=new A.iL() +B.c=new A.nX() +B.b8=new A.cW(A.ag("cW")) +B.b7=new A.cW(A.ag("cW<+name,parameters(d,d)>")) +B.b9=new A.fL() +B.ba=new A.fR() +B.i=new A.jq() +B.n=new A.js() +B.bb=new A.fX() +B.bc=new A.qw() +B.A=new A.qy() +B.be=new A.qW() +B.e=new A.kj() +B.r=new A.kq() +B.bf=new A.rR() +B.bg=new A.dJ(0,"established") +B.bh=new A.dJ(1,"end") +B.E=new A.ch(3,"updateSubscriptionManagement") +B.F=new A.ch(4,"notifyUpdates") +B.a2=new A.b_(0) +B.G=new A.b_(1e4) +B.u=new A.b_(5e6) +B.a3=new A.ci("l",1,"opfsAtomics") +B.a4=new A.ci("x",2,"opfsExternalLocks") +B.bv=new A.it(null) +B.bw=new A.iu(null) +B.a7=new A.iw(!1,255) +B.bx=new A.ix(255) +B.v=new A.cn("FINE",500) +B.j=new A.cn("INFO",800) +B.o=new A.cn("WARNING",900) +B.by=s([239,191,189],t.t) +B.x=new A.bD(0,"unknown") +B.as=new A.bD(1,"integer") +B.at=new A.bD(2,"bigInt") +B.au=new A.bD(3,"float") +B.av=new A.bD(4,"text") +B.aw=new A.bD(5,"blob") +B.ax=new A.bD(6,"$null") +B.ay=new A.bD(7,"boolean") +B.a8=s([B.x,B.as,B.at,B.au,B.av,B.aw,B.ax,B.ay],A.ag("A")) +B.bz=s([65533],t.t) +B.bi=new A.ch(0,"ok") +B.bj=new A.ch(1,"getAutoCommit") +B.bk=new A.ch(2,"executeBatch") +B.a9=s([B.bi,B.bj,B.bk,B.E,B.F],A.ag("A")) +B.bo=new A.f2(0,"database") +B.bp=new A.f2(1,"journal") +B.aa=s([B.bo,B.bp],A.ag("A")) +B.M=new A.jg(0,"rust") +B.bA=s([B.M],A.ag("A")) +B.ag=new A.e4(0,"insert") +B.ah=new A.e4(1,"update") +B.ai=new A.e4(2,"delete") +B.bB=s([B.ag,B.ah,B.ai],A.ag("A")) +B.N=new A.aE(0,"ping") +B.al=new A.aE(1,"startSynchronization") +B.ao=new A.aE(2,"updateSubscriptions") +B.ap=new A.aE(3,"abortSynchronization") +B.O=new A.aE(4,"requestEndpoint") +B.P=new A.aE(5,"uploadCrud") +B.Q=new A.aE(6,"invalidCredentialsCallback") +B.R=new A.aE(7,"credentialsCallback") +B.aq=new A.aE(8,"notifySyncStatus") +B.ar=new A.aE(9,"logEvent") +B.am=new A.aE(10,"okResponse") +B.an=new A.aE(11,"errorResponse") +B.bC=s([B.N,B.al,B.ao,B.ap,B.O,B.P,B.Q,B.R,B.aq,B.ar,B.am,B.an],A.ag("A")) +B.H=s([],t.s) +B.bE=s([],t.t) +B.w=s([],t.c) +B.bD=s([],t.bN) +B.ab=s([],t.cH) +B.bn=new A.ci("s",0,"opfsShared") +B.bl=new A.ci("i",3,"indexedDb") +B.bm=new A.ci("m",4,"inMemory") +B.bF=s([B.bn,B.a3,B.a4,B.bl,B.bm],A.ag("A")) +B.bq=new A.dN("/database",0,"database") +B.br=new A.dN("/database-journal",1,"journal") +B.ac=s([B.bq,B.br],A.ag("A")) +B.aj=new A.cr(0,"opfs") +B.ak=new A.cr(1,"indexedDb") +B.bO=new A.cr(2,"inMemory") +B.bG=s([B.aj,B.ak,B.bO],A.ag("A")) +B.aC=new A.ap(A.vl(),A.bs(),0,"xAccess",t.nY) +B.aD=new A.ap(A.vl(),A.ce(),1,"xDelete",A.ag("ap")) +B.aO=new A.ap(A.vl(),A.bs(),2,"xOpen",t.nY) +B.aM=new A.ap(A.bs(),A.bs(),3,"xRead",t.l2) +B.aH=new A.ap(A.bs(),A.ce(),4,"xWrite",t.R) +B.aI=new A.ap(A.bs(),A.ce(),5,"xSleep",t.R) +B.aJ=new A.ap(A.bs(),A.ce(),6,"xClose",t.R) +B.aN=new A.ap(A.bs(),A.bs(),7,"xFileSize",t.l2) +B.aK=new A.ap(A.bs(),A.ce(),8,"xSync",t.R) +B.aL=new A.ap(A.bs(),A.ce(),9,"xTruncate",t.R) +B.aF=new A.ap(A.bs(),A.ce(),10,"xLock",t.R) +B.aG=new A.ap(A.bs(),A.ce(),11,"xUnlock",t.R) +B.aE=new A.ap(A.ce(),A.ce(),12,"stopServer",A.ag("ap")) +B.bH=s([B.aC,B.aD,B.aO,B.aM,B.aH,B.aI,B.aJ,B.aN,B.aK,B.aL,B.aF,B.aG,B.aE],A.ag("A>")) +B.bL={"iso_8859-1:1987":0,"iso-ir-100":1,"iso_8859-1":2,"iso-8859-1":3,latin1:4,l1:5,ibm819:6,cp819:7,csisolatin1:8,"iso-ir-6":9,"ansi_x3.4-1968":10,"ansi_x3.4-1986":11,"iso_646.irv:1991":12,"iso646-us":13,"us-ascii":14,us:15,ibm367:16,cp367:17,csascii:18,ascii:19,csutf8:20,"utf-8":21} +B.k=new A.hN() +B.bI=new A.bw(B.bL,[B.m,B.m,B.m,B.m,B.m,B.m,B.m,B.m,B.m,B.k,B.k,B.k,B.k,B.k,B.k,B.k,B.k,B.k,B.k,B.k,B.i,B.i],A.ag("bw")) +B.B={} +B.J=new A.bw(B.B,[],A.ag("bw")) +B.bJ=new A.bw(B.B,[],A.ag("bw")) +B.I=new A.bw(B.B,[],A.ag("bw")) +B.p=new A.fn(12,"simpleSuccessResponse") +B.bK=new A.fn(14,"rowsResponse") +B.cq=new A.nt(2,"readWriteCreate") +B.af=new A.hj(!1) +B.L=new A.hk(!1,!1) +B.bM=new A.hm("BEGIN IMMEDIATE","COMMIT","ROLLBACK") +B.bN=new A.eU(B.B,0,A.ag("eU")) +B.bP=new A.ct(!1,!1,!1,null,!1,null,null,null,null,B.ab,null) +B.bQ=A.bt("eP") +B.bR=A.bt("uf") +B.bS=A.bt("ml") +B.bT=A.bt("mm") +B.bU=A.bt("n2") +B.bV=A.bt("n3") +B.bW=A.bt("n4") +B.bX=A.bt("w") +B.bY=A.bt("k") +B.bZ=A.bt("oR") +B.c_=A.bt("oS") +B.c0=A.bt("oT") +B.c1=A.bt("bj") +B.c2=new A.fQ("DELETE",2,"delete") +B.c3=new A.fQ("PATCH",1,"patch") +B.c4=new A.fQ("PUT",0,"put") +B.az=new A.jr(!1) +B.c5=new A.aR(10) +B.c6=new A.aR(12) +B.aA=new A.aR(14) +B.c7=new A.aR(2570) +B.c8=new A.aR(3850) +B.c9=new A.aR(522) +B.aB=new A.aR(778) +B.ca=new A.aR(8) +B.cb=new A.ep("reaches root") +B.T=new A.ep("below root") +B.U=new A.ep("at root") +B.V=new A.ep("above root") +B.q=new A.eq("different") +B.W=new A.eq("equal") +B.t=new A.eq("inconclusive") +B.X=new A.eq("within") +B.aP=new A.et("canceled") +B.aQ=new A.et("dormant") +B.aR=new A.et("listening") +B.aS=new A.et("paused") +B.cc=new A.aN(B.e,A.CQ()) +B.cd=new A.aN(B.e,A.CM()) +B.ce=new A.aN(B.e,A.CU()) +B.cf=new A.aN(B.e,A.CN()) +B.cg=new A.aN(B.e,A.CO()) +B.ch=new A.aN(B.e,A.CP()) +B.ci=new A.aN(B.e,A.CR()) +B.cj=new A.aN(B.e,A.CT()) +B.ck=new A.aN(B.e,A.CV()) +B.cl=new A.aN(B.e,A.CW()) +B.cm=new A.aN(B.e,A.CX()) +B.cn=new A.aN(B.e,A.CY()) +B.co=new A.aN(B.e,A.CS())})();(function staticFields(){$.qY=null +$.du=A.v([],t.hf) +$.y5=null +$.w6=null +$.vG=null +$.vF=null +$.xY=null +$.xP=null +$.y6=null +$.tD=null +$.tP=null +$.vg=null +$.r9=A.v([],A.ag("A?>")) +$.eF=null +$.hC=null +$.hD=null +$.v8=!1 +$.n=B.e +$.ra=null +$.wB=null +$.wC=null +$.wD=null +$.wE=null +$.uO=A.q5("_lastQuoRemDigits") +$.uP=A.q5("_lastQuoRemUsed") +$.h1=A.q5("_lastRemUsed") +$.uQ=A.q5("_lastRem_nsh") +$.ww="" +$.wx=null +$.eE=0 +$.eB=A.P(t.N,t.S) +$.w0=0 +$.zO=A.P(t.N,t.Y) +$.xo=null +$.t5=null})();(function lazyInitializers(){var s=hunkHelpers.lazyFinal,r=hunkHelpers.lazy +s($,"DT","dA",()=>A.De("_$dart_dartClosure")) +s($,"EP","yK",()=>B.e.bJ(new A.u1(),t.p8)) +s($,"EI","yG",()=>A.v([new J.im()],A.ag("A"))) +s($,"E7","yh",()=>A.c6(A.oQ({ +toString:function(){return"$receiver$"}}))) +s($,"E8","yi",()=>A.c6(A.oQ({$method$:null, +toString:function(){return"$receiver$"}}))) +s($,"E9","yj",()=>A.c6(A.oQ(null))) +s($,"Ea","yk",()=>A.c6(function(){var $argumentsExpr$="$arguments$" +try{null.$method$($argumentsExpr$)}catch(q){return q.message}}())) +s($,"Ed","yn",()=>A.c6(A.oQ(void 0))) +s($,"Ee","yo",()=>A.c6(function(){var $argumentsExpr$="$arguments$" +try{(void 0).$method$($argumentsExpr$)}catch(q){return q.message}}())) +s($,"Ec","ym",()=>A.c6(A.wt(null))) +s($,"Eb","yl",()=>A.c6(function(){try{null.$method$}catch(q){return q.message}}())) +s($,"Eg","yq",()=>A.c6(A.wt(void 0))) +s($,"Ef","yp",()=>A.c6(function(){try{(void 0).$method$}catch(q){return q.message}}())) +s($,"Ej","vq",()=>A.AF()) +s($,"DX","cH",()=>$.yK()) +s($,"DW","ye",()=>A.AX(!1,B.e,t.y)) +s($,"Er","yu",()=>{var q=t.z +return A.mC(null,null,null,q,q)}) +s($,"Eu","yx",()=>A.zW(4096)) +s($,"Es","yv",()=>new A.rO().$0()) +s($,"Et","yw",()=>new A.rN().$0()) +s($,"Ek","yr",()=>A.zU(A.xp(A.v([-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-1,-2,-2,-2,-2,-2,62,-2,62,-2,63,52,53,54,55,56,57,58,59,60,61,-2,-2,-2,-1,-2,-2,-2,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,-2,-2,-2,-2,63,-2,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,-2,-2,-2,-2,-2],t.t)))) +s($,"Ep","cf",()=>A.pX(0)) +s($,"Eo","kM",()=>A.pX(1)) +s($,"Em","vs",()=>$.kM().br(0)) +s($,"El","vr",()=>A.pX(1e4)) +r($,"En","ys",()=>A.ar("^\\s*([+-]?)((0x[a-f0-9]+)|(\\d+)|([a-z0-9]+))\\s*$",!1)) +s($,"Eq","yt",()=>typeof FinalizationRegistry=="function"?FinalizationRegistry:null) +s($,"Ex","bX",()=>A.kH(B.bY)) +r($,"ED","kN",()=>new A.tb().$0()) +r($,"EA","yB",()=>new A.t9().$0()) +s($,"Ez","yA",()=>Symbol("jsBoxedDartObjectProperty")) +s($,"E0","yf",()=>{var q=new A.qX(A.zS(8)) +q.ky() +return q}) +s($,"DR","vn",()=>A.ar("^[\\w!#%&'*+\\-.^`|~]+$",!0)) +s($,"Ew","yy",()=>A.ar('["\\x00-\\x1F\\x7F]',!0)) +s($,"EQ","yL",()=>A.ar('[^()<>@,;:"\\\\/[\\]?={} \\t\\x00-\\x1F\\x7F]+',!0)) +s($,"EC","yC",()=>A.ar("(?:\\r\\n)?[ \\t]+",!0)) +s($,"EF","yE",()=>A.ar('"(?:[^"\\x00-\\x1F\\x7F\\\\]|\\\\.)*"',!0)) +s($,"EE","yD",()=>A.ar("\\\\(.)",!0)) +s($,"EO","yJ",()=>A.ar('[()<>@,;:"\\\\/\\[\\]?={} \\t\\x00-\\x1F\\x7F]',!0)) +s($,"ES","yM",()=>A.ar("(?:"+$.yC().a+")*",!0)) +s($,"DY","ud",()=>A.uy("")) +s($,"ER","hI",()=>A.vL(null,$.dB())) +s($,"EM","kO",()=>new A.i3($.vo(),null)) +s($,"E4","yg",()=>new A.nv(A.ar("/",!0),A.ar("[^/]$",!0),A.ar("^/",!0))) +s($,"E6","kL",()=>new A.pv(A.ar("[/\\\\]",!0),A.ar("[^/\\\\]$",!0),A.ar("^(\\\\\\\\[^\\\\]+\\\\[^\\\\/]+|[a-zA-Z]:[/\\\\])",!0),A.ar("^[/\\\\](?![/\\\\])",!0))) +s($,"E5","dB",()=>new A.p2(A.ar("/",!0),A.ar("(^[a-zA-Z][-+.a-zA-Z\\d]*://|[^/])$",!0),A.ar("[a-zA-Z][-+.a-zA-Z\\d]*://[^/]*",!0),A.ar("^/",!0))) +s($,"E3","vo",()=>A.An()) +s($,"EJ","vt",()=>A.Cf()) +s($,"EB","dC",()=>$.vt()) +s($,"Ey","yz",()=>A.zE(A.Dg(),"SharedWorkerGlobalScope")) +s($,"EL","yI",()=>A.vD("-9223372036854775808")) +s($,"EK","yH",()=>A.vD("9223372036854775807")) +s($,"DS","hH",()=>$.yf()) +s($,"Eh","vp",()=>new A.id(new WeakMap())) +s($,"DQ","ub",()=>A.zM(A.v(["files","blocks"],t.s))) +s($,"DU","uc",()=>{var q,p,o=A.P(t.N,t.lF) +for(q=0;q<2;++q){p=B.ac[q] +o.m(0,p.c,p)}return o}) +s($,"EG","yF",()=>A.A4()) +r($,"Ei","ue",()=>{var q="navigator" +return A.zD(A.zF(A.tJ(A.y9(),q),"locks"))?new A.pn(A.tJ(A.tJ(A.y9(),q),"locks")):null})})();(function nativeSupport(){!function(){var s=function(a){var m={} +m[a]=1 +return Object.keys(hunkHelpers.convertToFastObject(m))[0]} +v.getIsolateTag=function(a){return s("___dart_"+a+v.isolateTag)} +var r="___dart_isolate_tags_" +var q=Object[r]||(Object[r]=Object.create(null)) +var p="_ZxYxX" +for(var o=0;;o++){var n=s(p+"_"+o+"_") +if(!(n in q)){q[n]=1 +v.isolateTag=n +break}}v.dispatchPropertyName=v.getIsolateTag("dispatch_record")}() +hunkHelpers.setOrUpdateInterceptorsByTag({SharedArrayBuffer:A.dX,ArrayBuffer:A.dW,ArrayBufferView:A.fp,DataView:A.cS,Float32Array:A.iC,Float64Array:A.iD,Int16Array:A.iE,Int32Array:A.dY,Int8Array:A.iF,Uint16Array:A.iG,Uint32Array:A.fq,Uint8ClampedArray:A.fr,CanvasPixelArray:A.fr,Uint8Array:A.cT}) +hunkHelpers.setOrUpdateLeafTags({SharedArrayBuffer:true,ArrayBuffer:true,ArrayBufferView:false,DataView:true,Float32Array:true,Float64Array:true,Int16Array:true,Int32Array:true,Int8Array:true,Uint16Array:true,Uint32Array:true,Uint8ClampedArray:true,CanvasPixelArray:true,Uint8Array:false}) +A.dZ.$nativeSuperclassTag="ArrayBufferView" +A.hf.$nativeSuperclassTag="ArrayBufferView" +A.hg.$nativeSuperclassTag="ArrayBufferView" +A.co.$nativeSuperclassTag="ArrayBufferView" +A.hh.$nativeSuperclassTag="ArrayBufferView" +A.hi.$nativeSuperclassTag="ArrayBufferView" +A.b4.$nativeSuperclassTag="ArrayBufferView"})() +Function.prototype.$0=function(){return this()} +Function.prototype.$1=function(a){return this(a)} +Function.prototype.$2=function(a,b){return this(a,b)} +Function.prototype.$3$3=function(a,b,c){return this(a,b,c)} +Function.prototype.$2$2=function(a,b){return this(a,b)} +Function.prototype.$1$1=function(a){return this(a)} +Function.prototype.$2$1=function(a){return this(a)} +Function.prototype.$3=function(a,b,c){return this(a,b,c)} +Function.prototype.$4=function(a,b,c,d){return this(a,b,c,d)} +Function.prototype.$3$1=function(a){return this(a)} +Function.prototype.$1$2=function(a,b){return this(a,b)} +Function.prototype.$5=function(a,b,c,d,e){return this(a,b,c,d,e)} +Function.prototype.$6=function(a,b,c,d,e,f){return this(a,b,c,d,e,f)} +Function.prototype.$1$0=function(){return this()} +Function.prototype.$2$3=function(a,b,c){return this(a,b,c)} +convertAllToFastObject(w) +convertToFastObject($);(function(a){if(typeof document==="undefined"){a(null) +return}if(typeof document.currentScript!="undefined"){a(document.currentScript) +return}var s=document.scripts +function onLoad(b){for(var q=0;q~L3=dK9^WX_S+pe*SID6P%J0!BorQcJBBLI{$3LqcvY5&8|u zWsxExMx;s=B(Ahp6kFS(fCg$Sifz@p)-57xT&ifbrK0kCy+6;)Ip;2+X#2pudW#>A0XZ!X)mmLj%;y?ykT3`f3h$ z?Q|_WJYeDm+5{V;7)Tmzho);(_uJBT>Rx{fAL_M}whA2j+lb*7Hx>u2eJ2wJySCV< zZkGOljM9Uy#G6*4Q6p~GR5}`*TWGW?0StI|atL?q*ad*XjvG1Utojp}x+=o#(8_F7 z9k?>sv08zRxQh{m8@nmkA_!tZu*=`#PMFr$VqD{{3OfdmEsQgb1f?9^c4}S*1kKF~ zHX%l9&=qtOn=sUmu@l|I~)s>tk3ZXmkdmYJADRFZKq9ah)&9p)TC1zs~SD{ zRP==C+;uavjZItDuiG@c@q!KBFIhu!@iR7U*|x#!+PC=P=WbYcu6Im%>Gx+g&Tc5b zH7-75=AzB(*U|9XW#8vrvU$BXZeJP~pR{q)hUabk;|*m)eDSllte-Bw)Gq$L%}iW< z9liJ&=Wkd){oD=PHq30_aPC0I$%}ty!}g8qH$3C~bu;VM&u*C6xNUah`fWYvPP=f! zW^c94SflHv4M6kD<=0~U`VHH*d8_Pe`{I)>+O+BPb+hMhUdLKpV|wv(*KIy`%LSX& z&)t?>{Jc#YHe6W3IdbtcE}EIyuzB_j5VUsl?6JRIUKl!eo$46Vx0$?pTBO?MH|ZIk$&&Bo40S6ffNgex#+@kh4sRQY4!TA zJYr*sm%+B}-Wuzd_4=*fa>0e`W;S@wu=dII=dauRVlY2@*18KX+;quV=WW~sp38ME zUwC!>md)pF+PHqUL|3~v>Nyu}yTlt0Z&-HyGCvG!K@tU_=QoCi!l)Mde(2Z2A>Rwb zaKdYPD}4WVMm*nCxbM{(-o%M$bC1hDcY3wD7sP?r$kcXHl7#h|_e&vT4E}4Ef1%%q z7`*1|KPIx1s2$G#=hx`s{=(X?)*6k5{%sk0-q~h z93_n~X-9s&5!LI=+N=S-SC4~)>wX-wLJtT-))Y4B?fEr-j1@KFny$9_-@z1l;MZXz z))6d1cCF9I3Rpsr5GWePAs0bUqu~K%#0}uR4mfa(Ag|Rt>nHkq%*I@Rqp9dR@E7`U zkpaMmJ2C@Q(ab_)!g^{^Ee0b=7D0>13!*p-JulG6f)6NT7}Lh5d2J6(A^2r-twK-- zFq$8Yu@1YfrfVKN1qJ*=Lzc-<^few2!z7|%#8z#KSnwgXV4~U+#2$2w5`RU+e4M(! z-U*|RcwPcz;3)D!uK`Jf2yhg8Ufb59#kAW1)3DWui32}6^odV=B2;Kg>Ht=M&-%5M zbQK8##`MEee(kW~kzxBgZ1}Ke|GGOJKJ1`VhllwRFff7`3vYg${6E#P{l=KkP3-0K-fh1VS z<`5SGU!ec$aRbUpDru7LsG)&luMsa#SPV)fNkF!^6I+5tVN~H&_j`$q6B@;<`_eQ( z3U1>h^O6jHP8c$gw8e29u?F|Z8K8m>xXDAcgP1TnIIjE6tRW6LJZfO@8Yf`K!Wxna zLg3;?Er1djFM0$+$xt(ZpAtW;0}hf8<{J!LW0^o0_(NRv!bS}=A>%c`7=q?OJ&S9o zKfs9+@LaDQ#N?wPdL4O0lVMYZ0I)TriNo5F(3}yVljqgFBwQxNtN);8%?F*Ra2TYK z3?(%`iRoQWf||F?_fO!Sz#HLTo9^Vs-FLGswY!P~McjsR3mKvYXx$!>~ycq!e!n-GGW2@v<1I(k-dQ zQ6{bfX6O&fg^kq@WBs!eoJW5L$&^2oJN3q6xy_g{?ziSuYt)o(kI3 z&3iIp0M(oP1>T_}VE_-pGwLOq;ClfSf_n8V5E!7dksQfCG=}#ZtRi0CX?1*ms8ws$ z{3q0*U?BT7Bn{Ru!SVmdizh*OGLbF!>qs|rS-yNZ6KGlW2rxw`Q46|bAe)CTt3fQ- zrVcNLAgAOshQk(u19G^_?~qPMbpEY42tT&lkWNU66guB{jqR8 ztN{^|Xc-XkCn1lt?xdMCQ6tT#z*WsgBWRS1PrzUV)|=kBvJvRA4IQSC$atd(C`l%p z%|oD8t(9O?_^XEyO_(`2Zbrd*bz?^pP&JMw0W+mITwXoF*Fv*3Ta(d?k7_|0={G6W zU@S`g_PO2mtV}y zMd>cD*z@UDTKntl`N;(vF4!`2N#KnsGCXVB)=flvzjoFI>oyUqc|WR+%xgYxX2S;W z`?bk=jpuCKeC}E2Y}#^;_n%8$ncZ;ltoLy3pn3PqYn|D)v{z_-IY}$0ry7klByf@Sjoi|bm(|S6r6Hwls`oZ%$oO{k$3Zpi@ z_^geaXT7)7M=DJBE_iR9kA?+NnD@5&d=x79I*T~&+)YGod;0@kL_mEO!Qwja2cx6a zEE_j(oZYx?lS1?B>aD8P$obBC*88qdRej!7Z&z(I3KPArX1$@dD{yu6{%B)CtI_(E zjj`(HvhO9c8@73OG{)yQpS$Ivb2e@8zB9U@I|w4?{_g0+{O;Q>I%oEh3paS*>#xcU zZ>`_&cL(B&Hq9D@jPoCj-Zr!<9%jPy94OM{biN?HTm%FC zM;~cry9wYAM|(J~nhp0q>SL#DFOl${i*9g>eR$FM2F~ZR-jd334EbMJ)NBctf9p4! zjwt7Rq)$L=b~FBTv?mhQYRLWB=phwutdSfBnH=d2hlaVa16@b*H(cypHr6Blw%H42 z&q8yYyJhpHOT5d+dQ`Iu8@Fw^_`*%=Hg5K=80(Q|!^Iocdw*W<@%V)^8L=K)Q*(HE5>>x@3)?{`JxNXL6ol>xVUcHEcyb0 z2J|g2*?6wEdu+Ixx6}{btHyfryIxs5fp^1Lj}MHg&)Ra{c@pE-jxDclM=NgtN9UeJ zhxJ>|-QfM%*x-`3&CYnQ>rb|YoSO!IeIIWxR`%$k*)2|4dY_y;bkWcy+iUo3-lrxj z-q}X#O^W;6jpv=`eR{GYQPu2psrR3YE}ZLfeGf0Xux@sC-TL#ruTEC0>kU=qxxD|L zTvmctU9(i4kTSPyAp{#XuajoFY}vwl;Hr%?8_0xRzO3S(YB)IPf(vIa@vi7y=_~MU zv)ESe%HBNvE1P8;-(IgU)4#CgLhqVo6%hxTH=XOfe2MnW8(!jFyR71>e*g0-jecjn zT1)@pM&%l3y;m&jG0?Ux7tO5S0H58^Tg6$ktP4_U**FR$M6Pa1 z#1F(@jXxbe7=JCkD|&zQ!|>YZZPC}`Z^ZXRe;<7_{%~@C^3mj_$q$ly;*G!e{Of8{ z8}3T(PJZiUwYS%%&p7;-p49zp{9y96%-%B1! z9!~x-{O{z4$u{pIy*>#wN) zTYP=})%6?de_DTS{l@z1>o?bLslT!Q7xinGeXMa${hjq&8eeU`Exk4Uc>0O-U(-*f zpXTf{=^g2N(|gi?s6W#BaqE+rcMX53{-NRfhd(^q$c-kA83DUq;_M{P&}Gj(%n2!I7_xyfnKxyEFTT>_gdo*>_v_ zXJ5>|pZ!;M|Hz(^+eY3wa@WY+Bkv!1)99_E?;icD(Z3k|#ON#2tH-Vxd(GHQV>gf8 zGPY;zjbm>bd-K>^#@;&iwy|5s{(S7}@t2NYHGbvz+sFQL>>Xo29{caH4~^e9{^9YD zjNd>0ne>72uZ}-BzJL6i{O!bhC%!iE=){jE z-ZAl~lQ&NO+2mU%e=`2s$&XClKl#zg{iC0m{OsiCCciwnZ}KaX4^001z>vPt&gYowmviZ+0oC9{@dv1 zN544wrO_{s?i+nz^xN(4w!hbYsQvx+f3$zt{;&4K?H{+_pWU6^l)XNCUG_JPziYg& zad%^HGvUyeWPns`pUfX;{ z^Pg+iHNQ}MWpiI`ck}+*ztlcn``6khYoDuqwf6Pe=W7qvzES&F?Gv?6)jnE#RrC7h ztD848-<fZ`r-5=>HX;|((BUQ>8sM~(^scAq<@;ehR@gXd1Lx#>Fd(h zr#GcHr*BAaN%y4Rjo+BQDg9CN;pTrgA8G!y`DpX9^z!tI^vd*Y&382as`)p~ziZyr z{QKrVG(Xh*aPuS0` zH*RhGMdR&_cQoGFcvs_XjdwTRF!mQ?w~g%|y>sI3iT6)@VB(&Mzn}Pri4RSDV&c;i zpPBgL#Fr-?n)v?2{?Q*y{BYtwCmx>o?};BzJTmc3GzIyV8$=6K2e)6WtnL+Tn|R&C%f??ee$Dvjp!mzjcaOh%{59jRAHQk*=JEHB?H#*k?B1~t zj{W1*OIlI*|@h;cHaRTAer#IE`~Z z4}Lt*x>7Il0C|d2MUiv9AD4H#w?nZ}u!D>ylmC-nC z?~r0mbH5XYOmw8@b>kDck`(@IH!M6(veQ=NVP4~o74!`ly6~quVICLWGoxJ`=60PV zAdk_#c{0^W=pIiM0iD!6IWe!zbZTlW!mY*fmvrkzJZm_Mw&ry^3CffD)}l5$GgBP8 zeTv@x_9ED-lfX`5R^J;wWpFJmkBadrfn=zQRtC#EAq(VW+#xS2z<-uUYC7cDpv74z zyhYHaxN;7HC#9bI#WR1I-(9y+9Y>0glkqtx%n2x4utGz+sBL?lQ8vc2DgBRikD{ zY7uouA!zN$PxtbEV+C=9uO=@QLueU?d+CrkvbWmwGvp+CkSE3xpm3|^hd*1LCd?2V zWz5hH#3oanC|Iq3dAzq<6XiK8=KPaDTwLt+Po`U9^KuZw=gFEqEHpU|REJPGoSo_h z;FHT7$PxK%H8U`z=mf2ZWx_R`0Ps17HF&3$#AIHS)-?3d8CgMopQvRzxNeYDLz>zA$gCsP|gy z1Fe}_!@Qw&9pguxhPvji5jJ;?G0Z<7CwY9^npIcjjoa2d`e=B?n*6FO^VXGvD1yF1 z5eGw&SE2}b)57nhf*S@Azi&56fu&B$G)Cn-O-G@#hTsFHKqii+h$D`G$$uG^XuB_b zoyv|$ANA1->RU%Xy>1ljDtwgIoWHh9E$QGQ(3fnjPSH^MG8e5~k#@aorGaSd2%f|k zu!ixkyFvd&^NFRx7@JD}r-=@pm3>}CS^j^g#4j-b1V-^~dmh35c;;6>~Wv_27 zqzR1p(hmZ(CS_pYb!w&6Fcg@OI`AGLZ|Kwk76JfaHN>G)ho%xrr_3TLvk+z0M486r!LgqW$amW<+o9cpXw`IcidkBu6POAN!IL;csx+W0gPsV=rT%sFvv;zhQn&wxF|pWax*H&wCeJVu zQ*a6m7 zX6WKRfE~srf10F!>23tFEi6Ez!?HG4bTZQfAO>1#HrC-!VHbtZr^q^tO94ZFA*sj4 z@^S=3WUhx3HyKdmK<{YUFsRXJ_rW|x>P`!%hFe^RR~Hz;bSnyrasNF9Vh zCz49@^JogoYV?;`ZTZM`hsp_v!<|9Y0azntzwR7A2q+PcQ^uvMYz#<0A{aMUMe@~h z-ey=FQhF@HQ&_B&UsN@LC%Y%ib;k0tU$J3Ayj93opuXlV1~s~ImBiwOJ&gqoSrFRD zv3y6gK~&6+qmqD!26)~tb)LRKBu^fUypf7Q4Dn7LE-xKY++UPxuv-6^1HWmb&}AH7UgwFpxdUpHOQVP z+q#Wnjy`!~dPe46$Wm*~wbrI*urCa9ae2VENu9&Z7~-?(J^;u+003fQo{Pj*z|!9s z+I`cP79I#VfJzdQL^vpI!ZUh0p&tRjM-p2RJZH(Jqi-y#JB{v<=pH&eemr0hp^lq; zxT;u%Q^^Fu3BKpBYM2f0=_ZBuEIMI@1xZ-oe{7YtS!?~h6Z{s}%=3&{YA|g+a~n6P z{{+_Kg3P2oP)csaQ78AU1iIpIZqG~hA2YuNsgUI%0*aI$f=xTr-dTK!C=); zz%|ZckH*Mcvp`~$x@ONqZay|J5KMy|qH%#iuCIl`b_Om%tboou+l4N^QLZsSw_BRqv9E3P4WF~*8o1$ znL7zi6Gcw6wa>H-g}Ac-0nirG$=oYIN{DJWpy;xr)eazT^_XBqd1 ziDbN_X3aN*hP7$9P|Cwg|Fr~ODkT8mpo;o*34ua8yyDBgufgx1F ze{m5_+eEwnnH7fxhfO$|ri5%>`M%}aJ>(&nCI+f42@8-%NV9Zp`r0t4?Fx3vIMKvH zF_GehV0E6Z`9|$bEf=Q`R!P@1&){3kjqq)H>%iPNkDAaSW8tjbKhQd=P_(p@f$BE{K-D!d!ug?NA4C zWIc8zD#Lz@iY$7X%3g$m==jDiVwv1K#&2~Sxi8VQo9eReayE#Ib1qEdo1T@+kAS3_ z`XsDaO_6_7KzLo>eC;klhD0zso6ATz8icDYqIAAD@+NIw_D9ZXSn@Vhn8F=aMlPz` zx>ZYi^pdT`?Hq#{^QSbMO&WDE0p)BZu<{hygiD%8ZE5yL z1mj-bA{|l4uK|pCE1(I)gA=7XM6WzapXvxr)aaZcB9Ca1C!25AkTOlY*WsOu&+ z`Rt5KwT=%c$`%BWcXxv|-XE-Zn%4xzV9l>zeQWXP@K$L2*Y)WWd~*fJo!DRPiZx(G zfAuTr>R+|yXYRuA6?w8kZhU-1=rNvu1?m^MXo`W3@rf64YZ4LO{UdnTeAoBnsZjTKl=R7tRg>GH;kZgf@0=tw@I3s!3< z_&JB@xNv#yt??Ck!2QRC@{dV@;=R_kJ1t|9a1sjlp5kNPrsDQniNF6jy8tqN%##A= zN)gR={bT(7Z%t0{HI9EoxbLlr8;tgEsogw;dwl4VBS*n%?n3RDS5oY~^0(yn!5H^` z<`|mDF2Lf&+zsFULb$eziFQ|9114LDND6}XV(zvFqOu2r*jHT_s}v7v^a3ZI^Zm85 z*MlDCu1kR~<5J@k1XY|A6`0pz&nuF$E8yuHol|E+l=?{@(Gr~TZ=ezSG{d4lA9QfI zG_Rj&E{mi!x%3Qu!VNU@`2tgB37wHFSQp`3OIi^tK-;CbM|O*(Q?di>1YXZUR2y=^ z`DkTN1$hk=n8n|ypL;MB@jcJhNT<#yV8-IAgTR%E#w@kPw|klT58QB zF*E(krRoZPiR+UBjIqJ_F5cXuz#b6^3NR#xBggd#A*oWz40UX<$LAR6V$*Xc$R|~R zs90&7ZA%Uh;AjL!)DvkOTxZk$H-~<`%mx&5O{Kv6;;^l#Kcsn&kJI8$qd0IH zW^->!HiJ><5^wJ+(O)&nr8#14Ckj`KPQYV@0j%pR9Oaoq3VIbV+OVi;#!1YxL9|Gr zg1u=N{Yj_DUIHS>=4+=yR3JCJ!(p_9 zu4S)h*@ogBG{vq7RVDfrdueKxO@V9LPt%y1z+-me!Rf2fq`ShhJR<(}xigUO9`^X^ z2v!3Za9#g5=ol-5-bI4v@bQRy#pJkid9w3T z8ayFWVgs$%lo@Wpu!|#=4OZ$y z2eyl*X&^k1wPk+9#TQbHvuzxbdO}xjI|jAR}08a-CKCiN#CukZ!l24GGyQb zIAm1oVu@N8?d7_RHb4eN94>rTWElv`8@UlC*}0=z6Ez$|ORn&@@|rTo1MtTN|4O;k zCHyL8J_h`!_o}9qzX2~f;-PvL@RFfIyu{3ol{7YNXv=XLc3+iX(c;9L&1ZlQgN>7AA)7j_1?QD-c}3@DOhv!^10id@D z9)Q_RRwZn<0$B+bXuKYl!8yhgMpnWgAUCe$PbHp03Ac8A)AArq5)oU>&KJo+e9ANo zU+gO23!!i7cC7H z&}b%ZN+06EoL+4N9tl>vXu%b0fmZSY%Vg)6F$L}9nU#pP1T%bBrJ7y3UCDpB_O)BF zs&ZY;KY@A1S)_(CP9-l*3k;*NLA(M8Tgl>hAaLHK(5P@y_EQTBPo;=d$kb6{>Bi_A zPL_B4a5bQ=cXf11bdpLJ*^(u}Cz1to$w7;ASqAuHY#0A91l8vn8TmkffLzjfhelbJM!8Y-67VIzF-EYgqUv!)23(S%{<+~*Vc{~%m7n6Zy)7( zj4U6JK{Qe#u~ar|2=7n8=pWpJtNDU_NS-6b=S!<08~CjWSGLmbndc(+4G`*Uf~zh) zlhb<7o9$hPG;ZPeKjtloVV~_1_X}gPu2&p(kxbk+kO{I~fW}uRaFC zv_F4BnC%+c>Fm{pN=8eIRLAte(#E zY&Bdk3-xaBIAyrMHqPgyx@gF$HlbNn+W$j(+5;&@Ua z=#^7fRMA_A%K^`a1?ua;HphQ?6&1gIIwK|ig*M|V{rH9Ulsxi&xBCv*jnF5sd+h!^ zwH#xPW&Mr@C6m101}~?x47g3FDdhhh?VsIk6|+U0x7anYViRQez)uy(xm7InNuefu zW_OH444gGq1(|dC3)&;PbY^E#htoTUK#ZM`EuGHOdo=hx9iqV^+Fnd(SB0=%?Ab-e zY3!2j=&jRTZ#pYzT}7C~1Tg$qK6+L5k=h!+Se-+PWBk)e0DDxe6UFaW!eIY>h0|k+3|}T#0xv5>qQuYUJ6oC7`C7 z;CJdh-aRBO2zoK7kOK!TT%9*bBqMpv&7Bc3`bq7&zU3pErU|KydvKXYfL!8*pep%- zy;NvZ1~biqo11cs$`*#!LOKVp@jJ_zVtIb>)nJs+feB7)WAQ*wm$9buWow!{ySA2c zhUGMLmb1KruIMa7wlzmrRZFqVUQ>y6npsZ1EFaI8XV0KS(z|Hzl0vcTqidRTdGoe> z;;MX{ZWjExF%)8E&q_y^U2d^Ambrt z_)az{=;M1bJ5sh+;x43u}_X!F`E;DV{UFsn_nZ`z$?R7VbV)z#p?U0VlPG)v+ zV+;1OXS#8-wU~3JY!d1S1S_@FQu6S?b`L{j&tyvrES60>X3VCIABVV2`ns@KJS=WV zr|4|UMPHS`qUEy_*1cY2{vrIo965pb>goRumP%Aq{ zggsoY6AEf&klPk42(L;Io>i1LWjdSkqmdS+;hy8$?igU#8t! z-C)Fz&&p1r^!>$PAIv+X}nH0J2>DHbzLz9;e(f26NQus_XRa#>Pk(*Wy%&@0vutOa3mpkNx z(q^$rD#!94kU;l4CBsx?4@{mBQ@*iv!Zg5YXq5_xd$dJP9>WlfRa>8i#_*-dSBo?G+}=0)7=4Dp-YsGD$vmRdns_E=^sm(!M*C_VfrrAP75n<# z%;+!K4Z|InhSa@AgC)DU0)jS;>~3~fID5^P8=Kv+)8T7Q_cXf`I-TI(iXGiUpO3O$ z@%)+m&;hGHqpnB$f8Oji-qE3|EnwmF75Uhun2<((=q25Qu-;MjyBZPPDS}$Tg?`H@ z`UOkR28DwAG4Kwg$u(YnXnv4vJk1D}8V+{fKITo?*HONjh6;*(G;!nc`Y5ga8h=N3 z8CsL@_89-h=9Jmg?h`N$ZLI#NdOlMhtK}SAx+d5muOWZJRb9;PrQOLZu%eUsvMV}= zsnePII9)wE2Z;^NCTL*TtDEu_u>5$Q=XPRLWK5WdVT?E9XZpp+NkUNZJ)lG3B&EWU z{_J=#7ya^tYGV5~=FJboMajosAO%aIU9LyR#{(E=R2vEZRwIF5#yZRyewH&hsGpC^ z{$tm5S#H4O72U(ml=Tnf^)O~Jf*J~nImQ6!mHId`AIT5H%NWNcIOx)y`N7!x=W6Ff zARQ0eYufqu<+|^1?gL`GOX-s*T(!pMMs!Flmj>4ug*p&DaLb%;1!$oThgySNiKNTW z1;w>^DoMTq!ygp4lTeDx2?&Z$DrJ^WSo=?+zC&Pa{xEb9yWg1PhrpVT;Sb1tZ#aK| zUm0vV9_P(>h5REXOi4{w#aYRpJuS&PW%1<;l&gY1s883;1z8cW1)ptqING^ zJm8eR>6e9XbKybEZvO)EqjZKW{D^bt!_}osi5^})Vun;l+2~USiR#DyzA{b0%5H%4qcL%1Y}r>@$5Gtfd0`?CU{)c+bW^Y9!Pg62dNvo;hNw66EKVk&ZCzqP0$}H z4=IoyXs|!&Z>%HoX8(t!b4ky1yl^!<>NbIJK~)6oBL+JUi)2bF%0+3^@ir3#x=ksI z_klol+XsTe7`S)g&VXvU8d{Z@CR?zwr+4H>C>gEj%PzQ`A)1=%pQh#T3-1gGHiv`Hy30VyO` z3_^gF!xcl|6nZn|-a{t}bkmIF@F1Hql*~;f(dAkczr*#fE>&t?-+q%?8* zVN9I2+pf%;Jo>@SC>>X;wd+zR-{K7^REd9BZuf#C1 zB6jTNLSu^Mg8ssc-jM;Ki%@*gMVg7M12;VIm2fXHmN?r6p|?~0Rkjv4*`;BLL!^5! zDOOAH4b5spD$xbDVOxrw`a(T@ZBW&Axu=*5vKP914`ME%=fFN>!Nu?<_YxjIbu{L3 zu$GT-?TjWlWk%%BgA>($>WpPUP`}a^Ftr4;6L2R(ah-`MrQ4~Si15s|bpmB?BDj^V zT;Lv%>M<0K(%H-jug&mz;acY8DA^m;Q+*G4TXXh%=>Z%a?Wvs>74xIKK+ohPJl=<@ zo6`;|zqFm2f^u(P4*i( ziMx@@Il8KdG_I*iZeR@bZN?L5QneCaC9AXq4r|jt>2HO|JxpAo=bv;}X(?z6Rb*lB zu?QMw>tth*qpo1z!SjmPl&#r1(1on}MX^ZKa0=2z(^s=AS zY-Ku5@eok7smQj&M?gl@#AaI-#=a#FnPNM*%5#F)D(^J?1e#J)?m!aT>p;jZLZOSg zVF>|tybpnpq>?*hFokU`fj&Jr0te3^98gpqLHR7F(LVs8QmW}~KE>W(wfm=8wbSp= z?EnhNhe9v#VNUd+WFsg*jd+ml#V&yOnYk|8HI(+5KI(0Cm0~t@*$O(o1_rsYBfsMU z6}_CFnj=*95R&l;$rQ0HaKdDgVIIeT8u1CfQcRNf^ms1-J0)j}GYnIX&Ah_bO8ZQV z@Cypn_0l7e|J;Yog4 zuPTPXDzssnXenBov_s9FI>Ogsc{OoWs3Kt8!!v1S=A9nGMu1nou~QC3Y@1whB;-_t z+4^n+PEPO(AQ{3`rw<1`Hxz?rt=+I}bxUWU?W&eo?6qxd>WBKFbFtqHQ3bTKkZPo7 zZz*&xR!c!kP?RxHifn;ZhZJSeSPTN zeBD-zP*t1KO&}=U9riY{yC(qE3ZyuVd)tG)_qHPp+2i8LJ~uiln=HcCui@B%_qxxSbf^;LOP zs&bB%D(7^ODwm;*rc$vQ(jo3ejMy)27$!W{4*Xbkd|(x{*^AVdg|xbnLXiV49;!ky zEJ7Ym7hZN2)H1Q8DAE(PL}h3KaOov^aSnrBz8nAPW**icO8z75%qt(24b%Th{UM1G z5nHUVOB~v=Q95k}0pb$v_U-C~y|&nl?hx7lwcm!{TOB`KtzJM+Et!NurLA*wqtU7O z8HyFcqI;6i>bYJ1Z}c(N6bZbOR8jU!$c;tWAA^AEIh8g=JF3XpdGM+>MCgMObh#j2 z{RU71#W|C@%lLvkt49oF<(fd_CJc{K#3o2>ISN`Md;5H@on?j}o17;z9S?zmjm)8< zuSg8Cu-9PRgE?4dh%#!?psr!nRk|K&@3RayoAt*wVC(^?vi9%+!;B^EhT8p6K5h`>HW()2k#Rr3c44%Rk(}u-i zr@)~!M#aQ-Utkc&Mqyhdz@|7Z;u~Go$ez;{1!7PSNRHYr433_7pXL>}{O? zv9(@){B-f(Uq6HHGZ;1QzT$WlXbief>`97WF5d9Qq_`<4-uLFDcuOEyq>PHWE2t5` zEVjFi?#{LbXr1b@Q3$F)$t%XUALZThSM{fPqz3s)oqd?6U#x|kAT4-0y_S}>v<6y4 zP7L=zi@4gKc`+`c^d=dRF668ikC?HoLzv=6b_%U=I&s@ze%Z937*b}}>(Az~lIGLA zHtXOKBrPX6!cUH~zCY!jVXmV`BW&6UJWuadzCcPoD9%Ou9OWIVk3-65l9WlbM=%#C zCAQL|k9taponE$L+?k4;r^A757_cUV5IOP>>FvkXhPUF=EwNRE&coty|;!{}0m9LjJD zOb!H2RARxY3&}W8SrBVgshBdtR}V%l0~wjDa$62V$wj=Gcl&}R=;z~Ao80^ikEOny zsiCfC;^E3p1THJjGEZVZ2Lu;f62m$QUgn#rz!{jKIICn6ensJLV=C>< zksF43sILVuwk67s6nQX-cy&Nsg5`X3MQ8>zi!lIN+Djo735^(Zt9xoFgNx1nnk~-G zO%J63XR2O&``u_LE!taQj}3S$xcX88fYKGVv?;o+V~-3Sg}1)kw$;gkvjW-gJ{z)d zMc`U)YszK&jP=Y7F}?!|y}Uw)^5qpmX#eFE5|r_VS8W5z02h z$Vg*hydtKqUSQ$W1q0d(EZ9#g@vxyN=V&O6*ni)J>t4OWqE$pG%LjaWMqon^3)*TW zCkU!HEM8;8Ddkz`DGHcR@tf0OA{+sp*eD2RTy|`3?%_whE4u#aE9SNQ?uvAo$QzzS*bYw-?tileJEkY5 zD@7t2;2{2AyMhZ{>pD%US*-Ug;Pjq3onBe)WW627Qfn1x%EA`znFyA?2X3cX9;4e( z!PD0+KG7Rs2z4dm_$r*n$mxYbyGGGBwPqK(4T);gs7=q<$F|dk#qB<+>-6d%XeyPA z2R(}nTUNnwlS1WousT?c5O{1B(07$HRqe(+GeKKINSa#PU$sjCxfY9>`-JKr>flJC zjBrwKGk210Q1rB&6uo`Ialw9RM&T_tFn9enb}Q;+m>t)dc3e}JWskK=6-;ApxMm|F zM-$@D$ysCBX&?<q>%(D}IR6>CU!F_SD_VG z2A_OY18#q>+l5kJ=k6l_KlPee@3^lIRR|gV4@_>n^m7`Oi9gRu4@N9)fttu zFx|WniI984A!qPhH8*zM^(-eN7o%`&;F(WWS0=fHdaztOJ@={yH3Jq>tx|xoR2TaK zt`zG>aHgRXeAab)mIq4%U|i465T!BA2lqG{y^g6^H#yLPZJ$MG; zS{z7w%7Y{gBF)G(Ne%gc-JC9009@Uz@9vuhnSYz@rDsU3hJG3sg&=lZ8C*+4rnUT8 z7_@p%h&EjUBmiVM4ptfO@^s%ddX7U?@S-KU(@cnI&CpGi3^}#T!4rAcUh<0}eK*hirvy)p1JhL4%$#6G!TwX#)vpi|7&SiqPZTx-~Ppm8|lH(N?cEfZ({h5V|fx3pkN7 z6)NT23iEK>V#3Ju#z*LteuK17+LV$o2q}3&WRf(1PsY#NqKNFXv_1?imWZ`>Ibs#U zX%i_z*ra;VhiDk~X^3OW{_h?Q9fibdMka^hatx-c#w9M)5{=3gol6!5+`IuVkOL;A zXST~xG+c_JAFmY0)xqP|9J|Aw=C~g4kw3|PUu8JoYoY8T-bjSt!DE)1L3!K+cQ2(p zmed;VPEE6<{gOT2tjS8wXlzNN^~jPTOf zv0mJW)7=PGGmX@77w!ZgX~d>fi;bLeB6mNvd!1_001S^-&}+81pS_CihH^6HzuaW* z2JL=iXt5}}4fNE6IaAsu#)B(mOU`yJuA$l`vP~W8k~U3rL~w1umaTemLtw%q^#-Q~ zPH$bS#06KJ=8{TFMd6U`Q(ek>?)-Qu+$Q|!AZJHSqncS;vzrE8H>=w6k>kP~X>mZU zVvA)|Ug9NR34NF5<*4T!LfP#gzi{(LO-l+F@gTCF>Vm3LYARtuCL}fWe7F=C8qDw_J;rZJ%$i_yGl$VW z^NB%WssRS=u_IOu*1~l*RvZz9lqK%#a30t(UTqKX>KOP)Op(uLPNjBCl^b)m+#8z0 z0_IHExeK99tYFS|$z!3IllTnCx}#N_s6WV@qj_3hLgLG_h0NJn$efH(G3OBXhyf)t zV$MY0CkB{vQq0K%>bAm2vL-z%6$);EhErX2(8H%Tmhq{L666e^-cmTFioqCe>LyY% zNM+*q%oGaJ%Rr{IEN*g_@U_|()-MlD7>2aVc%$D}U{f)Unf^w?eY65pfA z7&!WQvzp5pH4QC8p*}3wU1SW~{FVJNBE|-P20XB2++W$WJHp3p)Aq6lLpcq+;d?C) zWH#;R_J%)->Mw67CnIT9PUdcq zmS$-BYZPe!4-I09AU04iT6cv!B2_OwXk~OfJ{c`mYI@-=-OH7}(K5XdNf(r^ca&$x zg{u%7RDj%jVsRkk(`MAxD0db~9Sasx)-# zM0magxLK^69$d~slS1rL+=s4~JsL16&hv93g8pzqBEL^2Mg8Tf^!$6ixE>@@Fey8o zNx{F5iapAA^d0;I8WoKhEM-*o>M1Q2b%!v!(5QSY!WFZZCojpot!~q}l++Y{P>*@d zwDg&m_bC~_#*T^c_hDXR1aXJ)XSWR+D+(D~i~Yo!hI?KPTE<<*FhsTTaI>|FFgynw2%BV&p`>J_;WD}}B|la#(Rd#UtgR7{J9Sin-r zHq=j?rQ&4CQ($UwxDuBvDX^g&PivH1C#;vzC)w2gxQ`L8Fa}Sbq0V=jX1B=27}Ua* zMGV?Ybe#FPRr~*M6CIy+@o%tN|MFgbqnF6A*tMt7UE0IkEz!~3th$A}RE_OOnwIny z^pmmY_0$}zv<5{bRSZbwy4-*xl)zUFkyLz2sx zeKaTe5NbFw|0TB=u}ZqC!faHq$ggKh8%71?M4*C99*l*}ayFEDpEERFUY+zp(&N1I zOmyHCEq&D2Y!-Y4P2eI!T?bn0MEXs+61K=zxstLgs1{768bX&jRErBr)~=YlcctvD zs)jR&gR0B8tIU=4sV`6}+Pb^S)D5`>H=)vVrXLr%qTK5g*5x1;4v^e9CI|+Zpv#W0 zVz*v6ZF`A4yc9i2ejNZQKf8s>J6X-DAf^A`4b(SkKyyJ!@c&_+3vL7wj6 zN65<$NO~atzOvER80nM+?zlaL)*bNNn|3&;kM`1TaqpdGa~!FiTxCCf@T6>Um!I9T zOniDQ!-&;Xr3CR-EFztUC}(??xmuB?#Tz4Stb{h{Oe0CPx!1ladZiY$$wQw>3A#5t zvz-aK^NZ9Jvo4TiF)0HWGvW+p`bvyS#xQWsM>)QU1DUF8nLg(}@}tm+i%xwUECd7Q zEz3H-!&rraRIkMmBs>R8Dce&0EP~~CRpN&EwL7EmEh)=u%JjlV_f2@V(lp3qVH!|L z<&+;`ret8tVP^_AgDSDKKUFTLj8@8fEPto#a#R?iTq9Lf6*8*dKuQ0h;g73gyU*p` zxI|=X(4vF-$tCSjEZrjTAYn82i$s)5@8ay~?KtvMPblat85mOLFd(Yh?cqM=Ny!MBsXzH@H@x37qW2ubXPu5*E3y$nR<=3JcY#v z^;D5Ayb!0F!x`F0DQ{da#G(M9><2Lci|iqR;c*msqk?=#-teug@hT1=^hgfAP#5Oh zl`~8B5}Hb&=(pVUvlrs4yul>@tI;oFJb=-#05h(+j2#Y9+5Q(Dd0ZqH@I1KoYRT*W zdL)cHBQcI1(55ycx6{x~z8^l(wq+Og_h=F+8ZqvruA{eO8O_PxX0y6|HHOA09;jLCINYv>j=lBvs^0wk~kYPIUQ!?e5e$(VS87mqRuvbi1^9ss29<_ zHV$_cp*IN!T!e1EJ=T3f=2*EJpB9WJA(e_0L&a`B%QOL(@{EFmU#fDS*LZ^Cakm?; zZ9bHiJJPedN*2iNC+j|%k4?*2R6exZ;zW-(l(RB4@RMa#NhY%S%y=&ozl<=xsuJ+th~qs|c#pk6kQOW%B+HtOe`T+0q~6Xtk?;~#d~Gs!X>z3J6&vj z7-}ufab9Yi2hN$S^7q03zf+voAik&@=P7Msv9rc zM-_3t()5>!II1{lqp{(e%|V>oJ^O>Ml$JTo&hG2{lcG-{Q@WUhJA){X$VMsCMtx(86Dq3Sx!&yj%fAH8j~=A z<1YsBTvl_2YG)PFCy0)H7d;BjE=Iz&(+cCZi}V4Vz^D zkBbjlCXV5x8!)fD-`Ht;XF%2Ml72Uc-|ML#*pOYYF*IJ@%wA&_t!g1hM~i{B$u0AI zVTtz`bVpWFOJS3~{4p_Xy(BdZ5EkPk-NV&@Or)mrN#&Tp1_ayL#ZpokStH?s{Y|@g zp>XFQ(-XWI#&AG~l|3A&;)kGX@vu*2q8iiL{}cGu_A%OEqlJ zl$Od>cBEUoR5~yjQW?@$ywgG2tJ_d-%vL=gGaeDgYgeMKR+cI$>w_i{m{IZZP|3&R z3vJa99mL0_v9fX^Q7lFhjWHzE*e*qaSE`hXdssuF#1iy@REFZO()6i*Qx1hJ$cR!j zHKEWW0HqMLnX^}PB7C#vI))92m|($rtOSXeaH3TQ7?o5sK&~2%iYTVbs_KyGGW!Xc zIuOy2uU!Jo31_+yTS_ewCA zJ7qR(wZaryNG+C?L%5j`@y6ds-t@O z3!K+tmkI}zl|4Wg@^S&#`XICtOsIH6>|xb^dJ%(^p_T;HcEqBN)dN1cG3=s0q%E-o z)E*c?-9 z?}(g5M!+f`L7S5h_NFAt;+=6PLS5c6HE5ADXCbgytUxIiEOM!W!JxK0F0{z51}{9S zq%Rq(0;b9)lj?RpoNTsatn}ktDYdk>*dAm|ucUS2N*%(SqlU?trMpeWR1YkNQ~9VI zN|vLGTaLP`s*aU_FlSH3fUuOYA<5VfTrVFS85?pkCXconpkLTG$jb3xld{UM6rg0R ztWY{f0ZDNqncmflWGs^>8B-qBWjZCpk})0$;ImBHaS3X?<<69q zgvnj7WFaNwylE0PkkusCH#nTTz5WD3xR|~LXX{{eA8-<0jD!^!l#Xb zmMM_CV@{}N8LRXH+Y5?_NUzz4DeOZJ>vtIRIzc9!`LT zxirsL(+CJT!0D%&E|IXC%%w6tp+b7|s}>{@D4CS(tV{wWldf&%3XfMRfie=gYV)P! zZ<+=sL!`<{rFFyNK5Y%5m&z-ZS3_aW?dOuegtJxV1y$Wq9oC9ahWln9uS7fXD+`ax zVz_3ugDWV(3yJ9Zxd%ny;Ss^?aY3sz_H(}c_+T5NdYyM%CeIPN0mu{~C zN*EWGlqb&KZ(jbP%s3F^DC59SQmy2G(|R}umwzB5fJEXM@#vw8jI19oTvNZ6 zyb4Bl`G^0-=$A4cz-U+txT^evB{)L4tG@u;mU0sCh}lhL{^5T&lJXBXx%|Vp%RlHf zAK;tqFDof-x&(yuy248%3*Mc?2@dNeAVhg(Aa*#qA1DJcXSb=()plmoMtH?rqU!t{ z#ID6Th)Z1#g0P~OgRos}z+z=$D!U2>GlOnysyja5*-bdl&L(De7aoVJuFC54au9N` z(DY>yU8X|i5XqLyLe%l-TP9WV+{!FO&7`WAhEQ)d$Jt1W^AKirG7X7m$IJ87ty8nK zg(V_t{X|6h<8UpL&#GiPD*2RUVRyFVv$QO1ZcsP8ieC7FAEYK5ymwDl-5~FYO#-9n ztZq%XXI-da(l(U5Y5pjmwbIsL0Y<1(`i|^oIuKhDB$ATMilS^TVmcWQRm?}A>3H6r zHovReR+fW2>p$AeYLol@Y zLifsZmnx`ICt~1!>O|k<3t6&BZ^o^eFC<6FPOK5p(xe|SqWd9rmg*oYI1aLLcYTv5=q7?-*>1SB^f-R%c{RgZlU__B( z39~8+mXR?_y+lRf3JGgjQlKN2UPyoJlETk1{AH#K9)C^Y@eMs#Q@G^-HHDvVB-!*^ zoK0V%rf}xZRa2N_&Hq$$OJ>S6eoTU1nuSd<%_mq z^m`{&*QyxMH1Qgfc#V@|lx;h)JFK0QW4MnJv1)gGxgo$x0LtHcu)21o(2toARs1b0 z0gQ7q^^1Jm=`+iskg?F1)vRQMNB|F88n_(al^`z1Hp@Db04{3~lTMwHmcq`XB;8!D zA_1a%HHeY<$rCU&FL}a!I0LA&%VDTQ=6NEYls=m<^W3w``mF>Qg-m)`eORjIkRN2> zPQJ`3M6xgnZ$Q?8>@$R2GR@I1JnVVg_L-hPpMAbNv`u_6wPu*ny=Iu%QYyntmYWs~ zGcH`uFfXb&q*igjNC-D>laH$8IH0s(w_yg=w6t%jX9Qu)>OmSbw;0nl=Idv?#F#RF zz242LD?yBysT&x-$wK4Zsd#|6ckS1vvChVry3Uo0IE&N zio4nr^H?xoWAEg^ds?LHaYpmK%Z`;Bi!;K zNPM7iP`q2)%!!F?yG2-xPi4DJX_i@Qi*H-RN}w645SH_1CHCpFg$ZyFpfc-FWg!?x z3CVKn%A)NpgS%uF!};~cfOG#Z0Oy1MGn`M<){Tc3ZLztR^$0_@Rp-h@2*qzbAVTGG z{k#a>tNq;|^bYs-4$Gx1#Hc~KXAz|HHViF>^jqP9A$^D_?&pPcKQbdo_gNwczi@D$ z$sp|)Z+{xxwzQb`uXzlV-uMfk^oA0pkHMpc(Hj@Z(gUT;+1{ze7=6G$Fh=*ITYg@Q z?$g6-VDxr*PX~}D+rhPa(f+J~9bA_!;?Wl!7))%|&kg1aAD_sxn`^<|xob*vY7^OF zvST9;C`q#YKQA_~F0py$fsuJ82~mDMG{BGGcM|pD9A97DS6;6c_*6Ljf=#u4!N#m2 zu{U{<^K@V14mmdNan1lKPzkE#KA+!vnw!kvb%6|xl9r?**nDahlx_U)!H^eMV{X{g z%|29pl}eQYSHe%(TMMC*>Z`uM=o$IGSbIIwiS&RaT_LDlmyV{+ZfJtUMr3^i=5tDeQTylzE3 z(gzF4=SWwq%x)vN74c*(4zL3BM_fMQ?_z;O1M}7i;h5dv!ZC#x;n+PAF1A#ht5TGB zlXEgG?malTR^)Yh!7|Cck|^+r7zf%wa`oWVCxhg9-r(B46;^9|kaE)k)-2ib&XP8= zE(ciI|2r^kV!3U=F1Czu&%}p14$y|LMjKbC^;Bh2V!Ny(@iHbytKN5(E)T_hPtP1& z&ufIN|54J4!+YeCAv(aK`b>a4JkXPe_aLtW0`Yzm2!7+zTjOOPD5c{8^xOfY(l z$~@c>Rq|woDjf$1$AOjeRV#Oz+A#SsSxYnklETj5>jL~+nfT zd}aChdIJ+{AwEG!Plb9=Lz@M77?CezdyIr-IkO+)ILemVodu)wg%CdHy_JEQamU9a z2UYf#DY@OK2y?sPT*)}^j8T@)^sG5z>741AYDG%4&0A4#31(9tYYB!-9zC!nFzp~W zR$qlfaX?8k7QiOI`8fUhW8oCd-vdZ~{r@3OF(yml^g9Q_DIvlEae7_JB{Bi9iIt4z zv5D9kwPc2%x1cKJdO#*I3@>Dol3gpF=`)Gh7GZd2`lESVThW-5YsIx9ZX)6udb{>g zK(VIV>_jkxSm>iy8R6VnisD9Y?U8|H8HX>y+AiL%z^op-DA}Zh+C8zN$G4es9&6vk zEK9OBZrcIL@XzJ4Wvw8?w|I}mJl=*8O;WL0Jt8Qy=`H7fFR7TprMT*c%(CQih>~nM z`>3EV64g)4BjamSh@8h&edXh9R&P-XgZ>W}g&ACGQ8!7oETw*ks18UYgdaD@D`Y64 z(F0|&$?+x%o^qCGgto_A_7dU{c~pAQAd$2l*+j>7 zeCsVpfz|&Zg=G=W}tVEVk$m&b(CwzQvoP}wg~W!6})ZZ&VzS>xVvq))UQ zBaCWuvUhSjSh>-sRAqYM8k(e}&jM9e(x-A&$^R)EyYWEJ17&YhkqAw4pZUNG9tO&8 zg^TbcNbcFm<9HJ2GT*sPtZJHfEs_m%x_th_K$REowo=0$78USVabgem&U+llDuB}a zW!=`2V-92}z({*bNx7qZyzow+H%uc^#%18{7$2!uiLt|eVV5HgR&!l1j?VeP8|~sy z<&Z56_FA|m9tWgQF~6nX-K5&&1V?J7iGPsuOrMu+)+^fF>%rNgLjcU{p!|YuJiN@0 zoj>NCvUgs=s(dUWSmfDUOZSqoX~U|mhW$+1YvhrMJO=d1d!t}0+zI+?%MlouJ-IxQ z?na^D{LcDP{jV;A<;UCq!N(eZRSAz?6Hx)Q7k<%_mYkrvVIS{Gs&3ei3v<95mRRu( zk7t70dB0gT!M?{@^3jV?`ohNn4-ZdQ!#^fQk79Us5c8~^e>n7lk+L_Uvdjf{kuOS* zaE2g@Ozru$Kg`@49w@U~fQ6rKuN0>$oRbEq1R`E25Q-2~>+TDLL)Kv4dKgCfq0h@? zc~|8Qx4T(C*uBfWnj=xMlF$W{KJBfD|-uYMa?u>4gp!kdK4<66`$Z2$I3J{(X zumO<*1WSa!&ky}w{tkPc0$Wb)XUc=s`nS}Yr9;a#hx6C`lbB!mbJpu=A5(qE}v!JEMibJ}8cv zDjvGFqM6_wz;_*bsc*6S6}6(BeaXF&RkV(7&AyC{lDcOk>R1aXRln3;c)I1xg#$?Q zDTB}(AaetZLn@F~hA&2}z;-mGzubFnNH46lhi9ZP{p>I-z^##Q{K1d zj_m@P?r;?ADvskq->RwXn{l)n!4P4ra4hoE?}*^=<3cbFngLTUiR}O5MYwZw7niCV z>9nuDWwtjlupU;C>}IU9y_}5w%T{V*LT4EPVZ=2V0^1&!1S2}p&WXU%T1CrO!U-n zc{wmc?Pr+&P)l*M>70%8xY^#k?4UfOeDg1oBW}3x1@=QkrXj>mKFOhSvIRhkTDS71 zxB-%wKs+*_AxiD`3+nXUkEPS4Bf!_vI$d}E4?2B7Q!vcZ`f$N85a@qW?Ptr3qlF#k z>D8!S?o0CZbaht@$(fU!J^We%n!#~t#P1FsjjrJFA-&3To57&}4O69RNYfA*`ait7xewnebv2w9FUs!*>U$ z4Uh3dT}x2f(bN!`eL{nmMBvdkM}&ndcu3N}fb)JdFjI?}#tla$VnmkgKm2sLP|+ic zR>(g(FJ{vp_J*rFM&3Ta2hOxGl!N&n#Z$(J9F4a!Ych_b$r#u*9VybXsl;rVu?E*y zmGov&)zW+X_v8a1tk;#u%V7!o?`8i5+=xf^puxW~`>Gv}6qQ$1`~e826ZkdNhpF&> zUcVsdTwuB9^wbMqIRL1B_cAEFlL__2C-!D>1%-if&id7Lk23`UWG=KjACsiVp$UZv zAIv*4ADOLA_YvMRvU}ExvsNca#S3yMaHg`?+4b?>b>3iB{W_UU$S^cI7M#k>w-)~IL8M~%nR-xxWag1b_l`u#tH|S0UEocRMqa<5gxYzMs!Je#w zkl6VR)>V%*c}IEPP|VLF`u2DmIXh@&jx}U(G&0uJ+LdI_?sgP%mw41EJKE6qhmMj) zy+!&$VgZr{Btm+kr>h>Z%d1UM{eRrO3$$fdS?{?Xd+)Q)+2>TPB$cE{m9oz68h9dA zq_ksFgler-9UkcfD7f)9IvCyYj*M=)D%$2@8heDKLIOnOCC`8$0V4rSE^myA5)>7Y zt7%0qEvNy4yaFPEqVnk9|NqUk_S)xBsi?O{-%gF3wf0(b&H2r5e((9sIoFB{)Ad|^ za{!**$b;YV2VyJw3tOELjW^GhAbgT&PL)sgz)A%_Vtz=3Cgg>-vab0yg*X@p>d-g5 zcNaU9&Df-){aYlaRTLjOxU%gmJ!&=IZu8mg7I|bEsXoK0@9I7h_8zTXZ*h43DJ8Eol8$uMbeDkj%cddBg{d$^!_o{s;@IdrAT6_f%$l{t%(TGEq! z*|z2a2dsoLtunxoFu?;6{J#V86+>4)uQ?!YkpjJEdSY|%{Z%oJTx~6M569&`0)Dw* zIP@U@*fwEbc3D6KPQ2cmCqI^+?92WGOvyP=T3@qI=fRiF$2kW|hdunY&V%CmWv*IZ z_BF1g({YN9&WmDgYMf)UbPN@A9R)+xYWyLB4-p8u!Bb~F$Z1P-T^vNa# z32NmQM}au7@xY77J*-$;N8A!Iv7zd18@~9P?hCQTW*-!?O6A^J&moj`ZtA*rze+6w zx4UUAE9w!+d2_YsbgMq}g*-n34b>;)_LcQ?>2UcJAx^*6>}=FacMynOE57-2q_0*w zv6b?QD{WR0G?-Vf=4%eR51*zc#$9M+c(jHxE%_1);(}ut;;but# zBDw5p6b`#%+92>5AcsBZBi&D-0or5hYXni0!?C|C!UHc4{MLk+gh%7|sIV&*xs8w} zaRT`qK0qRcTh`h<3OWp_5CYSg8(kmeSEkW}*M3%~#0Qr5#E>Rbu=r1>K$g|(3$JC{ zba2u8^Hj>RGnv(Y_xw7PZE!4<|N zvAO}yG$;p=L8f%T?Ib15GlU=-QZEy!Umm1>8Oh9$y6NE=kUA404%wKv*UV9?ON~jJ zAdY=?h%T5QXvhJnnt+ax<*I^22F({0UpYz~o)biYVNGNEk@>N`aJqDUS&Luea~dHm zz+!K0MC-v3-9smr@vX@ngxqLw>yBzWKuoEhG?YCmW`sq>cs$jopyCvaYCcBVwAM&% zpphf0??O(OGtcvx$1SM)38HSMAG9_bVg^i+_42U@rLksxFjLc;$RE zf!o=NQFJ$oX|a&b78Bcrbz%&9+RtfuM~{y0ZyEgIy7{vGvkccC{lc8?P^4 z0mJmQ=Ez6eT&A-O&06zv_m-Dh?rPLg=IuDS0_>ly^qG&V1#RqKoS|YI<8Bk7;$RzA z8uT>drn*iYZcd^c9i7u+5Q%p=+N_tV()&0})@<#}44tIv0N3fiUU{BZ&nYJN6cKrf zP!QicKH6Cswk041lNdfp64Z;GEwR*0C9*eHTlfw(om#cU4!1%y+f_XtF_j!mQ6Ubp zx@8JM0)u-Xv6&QxLJxdf6d)x^G#`21LiJJ!)?|CEFKLFRPi}ZO!C~4Thvsklbi$9p z*V#pEYZU|CW+MvMw$=VV?_&@-XZzCgKH zKpnjcp0nyU%v!K4Yu08uwuA0iHv>Sz9|D9(4TFjE8yWItlhzWIBEgqgo0PI&WvRE% zKn;4U;%`tvEr!G9?*e^_qsdE=t0%AKM<@VBws6?w;Iv*%MC^8N60zGm0k7gH z$+LN>5TV|;|C-a?G>d#pC0FxJ_M~0gPN!V=-!LCSw|O~`Ctl%nQzg(_gFI>?`5>S9 zg&cad{!V$%or^6a@KGw$J&{0uR7@%Ja4b-Hx0}+D?i@zv!npM#M!0^mkoQum)Lgt9%L`w5*l6 zczaFmm8upb16ZN}5~_M0yuUB2I%&Ma7D78A+m61kbM!EKqwiVTXvw!PJ9Q>ou+6&$ zj&u!Nx_kz}@5`PdyF4%G;J)ms7;YcW6qyURFo$xA=C3e5?)=qJD~gxToY@NVt_92Z zEHskr0W&k#f_lzv9fg-$&Yg>cW1(ho=y{cF(qcHZ+ zcGiRS-YhR~)n(G8HgtI}Vhm}D;TrYHu`;u+>L_lccxPN2BWp6Q+rEH$0~m&N>Q_S0 z8l$tBSxqf-RyBxJCFk0v2A}?QaJtak{9IN5RzGfY4w3vkevbHwx=w)UoK*6)&MThA z1^ZBbA(rHkDBAqyFAb}=o(bYWM}Lqu`@(I_zQDc=0>GKAZua1`U_u7Q%Hvi%I0td; zbQZmokv6(pkf9@uI|q?KaD@wLs5PeQG&oAN5xb2MFI#(lW5?QVC{ohmfaz;vrg>}X zE~Yjfi9}yJd}>_;q+Y2*uK_}Wr24rDF68aaQAscKF;IAUVfl3R1}${18SY%ikWQP# zfOT+mt;;_4xR9Ncj!)g#wYD@9XDvo-X^3}TOAWmN?D{vEeFVVs&Mn>mK`uFrwDp7- zjSB+~jScZ4JWQE-nlX1*dM&VGBG|Y3g~SxZ)Ds4IM`D${01h6v+1$rMp!k3iI!+2q zeDF=d1U`WYHE#i?Xn&-Gfw2h>t}cuIMk^`%fdN?uH{}F!4`D48OL!t~UuLI9+j62r zICog#lE5zWQ!DvdC=8Ov*zQ4g1c_v#~J**r$QoG`X zW`JL;Q&CaHTnDJB1x-)1|C-z?Yns5kx<=qVX%`AB($c6H*?|$GNC(#lx&DB?F(Nee z$bv_)9Y=t}`Mi{)rCC~z$9@gD53+x;0Fn4Y`_saaJD2mfpGOvH^>FoMO=2Gmz5$%c zW1!QDyCBvhf6j(rf%>GFCxfC+h9Ug9v)K|82@gOzi+z9fMgoR9nqW7)Li;|BZA1pKvv;2b&x5@wD7eq-fhKv98lr1iJrx;BK;DU zfMZboBKv_c&V$1@B5Nwy;jtlYv)?(0PMqNIRZMk{B^G>-q}2;-0NE`xf@!aPD5zL* z1JWvfWq|#Gxj<5jOy5JBngEAn>g*s7j7udeoGS-C$Pgz8GFAL{5b@tZ6LH^x?E&q8 z8gbu&N(F$mk)ZZ0R79IW=h+1rTX6G4XiWJ};@YyAZ1#rGD_M@o z8pxV1dOL8u`8YYh8*4LOX5VzN+JR%(Z2rIT6C$g#X5@N0D$9<=nPRfUpagC8iwly& zyo}VowuzD{4xZYdApVNouV5>eJyM_A`iA?xv26&$#kL{MaI?N5E)+wgD#t8ny`psq z15LyY;*?!kdI5@^xE6+N77Ev~S@Q7sifl2lQ`szR(jGmLSL{}mjl&7V56esiA_k21 zTm>L5ncHu|GZ0CUf*8{cEVRiTu9blSHHyzo+HlaEuPKeOFY}>ZSx7HP)o}uZhGk1~ zT9b~E%|=C>)Y@}SJx`pz+pHA{38%08Ny_Ky;uZ3yVY+GAPK`KZ(Q~T}N;gFVsM{d_ zzsMA|V5A?D+gm2ZMDhVMs2_H~HU(jJd#MFJJ0y+gSl6K0YgbaE&^ppSxKwZmA=<>i zJ&BzOW#{rND&ckC!w+DJhHa?i@&bdLO;KzI4>(64c-WyH++Rew6%RKTWEu~#ihcW{ zL00s-vdS>K^;u7J2RLfUA&gfo6^k+5bZC7Xb)`xNqK!#;K~RW}fD zV2_(a$B-2U%0`&v8sN~uF8tFgcf#<`p<~Spex>s{HmwqD0#Sd6=y;7fqQTt&6BC^6 zG={8l3GXS&0+p(JM8O`WJO*ui!5!ewZp~$AoDX&0(uKAm=_OAac^VAAaP z?C*-7OYxuw6BZwRpWj6V-#;yhtU*Jqm*wd;93KN93=Sl3h0c0d{c>wDjZ{|yz5&6| zP0L*kJ0_ye%Y2Y23aXGXGHX{`!HUEG)YH5@<*KDb-GRh5yGCy^LBwiUZOSzx`~<~% zgoNb@EZ84I8|f>BAf%7j11a^dxd}peW-@$+)u%C(cTrvoc(^HJBE$_u@JlBSg#Er+ z#<#3)@Fz>^-4k^0y9@bvFiF*WiX9*}RF5Pz!_DJ%VG^bmUIrGNAzVd?wCM1B&M2Ns zGRl9jqkn*fmm!HPhYobZeK5(PN2e$IvJ0ATS*^~GEb%6b01T22qL&RY2*6mT6ah`! z+S8b*#kp}PWiqr-iTqGe?exGN8EDH@7-(y&3@!V+$W=y(BWN`DWgCM`rDIA};p=V= zU@v@(Q6|WQEL4;xz|0&fhAiJRooJcrB&A83*?Xj_uM)T8g2|>?7I17)&e{_44Gbt5 z1WJmCg6eR--oFULeTV6%r%mtD@osHP|SLwZWo(=&@$NGpX(p{{?OE*ov8%ZNE*Su$`PZL_`f4oz%_ zQWMFx3M?4LN)H}Pip-X2PHBB7jE6q9aPN0Su786ma-uTS+S7|3zo^aH6mxW@0Y5Ne zHC&iKn^@HNOz04;Z^)~6r8F(bQ<`FA!?+$jd8HIaWr<){+sM|@T$11m2_-O5+7Q1< zyA>0PR+RN^3lil_-(Q3=iDRq-gQJ1#9CYNbt-mK#|mHj+5*e`v6lg_RUR|{mKkE@F!@X7aBO?TE@Hvgs!UHR8(F@ zl+%hv*t+>TMWo(3&{RZlp>)uZ-hcQz|KMGpnF|O^88(KHA}wr!Q%TY+RaHZ}nPJs_1@#yn-i_wQ?!cYpkc_94z6ZH)$bMjJ{T;*UZc8%iTNd8TL}F0v%1;!ys-Xjk?61-amuUDcl!JjPR4hLvTK@+avXfc63>q_+X6 zfjZ3rbdxZV%0|ghEt6lE&DG@y^pni%5@XA4C=hC5Xp~>bI5wv1WpBl-Tj~sNnwV;{ zSuYfa?2&@nGVkjwuuAvz2bRE1RC@QagK?`FVCW@!oy*v{feJL>yoC#Q^K2?NItRfy zY0cn^#em&%%U2uaZjvyix$r=(Dq?8knHN6xPUFu|flHUo2MUQs$^c|GCJ+&WLS%ks|{UdczX{}^kS=(NOeaSasCF65DEG`Q<< z{oK{qf7g9y=rN81b_M`rgz6^kT%T4a6!rG=fxv-Q1u`*H6mP|*JC_Tt4M9U$1A8f? zpt(ETENLHmHsWO>sAvv`g;%{Nf}3l_Az9RVIYrtl)%BgZu1}!nTrL4^T#l?Rv$3Ae z2DHe0z66ms8}zOd>YEoPf<73RK50iCfY*czqo)w1P^x1Vv>iHwNb-kS>xH46wnrPfW_C(QBCr4uz*HL`2 z4V4i%mdD&`1o=I(NEH@s^Lx^IIAmDl_avJ|vGp(niEAAM(GoDo?c|=! zYzawMz5OE$r`NK8ubXJY(kQz$U_;U3o5Ha5qdO$S?HZI@Pe2JHDBI^#{hD*xw z%?n|yfzi|!b; z)`h%zP?XpEh&!CU1&of)|0)i(|LP)5wm?RT?d7u9(3JQR%_E82pxCAMcz`q8eiT=1 zPL;@uOt=%3<}|IP7EN{0N(t4VJrQ$(uJZ5?J@4>R^|Kt4+NnP< z0!5yR&5PK*YdU-u1ka{fPnBl+cUNOO_nK(epQt%!H>h!SeAX~0gYxATt)eZl@q8>z zY8B3>)p~H?GLq+s^vJOC+Ub>zt=|=Eurlo}XemIHm;rssg`FsZj zjjFhA_B~FhzPE3gU;*=8y=Hax>JlBu?_LOp|Bj{NVPTc%m=&34FfvnqVSON2bgDpB zoiA;!Yz8g|jMM!`u>$>gKmJm2k#_gcKWJ7#JSN7MG`GXlw7xl4k4kOXi|)CIp;&tI zRU~fQ(A@Jzv$7fVBWy*jH1DSwOK>LF0_6+xawS=D zIB%{GIIfC2#h-50b?Lm~xUMh~i=p-SCn*OXUcoPf$uqdXd6#oRpj^h~tnx2*PDRKIC+v>Bs1iHqB>$SlC+@2G zd3sI3nqG3LK+Bj3BYMRz=4&9JQz?!~0nYh~|1aKkboC5xX`|>091nG)FmP}+cn8dw}y)MxPd>8q3aw3cpoPq+-+I%ZZc5Vz0}sTuNFj!_h4 zcNF6cw>Z;y3ywk;j1V4SV5)6)k|x400G63YT%mEikOb1Q+8=9t(aIPN@=3P|T12G_ z+QHnZoD$4N9Mp>`Uo`I*X+lqGoCcvSL9m@Wu1qaJjWX#RKRn9dw9P|N>b&heUec;c z(jvFWQ*~Udabfa5Fj|p~L_UYgP}Dogc_bv!!e1t&qOyAdh|3xc_oh7Ih8%A0%Bo1M zKYNv)X)jul%MR&mY6ls zG?ePyu#85wPv2hIA=(D7oxKu~(m8tt+(M2-Z##RX7fwy~3LBh!V%?Fcp6hXdF{~!4^sD9jAlxW#Q!`X*^@$B238^?(Fb|ewK&e9L*%$O8f+&i@#%39-u@y z#}IQ&aLY=*EpdV5hUH2ubzPmam}`QOp(P~<6dsfOP`Uu|bl&J!%*}_kjwjnRuc6pj zu8bLp7p<6-l>2Li%qWx-Z;t}XozWZR%)pF21)}seY#W6PyJ?N()6_wX!FDdSm*}B2 zkIScSc;Ds7@P4T1{JQVEd>GyjNMN0RKdD}~pw*Nm39P(Xtk%L5#d@KC0D{P1qdf6T zXn&PLh=9x0vRYJ$zp*MI9?_HTNO%dVy64rN6e7hSBJuhc^_lE(h=Eo3q*0JM;GeLnp>!{SGuNYvO#u_k3Bz(Dvz&z$d_x2U#Xt%Q zk(h1}2a(i*OB_p8Y96d~5NRT2&?1lhQjENCjSYYsWpo(~Iu124w%ToguJedlYM2J3 z^aq;}0#=iu7EPuR3e5!Zd1Nx3B!Vsj2U-=n$v`2%?QsmP(dv>DV@SN1ZtL~@Mf9530Vk<)uv3IgX z?MSBl1u{Y|s}cb~jph}KSgJNs;tVB}YR@toqzN&ENC20PGK!%rd9k!m zoTQ5ExG1A6opoV+Y`_)=L?&%diL+%;YjC)6jaj2@31pTwQ;=1(%k*bu?T*E$n;le0Gy6<;o#7s35jB=d$g~mATyxgcvwtcYM!t(eU8*GQHFkX3I(i@{I5Hd9Iiu*jWIzEX;0 zrPW|N(kA+Y-p@`GW-%dXy&&_l2U^e7^+h*SqDej6($N>6o1 zU*M*wceZH6c?cRd)fbCKb|QFGAC{-d0>IAFgWX^lQ(gknztU=h_X30_$23(&%z}(9 zOdIeC1}#*&T6oP^EwSSd@t5_>T}otSbKkqVbwWdMnqZAJBCJkm2Sh{+kj}!5f$V&WY6)Y3GC!_Qi60BlrxydN%`Zh9othybp4w<@V7LD>-roJ4#-&taBBouU@hilhz^j|vJwj@1$3T`?<*OIaos?e8#;aLye; z3IObVE}`YZy=T%?ScV#B($`+IDL}Y95spBs^g=1t^k9Rh{cwu5f|0HPF~4C@)NQ%| z0u&h%H|2ei+FQ7k6W>Sx0xGjYni*+f+k-l!&TcCn^;|Vm(2hx7y`z}Ca(xG!1Jx=` z+SiyF90*8Cv%??B4S`XGK?0-tR?70-fzegymu|RJ8WZ_Af|QCn-1hfv?GXDjB-2VM zGJz6GQIVxNr6@F}c{RhQOd-H14(UaIdacE;wqe5hrxFB^tsHidv~_6gDom4;QZp^# zPus2Zh2Rvd~sGc`jo%I`Lw_GJ%? zC;3W{5`Okhnryd)z3sQ#ZEX>0ibVSr#(VV-KqimhQv&YwZv4iE$#zMN+#o_SmL0R@ z4EUnR591x43&hJpJ*?x8I$>>_6>bxaK;ve*{i5;pbfkf!OplJ$BUNa?qgQGM{_p1F6+kf@-&t|mY%Td zlY9v~m91U0F+=}u28r>!n}ZIw6zjZa9Og>m5ixQW_kn#eQAmaYvy0@nTOM?mhB@-E zIkQ(nC)UU^3$~vC`x}_EZysP;N3**h#Btu<1#?A&*iV=;iGzc2n19%4pn|y&|3l@4hm}u8>zI&XW7Xq!%r#m!J5St=Ew> zh|QK!JxV6%4Asj0;5poySiyb$256TN!d_-ZKYzc*AiN*m9QJ;<(xQ~* zFjEB+MeoU(_Toy%ks}(+RNDtkkUX}fNsXSU&xtMwL5GXg)39OQ{p+0#V+R}Y5qnC_ z>Un!xPuYWx5TBL|mUg$5Zn!GRF#9NWehQH@H#!iiniirpG9uCR-5Thqj?;&;OMpG? zaPd@`FzVoBsvwe7{95tBoLRi+HnFbxCROQNWA14TWJd=2n1)_pBV8F`{6-F`*D^;D zHH&D#>Z1io^M^AI%mtFCOb5GGPvcj0SAD8dv~I2kyQVwtz#87c(!x7dSAY(Cu8&O) zx@GE>Z2{F?B{v0eB*Si0VH6P^Wcyd!a`)+qJ^BeIAPt$Y1|-U zdn!y_^m$5z=it>K?IQ)*TRl;=7VSlC)aQrya1KE8m;dNUz6=66clYyiOw?Mlv1Rii z*2d@;mNmZ~;GHH6@Z|XQxBOaBM4L20f;0$@c+`3+nD+KuO=fsCEa2OwjPHexZT*eg z?|-3kOzr}+N3H9i;0+`#th`stoo1_aIY{D3y6mn zei;mj>C+&*&0U-g*>sXn1tS`8cN~vfZdPJ`JVDJo$GEi%;+JqW2iiWD+)WBY-7*Ws z%iM~8#Tjt2T{{r-_+9XS;^2D69jSJxtW;;Upx3U|Jt^R)!71lOl!}~PG8j9|OBToY zLPA@(o*@%GqVH@+)bbh`CkR#w7v5%oWqHdL2od#r@robI@=>v3z_9w9!&Ny8(lN%} z(aB^w2HED|^06JF9f@Axlw|Eq%9t8-|TtTo-C zEfyI=m%wAiWSRRC^&whkz`#T3Y-tp7)25JLgP05Ua1>x#S*q&YXOSEIFhDdYDwUWOsIQF5GXY$=A+>(+)qa3m36W zo77vnN0=68p*8jqPlxw#-TMAbK{S}CeYf}L;qs|O_DzYfQ%zx&aQM<+M_5ISuE&4j z4*5J4SY3l#_UBnrb1JGsI=VDTSww!PHw3}3rrN#nM8RqkDP5-O*}qX}W9w%mQ<5WD zWVk27(KMZkti|~ba=XB;BOjt#DCNJOKehk_A|=aKB`AcE#t5I(^2!l_@Zby$b-XE$ zz<5U%59Z_B3Wi_}UFYXpBeYLRiwz37#}anXDcYd=lUaG*wj!Q&Fy+TK1M@kK-{VsT z|E>>5^no(JZPs$v!oNL({8n`T|91>>-^nw`@fSXH28rtbEf^#M4?7ic*ex@J>mH5? zn0&WHY^I-h8YG0uZBEOke!rM(kiab+iG1P06d=2&n4C@>ed5lg+E0cpgECIQmdn&E zX)0))fG%Ja3iVbEtfv~E)QK1mnY+Ezy5-}JyIXEF&v)GY2Z_7=HbqBIF2d5b((&GZ z%KN)G!tkw&gkwB65aLCp6ly}uHls>m&AC|@W)w`lH#>(1pHn`S<~~@?c6OA z&V&vX!_>FNMGB~^zHUgLUu-?3#UcN*hEH>^VAR{W&krW!rOk0PQyH7$FpQe1ymwth zODzM6#==g+rn8j&LK`EZv%DQ#08$p{9NJuSqchroCT8hBipvKcXj!@FMu)<^Ehxf; z=^+I`HPCj4xGr;)$mFsu<&Y-uWn|HU=RK-74E42>N;BC1BoD;d5S|U-den4pG5H># zaEhp0c~V3Vi$%1F74qh=$lm)WWs_4WN5fwmPcx{2k!Il=02)f^U>Q8Dn51v|L-<|4 zCtEG5{8%xWy8C=Og`(t6#jcyGP)kPHZ*%ff-LaPu>95aC0vg;wESho9ObQpws~PRSKUt@98w9l0X$st*kZzS+LHIOcBXbnw znc%rJ*me$AyUEAG9d#l=nl0SvP54E;Hx)9;dltxyanCx&=$8s}lA}Wk@L{8q+!l*k zO^Ig>-x5~2!z;5m&`c8p;z|}bU^lin67KXA)F$Vq*F;C~|6exmKkjawlzGp&i4WPl zkLv#|td|!Ki-lOV(k2&sT9Qq5Ej=YeZ%`H(VF+2i1mNtf@FdM3a<>Fk?Kdr=g5;Di zbXxPkm8~@T>0X>FVLb+R4rkSyyo-P{mX(6-ykzZz0;3*geT~Rd{1e%EDH(8)RfCiy z3flW*!xzx%e3_W)n5z{Ulh94@PPK|jNreQQaK!F9{GI$J=U7bB1r}DQS2RCvh8NdMhhP1!zy65+f7ny6>i_!DIExUCqf9}~mMz69CN+EA z>Jki~9%#*_)Pymry%YrR=-8AAH>R=2Pi{A2W`PA!1yW2noVL`E`5ztf*~((qx7RZv7z)uevry|)k?uuGjaq;VN_S}Bj4gp9cI7paOY;f->e-DC{1dqC;ikR zt2KmQalOc;n;})~meD+HmlKir$a3eS%kkliU1bo8gHU2miA_^FM{%7f_EavC0i!m_ z2${D*CK9)HT<3jHA9p2Rk^LARj);B%y5^aZ!%smmFxy5`1CpLdn0FcayehEA&=eOG zPD}Re)hLA|m1H&uaRq_uW4n*iz*X#>%$+{=-pS~ff9>YYpDMcjnqNDVYXG#>?Cw7A z;Y@>};X)@G8g_jKkPaGcMg06cIu5_*%4U-p>^%gSt8a~96P!o3GkbYwv)8(iI@`Ie zSu(T8UiD!*eKXR>wK<(@2~#OvDk4HD+bsW_O!73|k+T4mC$twHlz0l1*#(&vx3dBf`^2;IDLumT@@c_fiZ=Z3^N|&RQqRNOC}Q6!)7ZM^ zqD#SbIF>5nPG?ib9VW~)2>UXxGK~d_slBfbqCq5A3_S5GV5~lir2|%IyoTCY<-pyZ zNO;zZjsH|&&6%Lg2cD7-M+@7E@#3~j|CNhN1M!Fk7_$0Yfv8e_u6`GbxRkc%cWeap z$I6VGdyClfI;X7qJf;nd9tA)-*>m20GFAKqzphut$|XzBz8KF~#zz15PGJ%qoRf1n zF@MgH43uRGvd+o&Jtq4u?s%a6L)m3y&-sz=gkC8RMTJ_WHO;E~U7b-wW&5&KO<*q2 zx4-6yDfX<5o5yPPBfi=RKv81&zYG+XaBc*K>Jl(41>2YHmSIP@FAb^j-GOQ7wBR^K zYW7y_ur4Nl zR~Dl%FQB@TM`rOLDM5{atHsY{20dr8WMu;KoE_^>a?<=>4y>SrU*^qCqsQb>g@jGn zJU7^Wk2y4LOjqenVuCZb`h6QC6|ir+o+t)ifjR-&zc;6O*$o-mTfUG!Dejw6h!-Idew3)Zb>fM$)2pBV>Gk zUJN>$(k>N-{NJQFWvRiJbx$>#I`qIo7jlm5G==km9{ytoubpixyQi1zMQM3dfiq%j zxE5MRP{?#8ueSOT33!l^5=7YfjkT{ui@qHiyYoRVmQ~voH453N#xO7Je@s^M08H@7 z32J?h?V#yL=ES$##-U75FKYpTVNk3brWtrE(X}jF#bJX|6q1iR+ZIAftL7ixq~_|Q zA}{)A<0sXfFxa6EBtA0Bu&U^q7)mfRc43|T;=1rxj0xN21hP24p+7QO?A8oKL`CzQ zU>-Dn@K{3DYA=YR!pEwwc#M}O$%~#vo z!r%F6wCr0JOST1uZo^MU&8OR^3miI^DjUzrF3t)pHD7ECOboDVSavsjyE1E7HhZdU zyL*q3=x8+Pni@4`@lUmda_^4kT8deV<*day@P*CnkQpbTa^rCgnS9pUph0&r0P*b{4n8}>f#?Gg zo^m#{gXZsP(oX|C-v)TT4e)$BX5YZHN@#hDh0( zU!RFuY?$CE(1Fc3L7SN_{QXuvlR$QX!Ay7z&3}G6nn{<1<{z`iJ9(iM6q+TO+%)Jh ziCZ5Y+qPI_gM7e$rTiEKpa%^DMEXLTg8DYZo^M0!`8LF!Z-;IjwP<%JnmV-xelE-)Q~x zz72uv+Yq?E4T0<15V*b#f$Q7lfWhWvGu$XC-;j4i0Qn7OduX*o@BYr|iw+?t)ObB< zFS%x5!s75PiQVnfkak-O^N-Basbs0CmB7kUvA~YSi1vj++A7S$WoJLA%eO&Yz76W~ zZBUnQgSvbh)aBctt`R+HD;EI}ViqsR2=c8?{Ytl`+0ReexmMqT`;#Vybau5L{rvAozA{5K73gWo3b2Dvx%28}0Zu zOb_3N{`YOPn9K(W=M?G>4dGQNtNe~lSYll zlcXlC+-?>&68vkzJnEV=Ddpf=13&5^-tbB+uJej|)jD&Nl_1WHyg0cgGvczO3MXvvrv@sh!!;h*S z&M9N|TRBfgadJGdg0Q4cS@lsqzjUT8&h>z?nn7hXTUHT!6^>R|qVq_mO9;~_c1C6O z@X;j)RG(CZY0-o;nmp{OngLFud^Z6a_oz$!V_~ zC!<-lKH`Y-lWd(e1qn5&x|A|Ojq@$5U>lTKPhznsJOVcB-zgxlfmzU8TULf<+W3Kz zbLs1*1g_ZLR_|b*E#GOnoWJ%(vGMj5%W-4Qh9JCvSc zt|moxTfS0BlL5IFpQ6fCWqszOd4kG3scPAvCxFP4Is-?1Dzr&UY|{{tCg2g6b-{Y6 z1t!$nokYtYx1v3&(Qm+UlI>eJdC)4=qjs5KfM@Qcebu-`jXxwDz0I3fIm%oY=VW)~ zkIDYKP&L{e0~_b|{Cw{s4?}A!vF%D*5%&Ggcl1ko4??yioed)o9${GrW%j^ zSPGgs1p*V!%3^V7^M5MxhL*PWWd~Wg29*&I?nD^lqjCA17GiY|Kc`Pt&QMps;sC>} zc0GGQXrHr)^3lpN0kWlHxBeR}EqbMfQAV|_>xHUbnW#!lsSjx^9}vA$>E~D@_Slv0 z;*~zbK$(4JvVs^tM?pi{Ef?;Hae23Bq^-I$5aui2wXX7!U^1C7ooVDVNw+tR`^TPPaQ{!i&qGtp}hjFDyf8+{;AR>WKAgJ5t&+$pB7w z8W)F3aT}Z7_zA#?Ozfb=yE=`bHtQUh!pycX>L!YuOX=RkRebb8ak3e}kjqR5> zx}^t(j{=5RJcPnQ7~PB^%c!>)Lkz$)id$65Zf|YVsxl032oOnO>PAv&*?kXe3)wyx zA*H@%nW9a6ky7r`gS2e^*s2~`^Vs1hQ;+3r@Q`{ChPavC7Ii@G@vFX0`tw|W`z+V% zF^j>w9`jAdJF>ggMwsUy-kY0!-sCR>J;1#HLW7P}_ynWrO|4j%6*UG6?+6wqKkiDI z{sF_CqKE3|&kP4)*3mGl0YVMY_=&pm=Cxd)v6km1UX?%no zf6+FgBtr&&=%h#6Adj1pCaJOlHL=?{{(w?_9( zXKKOy;o|)AHlWag_cybpB5d(he>tMGR_kK1d)50rDnz~S)0OlC)^xHjby|4J{s|w{ zZ#GbSx31c*$!mJgE8ec(r=MR^c~S2;obf;Fe$}Yq+l1BSaBT7zUn2boB2}}q(2Hoqy8Mcj8t>eG2x_c@t)$};Yk)VhgLr7Rz z=x9Rc$8TD>*cF6|>25TG4zy2rO!<4cI(aUf%mFSn{AI@CW~9KUJ(=IR-b-e`WrNZ{+I$b=JfuAZ)d`H0HAg}~OBO%Z_v z>x-yMellq*IByaWobX-U5?Qat5ZPylYdKgQ_%YgYvAaj`4LxF)x$jDz&!OIlyqxs| zauwg&#WzW6VD)rD165F)Qs1J!b#k^fhGngV&RosI4~S9FGV8=8NsirS4Ga#y62_YCDKWSRfR8)9gm9O^ z7sm5?8|Rdnjq_0K8-!d?^LG-HPgiDsb;r$;S213M;(Z3K_9$NISW>V;N8Wl6PLsM4 z_7%H>Q#f_@xOoL7=qvQ6JqT9@XVoNxE5k~M4TUKlM)LqWOUpBok)!}Cx1V6@@$oC) ztJJvBh(gYFyi}-F73!wy$RG8q&=h~uYtK?ht%&}QZLkwtTpT=OI*?3UR zF_Bb^I&Z0}?G&_!jIq(OUTYqKhPf`zx@VuYZS>(-NORp&S{O-poW^8^+by&@@9xT# z1rC8Afk~=K1S3^s)r9`CYGMh3RTFamLkyu7*jqKBqO6)oW06XdB9c6L$~3n!@{?_M z)u+XlGJ0qc-FPRN>Swi)R_xKk{U#DPB4`h=Kh8FVGk})+jpgbU_I*adN=0?j0MOnH z&R1=DOO6`%U$NY1i)lexWK2uX(zOzR-Vb_jPuK%WXFZF+i~I0F}~pprFO>!r`{(| zCyA)rVm+-IqFhG}uw(Y*~>i z21t>!evcR^dSs!@d2X8cizvf|iZ98|A;@sRF7~>{EDSe(BD0Hl!ls{Clo`IT6oF4W zsTbszh5$Nb&_)T_IO%}aebkA+ez(Tz*3MbC+HZIJTg$ryW60|RYHwfmXrWZX8dPeg z@6ns}P*Yr5aM3D_J%LWg{YGcWweT5u!dd9C1KKtxeq+H%%oKsznFQ(W)nf@FmTj534F?LlX@k?)xU`cU z`g#Rf6e;g@Sj@Yp1M-`BxamLer9JQ4l};oO@74X^4O}{atZTv>O38QWPjhV({7o4_RB0#LE z?x=sY`fj0}Y7X|;8G*Kkwd|qJK)(;$6fnnX(&xbdJH=gRg9|Y=-|2Jcesf$TRD#I% zW4SQ^s2VzqgIwigz*ww9(ZUTzFy*;x0#pVB7p#KJj*7y(v?`In`RVJh@BaQQ@))}j zYHSx&u*KJ($l4Db)DpA`OI5auT_GWvB8leJuJ~U|t-QvR8N9U0s`TC_B-sB*jf!%P zeP~_~uf3@t9c@fjHXH)O=vjKTpeCtMz}whSf8F zgX4PY5i>Wb$Ng51v!etK&GVglymvF@xIUewkX*EK{F#|@++%50g>qyZ*1(UX7js_? z3+?B$)LNf~)`Erhn`{tcd{~bjUG6v4&3`;IO@t)N4UX> zWusH=x1kx$e+k53-?hkNIlEJYOJm#9g@84@!>S6>@o=^E;Vy5e zFG03>eR4L2cSzkip^gFYqCJuLCOXhN%>UiO%;fBhXbqoPsz9$=GbA%wHf~t3t;`4~ z=)YtJ*>lS+Qph;Zx9=Uxu5O`v=;>o8sLVk-p(H6?AwgIX1GcGS0O?ruxUc9o%^f>I zL!Lj{Sc8OaKX2>=H8=!qP(ul>%Nc8O9fBNwu0vWLXf2FZhx+|WxnungY%nM2g7+RQ zhwKD^N;-kiV8qti7bBJ$!aJi~Ssyk9Qmeri50*Q2f>tfzjGj~#b3wERmuY7oK2(b; z)aH!!X(|E(8#w@(G|v*!@|#rvqt6%AQ^WDhz=#q}%z$9v_RKH|E@GOqQ zcobmQ+F*ht6B<1JOlD>Dd%%iyF5~LUx%5u#B{lIz{&a>Ts4^Uj(#ClI z^CpR6%zb>e02R4x1&u}8y8&ouB|)lNronhdkc34bP3Az5v=7g)>f$u^x90f>prN#e z)7f!USh}i%=CgD1=jSY?UcO>=!~w}eHN{&^Pu2^ZZ$RgDm?j&oXs;0B@PuTv)_T+| z>u6o=L>*yuP`F6fbcAqL{e--SIQkoJiK#G9nobpqdoZvnpxKz|8m41jkgv+_Qd{}e zllP5^ajc|14x(Cj$cNH%yv%fg{^|v`{I6kPK(8VvJ-Pvu>%cCTXU1&D(;#uzYGp^7cw0kr zAGbwAIua-rIl9Sfrb{{_dg%_%)S>1!ryIgrU=l<9)t}3B1Z7q;1C4~yUSbG48xIQL zUqVeV&=Ugk3+th1qM2~~AI+(Kt(50ng6wGZb4KCQ?DqL}%oo0Zl3IcwP_o2&`=kki8X1O5X&nTE#v=`^s{Yj@5d^*A*m%8_n@{B?@D7#w z=!pJYTD^!;EuIsRJ~M#01|s!>P_~_d`CJ22|B-^qyoNk5b~#W_kLQWgpNJ$8iG@7k zvMd)wO)Mi2JrbSZ(i`4|$pZbdi>vqG46(*z_6rJGw;Td4Kh`Y&#A-n=wQ8Bl`5L*v zHPKZe&el*O7ox|Kh#~b57VO0H)jS`koLEWytogz_#?2SsF|N*GV7E!qlt?XY>4fAM z($Wx)cY0hbl%^-uBc#Ln--Vcy_>gcSDt~mkw2!sEBWC^IHlDHrH-STC1q%}cea|-X zF+79jhO61h0?zn)W>wRC;tLOha3GIT$_G^5CBDs12_~` z<92bjc=HKDTgrP>SD1@BXJV2&&1e>#A{vhqSXsDsva5S0Qn~7XZSrfHAG{hd^>gLq zi&ef|IKmX(4vrW)PH-;{>7S@q|M+OtM^mokG?%Mx82hEAS2k&$as*J?KZVW5)WEfP z@OiUcJ4WCqU!j>x%YQcZ62=;B`9UQ<=-LED*Lp`_v&noCi&6*3?e>(dhRIpYw*h7K zN*D#~z7jO?5W51vn)2G|r1@xeg_K7|j@*9k7I=P7Ch#Su7C9dA!{urvsgiN#w^WnZ zbJPOB{3NI7`q?sfZhE>@y;w(au!jeU5$y+y>E)Q0^+fq0i!`vzOg82G?CM=KWDO5z z0><(cXq4(zU@Nd984FGzCB!Yyn=xI%q}r^7eKUPU0%Era7;-7fc&9)%wG6^A|3!ii%ofKN+JYZS$gs= z0ld167o&Qcwwsgc2I4U!BJvcHxB}mb?$qRTNq4Ey>8ZPpPEV^xX)sTtWUZ3w$wGbV zQJVs;Z>Q?>4w+ncr200i3RZvc+V-e6pVi2l}A zj=*@<$V|tkamGidl1!ND-Juc-<^=|6X?098>_*eZhx_|~cu`=+{0%&!m zB`i_T!ZF4U?FyZa(bkA1>-k}yOpLl7p=@gjrB5a+fxXmGqfX_5OjZJQ>a3tp+{tvD zo1Z{XpN{FD*d7|zzE{I~^2DIwgy$2|@F^{5Q|arV;XfWtw*lNrhpZJLOC)JXRK0$38X?SCOLpr*RVee>oTfeW2>INE~SUy;7)+0o^X?PL_ zst8gkD=qm-h;Kr^n24%YG>&Y1(;uz&Ol5W1X*vC*ai(+QXt)u8CY|x{ea4l6JX#tA2o8J^%-X213s$CSdVIo^B zOGRwQ2*R${PIY7!)kK(KSJH{({A3)t#73M_RErny5`-8^YK-b}yK2i6w>f~9;CN!R z^*D7<(ubkyM9_d7l~-xq2pIMls*aFLwnQ+==~7nRipt)OASQ_Z@kw_E8%#RhlNlXy zZ0|vT-!(ns`1H*BjO%+(pL!QYA-+$IkZi>CGG88kHKfJJUJIjNLmR_ukMaZgfTriC zOc35s0>!^;!b*xWuUkC>4KLHCA=3+c4_agFPoea-i!F86V(Tt`%T~}}Y)e~rW%V{u zGs8*?j3I94+Ug1q^J}Q!jQSM&Sv=i4oz$ly9z|5Po5<3av|LHLZ3W%N81OqpqBxe{ z($6%x0*Y{qN%05u*eTjD8!dZGUvSi!RBaVM=tq2pbCdKyGTKdJ{fg)(^sUNp6;524 z2RpP%zcx$*NYed+`q&K;zgv*HT2KcQuKV0=T3m z0E#|b1F`T67C2S^U$l!ynp$G7r7<%yoUB#zeA3iU-i0EWY|)vW>IG-aV6|m7ncJ!- z7@PUENAJ46-Xf3$f95+4=~udT4Hvc|2UtO=DqL=@3x` zJa7>Z78hLwWEFJ`>+0g^!mf+1!oASPbOnnF(}t=m|K>)LY8c8wMl}*?C6StB|Foh; zF1G2-MOgWXo5tzt-!S8Jgp5ow&w4Al0qQUmY3wY}k4pHvT0f^F(m@59q&V&!8J}Jd zF4d%Me&^xh+$@oMecI#b*#e1u1B%F<>D$y|_;p6TK&y65mlJ49635FkaXd0EgctCY z00W#QvB;7IvWH#cBl2mn&1p!b-FdhXfretBwBoLOJ6a_)%zn+s?A59x^+KRnW#O(= zJcHq~$)h5!*rpZKg#gT}mn*v5JN`3OAmja;}KR`6@d`1s}*R!gHpEtojz=0X}u8LSo9J()_7Bv?#?O z01ktrw{jm>@y=7+hXmxDFZc$}QSLUQoQK-IJI{yS5zz`?@2qBkfIHJVaTHx4D3Hv4 z!VaVG0JQ~5AWl->F)n!39@nf=khy9Avxx#3Q9(i5sz9zlKT1RGiiF5)_t%qEfYn$y ziiG2R%rp8LpJSTh&nA-t?vTtGDjHX>221<0%wc&AyAbP`_SXsH{Jq0CL=mdjl#>G* z`VD1?)KQ`KOvxWt{k$lFxkLy?k70o7Mdjp;1MQ@#rMW5lEJXAAzo~9kbYv{)7~*lc zr>eVpZNx!@zC6AYq)aAolxI$_7(@}EX2~-G{nhJ4NzkJsrpwhMys2%xdYP-r*z3t8htucShGY)#Inq9Fz5zzQDU5i>824K(0LU`kbbN&0N{|s8xnE4P z>h(O>m#sC7BQ|xj^NI+c?v)smm6JgW4rOVg3JK?mo8#XuuWXJN#wkhV3;;V(Dwc*$ zg$kOTie6;Rw{N_TVuPzzMZYoz8Th?iV8k`UQQa21%TQ~t1Cl*yUO3=?!wg97G~DX_Yapmk_k_ZHN{$>BfzKM2VE>O#GsK|@H>H?J3-P~}bEe$!um@{}L8g0e3(VJCb%Es1QZ zeAoC{tIJd&Bb@)nH)E}IRX4{!JFb`Sn3m0R-gw#-&5obKu^wE*4*6%7*>qcT3vYc- zGJM&5>epf${IdC>EAM)4^=p8`P))<-apgDY05w0Zr`6%?@N3%5`>PN8>G<%q*Atqm zt~-42xwuM$TW%(-vJK3Z$5iJn(`~9?TYc(voMfws3;lOrcHQCP(v|%1Qhmy^4r4t% zi%);(7HUz+f4_^P2czKeKIiy!<&G(;liQz|E~3yRSbEr=%H<;eeWXbvMGaN*ieq@ZoKKaFSvF8b?4JI_N(i>{#jGCD!oUPazjBe7-kjhQ8vGeC2}}_?&19ApT6T5 zVNV3Zu~lN(&Gvd}7uFBMG(6hu2&WwMA&j0ty+lQx#CAwHOmdH~qN$U|3}Rh|UYEr( zX!c{Vh&l$%qbxA%@llxsDvQ(Cl?7+01UYiNfbm&LK=8>Q0t4OG=iru&oP~3l=TkQz zYbV-6rleB-K0hjLF+i+h#-!HyT(#)EI&!gQ<%Z3}e~PAuSJQ7+<1GFULi4Ek=d~-E z)2?M)Vmv6Vpu?v#zaBStUw?&6wrj{#ddcF^bMiFupZT+_`OKfo+{&J`OT@sW&(VoQ zSUX*TKC-#75SGh@ny8Qhz|HNe+ptXERH}cs5rIaEOP=Jwf5e>ZBD^lUtP z_ClWF)yd{*%PG~t^A`fJdJ7aJRg2)l$gOh``}abKJh<_lW0=+-OjhshOl1LF~(FPK=+d*EK2u3QkdelK}tgB~Ii zGkV%hHgsP7GJnFFm(^5aBsF#bG*&IHkO`X^yimHKT4(V3ahf}&?g%Pge@3R6o>t>U(M~|Vd^AeHBQno*C9#~NnC+JxWqQ5`Qs^K zjzqE(o=uKC#P;YwTHiJs73dpvQ@tHR&nCyqm;|RYBXvfq6VARG#f@l1c@isWmSRaw zc?j@}=Cd(F+bk-0Qy?fy!fHr46eAdEmXxkR;1?BtjM7Q#4w~2Aa0SybDA+0EbK^@m zV-TfEF%1_Qm!Asn%1c3U;h)HTz^uFkS4kJbE) zI)P;Drm;ULhVhV^1~JEEOQd2PydYE+r8j41?4+p`zsfcOAv^!7(8{c>pT)8 z$;VI_dDQHIUo84o&3TNn`9|DP-GkjO>q99Dx|20Hga%r=nMh)bAKo2F42#VlZI=Mn z%zpKOa!TYlgW$ej`eUT*CoHsa^;_}=Sk$;s{Wj7;a+xT}f=TQBFB&%=c#(YgI;J`3 z&i7Qm!v``WkEBWz6dzO{ER~z@w?)%^$fMidKWgrN|A;&y$dn`LxHgYcB}Fss)fKh; zu50o0qB&cym1E+Qq(t|ssCi`ddk_YkmC$U7AzKHOfO6b=7Jj==)j!#C7$qoDqS^V*$+|WZ|+R)x9* z1DqHY@)*&-*$_oGAAJI!sJlySn~JHi3mHds1JyxB-UH6lDopzhPJ)=wENxgmYEL$* zaFZ;tLjVbGY(WaU-&6fP6el^g-)*dBNR?7^WVYx2VZ_)#8V9CwtGPL-!Cx!R^(0qt zbmoh+e(79mc@GQ>wG&j8m2f2GBQjz&PHU5ctm8S81!pzF{_)DUm^1oud~ngO30v}M z%Ali@qt(|(8f(u{jJN8`^k3B%%azPx=LCH@6b5a(-bHk$b$j5CKxNRE4*V<}Ra=v) zC8r$etg8q0>@=uQxcnaPhMdo2BA8QH15`|&Hb>2JWw5Bs%((+`5SB0oH6m@Ao9~en zYkyE=Cx0IMSCOTA%cd7vA{wA4TgR0iofTkbUzVNN^#vpEqMGgG zOD3n5s6;wJI@wx{r+9T(y&h!r;=gI@CJsOoil>{{0k#RiCKEjZ30Q+i(Xkiu#Yw`0 zzI}(+VTygI(Hi&>IF#MQo6tq2i9J56LPbKMkKB#`W2h6^R$ohSPPc!xFRk>R{iP3t{_Kf8?R!eZq zrh_EjpvH~nr&l7S+7iP+@9`U7x9^xv*bHWy;dRnhf)a3rm`r z_5gj#JmPT7{FT^IcCBTj;!0P4ifE#oU`Istzo&9WP-;s?onw>4?VUrT-VK^t^%F=n zQ_l1ju_Ak>7+RFiiDN63$w>=Z#-Isj%dC_`uUC6;?ZEymG@+#p?VIN1aPU9*;q6Q= zQH?f_;ikz~iF3-E))2G^Ay8xaN90p!Yj_64{$v{}a14Ir=y&)bK}}tvryMUF8{2{j zfa@^PzSr7pDdP#Q0|S3UEF)lyh`tg=kZm=T$kA}pigncs^DNA%SkFVtS@W$AkDBwU zKaY?T@n1p;+PaH7odr!V#?%HzK^cQZXQhuAj=b4fy|KVN&K{HN^mcbIm*Sj7dqX^; zr>w)Ok3%+sMM_KD^t?PYB0Ms{hS6EZmR{elq`e)^ci^T%ONO0nG%}#FU%_?JEQD1_ zAx%X&DPBbLz)E#9iGTC}h`Gp45sH5n6C2nclcV43;xW0JG+vg$lXIB9v~t$uwM=Ap z2BPdv8C_^NP+Or&BY$F`Fh$YappHS=bh zQ0vcRMsl{etkA!R_7F54K^_WF(Dyh{rU4g?Tg|lm&7p@$>nvO3+6RJ?%->0scl3 zzm_5duAjfl&uaGb&-i)9?B@ggT=7o^_;i2&k36C_jxg5_egGknJka#N*iJyW{WZ0| zx4NHQI9Ru|$_TEOGiRh|e)hL~@{%kfEaPvN)1vwt;E&PwWx4uFsh^0_Bs+@gZy}TF ztEo<=4wGA=IO1uOIe3`APVmt3ko^L+Wnc%bws;6={q~fJ79I2UaqK8AN?qLsf>mM) zC-=unGoTkc6%#(Hhu1XcUCH)6L$H9f2o@9y=(`R+=v)*>(;W4JY|Sx`E{HZrUhu1<>ZPa$o%!tuFCTq~I*3>r5^a!+ZlWt{ z=5F4=ZvtKJ{L8sUedn>3jXT!%@%JS__==(h;kpjU<7CMq7tMq;^Q`nDzS3&A>N91C zD``O16}HkVb<*GC?HQDt^C`1qw;6)9;y7zS&jSm*L!jI`56k=LtctK`!L_3Dys`$R z);0%$-}4@rXA{uL~b8R<_jT!x=GY|HJ6PYakl-0NJuanHO8NwX*7nT&MRtQstx6 zxzsY{prm$`wP&AOM;br7m|~u8*+e2;WtjzG6tJ#77K`e79h%1gJdSmI{JuG7Qd_Hr z$o`B-8Izbre2;9rj%tufQCao#BKkC?($jsN0B@zYD$XokxgkAr3mMDvlb5j}!DceH zPuXO}^@%>@S5W%(7;_r~KB>Zxc)>!?$R6D*|;l;t6fj~e7Ji{-13U1|bsCB1Tl_Qg*f z-e~3uS?7^%%l+6N-7ej<&mhm3XaAkz-lp$EdIfc zhZX{D0mj&F`NpeK(=X2HQ$Gxrc+;nJ{9sbVX)Z3dm}1~Jb}DMr*5SxG2~yMS zcV+|zAZ$xB8}oUxdL2H}Ns<*Lz8P7`O0=IVI5o!2&P_Ukeoq81$0Xzy)XHGm+Ja}r zA)C>56!>h31ECM_sWigtAo16mHH-^CYnaiw-L%4Hs{E@S#fbNSVcQ&Hh3R*=bKd_5@u(Vt?M{|Vg^Gc z-9jS}d{%v-gurlI<;fP0B`A=(`9+L!<)gJj8(0>F@TPqz`(OSe{-vySOTbN4YC8}n zDHu|w=mwf-%_&v}ZD~%m8rQ~@H*DCyk6)v68QVQW{4^ZF0OGTiSLbS4gY0;r=X4-YMvL1>FUQ~htWUr@{4j73WiCxJ> z$OM^K1I#sbr9>5p2unfi)+qK2F0l6HTp*^)#HxD<_^#}H)ofJn9>QBDWKZ(ii08#>vwITnuo7PAKsi#Gxg=pT;5)Es&)5r zxqEr%?pb)SQ#mn(q2eEVG<6_0`S7CYlC1+4q0B7e3Am6%1tuI+mgcL-Q;De_Jk_kZ z>s1TQe_OqL0E3k0$eot3{*XkRmilO8$XU&_Bk2!)BFbo7FksyF5ONDa`0bL~e z@)G8ycd*yFZjV3b9%WJ< z#HpiWHMXanPxTkOr|fqJ|FE?k?5=ZfK_k+257p%IQ>lvucEFkpDG5))#C%Ni9uNn6 z^%Q|gPAmh=ms_KVkx$cY@VPh=n7m)!=ZD2&#LJW)67+yrSx*lKI(j&m=;6^FJzQ`S z^gyhi7B!3iAT-f0ctX0MuI7RF{tIY>MIjPOd!!11X)OA{8mayY>`LG=i1up~$JihD zXvn?1WcGqxHO5-Akq8MOc+QSLDarz$JfXwXC1K6-?4s}ji~!ZWr@ECXE^I_(5SJ%R zlcO((fcmUmBG3+(K*Wk8BAlw@hHNiXA%Gth1x<&Vi&-L5t_#YtQG}=cp-kvFO<%I@ znZJC%&F)v)3=Sk|{v_>5`zUE*O!A%Yl-qm2xfvqiC8hRKK!S~^m5Ci7NC!z|u2>#) zk0YqB0TjlVP818CoQTYKZ%*b4AAhH%bk+ktL`vTg>0~XX-?v@cuHu(6ytZ_dy#@JvDjdBu%G?uXXZNY_aVaj0;o((dy# zli+#xaXT}vdG}Po?-xvZ&(CH|VdUmLuUKdfN^5{_c<~k7!0@qn5)6m>;UZLLl00de zih;_TzvbgotyB>pF&QHNN|PZ(cr8TJhg{-A#rJ7L1D!WFy^@s>{{A6+4MoM*@8au! zvNDaPr75;(8jUxnqV!AB1@Zx)Ksi*Y@zQ3Xgo8) z9fz^yT-kg@1)H4JLxau;;ZPhEYP6gMZT;0;=rux18lXJpNc-}0sY1$qVOEZrNLeB~ zRW&z9ylD?6w-Hm1@M8@&K!5O`b>zH(J|};35!6a$Z&lR3ZS+1G1^7D)1WI1-#9C2r z&dKkaHU6nt<89UN_~rtuXda~Mk9Acu)!D21W3#INd}Gxw|JFkDyAM*e;3bieVHz+z zT*l4lnxm_yAfNN%5D`!8p{rL<<>#3H2PJB8{vQK)iWDSPnKsoJA`2OBruz=8f=T4o z>ci?GM`KP;!&cc==4ELP4iipP0eb%&K>x7la3FCr!>tqZ*ILxVcJ9AQtaXOq_u_F9 zGbv@UW=&B)ESZXRS7O?hKvHH6R(8ciF?GmFjrPuJOJH-NlgM<85`A+swF0AN4J*M@ z2~d>kSW0#9nn~)r5){Ck-WTzHW>V&1u){@;Mtkri4GY}+5rKm*ABn_&<4+*rlrb5GYaN*}AQQ`5j z$&bWAOtJk%&el$7co<>tDk`+OjrtcG={p{c)7Frje#o!?hr0Ix*6Xb6eb-v=pZ(|E zJ6TDSG;Ma<^}d8QE!j=1p{=C{vqEXn%XrY^nPJZKoavll<}wMMY0){;dj>DN7$Bsj z#xs>en zuC<=^{P{h<|IhFFJ?jSR)BT$V7YZ3g7C1k$@-p@QNI~uiu7?Yy^vJW-3^0Q|652OJ- zmqqX#ExSen+^wFFnq zlwUTb9~e#OeSC+z&dSdPv3T36_feM27(?E%&uz<=SB^_Qz{|gZ^^!DKw^ZFbSe=@t zmWob$x_-!$cNVpDswM^u6L(vFr$!L9{I{s2RWG?I)@wXim0F4sM3({wP@AZxovOKQ zRLy@5CW2Iq5=`tyO)*zzW&?hz@^-#Zf3}fIBJr(L{yaiQAw)FbPpse#vx8u)>x0qO93ks5dnCFafJ zcJATv)Q}o?@*)Pa%MHUK8=F6|&@OpUP$T6rBt#izIW1>;OXq>iLe?w82*WB>djr)X z`emq7UQWN2K(u{W{+cL1JJ}7EdBmc-)!H{haN3)kmtS8y$oH`9gGZ{-*MZ;Lgi&HV z+w$+_mk4@W{sA6}`%0%64adtv8qxSO*jupVMvd!=2kB#}ak%QJN_>c#0Yj>CT*wf! zA75C^XynHV`6$}G%d<)`K;~)um~MW|H9ty!`0qmfX8)P&R)voiiepG_Ep*7~L8y{) zYDs}cV#M@J`m_`e)~J!cQn$VndpOQ>XCt+12DJxQE@rn?SIW6?n<7M(sUXrd!4579 zkeX^cs-DnheczuD-eEC;8hjLmFe&4oHuRh$p{Vz@FrT&*kF^J-I145BD@%bE+wceLt0gkz1des0HBh8e ze+7>Z=wtW?@W)y$pSFGSSev8k71zT+jS)?c&9ix+G8WN|{L5#^}{l(HvL92q10xwoG=r?=u0o+2tioZ<6Qd&|-73AH8i-Ijl@prj=3P>_YrO-d7EUa(pR z%_kO1gaNcd#}*P+Hr@Q%m%H_*iC&@+3PRCK((|qaRjc0`W?x+OTi=>eRBh0if`sq+ z!!EnOjORyah!v(pV2>ua9Z6#m*i0qpmoZ`#d1#jo$Y4*y9XHsABc5uEoT)H!hQu}% z@c%S@_)^G_8jW_W6Au+&1@JzUT9BdOuMY|+0{gHN2S=TFuV7BGWo8x}cqr7<@LbS7 z!hFxl4^hCv;{q=R8tE}zWZIA3U1Yw$Eq}}cbS0YbciP-{1V&%h1>Z6cFOz6t=|2-p ziAjPCwGR>GCr>@+L2F}cpCsN%(f1~pDFmi;By#smHXWacCXZaS41bM>k3so*8MBgYSVYW$MK@|S9O|uYaPcyZ~0)K z5t=`PJZQTQ>Hs%Px4>)2zFhc$+-;X1WvJ!H;CgGEaG1#jTH=UAHAnp*tuZ92ghO;6U-BQKcB7N-^;E+}=2AFeg(}?&=K*W$fhu5MgDOfkK13A)DMqN$oj{dtd4wGv z0U)s;vZAlt?=e)_GDMXv%TQ&@GE~_DmQ11w-93l^6{=X8JRo9m9}=pNbV1-XWM8hl zu26ADs4}xMs=(oA$&QLV#`FqQDf$NXK8G}{l}=!)Pa`lf!wxL&54Oay6p${wgL;cDKMqkDYN1*ayC(oK>4x$}Oz5R= z>G$QV4Xaq)oo%U7MGDpz1@D^ZJknefUMrE0b6M?YZ;Hqx$(5 zF|YE=Ow8-8{gq}OiS3`GvUWfX@VPy7bUitiyv3Wgoo_Uqq=Ot-g<>ZMO z`hk_;>~p{5^m)m|yvp$tUw!X+zIw^>*}*JbspyOh?7^5anSVs^F#33lx=5Okl1c^ng+?!{PPMe!)VrIE9ycn$&+6?k>EZCmC(sK;mZn`t`ZXZ0EBKe#5i+ftlX zHUo|si*3G3h1RKTbf*orbZK7u7UfR|Vi?d^x|`5ExxJ-NS2?P^U43WO*);ghJ^^FZ z4*lFQn3c|||62U3W`LIP>(xrzIiI(}7eRleuFkTHlF)Bl47AhcU$>f#a7LE9?E#o~ z3ebQ!qipum()QUl@aKA_J=ok#tby@-yFUl0Pl1S^=v14N?|*&`Za}n}8MQn^H*hfi zr>d>Jk$rz!1$bK=}|BHoBvh%zz z5YASs6By}x!edgVu6AUmVWqFn0)6?C$d+F2Hxv`F#ylgd#RBF9Fg`fe#up~NK5n0H z4d#5rl?1C{KuD8}kU2mKhbVr;mJ4Q%X4@!*=M}Ca<4k3rvXZBS z($N?MGilJ{2d}Y8;N#UQdbnLos>j>_b{%JsUJ0p`-HX1`j z27_0CQi8>x&69%URH9;a3=VOcWP(D*SOD)o$ zRTZkGKGF1HRs$}g2=;JZhZyDv>}uj% zIZRFi5U$X-A*Q_4V`}#Yy27QdDj5=qV)V`0>ZQh%_3F%YV9HB1ahDoXm;_IsFT(w>u#|^Eyd+88MhzL<}8a@@XxK>zVoPEwyxpssld^~|A1Z*&W zmj{*%TWUv32}|ZH4jXa(C!6|d>)98sdb)b4hI@I;iCe)>@o(p+Sk!qbpk@CdXbO2$ zc=xd{e#eW8ni9JlE*Ed*^`*OUEduFC=Ze!3>-6%95CN0u3HqaP9|tukeAT9EFsx2f zDDK>X`ApOOE+naAAjQx||3T39wZSTkSzrh_g*!$(&F2P6T;{tq7X1o55*woDnhx`@6rG=+HkVF}*v z<75eig?4hE4pKFePiJL$Wt(n6ebsj{x=v!#$So)|SWSzv>!+^O#oPy=cKTgy5r{K~Ju;9$KTo(PD!+fX2nf zD59;@O#&L+-6Js5@Bl03B6Ww)bA>n1hfk++M^&z4FK>mzfkhX9o#l8Udn6bA6)i^e z$eAu-R@6_7RYh5S&;ajqslF#vU;6sfz5ca8xLaLY{?l6Rr>EV|q;khpjwyA|kDAZ zT{aGEC5Wa+0AnR!FF@e{+dlqQ{}KTk=i4O-*tqTai0*N~4tpie(;WwnUNQk}n84VF zD~7F#(N&jBKpIRK`^t-f`3OC1q;^nS>CGj}fd#Iv2rc*sJzsgH0dh$J5GyT`^uFB+ zNRQ>eh?JljM^5-Yv2qy91Y(O*Wm2g#7Fd*IxtEq*1oEYh;+v1PIGLIF*A(EazzL>? zOExGrNp#4DY&o*RLP|$7qtqe`B|7-kftpz4SHx|u?K|W^k$uc)T&OON$a=jjQ#@neUZ&XPJN{_QT$0r z(@%BudXD=DfBIA?Mx#`Lu@PfVhKT39XdJVwd(txL)gF91=`R5tn5$Z?wghjEogP4g z%0Y{phfy-E%^9;y!_x?IrV)5uc1GtHHCZD` z>kMSxO5T^^)yVxfD?{Opgm*siWVg@$rO;Q|&pB1EQudQp{n2|n_3HAkrpS+S7VHHf zr|P}GOJTeL!W1wAW#-GMF2h7g{2C=n0=vQD-Tj<$7Wdk?TVL-)Nn#5-03(MKZwK9a z7L)5~A&rqf=t?zu+IiXQeREYlFE)4tLT)?%Px2~O-!A(PNoB$*0L;d-|ES@Oa0ul9 zte^Nzl4_UVh%LYndJJPRdn2&V=`CPWKM94}xzc~~q)7eG>nUD=T>jr{>++6Q3kq$f7`_asQob7<0gqmICuUdMs~x;^Iv^hY2=*A4+xz-XD;vBk z^&MB`G~?ZyFx9xc5yxcPHXyIomEW{6^9BN7cz2sau!Oxj)$Z*NDLC_oUX33U%tQjL zs{isJL9DDR;b<4&V(*i(x`9^_y#_)_6-DErdiNYih(4))vuqm%$zh3E00$Sqc)dx(rH^*{XIJogD;cWlKS-ZxNndYkYnaBeF@fp_@slzLQUQ1nkk_{# zfiq0DwMUNSyhvX3TB4|^<~1?XtoM_G^jq{oom8F_8+v4B2V37Iw1fj^x73_=gI8G5 zAK5Ltork|_FJ_p3J|2Sl9%1a#C(=({*#O*izAGcA8+eEYg zXIPd%{&(vW?^MJxhQsL5({CqoO~>psk0ju`^}l>W5#Jz4`2~FpH&QlmS6$yn??FQh zt>#^>cZ=B}Ax3*Ar#b}YMDd@bxDOC(NHA3dm8k$hXdD*b#k*;FQA&bmiw; zGElK>5F-FVnUfSO5h5L%)OrgueV?OOwfHWqay$jI;An~@eYC(~^gRgTD36b($XS`V zKVWk{1xiGMrg%+d^+PLBdSTT396~+#Aa5n6V;$0 z4XFlPXv^S&GK~XRPxR~hVZUx@MuLUqO43M#LQys>Eq!%OlwD~IxkkX@W`etRaZlB! zpXk=lW{UpeOBr3S25y-GcDESW*;NkTxZAt4Wjmy4hCm%r%SQy|gC@2Egp3UyD2Y-7 zv4sX=RpMsm!B8e0c;a#LR^{h`*aBUtBJG~zp~fJ#Ac)!Q8e=oEx1?dyt&4-tX!=K} z=yox+hP8qWM7<{vhT*01HoDTpN`n{Lz~;*5?GB+pm|7}oGfPDU5?4HqI}UOeng=bw z+kq5GBt%$p0uh1T>b*R4O)L;Au+jnWTuMJ6l&5~1)G(<&Qeelj55JMK8WGq{9QTW2 zmbbLuT4x<sX{TJ+wPYyvD+G14BqMuwaQge z-d&dONIUz*krEIXkuxyXIi3t=f-h*6_Vlw8IT;&$2Z&|*(k5Q--;68cw58tHG+X|& z=Yv(SS$Ccv)2b)hp`dl}?;3(Pr~eG{75M_+TfMc_77WAMWdd<-tE=}PhamQsysVc3 zzxy(*lw6VvJ|;68R0h0YvMu0uJSUAO_+V%;%K=vuLAntAdJVsXSk}^04Q;J+(Z!}q z9t)pF8txQ76-pdX;A`r`IQJG^FrmW$eV&TvL~~VsV@e)8oWlVkevE@iJ4rB8-Y}eE z2C9A9^bsJl(HD)BPcc)t2Byz~o*A-qZQDYOzYGD&Ro zp6VBMZmm}-KNI>1PrVKKN??m<%}1z8N9p*@n+7RQs8j*asrP)Y12MHpHW8auRX`k* z>x)UD5ZpclNBt}l$k&tripmdH{^^P&kq2LJ+8w*(IrJfgXcwsTY|R*f7JjDTAb=2xvQ*Q~u@-TDm|ZoKH?O$(U? z52ISb2)g7|Ri5WpZOO0A2j7|Ji`?XJlYAHdc49p-V-#%fS+(w_exBd>vMclJ@|Wz$ z^Q(EEzcRgldw0*O)m!uY`g}92dYfK-cY3vNde5q<@5*1wqwUS3SsvYxW5wp*(L9>t z(JS&RcyvvA^lXNOcIjoz+xvpx-^c3GP4!(}W-;TUoBM^>85iG}e(U>ZeZyI0dNZe+ z{i}Slf0f_F&HcXFuk4*!y`O`{sV#2q_sxFPeZ5)hH*s^nZ}zW?H|zW+ZtnNZ{tfYF zgWtr>{l3|cgG04#^qaW3-#7c&ovSw&>&^a!xLNScrEznqZ>qSdd^3offp0d)&1T{ECY>78pd~;RYT;-dsakJGoSI5oOzS$l(+kJCQ++5?E9dWb6H`m6^ zwZ6GNZm#zYo6j`38+>zP+}!A!SH{gNeRC{}yJK)opzxV=t5CI@%qwx}aDG}{@Q>X0 zcJq)WsP6J_ihtAmo8jLq|K$E5ysw_;-zxsC=3mLbHT+x4zYF-cj(>6&ZQ$R9{FD3W zBK}>>zfJsG;NP=KbN?0nxl7I7CD+n^{o3zeYI?ug-LFRXtHpQd?K|}L?fUh0|5CYo zb$hRF@Aa)dyH}sxtIzJ%+q?DlZoS>7+kLv-=UWxtr^5SGc&}dEuOj#Rmx{bkw-4&i zPwUT;|3EXa!rrSAYl30@Ut_pS)phg8at zO4+V=B9DFAgWMz1x1=m0BQM}(!%pqOfqlRx#(c*hgWbQbVx`0tuEODCQj6^D>OK8K z#m2X`C0t-b&cJ|l;s5K{eauBFQvbq#4fAEZHd&>#o~sn*jSm138QvOO&@JzC)+f>m zF&z%&1H_5)bP?gd^joBO#y0^h(b(V49Z8gdZoy{Fby5SS>UZ&Qh9YtXOx5>s*W(WH z%H7X!x7r`H`qO;W<)fOPe)$M5$>B_GEx*uE{o*lR5NIoVM3p?nEvcX7KIiUhn8oCF zo-gYA_D*sBpiT;!yHIXdOg)CtPQ%y<<#{|eDU0n<1vR1CKFU=Rj~GeB96O!Ub3#E| z2<{uQ$Ml2AJj8XE^fQ8ciTw4hRJJm+6!~tnKGp4GROm53lC`FFI*>DpkuJ@lYCNnV zd+R6VpX>D&S4hIzR%k1DHmB-oKtMjhmi-xH!+1?hhH_wj88`r1g4FT)36g3FOKPk% zOWtYJ4`L2uI@5Cv`yP#kVei#F&zI8k=cO`f*n~KH*k^gr4EwZR65GLFw!kTMb89g> z1@=fkV-(NjELD)6nx}uo(_7|o;pyh9AW%IBr(L8JZPl#Q8j4gzbpj)KjS8i{_FZb? zu$wsOO&o$xDnW8+KqW1+RnOm$xgx5lkC66}#K-m1``hI&DC4cGd%+UwuKM@@6*^5P z15ar>*|?`s?d6R^BLol{S=O`R;N3ZNbLygVEi^7ei2!x7Toz=?P5o6B?5|^Bns#PU zICW=&perGTcPloT)ep6rO&_#=B>=dt^+W;LwCBj$*Q(i-kh&9daiAD>hmHA4gKZL` zOFz-PS-I@K!vPqB)C zr4Wr1u3%tIAqre)0v4)2s7^ov#u{SUSd#lmnj}(qgvsQwaT0N!enXfHOST(%kxgp&EfPbPq8q1#VK}dtU5YQA1lqV@tde#q1Td^2;Gl-gO%cD6kJ5tgK!w+&JsgfhnoC;Uw6t=L0)bG!w*g9#Eb*w{^$JLBF! zhg38fa2d)7`H-7RM&Xt`9fwrSncwTy-6m~!UVoF@mgSH%3#ZT)H||vEZ_3&P-Wo$b z;Tovixj!I!zi$!uS}1Tc9RTOe3JTzR9!M#%I{4@YM+v+g*k|a-*H6UYL}7PlR9KGL zWhItn`NnD6LvB!Oa|WF`Dx`*C+_)5|ggl`vn4eOpPR76D7EY#22&hC9(IG96Yd|x- z)axcQw>`v$LrMj1k*Eg)c29~oLdl+EPNX$00qfaRs~O3}Fa^_t5(K#OI4hW+3&dZ? z`Kyz|9S#c?M_I+MmVYI=8$>m1Uus3uf#Ay{JYdp|zfLT-44fJV5vqxx?D9&%zv4Tj zBd>ClBE^Zru*b-VJcX97lA%9SNIuucLasR0HkcM;mTDqAIHD`-2v$uNu4^6A{TL&A zLid0ICf7Qu-#G3Lg1k&(tdxl%C0PLjsJAE>5!s-8!t<9b%V z1OLTkp-^4W!_9!!B6%ry@w)o)-|p6*vAY;w+ZM!|;3(DOMQNI_)bb13G74#y)s$5X zoux@`^<8%Xawf16z`W?zeMgl3D` z--WoH)P87VC~lt|iCgO{`sh(=ur@+plQyzOlZ+D18?+H8^MTtwCtl&nC1zRE91;~J zEeW+r9@(}^&~QkJwTzi=R8-YZUbi1N5kdptPf#0L3jDAnpgtt-VBxyrWWux)5{H8G zm_R>&^Q%mJV$i0#T^wMyL`9mfp{6M`IDJm94m7=Dl-8WOBh860TH&+h3xClOAj-ie zZN}jlw6)1Dt<-s*OBm^)+7xb_(G?OqZAJkMJv$6zFkOD0cbrfni6Syl-(ht*Yj~kH zq&G3!L&1Z2n85jNcYGb}fPSZSg{kQ96ym%jdjLXUeC9rBurmS8P!W-mW*Vk4!N7&( zf87*0r4CuHXyThrPjrkB<&iveg-r2J(5GfhY>`XaCK)iy=>;`qS1|0FCj#E-Li(pf zz|B?n&2)iP?f?kBR#eiuQy+M=QFa`ksn;tYa~E*G2xJDUkUDgO|FQ02`vbx?rqS6x zG$ECSMP4d9!)Bl;3CAE%IsGPF0F|O3|6S3)fPWiWkR?NDvC)&SSb&+;m6YU)!Wy53 zgVw)739us2fK2kj%0eSdMgUe#vUbj+a4>h1>PDJjk=IyjU>6l328(q5d@_9tod+om z#HMdi@8=GK*~WNLzn8mJ+=0;*AR zqY2N$s0oauRl=CzC||gPp0|mw$2<5-4Qqg`#Rb(-VTx2s8xOxs0c|rnso6t$I04@h zpHMu;@s{=;B^0&l_2TbH~0xm8Zd_Jz=GRJ00VJgOS9)XvUnZcn-BOSE!#*T);rv5_Of5UV%J`r>|?x+S2 zmfbSLxp?7T$Zw52!b}sAGBMLc?|c<#V>N)&Jeab3a014tsO7W%PO(E)?{Ue|_SgB4 z#M;8)Byuq`r0LQ(X|xAKeYms=GH zr)*ZRU(P1U1<51!{e{qxMW zoI&`HvOf@5q3P%_3&AMmK5SeR6A%xM_8=Q5IM^XUP=GVuT1pEjc31*9ejN{J)rA*L zmqE+0I)v|xg4jd+eF~*0GIzJ?qke*F)I1rE^xSOugG2beFua7Ek0qXX zH1R}on3yM$CFcUYf22hSBG<#M{(5gwIuCqNb%*d3ka3W}9YH%djKM%Md?&m6pNBD! zr~K@Skv|u6QZI0uE6yHD)OkPC;e8Xe{bNF#m9PT0hDOA66JutP`9M+Q$WvlTFmZkj z5rSxZVa^iQXHFp=#kB`7Po-|H?tru`kEU~+b@B+iSiye;Ew ziOM%eL`Z z12BzI*`7T%R`iiS?WwO3gL)@1Cw@}8kO_8_d zzce9lOL@G1TP~SaP>W|xxq1}KOwXQ_Q5qSdb^BCaV;62peYt(ul{eFIQ;ve$*&w@X z>4U)Z$`&WO^3_qCp>oqR`@?9vHx9qu9pAQx!LnX#kCS}ZB->Dz;=u(H5?9c?5Pe(z z`e7x?No*;tcyK{r*lRM)je7jL;RinuA1rH?#Vygg(+hcTm_`-JSBvteoh@ zlt|>Ydq8S|qPkG0Lpiuz!F=?_XFby`#qgRFND=nE`>ebRvoX{aWH(9YhOp96QGN5CKT1W)Fx%jB1QyZqQE6YJJ>wH zmK{Xi1k6#*3=KaPwM=tlpwm)Xo_`jEs#qC)Tvs%}W4fXNDm%C|z@xgN0a}Gb13aR8 zG(cwyM+4N#th@vEvZ4Xn%L;3fmsRpy&Q*ET%xd8B!D`@&sUw2bn1_q?1o?QvDZ#5l zu(k>39FP$Qso`|xdjLItW}+elYXJOK86Y#-hzw>iT>=yqom9hJ;j&`PLK$4Tg66Z( zgsw7gV{kO7MnKaB|{^j9WSyjT|sHjUqTru=&q5kG&gSk`fV zyiY%1*yV%#s4m2119=7j8y-2Q@DtbQj_g+hK+tdDV;SdL!47z@QDD;T*0{PPTQN1qsC5zE*k0=aT23=Efut zb3yw*@&FvXl?y2NdM;q#4{`wkU&jUfdo35x?=@V&zMHs!e7Hh|dpB|c^{(dvh_$N< z#A8>L@NO#?;Jbwj{0kkTbsN+wwsvt!)07Kx`_v~0Yp<@P(pb_JT)UG~Xjt>Qoo@)z zfjC@^mXYtFMEw91bx~sk2$MXIl$3!aILa^@u4$m-vdOD{{w(sya;Q`OESyigZ!v2o znmK3g3C}HmE*MZ+N6XD$`3+S>SIKlLqke#!QE+wo<1~XgJ4=(6kKc$0{TPrLa;Yh^ND)3 z0u&f9ErANsX{1vCRW)tDwJ`i=8N_0CK-*dv)dK>DK7fusmkAHFkark)SHTxLk(%BKJXbF=2%Xn z0ykp|0w4OzG~4sTZiV~~jS|nJ=J!cSGJwXyhx$2xi8|BPFwFxGQDwJ$k6R=*hee`G zRMC_8TO{-U;p5ZwKa#C%n|CXLJmVcb17sHObu}B zVErbxzBO=U_3wOO5}Obiq%NF_E+})Vm*kpZ9a{nPj)z!jSPH#b0rafc80Jq=7nbj0 z)-HdSMH2N*Tpo`~3)a&q|DYwOz4DTzOmXr1*BRhyWh3(&W~Rg+rdm9iO-Ee{_>T{M zh$BH@_756Mg20-t!ur6H=;QCHF?3FMP$`(lJIKB{7*R3LLWuhkDdOU^i@?+`GsM+| z@Q-6E>n$r|D*k;>K_YLn36ws;K{}A>d8YoW6{*{^vH zXwOpjnq{B`R-%+(Ue?gkgw$U#LAa9l{GFDm#^xkZOVjC3f2~`u#gXx0IuIm1hKj@4 zNHjY+8$q+44OE>csmKZY5hoINPYZD_fLxyD1+FDRgY;)(^MclboC%Rw%oY&0?fOd; zot>a{uOXgG>!Pj*-wIxaNFGo5^QY9)X3(-q@YJXSx|2t){%h{0bSDI<|1One7RM0C zz$t$|Arai8L8AK41zCOC%+NTj|B=cjCMm&sSy|{K#T98i^)2G+Zyw;FXVF}c>zhB6 z;+Jx2)bC^lhdf3ei+R*T#jg$8n(q+YrnHtnh*=n9>chgesAiF+@G=Uy7Dy8$Qngg(Gf* zHq($WK}f&row!r6y|bE%Y*i~@$ZY)y8uyM`?iT>$4 zG1hJ3r25o5>@tAE8-*o!Z}2(PgPfkA9_; z=gO6jNDhq^sJ~4A);LI>vXT4c7r}&c*j__(%vD2k%GIayoB#-@YQ#dzEaqpd&dkRW zopgu^Cs!k`Y}Xqx4=6G+x&4gZC;&(kQKzJezs5*CXl33`Nk2;d1=Dh238+rX!vl#I zq@+;H!+N?UB4X7|m#v^l@Y@KB3LFVkwr&rwTz`Pib4+Q(4Mr zzzN#8b987lvqXCKt%$tPN7#Z%WoCptUrf&zv#WhNdQO*M$(s?f0}`rlrUOun<_G~8 zK0u`@^y%nYd!h6w*q?Eb#6rE-vE?%N_-J>X)or$wEjzY+L0Nj!vE@{}m7+J2`FVI^ zIhi>M^g*_(L|57DCGUs4*Jvw620UPjH_`^)v+@uWqIBzI1Es0)1IP6=($dGF;_J%h zP;p}r`BtY8it=G*B;f3%HB|P6v3-a?>0ZH@Vw7@!(gIWo44HWikrtHa6i66utXggc z^oKZf$vtnpgn*C)E>dFLU8$e$t7g_4ue4-}{@`xqS|`=w7Dn3#3bNMHvK#!f+5W^Yk2Wz0uQL4qy?(2rZgrK4qF<~tv7z&~+bwPD_o0Q` zhq*T*4BTkS;!Q$Rt?hxPZ%tYFKvf9B?-sM7{u}M9lD^6`3C)G#xWUr54tC_)qKsm^Au% z>t(HJlrR<>Z(z>DmMaUX^&EKFExC-5b0ODMJMt@D0^Cy zgfLKnY#IWdP=ZrV6H2fX-?hfOcsoBtk5wat+A>0@EyJ(3dg2$eE4AB_iC)aEVoRUa zT*0UFb4&6xQCNA36tlJ>x7--qk|j3H&gQs~g%oB56%|?mA)`C#LDc2?Io4GQ*iU)Z z>l!gklA$#Lg5XIzamL#ttr9sFZwF(Tj@vO@NG{*h@C`WEVs>ZEaRl^nOYs((lYwBd zBg$H!6>hwutNFWA45KYJQPixrr&Spv2r_Q?KodlXgFGl$*1zL?q#q<+F^Ji}VTWgTWpGQq>gul68fw$eZO3 zA7DX%`SG@6M{;=H0H07!?v%E9JJLdyZNo^AH`<3|lov(GZXM@N%h*PyD2YPMcoMB> zf^O8mh2m$80w#kMuVdw(Q;WtGqVuK3mgQ^;~{7FhMlt#gk z(W6#a+ff7Q3lbQ+Km9fu)xObYSB*#Wk`JNO7L@F(T@gw<0!k=f4X_0;*A4Lv8;Qo) zTi)6Ua;Jf9C%VBJnI28TRc_LF+CUb&03gEGq`DxSEPDPq6 z8hj!cV4NNsZ3jgWs(*`H(h|HHAf?SGiG^(+bQQ?m(#}wntN`l90OC5-JP$}rdj{SJ zF{SDDiGWf78IjnJkO*SGKH6=z2*imzD;0BpM#Ugw&)qB_gZGplWq4XQ7rJ1qnX3V2 zkvoa`&uMkktD=@hn!0`SpvBJ5nthNJKQNLjOmq(1aW%lXB)A^+5(cwUrO4VenzUOq zp|;gnjaxGnzzbC$lITJYdch$Cx~yX&5&*>mR_J+yIOY+;US=I$+D*Fh5E=>WT0}5~ zeuerd*nB=hWNXri`@(Iv1^^`_h+Ejq1tP4XD@aUDounyKw~ir=wGziGp~y%gfRLfp zkI4)l<>9>XEf}>y3WgE@-tb-sfj^`22&!vJmAhAD6y%xMmQ1TSVND1`Qw$WsGz*t(g=7!Y+_n zp$NobTvGqg4J2XEFX8TLC^Y^vZH_w+wKiqileRvlqKyfcWhz>TB5|29aWhiX&ZGH@ zuUhkmDKHRzOZWdnojVM}k%-FSl!a8<2S*}G&Uc=p%261Na>PM(ngC{!jkdsXGLp2;{d6>p?%qQY$S}c_=Sd~(^7#spe{!m1bn9a)ZVzwq^hB7}gOCS}=%2wp3 zR5ig(IZO8Dpjx#4Tkf!KhvB%X&mU7bRV9`o^bs#a57{K+Gr)kg>mh6BbovI)=vyMNbrW9&E@9#S1G}pZj|8dRZ8%t8zp$-W;Sj}@W#zt+>qeyRZ8%t zVoLDFo48SeH{NK$jW>9vc@sBE@Wz|9@rImkyonnnc;n5wctcJ%-o%X(yzyp3ydkF> zZ{kJ?-gv`FDr$$EZoG*bC3xe_#qov&Z`>@z4GG@3xioG_@WxFQHqdno2%o71aI6Z1tQ;&;EkJW z;)Voo-0X-O61;J9ZQPLHjhpM^=6c`U5H}=vyxL&SAaa>=jTyb1ClNC<) zFX5_t;iGt)s&_qC)k_AkdsXiR$}q=ut&+@fWjoK^D$mxLeR@vDI8s=&m6T_np0g)r zpX%Yb(S53iL>>E752y0%RXH-5pkK5R6QrQy%BeW_>pScna#cCh@IhU1hW@m!`1qG} zMWIe}jNfWv^4L*IlSccS;O(%42TWXvf;0q!H~{1!{#1BIl(PAM%&Bkq!2f znEj428qd-UZzj{j%EN+%t~7U+UiyP~K#Kt|PHqU})S?T&N-w_X{D$7sd8k>9Q&qmd zX`~?%7)4nzq9~EWKw0!->Oyj7*bL}J{0czw1^zFF%Y#9iJKV_%qLqHKMXjNNL~=*# zib*VL?89(cRD%zSGLy`-sn-b3 zYgh7u1C@S@mgJK!(t0_4169(3Os?~((zL_`)TK>Qxcq>R%2Y*;dgNm-I}+dc!3$NQ6cnxb zuyn+|k}f*|_^2P5=39%}XHlvsvPFlDGPpfs%_W@JQhpE>j51sKV!QgzzF7~M=@30J z3!D^sdxNUfOxsa2CDsi%cnl69I1mTl}{A>A1LQmxal1~!y`{7AKO*?UP5Ik_==%A)z zOK{nXo@9;=B0YrWLxCi0+PN2#c6f9U2vIi8$=p}Nqwh60@G^%_LmLHW2R%UgjxH5- z0T5ndf{^V7meV}W4gw2grHhh!ve@~hlY>AC)c$cK)=Gg+3L?hA`D_RR5wC5@89Wfc zakLoBypTJ!4^k0DqPF<}SQm!6nC+CIjx$71m$J+SlO8Gp2CmhxXYYFXD+%_j&Kk;d zfQKqY0@7v6rxWa(_joJ3b-X4;KK)1wG~T_R8piokO{wRuW7NjReiqPli%f&p{Ne;B zazgJ%Bn+p4UXHhniL7Zzldl128boPQw>1r%d@(x0X-eL4$sJjm*>D<^$qI~N8gTpZ zOVc2@+7e;W%ZZm!N}Nbgk_h3t5fMTSm^G}kujW-f?WH_z@$Y>( zH>(mhj{XbRj4OgCC6~|eXsG|coZ!@ZsfGp`5}0Jr7XV7w;g}OWv7WFBi1>BRzLZ|? z)TPCS$b|dUa2bttnSsR8ljM?F%ajo2X=q6Y@YyV(25>e@E*v<_nW{}t4G-ksxJl@1 z$0n&^VLE?IP4EU(2nBT!`+(^9OJ%Cjvn$gx@Q4O>1`n6dk;z;Kuj-VrSI_}0OI&N#%TO+fiL+FFSWrO| zTFjPm+)bVOi?l_g71T@_``F5ld02kW_JQ4tna;lG)X!4Vq0;^s7t%b;gN?}fa^MIx z`Z+L!A&F3>;WEI8*!Awu2)mLk z6I2lGl)R+!cR!->!qtbh9AX3@M7?x|7(d18(dC_Ev$>ugjs;>e|JlIDZTiJ1I>;&_ z9O?(DjthQuA1`FpLy>yIlUQ?9-+zZC?t*9I{|Q3ZCw2D5!3O=>AOY+-e63Cl^zQ%H z^=DeB2=$|IYi4Cz{%68iUB+sJapxf;}ReOy(wL>cV8eR=@exRyU`NS#JTPE6|Ikn+! zijXsP(!Z#4%c#z+QD=MQIvJG*?DdX2HSG)ur27%T4rr=b4tBVmd?9UTBwpdqkH+ev`=C+%8xRCDmABN0#>*68(mb0FAYs+ znlm8@IXQ}2%lJHoiN{oN11v>}k5Z01%~Gqnh|mo{xKT^=LOhj0G@k-oI^_JMt~7GQ zX={awI_8|ZxADr*Ay18r?#&FGs^J?g}()6}^Tb%IK= z9hcY1h&MPfmrQ=%jW zOI=^z4Xi#+&9EOf1;3D5(P_N`pN+SgOxKKfhK&tzg3wrLgg@!IPUG{RQY zMMY|wKXQ6Ifn`sTU)-_Kv`gLs?H^=pw=NfJ(b2@>*kAen6*~&o<>Qq)D&nG}Vxx5Q zHYi*cP^RKba6eX9VP2S!1}S*lF(r#V!@r#qtP5Xb5e{J>V-l@lk3^x_wT)$}WJLgY zLpD|uBwlYDldcbWFV|Xf=9M#6DCve&@{meLE3tIlRFyiG&C&K__vNn)CG%le6 z9Sb-5ilbJ$^He40#K8FpnDTki#yoBmMXIB=SPs!+t^E`=Oh~*f)x?@FL@5?$I1{h^ z3-1IHVs_VP3-UEYmxt2gh_?^&T%4*|VjIKYLRGI3cx4Wdwj$IY#J)nQU(8-tep2_xIXn}yXfgZ#xc_AN5l{ZIE+@vfl3Dac{92;KhZk{9D;@89bmUpO8LT)PKob z`v&Ziq6Zg6_bb1I%&xNXYmEI{tvkxEqA<%f1V^-2IxGK9*yyE$AIB-9m4_uvdX_jC zJV7XI$CN>Zc90UFyhh{zNb%|26h>G}UY(XibP9TBUL~UO%i-CQeg(A4hmlWP3cmK2 z11LcA(2`@QC7v3D9$&CzB2$H^oXC3s&c20GB-m`#&%=J%)aPnmBi}Rq5K+5RQtu^a zJh%{2UT|=rS05m^!fPX6gZuUx6{5deir0#BLu-n(a_cv#G-SeG1>-Ea|JZOUd^@hH zR53k}%j_`B78QAS`Pa78p8wr$eF<>?kb^1t;!eruDA<&%G=H$0*uo*6=5D)_j<$SH zx9COS!O5YQ1$7AhjMPo{O%X@0WLJtF*I<+-3mn3$8wjtIs(>S_y%c~?jhWDB3oqv= zfebmW;Lof*%2T701RXU*l&C{Y+Uaz=wWuZdF$ptKb)87sG^{n|S1P+2zlR!GRJa=R zkmKnPKy(FPgM(;zjb0D_=@n#v2_uV$=&WIfHuFgO9!EFqpQ>N((M2jQG-Ucp8*-?s z3nHX8o@B{5wX05QBM3&QW1qE5Yjaz}vPqPNgK$tSnAI9ioP{dux|O=g*)Q~5L3ZlO zV2%<0xUKpFXgbmf_T_{5UB&mqlV&jlp3EelfE-#N(U8^rb*=4)iURw!{GwUVW)yT< zDp)B3IHWdtP{w=~3yRFMRbf`$PMXrn)G3B=G(SU1O9vz*c}9E%UIC@a6d z7Lf9>P8GDt&XlALEUI7g5-kHP40)7%TuJt?GDYn@GW0Tj&F#?`^lROoT{v^Z2W-@Y z1}WYJNPtm=Ug=5SEZ1v7GN1r+VU9l{9^>I&t@D)K{MAA~JX4}g z(W%}$TX{DHnB_1#Ua})W7aXBFE29`L0I|dg5}_xj{Q)h>-FdA?SaFx)uYskdg?9OA zh;g(m!)xvqdVK~iRlQ4HR!ORCxGVr(mCxiVNAR%K8__ZB!POY#UZ;SQTTegJ8e0|U zD3ui4rJ(j^amLE6mS-}MNtsvmWm*?_7LHz@Dg@HND-`1v)Wo7>rw~Vu(LuPtlBs;5 zHLhl`UgPormalO&!@@V__ro==9L~>rb+7mRKwTWRp8**$aC+*^#<<{hO~{{tkKFOr zJC#KOS^0D5;Q}hZEq|^5a?~7i1MH?_WsLYU-67cc-<^^=;4~1wjQl4cp4N|eAjC~Y zAcWr4w8S?9{TY2|WHKgAmv7MLaC|uL8+&t6oZpYMdmBA-S^W_Lj@fIEyw&;xx!yFT z%u|Y`Rj2P#mqd8Hp$Fq7K73(+r4qZq{0I*qF0ue(!N^!~-mOaN{J)l$2_2gUrTi8W zOb?^GgrNV&mlq3fEp(3E>7v;Lqf^RN0qJeCdj7T&~5So_B76vQp{3)fLmALWXHQ$NCW1J}b`OEH1g zP5|$L@pnhFSKtu9H>cN8e92~C%-+va#f}$Bb|CJ(V=$jKo;?A{rVpNE*B|{BdS11G()Sa*97qiVG^%1_N1Rt#~Gi-1FvAzAv>UT%wRSTOD9~nlq9>B5U2OhaK0sfl@ z?M>`?HJ&K5SiHZPT=YBZyuUg|*6^8Te}xi`GzC`&&O9ZQtVs_DmlIiv3Mi!<2Ejvk z`cyOjocqFaaT6$70NLlMxTh%JYefe*c}@=D|Ek~So*S4^piJjMdhZv1&y4nWCFdW#9Pi(A^iN8vC{}q9miT=oCUgH%zTQh?q~9eLO>i*$ z`YFl92p&YF{j~b0P_Uo!4<_X&^`nPM^i@)TP3>OAmeWqA;Z~Pj*E&XP)kRLf>2(v* zZzh7Sh5dJNJAst#sO}b7q4iPvbT>`WBjte}n^yJF7&_VpLV*A`HtmvHW)=(f~b@9X#ESl&1ej>*nX;dO);Z- zwPZJjRr63Xg-l4JLaa)aG=D6zvZGJZgDYD{nYcq~5})F?y?Y2@9AjRll9$hi;<@AH zk1J|iwZj3tRfkniGt{YZUP|P?8H?D?B=cme8B7wc@`B7$CP|W3EzQ(2E(D`IOy{L3 zKg1PbTB87s_34Jl-B?r`wdB-K$p@_XH(=t3g0y>M5+NkArCN+lI0x}*@+=kY zs2E8_fxIQD*zAR4yom!#CeBckZ;2`xN-FXFG2x`80g|Z*Hrn9AnTRaqX;fHeo?aH@ zPZpDOugKhqIXurt6O1iw+)R?=crjvbo=am(>V~la=Ef33{{orj%&1@>!dIllvd-c1 zT_<7<*aC`17`YxQPClQcBv3JVR{FDY%ARMq&EO)3O!NxI8q6;#@5Ga8wa^O-E`iB7 zhtoH8CjG1w-YxE{5oq5Gn_#p`wlc&d^O1s<$&XITyhjQh(?V8}OCKh_no^-Oi-;4$ za-4PQNs=i|S7WH2N&2S%>GSoVp^_d8duh>WMT8Jh^%}&HMb+wGEQKLmf_jew;RJ^X zd>I06NR%U*OI!Ck+Bl6guvDN2mI|Pxq{1*pgk_Y&y2nC&NLTy{hw@4G5$<)nje zhAs3@qyxN8wDbv=~T&7LWvI+1+tpru37YeyMsy;HjCR$(7r@7NnPmaGC2?rlB_Wp`k0cCeO=w4ggl(oV z6#=>FS+J?uRfZJx`G16_1gOu|lq?(3l#UAy)5|p_%NWpz%;qP@H6;ngF{!ViCu=%s zO3U=52~pYTMhJ_Uv4JQ3!{g?#w>07x(v1j(vu<=EbfcM=CeDu`hy4n=(P}+krW=9l z-nI=Eg2DPj#|iW8n+cY{~Hfc7>mhN9_3d+RoCjL_b?_0!6Gs>6erm*6=Vnu+y}G^=F_ki1ZA zRZaTHBi2V08=|NHo{H zQBX_MF@7udQVL#rKN>)OF{7DO|CIfuLA4=8pu`~9Cn%y(VxW{2DWancAy#9?@_+Td z4NX9?4NdeUczU7<35(@40WF6L5Q1?6KuDyqBYR0Q99-{TtpuD(O2Ema1YpRGD*;#) zp#&iRr39ceNeO_l*(rb$;9UaCm4H*rlz_)3lmG|jgi1W21UO*z+_4HW23S%8=tw95 z&n6|{6sm)^MuE{M&rt(NACmkC<7xnPBsHKRNvP9{IiUu;&xsJIkd}sN+ahUOQUj!x zE5%~iaE%(!IQK#gXk2I(`)4Hn8TDyhL8#~QSPhg9Sf>4Bkz;K>Cellq0iy{gF()#} zYjYO>xwF4U)S%nIC$?hdM1&!Cv?oVeZ}`{XO{*1cLw_4J*j;Rc$?AV`t#VD^_*%bm$wDO|f>^o1y#Lke^qQfhi7J0Ch+M1r}}4O0OM>$zh25 z-ZAOCTOaRO3mrNW7m_&Q921Kp)RmKhCFfrr*OfVaMprQODP1q&v;kc?nB)mvf#&17 zl2KPA3sf2p9m{x(g5zg-*|<3cEfvRWoy-5Z0cU8nUarwiP+&y;q4_(k=;n2$l6lYdEZaLuV{%ByC&zXqT|0wvK5LozY9^ETBMTsVbqj zwCtPcz!+Ur42$7!Rx>-JnVYmBkJ}shA~xj3yaY$tBEYkZHv}#|*&AiFR@kgEjqTHkSNT{u~_8h_^S@4=3CXu5T zJliY<-E)Q_ediEVZ;3=NJo`VI^)8p!+WX_wr9^R7Q0^Se#H@uq(##-!JCV~mmD~=_ zwzR>M-&?`h*x;FvP5vP5(wGL-BVs~@;;_2N)XDIgRGsiVthav_*5|rW$>D?T6XI!UbhFWRjqcBXX4}#A>_#t$5HZ162qz2UCsU! ztJAQQR^3v7lh(ZZnQ#WBS36bsscrO35pGD<9O!hX{&niW7p^aj&<^B~wX*;>k+-y4 zKl34p4EEH$1UDng`K<8^%zHpa;xP6Q#;D-O)E*@vS7N?g(x{*ZLJV3Z(34)d%k}Ap znrb~s6W^~;eAEj$tqR8-RBZ*<-HPcKjxh-DY}!9peu}#5e`f8!)afyRGuDDeT&nkt>Aa&w{GEi?ZBBh?Chgl@>-B~N zlV}nB!^}kXeLiq!J=@jWo$l21%xrINe%0!7&DsmrtrvV#1T|bz+#k>BwR3kIy^5qJ{QUV65km*3%;xVN2 z%MhvoWw|A)E|WgmgUtdGkg+O_nPP`G4Q`aNpan#^LMFB-#hI58;2oL zIgxxI&TiGk|JRkjYLN$%F2F77XI4F)(3s!-c}}FE>WI=saa0+$6GlX?G^XeYbvi=& z?-_228H8{1TG3zi-&4yrVpVB^z)~zFSH3OpHlea^am4V(DnQVyV@}W0Ek0J{ zE2Q8bjil`TTG6CGi@X_(7}8Auu8%VKS9tJ&J=*-j3Z+26uMJLhV(W0aeTf zDgrxT8?>gzcqd%(WnlzhXu|+jcfb{YE7%POK|_B`L(?=w5lxrT>9@bsOryr(Ir-TC zR$P`M>xqDDkHIFG-~=nQU}`G^vZyI5|D5G3Z&@?D;ODfq4e8mVEH*n}4kG02Qk*jE z=qi@D%9due#?Q}6PAw5X!E$a}auGjH+fiV|jXbB91RnAiUk?o8Kr)T{7Zy9!#!(Wf z#MH>+0%%#>#9$d_$!=?#n5qmA}6wv>*IO9()C!* z?%cK_D@9*o+3XDd6DI6JVT)m)L|2Dte_Q^MyeiAj^LSg1SIj(yDo!ib674Svm*vx* z3O!W=&*hB>le5{c&iJfN%uCP8v$~=WoaEX}hOP`5iTWldqLF4F)q_>050)y+-v?Cn z!X7abM6W%o>cvrme-y<=n^#iXbuE^J)>RkFCqoD(aUp01?8sH>>uEMf?%dO#ju~bp zlI24Z^$N4Py0i&dU@VUo{T?5&m3qGdC9%~CHng!CtqmN~GwgH1s)AdoqinX|!d6b0 z29~@Agwdt$Vu|qy`xLr()Mh5PvjK%0vZD&2?lJ5R-xh2Qy@v{l8lb8jDjmQn_vfmt z>Pd}&u~Vl6&Zs!VDS!aXCq)7P`3YUAW{<-P)AV}T&PZSWER(w}aPdJ$zF> z96*$>&=0Uzk?H}HrxF)1jPOwiMxuTGY6*87FaUu4GaC4-QEmC0-$w(SN2TGw36G8l z13ZGtXH|%Wd0f#*a90Fbbfofq?lIrC%?*DSu zQWFv(_9G6;%1K@y{xhW+fF)|@aLhWdYZi8hE9Cw(=bmi<-y%Gu`^7=N5)_-L1@&fI-p@!N)j-ABr_i9T(?2=V97Y+L}*0wF8}) zzS2jM8NbReg_dY^E|_kQRgo{VF`P@%oR>h zfyF+7)zM$bQetWN@FX{eLs{STg>L=b+7LG`{I=zP6Nr)8`leH~G(x7Y{+U-c!lo}y z9b`N$TAW61nVIn`()|&g0}W4G8nt$sY!ZVv3LF!lV3}vtF;Ih*CykRBq0nNYatlur zOZ_z4{$t)myi!W|i-JO{V!S-N5`=d6Z(EF(keDyaFpz1$NI7%Ek^KC;&rZ{}L&9OY z8k^XJaI3tFdSaiT){gkOKNVhbjq1a;{B39gx-O|sj0w2;4D5x?iFiH(%u_O-s1l`N zM$@}3Kb`kXd3p*`yU~QI{Dx!l_tAzF@iK7>#Nq$mH0KYF79quu-sZ)}a>TzYl2mV0 z4$pa`|AFT)>5roZ{o0qh98KM+2jyo`XT+r~9)$bzi0@V9Z?!INUUyWw{>(~W%vZDU z_KC7vT%$IBAb6z2GbPq{xJk(@n+7HFjW{%0KEYmkYZv;18l>cr{8?B2As7M|kq**P zFKYB|?L-gd2$mvMDX9buu)fkA5Z?|>4Y(PcNOhLdMP8D`^ex3XKumy3a^N@F{7ikp zG5A_8EKlG70J}uP4IZaP#S0p51+-Q50rLYeb|_9_(wbQnUt`zjDz##qmv$~;WL#$g z5Lk1Fvfp%S^>dLxeUY}m!e{20(NjKGpC`tok%&7qgMr)B#~}N!<)(Wd!6OdDK3|%B zixdDhhz5b^TWKBQ7U(9zarkLJhStC}wrd;Zq;%-z8Cazi4+bdAq6Wa^L0Wy5cC3^n zoQa1B1%OLem+o|aovr1J@6^U@!;(y0{ z0hYDvQz{^*2(;%o!EN~)^|r%yQ<3U;V-(aj)G7a^z7u(X#FE{kdxp_@9+(g58CLY> z-ZTIaSnq;~DgH)2#-!_k$66h+zXrosXHYa+nr7d?&C7QK2($KY=9T8-gxdmrrBc(6 zk;!3pt4CpSh~Y~MmTVG=99Nz2LE{xce zDqXM(qLkWb%m#kKYPx(FhQhQLbubjjwIv4xFPwdh1Hoh{NP*=QZiGF6PfjM-lr(&} zBlZI7G^2tZphhf_WvMR+VU)TrE&5i)$96S8(gT%~z^{@XC`-Ub4Z#ym+2R3P&x54k ztnO>1Aq!q?LUzNw@;7YK1pL=C;hz9-R{wnk@*4uk{f*__L`$WC%j{9MsNxl}4?6V; zvKeRtFdf$LX058qCLleewxe@tldc4ln$kBf+NxJ1j9QpnJl5rQ@*j-K$?h~yyQQB#S+^z_Hd8$CrB}6l&Yic%- zehhM7Pd^~{A(VwP3<9%(S{XxF5FXj1MR_D2;26Wd;lZN-@d6ijC2!fs)f!*yP&lj& z3U0O!C=F5z_)0eh8@1`0D>IH*yDRFrue#A(pnPcj;HAc|SRvsTkiQ4yjL8#2zg0%s zE3op!doX6?HkzT4ioKSef7yRPD;nGo$;JY)@@43Q{2^zl<@SQcNT{WIxrWw+KJ(E( zqt8X|!Qc`j5aUjrUL^$TR9xDU?Or}!&E%&!<)_B9TBaWfWcUc=S+j@DOa&Xaggi`y za${Udk5}vGYc;>ZtHFRDmoias-^L?dnADt=FJ#)bRwsT`Fg>>TG&J$vr$8uT9M?O_zIM& z2G)$0%ehSsPiilCjzNbY`8&(E=Y8GT# zBMXmOYf(G;kLf)Xn#iOUdju`^WR73QVo@RmKy<65a&G~L%RjQ;;YK=7o!00FOxt%o`<;(G8VA6tJY&rPp~qq1aj8PS*#|boT$)>`rs<|H5NlJDga#BY<4%fZDw2->Rv1On{zx`) z$6y*Wuh5yhh6U*hGr{B}ru-)Ck3zW@j1JGDOj|t57X{tmIbQ83dL%PwfFek(g}h>2 z3MA2EFGsW{-p4;jJ+{LhV`^=?{;^IDNL~xRH_<0Al6YWOEwzMqKsfo6x=|?;tF=l| zKNw@|SJpK7hm`n}oy*c9L=02L+yQ>LN&gUC*Q#w)(5Dsml8q@+rEg71v<1mOVZ{nx zpmTW{`hzLKzi8kO?`zlaciB8iCAe;xA~W64$FyWTxrzx&-J ztwNt0zm+BlpKCgot7G*8>Kx!w`ZJKz1f`R6$VhthNDRQ1g=H5Cf|($e1xeof zvm#6w4p3~}Q7cMS6DQTRjTIbp3NmgE+^nn;NJ|4DC5WW7h+w5cHw4=N2oj_9dNnMv zQ~q~#2-4%{pX%qY^X9YeTVDV9-*o>U*4{tbva71|-Fu&N&;4=kt=d(oPz63YKK{Ud^y}Coj!~VIPZH8^q{oP+f`tT#Qv8xAfi5w0qJ$^U zhZvP1r45?b9e%aQ3mV%fsA-!YQKTC*$oqW1bFF>$x%bqM^n0n1d-mC9@3rQdYp(fg z%{f=|E^R~LlkFkKmDGdUx~o42_B@R7>9~uTSNWa zXStH9tMgb-CB5=)z2Vh9hT}b5g1gYA7VD3RzC_C`N|{_`^MC*BeDmnfGQV8Yck_n{ zVm_OCGOltmuK&s}Z~x60%RIibB7o;s^IqRJx}G7}NX&Z#J}tyfikP)QPe3c1g;^5j zJKiu`Rj4$bVfbfFq8=Wn;4>zDq*!=8@4^8{G|3aR(2(n8q9~sjWtb^3b=F_iUXnytWc-HIZ6jiYfb$vY3`GY$uwmDRoYDhf-}}hf{@kqW|Hs(Yii^#N>-|8nIU`W zd5DP7ZS|w}BSjV#z^fs@$`J2i>TFjcUc?fjqo zQ;PYJT0k$mLg3X|A9zqm19=benT^CuX(EaSZ!S{iF;g0zY*89~kb#t9H{^%hVg-+q z={)iiA7)lXTzYGo6}{!Y`Q}gW(}EAuqi%JOo*+mWEOrRN!)KG89)s{MN`^B_%OjF+ zLF+?gXG`xQGf^B{&!GKtv^_=Aw9R~!8ff#A8tfu$~EGz~u}FAMyvKcaWLsVou`JD3o&%pSrnl-7HAd`0Voh9MHlT4vOsSd2 zjwJSA5ylxByC*>%XV&)wmcO=5fj~Q-d=ylZ<^vsa4rOIvb6^?OA7xA45VX08H)J`y zA<|)0>qjqRfk^%9_(qa9UeIu>&F~ zFk`T6Ua%XfCZ9aHh$yavKY(krQHFD7^DggiK&2H5cX4euA3w39~7kD8aPx9iLb~hDEL|dXymJ+Ay%x4>B0C|PWLjIjbvyxpJi+hnZmNp)gEQvPWoy-|eZR zvYRYjvQ*?oh2QE}D3+E|3Fcs75CV6G1gT&X5)@YpAu?O6;y;1w8T;!1L4wbJVDx+i zcg2RXSKDRf4f09<%QgZ9s*4B|F}0XX7MOM>5?Y`;7$V+csB;*po>MdzPp%>+K~rL7Gl+Bvp1P)XUjh`vOAn2LQ)_lX;q zmA9IwclNzn*FIqZabaz07a!--`QiNNGhp)XV(>4X8AIOC8Y{}QCLQZu^!-8o!(#-UOmP>+;P$)Xwr?)wo3FoLk$> zy^#m2UJxKrMd)TdYu=SOkL0Gbbt>;D(2AcBn23!Q4-y?Vsy_Ip zWK>n*?{T;M>aleFJXEE}g2akM@2#3&`0Swh#AnfgnQhRrk;XP!xRv2m`ls3xLD3qV>47!D*Mo` zK3E`3J z-q`$Xu)dg$QlK;#pkS(v_|$-2Zi`L&*@Ee)X>FuLbvBVS-+{n{CY$8Ej>2ioL05U* znuYVzlb{xDAJkgYH#vO>p``_xdBVdHz$faqsdTP>ycIv8UxDmNRX=Amg)qL!>eYUq zjMq8rPw;Q&LrlWcAcBctUU&x`Qr$?&35^6fISB4d~rcHU*`ZO#vnRmAarZq>wkICqToIcBo|8 z&U4b%w>~J^rln7?Y95Wm_j6?l1CsVR#G{nbgZMut2^futI0-isgr$#(Btpe#{NVojqh3AmTI34qxR{f?Px8xEx9&nng06Y?em>i&ye~O2 zPzF}w!K35ly@aw~+!Ui7dogNvbFouL8|}D;efgc&`1B|$2KR4W-J9rOE`3s9PZTiy zZ-fh=!U2F1p8}&tIt7lkUoclLGI~@M;ac#edW|s{?`<-c@%HAOOf)tm^-x^;6pM0u^8oj@ zagT7Cy}D6f5|izzMspmSz0pYfHj!2sI_W(u03|!BpHzXD%VnNkx_R4NvADQc@+;;v zCzQYs@DHsQW%DQbvTFV`U&8qFWxx5ed^y+ralRZhU&@#BlNJ`5FXp?8ymoYDNr1b< zukZt25q0?Wd|9~>-|aP@%Xgzg-}R9N@!G(B`|kYY*@foI`L#uZ><5;bujac$L*%>5 zlipUEuK?VFk~={VO08x*O;6y8c`X&km2rjUpK;~Oz?ClpSH29;eHpm&W#Gz}fh%7I zu6!A|TGVoDafPJLxbkJ-%9nvFUq*+%3|#p#aOKOul`jKVz6@OXGH~U~z?ClpSH28f z%`3JK{pf`iTu4^UzF-u(Ede5K|LMmD^;d|gX3o>X)Ka)hiy|$B^`d`8u`lW#Vtm3i z7UTD$+O5Y)<%cPsEN!oHlxnw#2weRD>_kzj(q59FmO?rViJ;h^*tZT#>vSoWd4HJ- zBWn{$ER^;CYC$D|?dsz7!-{m-CjEooPvNIjEhgHx4QW7p$asXpkUFa@9*%@KP>oi> zN5kpB6gy@QJK(2T?K}{J?A=6oUjfp1?e?gz3#Ga^GJQ^?^o@dQC}Vk#^g=RpaHD*N zt>9oRC^Nw;Y)=Ibb1JZT_OEype}|;w93^hnna#AATZ>iB#2G zGG`ziunK9F%?miXqXRav_4?**y{h1V0P%udD*d84j{ee)u8ad0NAZKyx z!7s$^1?|PvTvH!eoEOXbHfuI>XnW1?|DydN`ux7V4QCM6@7~?pFySBOtVrZf{ojHW z;le%4%S`);+ewM63P9rC5wNJ1*7{I=O<*)HPdVKrbpU`|Mo=E@8H4V9`4PSjPYmX%K;@GcCcUIpRB)6e&Vg9(xNo`0ju3p57<`8Mc`W~vL+l`8Y40_RlyH6 zq_Si688b@6j2W1!w!6C6a#|oO(I~FQ`T9-b@WlFw;dxa>8aTT|J9vuUIL>$VVu_D5z>6s9}KNkd9H0mge)8_Uz4#G6uDx8 z1IwdlE0^SAXpY$!Jx5=5`%9kc(LncFdSuD0)@cziOJ%p(RjEQ{GF^@cF=sjz$fzhL z2?GJDR1@^KpAW(}k?uN$cpo1~pBwmKqf4ivVlls*4|Y#p#s|A#*Ym-i$aQ>-#FEWm zHuyj$a+rz48CrvjJ1rsKEAjn21mI`OlxHN>zHa4apnEPsRx6EVfXz>h+H$nj-Ucr& zCnF-s{%3O8#AV1`H4~|tPr5+S^WdIglNKO`OU|faPsm?5pQefW4Q+V=R*G5EB$RgZg?<4X_ z0w5;h?_LeV79u`MM{+s$nt}24O^-{%|lTiC`aIxg<%1%J~7+OO(VF7WsaO zBst>|B<^`xo#Ndy2W@Es)e_nH)nZvO*=GdW0)fiN= zfp;kQLRNmLA7|_Rw|;-R_nH1K+A?}squP)YS^GkOy3)%S%2iNV^LGj?8+oyT)qef- zfFDL=U-`5tC}LE|+^lY+HszZa8f+oR5!7bv@HPL;b*!I%{7X@a|t5SyBR6z+Yhf=}S`hR5IhCXG3Y%<9MRZ41?6 zI3n9pSuAaQi9QWIaa(4X(KD@D-pb*Yqh&u)DY5B}vc@)9f3Koy3mk0XCNjbD+rbbH zg|A&y8^KOOuW$nw!1u;GY%uRt%pDTz@OY6uIWDMF7eD!kQr$UXe`1_!XCAx^V60$~ zaBsa>ZWeBF9qt2F?SnvJD|D@hD*@iZJfEPL??55=Hv^KvND!(QDS6hAEISiSBbXfu zr+6MZw0IsI5b>ao!;%uF|0BNI!%D#V(Rqcx{AKY(jxvNNGPwP6GO^NDVWBKR{ZV}m zm4x8Z!aQkb@N|uuxQ^#|U>FsX8PR<7_VNOna|vfc-7K14edm1hxQR#1dOTnx9M!?i zKhaG*6lv(b&%Ndgai6F6X(y7Wxzl{rceTDbDVFcwh^Go&G_QSEhd3({r^b0m#JK?d zD-r0SHxt)DP3!%c@T#0UlGnm{VdQtS_-@g$Dndr9rV50NXkEKtl z64q&1fu<6D8E7V2Du#wDmN#id z6D`o0L9hXF$Be>93>s9EK*NC93|hj3nwUn0iRSr43oN-2Xii;r;X&n@J*xKALi5$U zUUP=1vV|?iHkungr|>qg{;>E>e@+6YlGVRxHG%$qgqRM=LP>}uV)y}BDZTo)2>Th4 z6}Atex=RaN61+g@pJ6;vjz>RN(JLLkxGZ?o;{UoCU5oSqrIw|lNpFyyn(EGtcG}JL#MrN(y)-AqPFB6JM8= z>4eI|+$B{Zv5AQ+Zak_Axw=X@O`KkF+N(1yi34-h<0w9bMH?btD@co-oMeWq9;F3@c|OQg+~tHe z5i}TGU75RLxCg4k0)lsNafroLa7T23eW}hTIXBW&5y;jBcz&OsX9^IuGBPL(s;d=x zS-hgy!w3<^D|*NZ!?GWz0aOKTHAz)aG#uM(D!ah&&Hn?R@(Kh343StO3~M-sPJoD_ z(VBb#N2E~|qWJ4eL#T~8a+|4CNEB03ras03( z-Zq`Pxzv!mxr81~?j{vYXlzwlHPMQeimdwDOn6hkB4Vu~w~q0=LQ}a&yGV|kr7^}& z4h$3Jz+?oR{V!LuDwY^3bg;o4R?L+FY$R{a=^SW&MnPh-wb<1>MWqNli?T(VjdLJhRogcCfm}q@ipNPw8ciNCAKWwznNsW^N2!0oxcGu~L z8anD{Aa7(rK+{eE8f~vl+CCUn-MuTjnqt;bVl!WpjUHoF}vB<;t8?ys>Nzk zu3jWM0g1%D(xiz91sbIbd?=IAreI`i5~L-aQmYu+{Xabis?x}$!o3vZqiZG;(@p%6 zY)zP?XQQVCI^n!0{6#XYzGwoAu|HG%PE_yhQO1zUA|KsOrTQhI}YSrbg)&^fi^XF2_G zyxSzR!0b5Fa*T*29ZZt8&*r;y{8jS#bEYPjUfs4|8Nm(>*=fE5(?y!rkz>Ee{Y53x zv^veV-UN?DbI(<~|T>WL?6nMr2L5nVRB{aF3SYg3RVcW)jQ_y7s_y zDI$Z7>S=r`L1YuQB^Lt3j?_c_V8?F`4vW`PrG4|if8JUwcm_1!_2Np@0WBCSzX%7l zy8uid#RjfeA}G6I6QDA6e*V8EKZ~)uAmlw|t8U2g;HK0FJG!OD$SsR~hk* z<3RZmE%~daApFL|e=>!c!o`pLsxglyCItpf4E0!=ST)hadgx&)91)`$^@i`P%K1Ar z(W7xxqYmE@3wYm(H@41-vn~4xtA)kMeJ9hG(ZdCbHmIGToPgxGzfU=0|aZ4J04!? z2;g0IsurS3Wy5E{MUI0PtM#fuE@OU*4(Z)wDh4V~ShPk4`0x*_s3x8KXBuzA^r(HZ zX=T6_FA=L3%PZ6~#1Bvs>3l@hUE0Q}riHeww&_VIi#oK-^asr`saL9zGG;KSoZG0r z34J~0yFR?!SDQ}x-Yz}FDuj~_Q$fz$EDx*ACwLgCgku~3vD zBBHArw!dW}5G>;4YZZlIF;t;Cp)>&Hq$uRsj4i0?0xwZiWuF41bE;wL@Rw`jSneA(H+@DLB; zk;-itfwWV(ooCVt+X-7*={@6CIJ9GHGky7aV?c`3JNtO_(AD~LZT1^*jU$Qdjn0jO zx^bv;W277V*&d)EtuU88|?AIsf-b7it9$@hf`jTY4zWYApcSbqF8=F?gMi>k$= zq;W{$?8t$IN=mvTH8`mM%xj#EY#p-u$s)S~ZXz~eHiD@tZn!^8&ZDq!j|kSo18ne? za*n5)a7w>wi5I+y_QkFAtUGyQ;x~u#9x>hoiH}N&Bt5zR+H?UK#m&1YkJY0KdHirV za@Yf6H>QdE;JDD8U1NXjAv9I2QfxZ89=P%@1&fX6aI3WM9`b(j@VRir<|J$fw=`bB zW_yG0f3G2WdN4fRGS)sC41iuyz?KvsPt+I}J$M=;wA1Y)M91UJu=A=2{fM6=!FT~2 zVSK6-Li(xbgvhH1J9z)b1UE@J6z+_`sJwudciF+ZyUQ-sxic&n^V})M$%-};Oao0Zo_OAV05RRondv||&)_keG z>#Zi9b<$nBD+79!2@;QxOdAd#pR49#BjYK2Ky3)x}r8PvJfd$T>HCkX`Xu1 zlFF}wv$^jfd8izfw>2+B#5LrK%VSeEDk}nhAU0T8q5K~9?6f(CEY-!nafOx#{~68Q z1WYv6jE}lnYftK=mcISF;GHMr-_<1X2j}Hqm;vm94}C->NSkW41e4|go!>b*C_RBZ zZYqQ;3xV+qOx}QFK902@0&L3kmA#TyA7qQ8CT7!E2ilD5Q>nZJr_Zp>f9p{s=UC%S}+F^QK%N_htJ z;H|4Apn@b70@0J4fNi90NZAmqxlVSp&#;g(42{?mKd#zvyiV&BY-(pdT4GGx-OxFF zOq8=2HpbB9;Q;v%z#KyA)*zhKLuU)hH*{rFjCif?%ATi@4!p)&GwR1z11^x0on{4Q@D8E&MW2_m!WUnbO>=o4g z-(8eQw&ehf|BJ+ov>96jZ~N4sc>&KvzQ9wmw}*dH+CZ!B1cZ;!vE&iF8P$ zELj?)xIxAAvL{zu>yZIN+?NAe#NImtMQ!NhonCXvEzpf$?xmM0f_lrWC%3mxLM`q_ zPrAis{uXL(-#YXkcAO02oPH(z{kB`3y=Mw%w*rtCMhiEODk+|G*yacW?`{tFcvwaA z-zaQJA{iruRcePc|M2zK%{OrBtv~$uV=6yC5R(_eZRW?%W}Ar-}QyvC8Yjx{pJ2}pKky&V|gjK8NgA#KlMo?%h?x)BG>~0D=vfJ)k3$IRyS#V2 zu*wYyk~lSkUW&cL?=AbC=K+==Kf#O*EQQ=dUKm??(-CXF>HqI61CY@C4bgdTuU640 zqQOx$Tx->Y5Hm&nHx@I9=1;x8U%!_k_aWkcx(A$%&;JPidj7w}XaYTi80+a(GEfF&76BYj8Fzr455l-!8pkp4m0-iRZRzKhVi^h7@Ph=~1oB|IF5 zq7BBb0{39`JEINVh$D>-M;p44k1=u^x*;#dsQk97{~Ir&)gE_rUeTz$w6~CN5HrRN zCWD#9u&NX`&kdi;+;3D{E1g56D8aY^kDffCf>>~A-H0c2g9zApK8S-{NJn@Ad9gt1`{#%6-d^np)#?8v@ zNFTtkvAOJ;T%Cdq`z3(jDA|`8N|@`hv9Kz4#(3VIS*q zq7+f#w3`K`s^SA)eQhJ+>Gw9GX7&i*HBZKWE(jYH7g68PBXD~3>bJXgEl9eKxpYNF zid6CS`c&NWNusj#osw^O7F2CxwXWYf&z6|m1w$(BIe0O1%%I7wW6+D5dCBfOJ*1b|E?i-Ch@M0osTycI-9)@A z@cLeUW?KxSYITLNd+NPe$Q#F9!igg~rfPI56oFUT7G0SZkcGYiXxMYY2G&M9lpPIv zn@V%*z#1xPjt1SG&C$Fu(bsm~qXNBvW|!D^e1tD34s-tUk&^ALe)AEA+)e}39-IwA za^0TW`AroG`nQfb0-!lfg3f+>PcQ&fTfX1?4WGc*+|B1ABst$>w#lQ=85K;t7rpl} zwK(gy&EptH^SHMs-;3{SueDeIv?th`c^YhAJNIIMJj)7T9mggn4hh!rTx?=8*=i-` zU0*csw4)RmRUVpe-dbsJcpxDJHKd0$>1f~wdUul-!Xhzul_Niqy7$L?fIzyToJW?a z=Hp&z?DTg^pMZ9^1S6aC<;|mcZGJ+Ir&!DKpaACf*!m2D-btwkcFP$X)NHbVie1qN zs@?@+An|1Yj3v92C{WaCzSMqa2`^136sL>2;w`NJLR+|&?j&ksN4+KcCrhR_EeNV7 zim0}M9}zP1|A*dc1?_Wlw#yqeFW>vQHlH zv^=;}i{!wn#U=IY6&n)v1K;|vsyj~Qw1>#4c< z=3YMC9^^pM%zz}DXp}8bai7hNIw}Q5o~VwQZ3%0W; z>!0Cag+++_q^D9Uu@3)%^Xn*JMfyxF6j4PgI7*XXveO<66>g?|6Pdae`@$C9pG@o8 zSQD)q6}r^y0l?;7 zy$4$i|6wbX9c#L6u0*HmNpqI}?rWtF3uF!3iU})K0$xAfH$VKLR@?9Fw!9AWAuK_< z*4$ZX(e{0#9ZNVE+R9?HWcw3D)!(opX8nPl^jvup(ue0*92l`r40=8Ei1LmUs1toj z=E!RP3-&1^i%TN)+T`X#;N^kW!s4(33uVb+ zt@g@o+^lujJav!qu;1mg=dWI+>nKxHHS<>#*8-~msz^$`$xq7KdCzXFccm2DztLl; ze~F75Aa(E3-t`bEr5^qEXz(K3-ms#r4LC=|qphFx>({N8Zbdv-jf)sSMHfCI&www# z=^fqw%Dd~u%NqPSQh_;Q7yrnt{+#GYf1BUZMh|sS&hi!pOQ4{vzYtKeB}~|W<`CY` zsINDKgM>jmWk5K23+wnlSq18jPF>t}|>_K#6+e2ux(3{N{m%h)>R?THMHK$)UU%y!_`z`jKwl9@>NjA-q5R#z2r2(SY7T-E!2dJ!vI?LpztVSj%iOc)!A$UraKR+UPCoWo5bDKE|`gJAwe zJ?XOyzu#P97?Z0>%A3POSQ%1X1daUga~R&^!BxfoOqb9?v@+1S@yPtCc7E9|E6uLg z5W0%oKu$i=n(9f$TN9Nj-&17+)1Rhav4p~ZAj>4nKx>}5%3B2dCKD?HU_`tN!>g*H zwAc!B7peD03lpiAiA2nBLdV^5CM9t()VVL9G_~awuL34Z7Qp!Adua<_Em&%b%FqTA zUFh1Wyp=E(@5@eM$z13^H*sMSRdc1e5kVnS-?^Lf&Y4+@Qa0ka0L)Brp3V1APmqwW z01iK%NpB)i_o&dyrM*#>sA>d>I^shzE=YH4L-_yPRhc9!g2hOYWaSKqELl5n;vj6h z4vD&75>?hD5>?%vt3SMnv)Efe+jEB;aemuwP?^l zM&&~+Ir&0cl!Qdpad)cL%5oVYRlQ7^_~?7?^39W~HEQaJHHx-OvtFYMQM18oCN+JH zR;63kat~1a)MomZVzT+36%MTq0S`IK1-s=TD;3>`lpbNC>JYHF-sXE!H8`;h~BWMdY#%OFb_o5#AxEIu*MTm`vFwU|O5h6s&L86oi;Fci? zhL9mwc4P=U&==IPWr;gh8<;K33aa0g&@ci?G`CwQ`ECU&$UDzNmlgl;#RVSe5t7NWav3h^Rx7eg}F-tjW-= zdPnQLP;#{KZj`0wB=LmdUI$(3Sr0~lLucL+%a!mu+i%*3obbZ*f&}5&UeNZ%mKP)x zgJieQ2xg}h$ejaW@m;2ug6U{O^YQ~y%+#A-uNzb-$m64aVM z>hmNE_0x!!)R0bv^ip3hen0L;i)!IBCF~3}Lo$w%l~}s?Tk(-G`1}5TAcbH~@~)?? zeL}M>LW$y%5VcqDnibVv!hXyy1t7Yjpl&@cQh1tA%?f`%&D{JJU&*Ya2!Ar>?U&T< zN29ZdMvDO_b_=_I>D95!1YRC<`X{#%hGY-K@U@M{BWv_kzsKzxg?ivX?^LzQIf6Pk zwEL2GAduL*yu&{rma%R7%h>+=JbTOW6f=>#znI*rVf{NC zvj|WUl^z)@!n1$zaI{Y`Uu+TXdtkMf;9O?=&fcb@d)XsnbGH~hmknNa6izaUJ1D@0 z!8Mx%7tcw84cV_(6|4J*@))G?WWUx@`aYjNaXqYVV^3}xD^c4t$ZBqCF>WFxhWF`e z+z$klzEBU%&pDcKU=2+9Wl2d$&yqL1Bw#KL2$cR`$c!Xl?TTtN(V8%9QHc zYx!1Dw^H-bZ(hf@MZT%%Xuo+g--dicr{mi@`KDs0IBWPuwgn0ZRmq9=icue38N}U5 zy`^=))mcfX1AH%WW<4F%ABg+69z=02(@W^ zP1>|Fd5$*In>we~6Gzq59%bG`{EHkE#1V()VQ<{yn5Y;y=^B0qhG>QXBg|96l>$Uyd$=aUJbZ~U9@IW;hKM&95=y?zL3A0f&woESKyzDK* zahcB1a{cj&IlM?8^P`=71ma`{X9k+gU|fThD)2r+CnI8vD9~OhiTR8^FQIm(J_*j@ z(6>rOwjR#!ur^Ku3uANP|iV(#8X&$2iu&-eu!sXTk?~xL^0d*J*u% zr~CAI9=yPmJ#JxTumvjw5oWAVuuP%Zj9v6-n(5$Xpy8#aq}AY(56w-Qy@F8)N-@F% z-CQ5kws@Vs_OoUqi1A$<Pmejnd?KAP&wNvv&$M$?g`g>fBRh7 zQ*>^-G3Sje)p{SL?V#|}r7bPiyj=Gou*Ftb^+GmRl)WayK=Rmnv{WhxVpq%+D;+5* zipL+8OceBFEOMC&{n+*NzTPjkyY10Qwb39X*5y@LFGw@ptDtVSYH}&ZLVQRS;(Np_ zX&XgBsj{j|atdwf(NApY>0nHwuh7np0tN7>7kVMjLszqBRNS7ZFlpZr_ykC6T(AS_ zJtiPzfjVNbhZ<7WpJTn{A%IOlqtt7JKJ;L*EsSvN9=ZvvG{MB^`rEOSaH46F(E^Y0=-qZGGcT@Ho zg{HrYC{#$-UybRf_XO!%6nGKT0BpN9{a`TZNR;Uv)+-eSN|g8lVJQk0S2viF*pnpc z@w7aX6;b^1#+-=*cL3V^x(maOs#)sha< zl5Gi7oW_NICv(|app)6%e5H3z>ZmY)o&p#o3I1D{AIPtOMCwJavbMwV z_$*-oZhS(h6Vz4nWrFGS`#C(1Xs3R9EHeB{$W-H7!PL2w=rs-txARd=j-K}-?%?sf zpk`UO0Yu)zGN#-(o>hFC@||WEWIF`AK>^*sgb(r_QWQ}3X^mwE92eNXby`nd9e&6Y znkh*29+~cGYwRBgM{g{rT#-Lnz8Iex{3v`}C=n;AhPlQaSk8W)Kiu_Be2kDpZL<9t zpH#4QcD~&DB#*9MY?DZ%5m(z!P(=NCvjNl3ihbytNg%!p&bu;^trv&F&X!pr0t0_nIo7Fd}r5ij1ads2cBSiZv=L6ic$w^Ht+EFfq5dYItS6_tDR$8i$>!gmVna`GuwOGX1x> zG=y`Io(%3t{HgDrTh1KAQy0_}6L(KLaXtnugJE9L8SsP_%rAg%D=mbNQPgmjM$IW= zLcw9$@qXMhY9d$=x0nLkT%rp`aEi}&IfaKL1fHX3^N6#xqgBo3_l2}$TX!;o*c4!S zu??OP4kVht`oEK1GNF0K+wKWWk67F1d7^m7I}Tk)zi39nBD9tE*;0NmsV@e1;R*;$ zM1Ahz+i3Czpzt1h;sodkQm8Uw%>)CR15920j@=P6I2mZT@&Q8pjm|&GyH^3w0l30= zAKt3@f429^;!`Q$Z^nB(?@{`V+zFboN|tr(^?$c}wOoES78-JKi>>Y-$;7iE_90pv zVOTt@&*xs=JE0!#+U(Ga;@#;!rd_P{pFgB`wa^ekKP zsW|xd_ltJMVD@M*iEDRwK;HTnGw8_st6wlwhZC)ow=Gw{55tGLa*#Ah(>Wk zm#fRTD)!2k1!ZA0b)0eSWLV5rjhc8JZijVN*$P_{V&iy6a9Jga(CZU-onOZ#*oENf zgq2ppWm?iYHiLimsKjT>Tz6c66L+24G}oT2rY&u@EkxoCG!8p~Y?OILc8c)idQ!D_ zxt^AR3PT_jj!YZAxuoRsC7Q((u)F(_U$AldS?58M#N&LDA>ern6f62?1N;ebd*_YGqJAD zsm#uuO__yOV=bRB{WDUt1GMTg%!z5JaJ4Wyb6sX^ixqX!E5^uc72V#HqKobkDlL0i z!L%PueJJL8qTiBM!;O{=%W1rnIPqTp66R58&&otg%_dio8hy#ix9G`kqw>S_TE?tK z!MG_2Etg;ZF3Bv$?OjNK+&Ux4LfVn`2J1H{UVR!vG5N5Y&?ibTW7=Q`AWb-X7$!L7 zz>C&YpiS&_yfgt~K06FkU2$B^nP2Z^+cX#C#Vo0_QV$ z1LRo-&)YUnz^_xYT44eYriU%klAeYMSbrsl5?)9ms+DQz8<`MB9(}BS6&UcWaILkSH zmJ&DPEtwMW*392oTM{y=Vx$u+?>Ft1N>a(lLQ9pOCo{($SzJ;_td((ABQbAe)6R+o?u;D;hmW-e7`N zQf9NeE(fV?9e}gDViH3(o`gi)LL!vOJR%{92-gehXXZ=}gDAFAzpaj~x1rT(s;B8- z?x`31l7)zwVNASCcr4o`=ZN+?3#0(F6&V8OY*-hhPpO{UcEsKSfTOisX z@D_*?R!FtfuZ*u~&_VnOT^7SvUp&bVX=8SYX`bYVthEN9(%0Ng0e!&`!qLB89O&+W zR2{VZk6t%FuV{|u3{gM3jL>KZW=DEur0;-3`3d6C16+g$n^OBY@~cqX&iQN8pnjX? z&vo6hi#uoI$-zyXCYTE6I1R8eO0l~2A`KjNMrE}a?PNfD;J{?J2mhjQ14`2rcruLm zE&EaA&k29pa5>NcDtx1Bv_sRxGt2A!t-ySVSnlN$e<<;{-}U0+x$r_AddUZ75Mkqr z#AQyDeN?fxo^*t%$y@$bx--}>S&{dfY1X5ydXOJZ1K!WCKsj3e7G87I?Z!2}vME1B*?MwUvYN zXH(pryUf4ZAywy)u)v+OwY45?MN8qpZpi@m^|$fE1jd8Y9f#ZUu;zu{w7;QF&(+(d zXKg?ei@WlC4J4usaDzP`l%*})04IGjFZH_ynUMToD-*gS8^;^MK98?W0~T^?ZDnNg zus#eW3pg-);5>tTf!eZPlF8ZXmXwQGpgyF@l~+){fP* z6qd7m*7}g+b=LhY4ang&>p{F-(>=+4Rn48vD>0%Cx84r}W$S%xi0OvIBHOyerLh~f z*mt(%%$oqH53M+}r%k_V^10vU|8*a-FRS~ItVa*1(-OMgmV4RhPcBW{qnTc}G4at& zpmJv&qXEuKg+Sb`dPG%TO2USyB^H$eULO-p$O-F+ZTX~V^$^RX8gIgrl^=R7M9fK9 zoWdkML;}V4w3W^xc>?~t0Du(ZQ81T9%Oz90(~xX7l~dL~Bn+Phh#!brB5kA-QR5EE z5(sM4&RiE|maQd#!8dCTkjTqDr#mww@ve|(`FD2PfYHV2&KH95ya zo;O#wMDE!80^NbYEYO76Z%Mzb7vD-Ef*vYo_j=Ek6NP>avV&9;~N&QErtqlTWR^$~l6ShWID)mH(O&(BZhV30Bq^o2Xo6}P z5E$mEiRf1`0I@K;57$B^*l!1X+X(CAwrHeJLS{Qe$8&E3#AMe_~kzD&>BTR4P6#5IMdLGxqOuyJ&-()Lt`>!YfjsI|&sX$Rt$;Xfi% zov{7>yW70laG`tD6CxI?Qb=3dfa8FJ$z1KK)v7!VTGps=x{xdI4iRu=HWtv+&T zF1!+=NdiVSaj+x3D8@C=UByg}dXqD5hpT96W^1ZEg^-eyQbrSyip{Xre{-(>_X8d) zg!5opeDIjK3uWSAFK;Oe7Q6oeS=Ib!5)DW@uu34?^flfJ1J;ZPFerv z0Bh9^(1d~k*Q#UZx7qdBjhn;U%EA1w>Mbo+w(I(f3ldCHktY#u&_YDG*~9ikAubuF z){WtOyg`vdbyBURULjUa+%6i5_v}+c)4hR~^}3PopF`J;_zE}NYHi)7x6N$2bzUiR zG;3WHKhQAu;PkT}R3mBqej@6eT}f{pRr&^b;4bYQ(;Alb38X3@a?DDf4iwN>60ml# z<<~V}2KZtj=F7#DW8jBhT)aq>eMNDE z4;%wGUankO2|vtbiex8I!Sx>z@+Nihi+t)N7Nc6dC}O&w+y|zY`D!Eo6AVAt@|7dn z;3*+z6oRzFa04l6e5Xi`9`)-)=I~O4LNzC!rkdR*#|{J_w@y(?chQ7(DXZ`YRB5d?6c=x+m>g;cJ^&1PD-juLW z@hy0Zg^dG{1fRG$@P)_b2KBC~ae5G^np5*HV%cz4u`ar?Y$PANqMewR#l&21;Q-Op zuLfxvsJ-YGCO7)v?}j>q*QRukV>;H0F&ZZ~(q|fIaG*guQccUCGg2T{E79e3A+3>G z{rY4N-xUKwUIiKvU>srV?ihAFL6C!<=9FRu^@<~q?TIg@W$Tak_Aa_J+qZ)@di2av@%p23uj(KeCIdCb8%XG z6LV>V?!4vPQXg&0vYBq%JVk8IgPmc?DJ9mmx*iH>#81rLL`KY^$SADy z8&+(cZ)GWZN-SjxSO+F_+UW2V#iE(7%zV)S1G8U5J(hWWgJ^4Oj}9kj5g%4HOqbjy zp7sz$A;%KZjTn@(E>}sx12-r>P#i(r2ZLF>6NdGJv%78ATtZe@tv4t&^#J@=Fe4w; z2BnjZ&2Gb!--4c7kT*eatDVW@pb)jLWwlD>+aK;h(sx=-(05RStUbuXAT768pc{c#P%2VycG(2w%jeN26pVWkGsq^ebqhe|-j&%dy`#m$i4rTM zn#8d>~WO4YrxZSXeQ6R%n?$RKJXTy8~inl1n!yIzBT zJPH5|B8?38NGQe(dvd4lZ!*Fi`anp8LLZtK;adrY`>;j#9n>U_4R#sz-Y|9Q`dmad zs4^^f*|xPA2pXhR(1^o4XlC52KS4w5#T`)w;76%TjbX-Nqu69&-3yB=D<}hj8l4pr z?vgb?s!!RNU6wMHB80b-w99cZqaJKi#-GDy9&S_XC75Op-uKTscx>XO7}Tm#%k6Hc zHkft_u=_K{rNu7vM54K2_cSr7j`0!DFff<(nzBii(A`NZ4^*vHcL*KM%8&vjR5HgO zd(FHPzrnd`{XE7#;u1EOw%vQppK4dJ{$YNb$7XCevovnyH0XXxMFx4K?IQ49P$K0E zD`HY_xQEFb4qfov8|feQ+_xZ->U@`M8!JVgZ3<(rZWC8oLU~S28p=tNHk&3*{$8R2 z_8V9M4uKpB5{(&*^^_dk@88*!(z{UDdTHi&h0aCIIL?9k&8FvlF)ABu2-_tFx0))n9n!m-j)A*+y#4at+GzU& z5y^GIBt++RMu5hsTDb*@hB#hj@h^&CIf>`5|F=-gU+{f zl7**n2nI97uf=rABFn8_1*mv^)dbzA<&(^D$U)<q1fMh-K~i8dFY6V2GqCRVff}9>s(% zkc_0<;C;uf4+xW~w)E>G`*8CcXbwSMk=_8Ivi_&{3T!WL*+Gzt!7FlR%ct9x5t|{S zGbCaj^oifGrwr{mQGePpFz-yuO}3>9EMxpD*jc_*Bp8faOjB$e^&ehlO-0NsvwyUf z*uU8q6tl-l!gp1 zj3P;ZD&7@J6YGQ@8un{rB=BDq^4JhOh`CBzkJ7+|Tu<QtZh{(Bq>++mkKtM*22|(6*_%xk9gHQ8z zhAv2}=EGskw?54oqTl{Bo>rCi+7wv)1{wE;+O{+CsYa0qoVVI`Pl-rg0*gU92w)WnG(Bo01Gp><2 zF};VUdk-;aSsRmj6y*RTxi?&@_UXU=(xTa`@x@bjiS5V-(jh5zXkSfi+C5ZC`C20p zL?VK?Qxlt3?53AZTF~Qz!a_76f>h(JlilG*xr27|Zn_JF`wPD5F6`sKZ))ibY|(LJ@A+aUyzIP$w|=|wm|PKf((7y{&~>o zSuZ{y_XFg%hjfdj*Z@K%DcO`wAPnTmJ{>E}$ukh#LliV{;tXo&sf1AJOs(T2Iy;5i zv9HOQQTcdP^kwTKk(We%;19jTViuDUz3_9q&{p*t#KWlJ9Z#_G=VvsNmk~dSXDKp>-CeR*0ABP-X`-*YR`DZV4S+$(2H{ zVST&xH=Upg5)0|(w){GX!VJQPpWgC2Q3SxAq|S3jsLZrX5Kv=_aGa_t$8Dd?MrVugOV)k+jl4ntfyDy?txs*x;W9|Y_dMf z3A;_`JGL_E+q>5@S)e%B7M&qW4}PPS3A46X4mAuEcSjLvc3M_i7ht#*9O4o*5^C;d z#bd`r@pvXm2M(5)@^7Z4)z+l#DhE@OMg&;ero+kLsfJhutu%(1X7NhRn|-VhKO1$S z5{Lep=u-=kUf0owLi^?9&yIqE?}Qt5rfMm^0Gs5Ge(5%e$rZmPk6bkR5|+ZIMm0+# zLL2L#@%;i3b6pIS8B}Q;_3xb1j=>L?vc*M>id~ zJ9ojSq!mC}3~H(^&)!0c$dG*}?@xfgsaHK2^rNi|B>z-9EIgm`ch)}SCklEBvhg6= zzNUXuS3uF6@kYnLC?w-YlH8qbV||@$8|z!zHumgCLM;-yh}xa*ow@Yzkdxii+TWX> z9EUT?QMo9d; zaiO0(tBAq1uvw~cNH{invdR`D7%%PB^85%(G^7J?A|LhKjaj}4G0A(B0}5*BK96UlppQdnhoaq~}&AjzN zB;oXfa$@0{-qNC$_pJM}9A@43`t1^SEV?Za@H$EZKJNcG>J9Uuo9}NO#ZMyqy36`l zTGs0Hc+^VCJnG)e314}k3)_v1=Y{TxTW^+^xN7djy5#`yBwyJS?YQ;u%t>5VjexD1 zU&8OA2V#M5fzfM{C&&GEg&zw)@he?9l@+emw!NtLN_wX9XA5w%rELWp?oljoLZCSldBQ}|i>FFS>WA9Y0} z17Z4(XEjGsXO2=0XW4ypp=R$@vvyI>c+qMUB&QfTPi+v08qOjm>Y3)oTfJqvd4i%% zuoJkl>XMuz1W<=BX;eOL3e5IG^Zf&J7n#s zKIr;%ITGw5>nW%$`ZCnX7Q5FdSugIFa|q(xDHXnT0&9~TC-A~1C$OV@uzc^s32Y++ z_2fjN|KJTKo9OBU4umjwJlV{qClyG)Y(NZ>vi`S&wv4S2onaE1A8m;?wDtuu#oOmlaZMs40tP`ib)$tsf@xC3^cchir+2xfGD*A46;ou_AW*6wh+ z?Ibtwg|uUaIvGJ|A}n=O5mCAz2NH3F`md84xP5xoVq?T|1LqO*j+tf`($GpkyxqV* zLpe=eKrb=4pjC8ukX{Mx10dj~2efX5 zGk{RI874UFYqWAz`A_$bR<7TBMe(NO4sM4&d0)?~C%2M2lRMbXw^5R!^5jl}Ch(pu z8X%pMAp?$}fZO=MOtBn%VRf8u*dWL7qNdo5cdr){iHUSuj5vqO@86BjEK$&bFAO%04M|znuX<@NJ7yqsM^9w2c8T!Cpa;N zHo9cqmT&Af%t!@63CvuL<`70LQHz)*PJ<|0;bpTmFU9Wjs6p)_V5+Gw9~F=Pa!K>x>R1#B%`vTwM@1M!OoflnhEc*lo9;ACDxO43CnDeld#ErTRRXK zp$~19qLk89VaQgB&XAy#v6?MGZ34n6tYt;!>pF;$id0k^lYdj9Q6~*(l6AHikER`X zvpR0H6d`Z0DZ=qe3k1~jV@EEB!6t#5Je$#~&M2E|l}KV5{%xYZ>62^CZLoI~ew z1;Y-_MXtE<8%zl9BPPW95EFv?hzSvybWBlS4z-)r$^aUbw_Xg%DcG5=Y3{h=mGp~p zuv>~((|)5>-pO%{5DP+pK+cQ+rTvt>`K&tw%S}KSlY2Y6RRy+q76$B7LWJ?Yv%$D8nrmP zFH`r3HIT&H{?JTE$%Poew{Ap*u7(>Ck*IKtR8y1Lyf3ARWw>Lt2HcnlYF3NYyzz?S zUjferm_l+2%!b<*zH!~^2kktos9*p-ZKF-J@vX31-x*_SBuAo&sjjG`N+VL#99>!x0pPZ4%?p9i6!3Ffn=$-g{;&)q-?}I zqky^h+XML!15rtljdXT86w`)Ti&0IIp&a;ua$_|<#nK^J#nd!0>xGUNrLf2f%P5`9 zq%l+6?dfkgeE6)f;Pw%*u4~PR!kyp28Q>-EmV}04RD>&@*(_(&ETMaDcf)2`NkDkSSV*46SS8e2ti?ornN2WAw+Jde_OwdLIE>phRVae;oj@ZdH z_dHymG;gj+a53m9ba_wgVX7m}Uh^-`^?3X@+Nk7yu*u0C2uCD#!NWZi!#&7I6V?v5 zJ>A8f+jD?2bDIA3_{`Wrf<{u5MyBc1$aUnq9`PvC-GfZ>tUzTjq&Uz{pfc~PnOfYr zq8WeNLkd}^UUIf#%&HZ$S77wb00{dq3%YYlq>@1 zQs}wl(c$QK60~gA5aA35>qwf)AWwqJ4TwmAG9F&bDoC^#%Xe!RBhH2+)u;vK=y<_A zi!~5~j_jtko3yExj>)7ODW{&kmvG3Z>u#nf=n?yUpKyM2<~EP$4r&755B)IYSuJX*LWqyMN=jpo_hRH{*r1f`aUQ>qcg7kw7s z8uLR9pZ$CwcSn2>mb;G+;Mv0m8=t%Q5MzLRGGahz9NuT9w}*uZ`}3Az!Na|{5|%I~ z1qG=&PUwZL{8jdB5bMoaoHySgp1Z?$X0ja_H>?M3ln%Tt+Kzk1*hrRPG@Y>#{i3m< z3u$a@+n#l7T4Fu)F0gE8Sz_#G8>8k<+_Ez)(CxPsMxK|ymT;axIzV`j(5l%z|>g z_@}Ly`2GvX&AAbpiJjc@BpG)foGmjzrrLTV&Yh<}cy2iq{C1NZ(M%#opi;1dKHP8i z)qjlkA})w<5oIioDN-&5%r&zu9;{g1MF~&)&)AaY9%y+>}xpUFX?E*iHm* z4s(DD|4@2QvF_??f17*U!8a|+t~17Aj@5>)zu%%+k}a3dlH0^MdDu8{URqy%u}S(Z zw%2}k;i-dh4Q{TjxyO!9fUM;>)UG>!$+%7lI#lgR`}}kYtQslqqD+YLoS-9zV5AUG zw18&pHpau0czol^wiv(LzzqsINZA1ry)9FM&v<9agcrdpj(i!61&>O6@&s96`+lezZ)WE}VOv~e9&|B$DbM_NVnu{Fd8)fo8U{NbeS`cP!Ug+iu+Ga7v0tpz2#_Kt!)+)dO(KiQ%3fIA*=vcJ-WZiHT1wxv|= zL>@})ozNe{CT_=*lcak}X@yFapj3dB_Qk+*KCu}XW7v=cunw82c1Uoq1$m^fQr853}Hy1{!G$paRi8JoIilz z-h}Vix;-X2tx{ltiQMdrp@yjTP}z0YP#Kkm%7{BdMP&c~I@F11)=(9gp3{f74s}A* z7Tt+WTk=1GCqcV>;5fT6lwv6g1j~}+J>-|#h6eh!IpmB{-9`)vETbp)*6x|&WEm(=Jnj^Xnv?26xw@WgiWzOht#oq6g2c15o%!b9|HOPVbRj`3GqU+FY3|XxKB*Ab zKs>n6c@WK9sxSJ7=KRX$C;hl~cN7xQwmWa6pUam$G_3SM zzxisuoI?#scL&X%z;fk;ING$QLbGC0iAB-Efr>y)6~~?6V9jHZkJLSkR60UF zmhg=BjQA3|R#o0qRvQjjB<|C6tszsZ?zk8;6~7H(P< zyBlrSA7W{hy$%l+N;EmvCsc_i+50{3Q;~n0Z)}6Z?aN@nlO@iXs{>X4@*WYz&iocv zRXlc@Q-F7tXhyg=`^L>kqgF{V%VPASWp{f76`Jun99eq}0aikV`@FlH=D|!`@;tnX z3vpmgS^?jUMZ_LbuVJ@Hkem|x=ku*Rm?}&@pcG6iGy35~xmw8+WlBB82r<8WeGhLc zdcza8ro*q?TwXSB<6R|pN!qDT%a9yw1sDB5`p6V5>+cXB`cik0T{j`l)zMgHpnc z$CB9O1|p9epW{XyPb~PxSGe(9-|*K{ubpo$(jq|C55e~0tz=k~&7H5h1wL2avKLpR z3?JrScu9~li9i|?D2(MrYhe;j9VP|MPNiFZgHN2ak@GVpqxt=fk}|NpxX=uRxc|(c z`6>M*j|*>K88mjFx8Yotmc+FFL}J$Z2}-)D{-Aq%MZEU%aprQ&E561pR)02cDpf2FXMgK+!3T?rr zxJ1_^c9-oL9(XsStACat zCYUczZ4|K5QrHtL1sLWtv5JCs;;zlF`Y%P<<;|-M0e-n!h;W97_0}@qayw1`X1_FP zXX{HplF+e_cSr6~82NSqq4SpIO00hY^c4;>3>Yjc>R;^Zp|vQ^umD-q-_9y6nmZLL z!9SEQKosR7UUf@|9Da}c6ADz+!R_-|-;aVJQRhkzHWx0St(WNiJ>3DqJrKHM7{fcn`I;|it>!Xh+UPUZc=b0@{_9`u3RuVw6*eo9>XZy~uB=CIm-XcpH zHYo{(uVtvmw~1*`Y8l3}d^4KHP*X4W@WN<$v`x5O5n}&0Iloj4xnhl294{p9&rs%R zdR}y-$Z=v)Zi4`xtb&|Ib2_wom5?}^5x1Cv9o}E~mm%vO?jgmt94&AY=1Cbfx95@> z2dL8}6Kn}WWuOYu&?Jj>EdesOFv-RkYEf4M4xlSL=JI9$kBaeR&jWrF!0C@Jx*XO3 z5vtr1BqoCd!&6&DeK|Ic@B#Or($z?)H*%Y$@8z=JJOO|0yUJEyM-cYv4_eZqnyBEA ze;=cMtX0?5zl`eSZE?It*(FpJVuz}@)GdE@$q&pR`73ae|m{C+d!xEGU-xsIM5gE zw+)l_rG^LcrxbvK!a#5o%~6pix4b`E-gSIDN6VBP8qIZDW}rq4MDW7u5p*Fb#!Z3h zwag*vA7Mzy$HHIEl++aLH)lLQX=Wf%Q<#K!zqtqeU{*@)NN$V1%0Q0l_M0@C9WCxq z`cJ5bzQURu7>mSED0RPil246WrA3!}kO(iO$Yxy?oIve0THjw1iUOTIS;-~h`|?T^ z^?PWUM@9C!=xetKQq;4|0W@3QEQ2o=cCQ(JA3j^jBwER|(lZSJ&H=PM6TTi0{aDwP zlR?s51uHc(XR%2{x{SzptWo=Hnui$LM1BYIS1k%5zv1yrABvD2;+y}GRn zTVPG0D{zaO@n8UV&)lhI5KM6_p<=Q+Okk{v9l`^vWIITMOpv=!lBj%;X&B5{(;j59 z_YJwBIyAo+-SMv0RguU?Z3#2d_PiXPP+6+5aYQ!Af+Z-Fg|QR9k`@NMNRzTO+NnuF zMN>RIK(a#%`7(Tx-$lN>aD2RzizW2RogMP+$17fhwBI(&RH|W=&{%O=7`FIoZO=!` zyP{w?s$LX9gikq98q{F_T z6brGUp~SMWy&){9*k`~aRbYfs@~IU_(4Lk_J{F#tfp4=kgBF3$(v2HgPzs8IK|J6Nu0|6W#zWEiOOg{3>CPj+)<{r{@j=H@`YJ9A29Awt}KW zA%CH-(r;#+QSbfI!gGehf6TL?t@hDBi~gNpQXC5fwf+PMGV9WxGQmq9Y)`lB`j1Uw@H!N1c}6(Qf}cnuzX+K;MsSR1lfs;<~!T>Qkzy}Qa9 zmD{-LE_J8>?$nu_OqxR+bl}u7&hLj=08QZ`=@1N&AHhS3)x`j-;TM;y zIyU{!%u-vcjrb?IS6G<$n^Ulw5QJ*FuT2R}^%AtBNq?(~;_-Q^f&!VOd#lx5QdYTj zU$qLrv;=R*^q{DmY<;12$xXR5k@kPoSrrr@J#ru&64C+eM3Cd6*)PV)-CkUorbc-8 zuLjK!8GUQ&rToE^}=l8SSpF?zFVJ5 z!;>^e6D~H|q!S30WUm$b66w6A_H@W+?xZP`D&+AsLx!`J*7ni3OSTi)uh^lGubyKV z;D`J*TA?UtYd^qffdCJsK)^nTL2I*Vg)y11kmG_{e0p^waba4Un>Wn128KkSX4RuI z6*rZJ8YbAbrhF?xO&tR|)cCMLiI1d}(dxrBQB)x(Vt z1p00oYEJuVW)wEwmVr$bqjA~Hl4B7xE>h50#>vO1)HRb?rg1&OZ?B43fq4_3512HD zq+m=fGb%uEsS)Cu0ejgDDqqd=As$N6hTUbK6P~lfg_uVuRgnU91yay5v;hKuFJfc6 z#v@VMU!gRf-MEwCnL*Fsx%b*(@6amLPn)vP58&C=odD zTxp|5u&NoM#h9rN-3EqO93H0DoMA#Wd?nXyl>Hjo%f}LTi6M7u@|>6i;BA_H8Zp~( z10OpVNSddQCx`^E7*(0#HW3rhZU|rT{-G)wos&%fCCXUmRzq$56p~=QGmS zl)nd)?kk|gK67ZUSAJMcr_&)z3LFUtkpk6iBd=@ZeYtQEKUJQXo~`HC6jvg+Y<2Op z<^^Z$X8>~cf7O_gx~X9{IA%%XgGD0qYEtWXEu2Od1E`XEOq;1^g0bWi>dSA|R z5B3cOCNSdgOU*+)!_2{?X+bVwNy)E>hdZ`C7v`|0bH|To(gnf2W5A>%) z!@M}4k)FbtVl-I1VvPpfR<(qtV?P^+E@!_7p+XRg`q3j z7j)5dwnT5KI;@F(vwB)Vi%w)mQoae=A_W{uu3CW%V6x3aRRlo(Bo$ZVV0E}YN^Klp zVjK+(A#@j8hR_>eRd+)lv8m#|g=4Wi>g|pB8pX;~8_fv=q15jx zJYW+KlDeYG3!}U*CvG5M_QY-=V8@AA&{x+9`HJW|Cx}`nVuI^HAb~1qiHn3tEH8m2 zB|8pkvY8WuDDaE2-(uv{%RFXQE^&LI~Ep{3*;?VgY^5~65z)Rb*@AyF9sY#)_G<=LA6Nd_dYs3bN z6xPHo;7}$pQQm@;e=xd;|4C`FTfPGy{Rx#t?W-!Y)Nq4o16WD+{ngnnCT1oc?EpLd zK)i*tW43z<@uI!1rmi9H>oJ`wq zj4F#Uf-G0m8F8X@xe94yFU4)Lf-2GCq@^c%`CDd&yU-~t=+s`#Gz%RiFM-CV(G)g- zB*m54FA=UYQy9Y=loYN+#bz@O;`W$-<{zBH2Xt0wDP)AoDGyar-}gZ~l@+PS*&~hi z9fRBn5K-!P zXpEoyto2T81_T7C5WZ89=Gy-I0N?X;f_jkpRO?D- zF;3VWkz8Fi0|(&0(SU#|2aWKP)k(i1#(5;#Ddr?$3MA^;V=k%!pdN`M6rL(%iW;__ zvcIqocm7&6t7`2s$z|{TJkE0w<61*|#aN5KHu0WI6HB%VLSx+|*qcJKnK;Y7pB8_2 zyirUr?H>tlCdy@9pa#O7fU82Fl>0(F*J8C0vq4;_)HQq(O55}>x3sh>iQ8+v zQ1J>a>w@hgr734>*U* zvdq({q{jrAQ+)sD1apG88??$-%zKP?=+pdCDZRx^QywG!aYBsLP8R-PbFO zJSVkpSF>R2Z=VcHD(M^YX-hl3>s?i+;q51B>@&T_;HZ7z=mEH(4WT|GoaAB{oEH0p zs*F>Q8G)m&v>yjHnu^{53or&E`TR@rBWU_)#c}U5fjl$c0#114(mm>cR$zLGlw+Q9 zC#y>sEC8cgYMO~a^Fysj-AjuP|2#fDF{%x==Wj9LMHE~wi+tk&eT%MKqfk^h7Y)=j z9^>M49K^N0wEDvUi_c&|WVus|Ih4N=T(4+WI$zp4t&u}S9r&s)bxXbxp>Ra()epoOcyYS0-PxIDf<1aP69R~1JN9?+ zaeAzN(rIwPzEKgeKoNwBd=>E+iUwuN~dU_P6Y+QuPOM`XW^e>s#t@qq~$JL9jR1%!QzZb zY^Nnns+s3n_cDnCd+qID3cqo3GZN`Rd1F&_nf|%7`18;I`G5T1asLlqT~ibaX&TU3 z^n)dki%X#rSgu$L)w?@7o9}*%qN9&3Er<@LMIRGrX~}ivYlH)fILbTfO0Jc5jda~f zkCA7-_Z64m{166UIF?^@5@3+}POVIf#)#hIeN-ad4NMp+uA<_k-J~e!jNhJRpEn;_ zP``y)2&I6BTurP|5MNF96eX27pM~*_KBhYy;Wk>oE62_qb=@FfRUIiibgEbDxT}(k zhqbyI)@G*hnFtM3DQ11+XG{P}v|fiv`ql^#pxz>-`^^b(N?67oYeeRS4m9&ZLg zqQ2R&^I?SqfW+3#VEzgm3{kFnxx4x5hH_*Tz*Cu=0pbqsKd`T`o>ZHYJJ&%lOIJ~S zIcT9K%4#(;EPv~uN3$Xh$0Jm)fDuI}bKGq{-yLypAuAY#8{EFRseHNIm8#3i=u8iX zhrv$`P^MIW|k4K`Mp z*sO^#@6-ff$8n7|WRNYcWO>-5Yiwl1^o5(|a?@q9_Xs8lW< zYWj>3ke|0xvl*i77zPrT%4P*p_Ec0Po}fw598eXc%Kmg|Nu;stdkGkZDpgUcI1xp6 z8_6;GwRF2LU|HZyI$J!$FjdJ+{CrBYa^rb~l>Vs^jaI}qUpPaHW62Kr3Og~tM=F$Vxb~iUlHOf= zI|?b%sxIa4V{$nAU%nnm-cH@Clo-J<@ode0@jvYO*zQ02@&_k7t=T94=661@@R{fK zoQ92wnWuh(!ng}nKAnz8kGa3wm=)Vzc^`lT|DtI2=wH43)Q5Nd;jTyTzQ5ao!@NCj zl|>r5oz35MKNU(jUqCo`T>I1UpuAxCyH9AKjZsl>j#S@jpLu$@fup;=MH9< zpR=uZ>>BtekMy*f^!wZ0=D=j#pI7tSEGLg5l9HmKyxGU3KleK?eJ{Gb_~oyEiim@( z*Lv{tI4MN!U;lrDj0wU8S$k44ejpBH3@r{)G4Y5KP%*KxekxXrKL`~oUf4&&wc#ZE zXaDsbc~3xR(!iE;`%`q}d9^2TKbS)PB< z_w)QUoq&PV>vPp6u2h#+^+8I)(R)_)zjvcr{LWNjz3Rf{r3t6SeX^k8eT7Ye{4!|a zU$pEAbB@3pDQny@lt_Z@W8fprMzm=BKr{(}#7M<`iO+12Uu7bDQFC8Yx&@eEuVoa$ zYPAm0WTDF2XaTjoY5%LL*Rp~P5Wp|{W7D(8u;`F_hEx*R;)?L_4sr}_MGK9Awi==h zRp+Lag#CT&ju&!%sFz9{8b!@y6s=E1n(JsAz)6bx7Xe3#nSi4yuzq?o;hVn!)3ZDRm!I27(SA_(;&75 z-k4Rdm=$Dp(QB#a{BsjdD=u4!__EuD5Yfo^_1Txg6fk>CfawJQT4TDwFRy1nbylBo zVSdR(xsqSZ!=+rXH8*>yoIBc$u8wZeubKGGSH(B+YU@r~tClxWLOY#vSql^7lA|Kn zhjF07ID<^va0WPC}MW8BRUzKF^ z@+Vq_5h{xIfY<5?=nLJlM_&)DjvGR$Ir4@?VRy6UQhwQ5&o3*hN_kSn)XUhuHQ!6D zCN=QQ+<{~wL87X9aw$DIN`^z^9ymS%t9f=t5oLc?FO@4(HwXdzaV(Fxf&oX7$9HJ0 zoE+umCbvM)5vyv~K78a=Rcp$sSj{vOlXz^wVSfI;1(;Jy!ikE9_=vuS_WdJ$a9AIh zK%0qwH&&v1uSxLH*Aqp>A7l8hD3aoyy>p5UVY6DZs^QaBuMZ9H^+D_P`~Ij=TtM^p z5C-7;Gpp}ES$)rWF2hE>-@gBC1~5_F-Rr?w0|VC)DT<2Ip^t`v&)au@%Xe$k?%u=R z^L16r^L=Cg@Y1oPLc&9&aKi?{MAkIXaZ=Wq&cFE%%bcjyr*nPTm92^OU|+8@zZjc_ z!G~x9e0c0z0%SQ4XIFjEMxdep4_2R$#>6KNf0v*{)z&%HC%b!}JP#KcfmRMG0TQXqKGy?yhDl2s?`PfKD$(8CJk~DFoNvu&>EcYx9y$SKZt;;S zWFHkT|3L%TGE#jbTD!o;TJ~z2jW=Q!&YAdntx>LJUxNMj(LHmw6!i(Izz363r9nc_ zX^8-}0H2SFUxr7Vp*ai*#dN^2*|PDi%YtMvomtTPvtQDrl{kB01>!5Kk#C_( z3M0y=n=0ilDn5KvoCaIn27ggC(VMG}F6sMd$WW>18LXgRXMQKLOGMIB$ul8OAh=={ z*mZ?sQ?#eI&&J(W@$}~uUd^*Tr-j(=g>oCr#8U`6Ow8=v7JRX~v*+tYvy&8T1}zd1 z*C!!w3CADaw_Jn#*n3&4|M|WK)yto;VWI3MVlJRczl zEfs*t7AhlgQDhjCA%yUAL4BdK)+b0#{h>$d$7@s==Z?k~PLpN_T6t15IFvWwVgBX9 z;$jqjI_^f(4U0GrXi|l-Ru#beO9S5|vto#y!-@Pan>4(?B?1{$ z@&l*7lllN2#O#vg1fq7FJ|N+a#*mY~$^7SuQY1!QwL4IAMeR7z0k_9snB4@WD~-qt zB5E;grZ|pXY#`=|5WKLBB*z=ag`AUQx$h`8VQ~iEp%6!No|9`NDL^@JPvwwD zgrYR1sgB`=!PS0dQ}n@4wq&j*#Rk?; zxiVa@QStjx_K8u790FqG3BxHrA+2ix(2Qh%0B7A4EyPHihRF_nek@kX#-mj41HC+% z_Z3J*Zt7?fALS9Up&=UO{QjvwY5AQgqZOG3hkZBb%9<{2E+ zIHIW~n3xG2xs8`nfs_7M=ByyeJe6mSe^Gd|_c2%e{GhzXyEQqxg~VK$4nu+k?iJQQ z!d26RN>A2#s>JusNUBaVx!T8{XK(}#skUr9%@td?U*V#Mi83)MW6nfm#(wd9cLbzR z?jAp%(&h~I@TjCAxmXaOkW%vOFg;RM)f2i>*&$r>R0Q^Azu4PKgDgu04v>|Af{xV< z--V)Z4%h+_2BmSCG|l$Wmil-IzVwKyr4NS_U`cKfmM$OEZ)B;%qr@C(=@HhHjA!rD zAYYZ~; zJDBYEmiIG82i75iDeCbPNjH;+xyv$Z61|!5JqcO*^lh4|uHX(ldYE3qB_dW;F!ZuT z`2c+#u>!uMf->Jb<|?y{50wc}J+b6cvY$sQ>}E5)1*TYS&80$kwt5UNNAnd`F|xUA zu9!3CijioZ5X9AN3Rn7O(^-ejsKHqh8Cw>SYH#p6jKYCO)WtrO#=#hpo0z=$Qd|Hq#V$mWT=a;{1oUT>zT_gqjb3*v0 znVZ659I6{r5nLx zLeih!B9D9#aa(JD^tvtf$`2saZ6jpRmc>d@=O1D}Yh!a5Qzod^^>mg__(5n4#MebBE9(H@gyAVK#ptp`{o(Mnfg`u@Hv|Ei4d(mT-dLYQwXG zY-gn3(D_J=s4i7Hkpd{xQThk;)h<4T2kOl1v#BXyWL4Q9|QNnBHGV{-@|O3T*YJT@}J=sZT8BqpR1Lur;1XO5TB_>q~U zb};U*={7JE`JHvRaCU}LbN~y#$pnkDZ$kPc?$qusqB>7&N-02vExs3B+twS8+0YK0 zQMCL&sV)M6x@dDvE*p!)Qb%MAZ_NGy?}-?ZspA-@!jSV(UTe65O8lVZuj1?{CgfPb zhu@3%vd=`zHS-($mCj_6(37#B(%dUSEw;_ns4w{vz=BJ~7Zs5R*rwzx3Qm<^j|+=e z*c1svH!Fg=PBU%ETyRyf%qK?Zo2Wv#;G=)WvcF`p zfY_~vBC3WI6I5(;Pf~@V!Q*nOne0$g7oWzzQ~=YQAyIY@=&Gqj#PLTP9VxLz7!2IM zNVKD#-3QRgCpX{PGM{O$UUM0+qc7hy!Z-5A$+Y3~QMnP#a>26B?rmE7jG87BN0;>_ zs2Q^Z)A}(JueJ|0vsKUO*}IVHO8|&dR{PY%EhN*;$JBd{*k@7t7#0*h45;*1#y znnA;0sg!n1F84REL0w6;WUrC};mjGIES20K03grx`zE|6tzt^2Z}`$B!sjgp@A{ZGNorbvTMQt81OraXH05|TiPayI}4Bowt|hlUj!>DI{& z6({sqHPD8INbS7!igd4yIamqQhjaep@8%Sum{sS_6%_{}*5xV3Gx3*WmK3^r{w%6k zH4K|0Exf+fx+r%QV>bBRO=pANfQuLJKd;7icD0QXzetVLhS1qP4A_HDLLy>MN|iC@ z+Q?YaB-SQRXvtj(_IDEgui-^H7D@YSAUMCXnxWHzez*t_>#prE2d=@3xSQIwCRw*7 z+++6+^SnH-D^dfjJEkcE)7W0>L9OMHmfFa^Oa!BtQmiQ!kSSB~Y|6m2+jPgi%ie*v zP3!prsBYsMO`qORCAW5@K5MmKvl6a&-zCoic7fb9HAZgphsn%fOxm|oD;c^CQcaSr zK$LNs9Z7%j#Jh|Zds>A zzcmC)s7~OZXkTE8>SABQR(-FuUYwGmnL5inyODxaz*xjDp46QdEA)u9=04}bj7#OalcK2Y-EJnuafV6%^Y|f=YSNDE!NH1 znK=7HP{8WF)hc&ZgtX2csHve)qcC!?N(ppJp{STN)_580fJvs#s#~sC;h|UVguD%HPiz z?eqQf{q4gB`r21AW-A<2ppB)&d>IEtD<$QP*dC^d5X(uVqv|$0zHJm52O%;k`j|>CRm9O~h-Hm@ z>icu3j4l7+sw@>tQK0k<*#|4ac}t<=YxI%!1W#`=;lhH|FTf4-+zxBv^j6>b!L93g zYcu!Wuq}skAaNurpY&zt{0jRd(<3*UWP*B8fyJ;|$V?AE2r-9BXfyWY5yNH+c7q-j z#gq{?0hYgPu$5xqC};^3&JtStoj5pF*2iLWePs)vUrSI3w;^oUe1&~sialK4rIg*89VA!x?$Pve-Pz*$>idmFBa)tx@1jWScKnc;2N?C4{Z5I3*>#Y#Szpr2` zu&8W{xVVm;C`aF(h`b@8mby^Oey`sCz0`M434RNGS9m$?;h-dj=sKe9Jg;?X%((Uo z5V5;u#;>}y2X5NxR$EA-C{wj0Mq(p71j30uSPDG9`nPcqcmATmWTJY%H2d2Q2dkQ8 z>l&KX(oT6t86nnJubuTgr;#!%yxp;60(LQ=|7?QryO3d(4z5`&TPIBfXeoeOe(!Qm zX^s(X<3=o=!j~X8C>#WvJEB{Fg!+_dXNQpdq3yE((GE#^eW@4|+=6WNq&S?)j}Wv+ zy+l(J?<0d1v$1?mFS_6<<+K5@ZCB3kPFhIJH?fH+ygvJ!uK~I-N|+S=%_D7REDM}( z={A>^y3e&nx`qKu6A_W!tV#f#ZJyneAlire+5xA6sWsLhJ;=ey>_U-{D!IpOex`!^ zF?J=WNXk--wPS(kNBJhlUsTpwoBu9}dY#=A9ZpstcG5Va&yfP8*>U7SRNxx7tfK6j z>~kt!H{cs5$k|=~ z+82JIre(iKFYS1Q0CX_HvUze-7vk*0>Q?rM#sa^{K2U3a=#xo$&E2DKkMF`cw!qq& znz;xJQ_CSPzUFavg$B*EuBg>Vm@5~4ohx)`*9r|ApS7-LlaTSbB|6G6O z5VEb{3O|{0Dm-cxrfE}I`0Q_eBvFExef5-l8;q%Y1t=iz4L$*}w|2q#Z;>DQh45n4 zuVajDn}xkVEl=@icbq+D>121o3_$v~bz238v!MYM^EHB+T<6Hq^5hKLWR#_c_H8$( zbScV&7~q!d^7) z(a<`)dP_8gxUk}kX`1(-A8NcQ@_5(m`pjY30ctMF->aZiQ2+Ab{L(BZ>1V$ zVFYbnL^*B{$Jw(fZ=!QEgT9BEOqP$(q@X_nG4uIdeAn)^>7%0Iv+8T_nRVhc2Al-4f+DzHT zZ=jh?oo(1hs6C=IC}gN~$ate(1Fz}iAkp!+sRpf}5YmER^h#rV!Qk;C2NNRCSskry ze5cd5sXB#T)PrgdYzui?_}waoR^Y}yd+-HJp}=&dWdSs41+em`$?;z&H*G`yJ2%8q zhpf7y00Ylq{-$1{s#(e)vc*ycojfEz-8@r1VD@!Gr2U`={X_F+&mfjn(0c=qxt!rO0ArVN>SG7X~L&%!X-{kQ{Tut5zI)xy-{VWD&O3BqgSaV8Anv8V9P?l z94b+O#`a?F-t2iB!nrOr9~ALpYw74OA+$2^t?H3Hl(gp2O}9kiePzKS2`|1Bfr-XM8*o?XXi>2McP($MX?`=d=Xc`!VcoT z%_!Uc6;E-$i7TM#oX7Q`DD=*bi1i7yql>vd&Xp|%izm7M2-ibgXSgzHor3Fr?q|9F zR=T6Jh3osM>ME{k>S~(Y$Mwfix=e(fR*A5vz(+tjtVAb^DiC7*;x?}>**xa$yx0HX z0J^HQi|i8$?y4+b=cj=YlHjd)B;h!+Q6LPAwZI4E3}o7u43m4{v?ss!$-Qzf@$7T# zHW4zf%(OH3Gpey2$S4Vuu?sn{5G4qtU#Uro;Ji38XY%v%i$b9JJW0cg_@}yu5ncp0 ztnP8FT?C5=_eK+jx$tfEdw){=#(SH^jETRQcph{$s+@^GY;LE<7`yzyeT}t0D(eU* z%zG5CxyR(2a>B)>l@i_<+oZAzBnRoYS_m5)a~o%)&J747TQua3xfm&jWO~zV+_?c= zK{}emQGUaHvyt>ZNR5c{n`Wc@hR4?ZYvN4Qf5S~EvD+RqV`QV6lTH$|A*-Wdgz51e zxCjwZPQ=o6UP}?~mDYOgI7q8U`>|d2LcXE|97h7V%8F26lS$BVu?%X(Yj8aAO|j=C z%I?=MG>(H@XH}*BVI%5!s)C{WmIEj(0QMj;XZj(KI(z~=sX+|xAL|0_%KS79a(^xR zr@Cg_b~}ER__Wc7Iduy7#8GhIag7uaP79Gb59ir0IKXkp*t2zLkLaG~`@-{wxk9yD zq67K?>lNT;HE{gyK#RTajcIC7@8ot<^gPnmaA+!dApwieV_ch}zvU@sKR8k8!Pn$N z>q-NPi3-a6|ZH1^pH_Z3$?=}if zr^0e&Hj?5&ZddkhKg2C3PZ3?#%g4D}$6ac7b})Uf=ofg&Ay%rWR*Y|ntb5XV4(@3d zhxq@G?0_%Yzet@tVF{by6PB1hX&nZN6V`D7bqKx(063WmHgT~Z`Fx(ECxE$l4y8v5G#cX5;lJ3~}WYN(1 z-ma&jK^r<9OhKpdu7>mpesTsxvQ>Ze;FBph^k=3+S?C-HLfC8`G z6io)0OoDWF3ihP)V2p5hFk6U7rm*6`Si>Aw0SNX232;qx#yQQ-i%X85tDhg@rxJ|~ zICHh^BLRw`fM^@8$7r+P3~#ywQJ@~U(Yd9_mc!1`7?sxH4W02zBAb1c&=28EhOGlV zIJ@kD$gS+by>-<9*9-cxWO0(4~&^s2)(pMXKwY~N)*vQZ3tR3YH*TK1_$5b0q$#Ba<>Q~WTK^JxtU6$~Ga zNq2UbH}q%To0!{lX3RUIWmL4q2U~cPvgk1b-}D&GmC(q35m@+<0z>ftzl7svucxUJ^q8*-lrCh2 z`heEx1FD%)5!2xuYZ%m22Ww>+2#&!E90%4=j(zw#V{Wq#@0BRBuLsM|jQd2qZ+$!- zV}6;AsNP3wjKW1g5yMO3&URwg^uEHo>&DlPorw>*kDGOp*<|cowgwz{Hg>kA=!Ak_ zde(_cNSGI|3mWZjRT?cO)o_j0us3M65}{@}U=*tTEp=s@mgm_E$PG105X+gHI4= zf$Ll(6TC{5dD2zpykK+MDHjnKcPIc+er0KZNgelA2un!`g+@$y&P;X2+h3C@@Cb5B zS~2Tm2znw>bY`MHaw9C~RAYvv8XYu}$`4s!g zxPI-OV_4SNu?bh+PEEs>*4WdqNlD@EBpSAC3h(@Yj{T7}i-5(}ZieosTK6;; z2<}wt2QIIwKc|H$;ww$i--U_M1Z|2QmT^?Oc09|s{ocT%SM4XQcB{)#iyc^<-yjX}kEyLNjFY&>En5Xo2h&Kkk z#TwOddEk36>6FP6OuBLH-P$Nhi;l=5cRhRzSmVBjsb(N!4e$`M*=j$un zL#^y@tpVG~6RTg*jElkt$pQ5&d*2#a>DC4uqm_Lv^k{rKPcKRt|1G>Ecyxs4`7J-f z6Pikkzr^pSfyoB`$QpPgJhL1e_-upH;5J21Q(y5}&^Rr=RN*oC0yEK{Bv(bB zQSHqw(UZES)4(_^hXbea0|ie!oU-pXR<7vfwvFtB#wk z(RfOKMn>BxHLSY>I9LNkv-lOX^=5Gt7egj}JbV9|F<4ky03e$N2eePZfoc*4ItB|H z077qGPg{0giH-1)W_FoM;d5ovz;!7S6MWX$M<*Nzyl=2$!WLnPaAYO#+F_6<>C5U9 z)uZLMqo-ESs=+b6KyQr1(P;HXW3fj^lkA%$)DM(qx059#2@JICD!@SB5WXH9@qY=} zh%sg;CpwWrVRMT{_BrlPHWe$Vl&>@mPU1=_qUVuFT;r4YY%E8N`t-@y zt?ZG+*1pm59`0ueW=%uh-J9~8W@|sO?&|10j^A?3ho-9U!~)cUeNi@oZp&$0T7%tZ znf%HcoWrltISG~Nm*!_+dQyd)u%z2qz5|*aw#F5#R_A)Xt(gTVgfatlUrUAx>*45V zVcrmK=5)i_vRrRo)_2?S6JWF7vQCMGW0$mzhMHz~SU>3DFe=6v9QL@SiIaCHuVDUmwH~5`*{ixy$wIH=`%H^S6^))N zbyZ$3P{Ek;8FYIdbjKH9IJaL{a1MvJ;oQ6qWGN90r$k`9Rf8r*d)WVQDSoYf8Z1$B zd_!nZlz)H*-wA}|zOL{UO5->XJ3rELvpdFLD>ssT+^1ta6OXf0>7tZQ1|7qafzdG* z;!4LDXyuYn39UR2L`@~ND_SZF6dUzeR5nJ4>brXOA4wp#^{$CnP_F|nbB>vwEm?ug zp>2&%Kp$um@8$6Cw8aB5X1VWXdVO`70{rQtp6)Ys&8ElNYJ`410?ZT|awv0*VvsH< z-hHE#nm8X6KJ~Qx`cZwoslT@_X8`jGp~|0HEs`?^X5`GDE;D3eYrP>m%cm0HG|kLF zv0~({H{*3Qo@cdfFE=@;0QkvLF}!GKZ9uQTwx6{NO5Cv7zclz$U? z#mnE53Ghp6DqWz$VAz+e8*0_CPDu)-J+LQW z)NZHF@Ff;O)oAvsYfQOSlYKOc$^ey}#4gZiE6!jvYhah13k1nl<7##RsV>(V)t=Zc z;FPlAc0pjIjp82q{kBds4VpjWk(Se^^$V{|RwQ_ShtIC;-JSu^C-0Uw1jN>*Ai&mt zNJC&oe@a6D2_i!v`%q(fwf}_*#x7k#H!8JXJrfKnbM8cHzY2@-EmP?!s{MW4u9W<; z8_OyACuzA-@`u&aujHSko|7o~>Nzz?6ldRl6-s_NV!o-A{IaJ@lziDUrIOF?Sf=Ek zq}xXGq|Jo6bVYwxpxrs6U8(xk{R5i4lc@Txta(2UsBNr;LeaJO=5_hDo7QcKK6YZ2 zUoDxcRyKxMS9hA7QS>&W!fU%NW8#o}@fX;!jSL5KpgkvlcH!}oIB*Q+9gN$C(=xWV zJ8deRon9JK3S07_y|G2#DAd7*clZHKud~Hk=-FmFkVv4FL{h)dBTVh$3)B0s3G5T= z&H&x?TL67jU0MJFmbupkEr7m~r3KJevfl#eE7@-W^gZsk0Qw%AfHM|T^`PGZ=#>N% z9{890f&m_c&Enj0UZ-~-OQiiLc!(uQ#2Z}qPoyn5 zR6GHvokWcUVZ{^N@CS;+#B>dl`oh0Td=Hns7g6E| zl)a`c3yWScjFA3P+$%9`_@_B~$|jZ;D*9#dmmSL$Hn3C}ILxJ&mnCOC7F?*CIBTB_ zA15?2Afh*+77!8#^AzIld2c-eo|P!X4n2-S&A?;EYb^N{e=xoQM=`#EnPz;$dbDUm zP<-C-juA!Xp8Y=?*}IkKI(CPGcYcdwyl0`E6yQU=UYwsj=PRoM-Ues@flUdDj z?+2(b84c)NM)oui>1yiF+DZf4Nl7R49?*ymbdKQ^L~S~ zJsP7w$7}Sb*{B=OBmdQu`X@|vc0=wi`e*v&QfpMjDxGt{)^iOA17`I}jm?2dO2e;G z)d$V68NjePxZV4@cqv`%GyRVOzh8~ZkPhbjO!U|6+W05n zN{)i?w609Wvup|YlzxK-7%Z$HG&R@#0D)WrnfOGkS`PTK0o_9c#Z>fSABhruc6|3G zsgs9Ft#%<^E-D!&)ZE+s6B^vx7wJ!_)k4o`wQ)Az(`wDi3;fr(-hpZs2fATeZQp={ zR?D24R!jeuIuM{i;A_t8;gyGZVFulXLet=O{?wihHJI%MhLEJ8`4Qy zHm!EKdV*GaD)pFF+Z%z4hMt>2J-5Lu#Mz5MtF21+I~V#!EC*IOV!ko0w!f#9OTk>% zLtP3d1-xmsTX)iu;O%~E*lC7op+WL<7x=*F8x*ojO%swb9@u>ijq%sUDx@AlB zsi4CS`9Kq+s;zKYJL->iAC}wt2qBkNsd^@3SD_d|&zkok#8K*u&%*4rV_Be?QfIVY z*(*~{hE!1MjD00bo$;B$I!m2#(c@BQT=clq8T;xjb;iNIGOkTPukso{A)QftZxcWS z9>`mZblTzKK3(54W&UbOgsDUutZRvdfP}N%p%=WT^0*;|w5cSZ(M-F#$%$m96KQr@ zy{Q!WuobV*O!k-wr?*n=fJ3YDMd8rWbd!{>HoN00y(Vi zx`;ZoY%4hsn*Ax79dKGzNRPd85o{D&3rZ`X!DsVt;40y1(f&!rH_G^xg;Id6|>6M+>W!e?vxPTRlIPEym`Zm%R+&%7~EcHBZ z+ROwnq0f2el(xj#4U9_{cT`8?z-1SEyuQz6H^i}w92l271_$0g8AOJ?OBIkuDk(#m z5^M1_4sqgABf1$iHoQhd}EZgjXz2`*T=2MX%WenD*ypk<}lfR-omq7QVghnB%3 zw^%)qr{V`hkWw|#OhfYhNJ6cSBr*si24k5Qno}#=hbZu)@s@Ob>8ayGPlYa3oInND z#1gw`$IVvzZ2d`EBjS*5-}MQ3IkT&PL}=4qugeN4FwJq zd&+6~LG%-eB55a7(oXy6omy+T@d+wNhRtNWt z8^#TYB80V4bOLFY%(GuGp%F>c^k~qckR^w$Itg~4L?R8)phXtTN-9t;uT+)TGEm8R zCBRx)Ib(7Q#FcOe{H15I8NtgA(V>r`jWD}i`zZs5VEQ;#BqnPUw!6N1t|U}L$tB^F zb*g1?28^DWlLEt{$9LoUSi6yojx?J3la92c*Hl3ZmTfQ*WG|Io-^%hD40LpceN%aO zdmMN0t9;Q{mM3M%$O5-gFW__m^;|34Sfb%wga6Vq|TC!8l+%JWuLj-k1J@Do<*}pO6=59vKc09mtpB3v^?6Hq4ci zgF?QPS%!qJEtHKPL>@=EHx3x2o>D-&WX6M6)ljBW`G~FliqBi{t+YGkD|%FYW^6>| zf5>QxasSfpBmuyLt))_Nwv{Ktv&`UxX;m*&b2iYiD!w8aDQKNid_{#VzB0VhQ;e^i za3EF0rRFwK#u*BZRanjl~d%h|52`iik^q zfxfAzvFxd{Q6iMaMu&P=dudh64Bu>1RmIFPXN+(GFpqi1DC@Y00v;;1av2LCO1by5d>2tgJ;@U1SLLXK9_? zzdB@CA6O+pWhPJtXtA`v7x9kL{uborGkbI}X4A)IO8dL7WNCjZYpbthX@4(zT-x7@ z9+&oaU%jRMJ=j;q#p1ifmk};E!3_LS_aHBt(sDJV*sd!25EYT$6)V51%;W+DLwWaL zhS!p5cXba6!HFSne4)>a?GP@vc3;vWoYGtyR zvLQc}5YsZd@Z__g7239?%QorAKXyt`I$19YwJa=ptGrI7bN89aiE?4evBf=%uCA@Z znTm-vq?~#x+_LQsq{uCu1LH+KpbVzWjfRQ1-u)|@q|(2#Tzfn>UQp7wnLxwQgQ9`n z7SKE$@7!KP+g8X+#tHvQs7YMC|D*81}!)VJ0?h=mD z2mK!Aea|2OrY9b5Rm5!=f{caLY{bWwN)oWm2SfiVIpiG)wH*aJ_tTWLOYvS|t5|55 zU*klT;lDY}w*Vj$EF2o>R$!zovzvb#hhEV>)sb=);JUiHvKFxP%Pc%X-k;Pvnx z#?rCNURr3hoT^*tl=f#T*PJkoiU$smxzuCfo}~>NSB0*PYf5kj2s{DJz*jN zUJ%UuK7gU%!k8tTuk*;~w%2*&Gh|yYlFQ7dt+XBv5=q1=v^j`IbdWp|7K598?x;rT z!9p4aYK}AuiQyBmAwhki$A%;h@_d ztFr9+xu6v79>aXX%}-NoO14w+R=AW)WIh?rF!{X4poPi-U0YejFnM?qhB;7)%rjL& zm-`ia!A`w4`hsEV8)KN#syy+pT5e?wdpPfuZ2xEBofX#<7vy% z_#6%6R}jH6+~AhQ&DZo`8Ezno>2U*1gsW+b8TCLa#zK#TNPz@4CuGFN=3vCe<{&_s z`!?m?(lKsJc;Z_OOc-;5Whgjz*;Q1!7tfze6bj(zz5yPY^z%I*X_Z`Ga_4gYYPd6p zwj{;Er<8axM6gJa5R+_M7KZc2MVt`s=Ozjx+C>8rEpxiU99|t)Y)Qr~+ZAaB%P>43 zAZ7!i%9xqeqnWN4C-#&k(duD^L6y!Tx^rG?%`cF%A^dQj#Wo!T8*n_C zukD|Ucd@V(-qps16t#)Mjpy61U<(!EB-$BvT;Q#7flF(tD1A9Z62p3aZp#}V+jT|S zVjbzEu$KNzbC{lrv8^!=V5QZAOZ5Ow>O6EnnpDeB)e@sF7PPaqbm6#2W#RU%z;;~I zTK~i&ovfN2vH}|q3XfShLz>aBzvV~7MQNmrI8;mduG1g)Ir(~c#5s%jUif~<{Dx<; z*1LaAG56Z7MkQQ-UbM;?=j-4?ZhTEP3Y6s9MJYYKg#s+i+^uEPESuP?tM-veM5lB4R#3rZ3~u_)1nP@h)!5vPTB|5_T_8Rc5^JO zIJKJ4-TS6m;>hQgQDnUFQP{kOkyvURWYzbT$LQ?4ekiP{zAvFrrqvDh9Vu$d`?3Fg z2|xErORxcu)%Py$Pd+Vp7HbyS^)=zRp6Cz}U0rm9JCVSER|k9~%Fu^h4E6=JfbzQ& z?aDGO6k#hLsRh;-{4l46A|i!iH)6&6QdP?a-j{WM8Z=qA{F^uWIE!HuI+W3;V%$y ziz^W+4y^_w)v?VNKxe788;DMfhJ;1C$~iY>UH%5ux9 zoAE@j*$s%jUi&7x`p<?0&Oe8z+_zIGmOTYfI(5man&0$LSN6RE=#zw>{QjpI#J> zu})5$%r=T6z5OI~O%(?OJ6*C$j7K}9N#WzC?TxlcH@3^C4pn=o+iRMBxx@ zI;%e~EtFNNQ4h2M?!P3sqw#R^ARPy*nHp}Gx$J|`!B~mxMC;fT)%ecCX zvNiAMTv9@e&7Yj^n17pn3g}GWm|1uIY6w!9f;41?iJp1_h9y#z7?WYXI%Y`355pZ( z6MP3n-+!^%$1UOmZD;qwQbs|Cn>iaRbB4K{D?XzgUu-Y8;B>0xH?UVV(%Itmx+4Gg z>xyA^R9ALJdrH?SQp9&`x?mq?lZt(Eld~-b<>ry$a#M`b zDBR5=Fi&i=U2KS#Lh%AW23W3(`E@>1GnPp_uVRg|1!19Q|U=dHY zv@Qp|NrNtjyGi?Smjm6T4ZH`2_#`cBh=+A$h@^=d<>T_RAs*zaUg&&8+xrU`=kiLW znvHTB=XgauitF_R4edORFOz%eJcw9xx;6Wz`Tk2HF2^LGXj5ma#->=Nnp`ogtJ3L; zvj3zY6svp!iqFa<&?ej^-I+5BAloNvtEB%IkOh~D780OvOU1uO%FfGc~Tbpdj>t^jgQS6J*_ zTv&x_1}Y9Q4Ah9=#6i7fI2>`IV;GbC$LT1IbP;}0t?$GXfW2hr_)8%`Ahp@IEi{42 z+Fj5fLZWgkf;_Hfk)YXhQvTOIZ|s`edBX-y*Qs;d<7DKhD0J%n=&~EN`G)x*ZU0}v zkP|jDb(j=`#tPPGr7`FGY_+`{Z4(#fZMBYiYHglJQ(WGT?_2PNvoSEPpO?{w^g%^lA(*>G6#=aZ`GJZbQOGSL34E^TH zK~`M7S4hV2lzsQ=^0#^)vv;R5li0Q2qL+4^=g>eMddSfjV%r7~_P$)ZBm1zvJqn-Y z%se=+8=M?0Wlyj}t0$7Jb(yMgtWc`#x_YJBS=UH^_ok>(0mfjC!B~BNhEf`w{sp(0 zhueXVL6*d318;T@lQos(cA%Sgk_<$_77lBtuLE-wcP1&on|U(uQ6j)w%m@~&6Cu;fE#0VV$+X<+oUOWQopXfKVjE!iPB7fz4+WAjk)M;Fo&8(s``Q3h z`Yuq|63t14iv@lWQo5oB4LCLZ`c$r=t_kEHaN>r+Ta#^2Py^W=?mt))RRnwfYv z3XOwJ%RX3nMQ3%A8mA`KQ0r(BMVBZx;U+rIc~0V+Lbf;`ty+i2AJWd1qHR^6L)`WX zY_P;pu|`_goM15QacJf#J?N&_ah{uK)qsq{tu7A!lqiihj&POE{G`e1x^Vm@kvOkl zT6G1lHC|&`PDdq}R1q0^JG<<7w%m-E8Q@$XHVtlp-<$vA6N^`RZPcs;|8i*vU#TnAzJESx;#unMqKbmBB- zeCS5P;F?&>x9hWwSv18%iI`+dGE4Mht=I9lIcAfx$OG?c) zQ>UkF=J0YhFg z2xP=N8nX>qSrxcBF7G8yg%+{S#D|~=sbT#5bgJ`*ZS@D#cQ|Fs*g8|&Y!@H@PNTRo z`$xURhaPrx#%?_Rj&8UwHLUabcYYO8B{xP=bM0LfE)# zbp%XZJLCd*nJHItSo5Q6z)DcsV7v@`vH$zwo3>{gZQyKTZ2{Uh%KZ4ZOUeX^HN%T2 zvu~=wzN+cf**i5YhmCoodIW0>rNf?J?3Pc|ha*^hD6Wpfft<^}1w?Jn);iZ~%Mj#E z-n!F7RltYc+CiXM>V@t+aJgZ}-=B>m;z1Yj)Pz^EhYx2|_J`R3*|K-+oaa%Ru!`U&n*jrtOS|Wr>UII2>}T-k zF;X2-DgXv*1p18BN7Mv{!w}EO(K7&0oL>O%F$x2yFU8$)=^#O`0}w&ebC|yzn_e<5 zcESXW>)6!p$PmS3#AaJSZFMGGFUaz<*2_}Pg139tFUAm6IS2;O_pFte&s*BaXEZb88 zW?$VEHnbKRawpFk zKd8ctOiSEv1NmraZ=EGO;?Fp+=`2(R578o_DjNA++YrcjQN~1`F~uNBM&(U za#Wz41E_!i9qQ$vLpiGBwqK=kFUQ?fS`-@J5rnf@2C5eUN1Y`8nCkWt1yH8knT8St zg*iS%Yyu6vHbd7=7ww;2-QWrssXEPU%?rfre{1Sjt*r$UZ8c72=}!1iKy~$`Gj3`Z z3>-sr-ePPRtwK($N{k14TYUFhjpEWQa8z*u*)b3Sza~2}k(-jy#Syc$TQW*qGcZUI z-edd=34GAS(+D<#%tP!Gj!o1l!!4OGE%ZnlH1jRbrqi{SCso(a%p7(0vt{Tj zc4kR31l`mP4brT5?XeE3>0m}U0Wm7s;te>F@Y~ex>{x1tz+qgx`*-nyq5LG_Ow=Gn zdpe_R6Dc*_M)rUSgeTB3H7e6j2&xG|ra?wK+Q=9H99zf1HS*K4cZW|LVbI6IayaEK z8=SIVB4vS{Y;7@Km97KF4Kk(m#rrbYd&W?$l)J}QP08#x>aKOqOPKt08m zZe~Tr{m^JO6Y50ibFYq|UkVwTy26Qdy`CKK=r5L;bKE2j^)cG;40KycA=P7qVDtz=@ zS^rnqc}?Exj!X2mNDY99l%A*Ayoc^$)Qs}LPi?LqxV#vLm3P-sk^*>9y8HsD@T|yL zG1zh&(+_Im1~h`xslLoH+j>h%r8Dt+wUrq2KZ;KTZ!_;kEjaCKY7$s7N)X${??3~= zAQq+IL)i&7;V_^MdM{VUggSA$+8^|c=!ljhnm)8?bLpYP*>qxfnOxR{yv?N=XV3IZ zYAACAfJnh$1T1g!86n9^ZUK=k(k@?1i$3AhFXR!vX|i=xu(calgGo60LLVBPGc@6^ z%U{RGCYvI$3RzZ*E3H08%!kUYQme?|3462=h|^^{r59K;E|Js@5v+-EbxQsSUFQX{ zl#vbdjBIg(f0@ri=aZRclXpS1TpH@>;6<}NqNPh`7d zq`Xzn9NqozpslNzW}EY_^vAK=yA9MbMh^?X^@7XC^lyAy>T;Kpta&X*D03=pqC3r; zo2GBDF6KZ3=vPQ_CPXwwt_rdR{vt*>nxCms5=UKBQXvcjnTtE}LRhDFnZYZp>z>Kz zSHT6(l*%KQkPLLWzxirJo$0LVHBfP4yeAkH`MII)w~5bt)pyC1wwhypjEZab)r^^F zkf`SqU}5lHKEV|Rh9-jV4OgZ*Aphdx$&g#SdwT8)Q^ae68r=!5b7);1fa!QB;pHCh zMbn3zHcZiJVVD<^ZG!Iwg71d>ba6Mk&31GzHFEO$P!EsQOv`kuLB7AP1&WN!QNAWb z@U`s2s9(|-_a;y&C3s%9{cd=g{jvf6Eg%byS56Jrnag->RbG%pdzsw2&swl(p*mnZxzD{SU$}VDg=kM2@Vtz@iYUp3m8riXG*uP2s{BG#e`bDNsQI?enPttlS2at0J(HT7eKj*e8{24@^|2dK zGi=VA`DC6NMl_kVu09@>GXR`0$yl%&G*rdtwV%@{@*gDY(Z@u-3{CtL6TzTw1(lNg z+I6|<%*^@uC`RO%GvX8#7Iz#B1LYPzu1 zlAkL&5(4c&5DG(@tXP#G0kdgD6z@IYAsiF~Arre6k(di1HXt=b8acd1qR@b+pCSy{ zu-P$up`Y-1QYm9`TTw>O{$;a#Q(y5e_z2sy00GN2uk5c-pC-FaFBSR_mH`-kU#j2A zE8Po*t6zJ#&N9|ry65>^c>c1ajSdrjm@?1@uwy= zcZ(mw!f^!E1vTKz6kDIsyOa%x1-O9WCEQ{nk69F&XJ6r&Dg2wF=j@PJy;tFfWdZ7d zTsq?^b$X+mlR<5caJ2&4e8W-pd;>f*E@f2D@sMA1086rll2USKc^Vd<45~IOx2pO6 zJut{VYt(w3k8Q=;F7$ShBD>Iseio`KGOz)$0C0H*i`S+Bpd4*%4WTcWtYkf`o6egY?YsAyNUJ0`D zxeyar5HEn+Ce9sOM#VMCTF6o1@klF@7h(* zPbYy58Zgzzg1a(b#JZYr0#id#C3@lA7*hsX<(-|YI7;A+egN?nVw6j*v#t~bsEUfx ziA$R))5s_Cb@{46BZgCv(4jf;hrm9uYf8uL@GGY-QO*2x`KOH_@^kYuF~4N`;RekJ z6U&EXiegYR&PK`L0h(m_K%&AGL59gTw8A`eB+%g;eu0C9hZaOV=KGPV1{&(5CNoj@ z5tR%Mu41n_U%l;+G=X~pc(N#?F6WfRlQ)1e#YlBQMZtMFIjA0K_dtvf55o`> zLi#oDOzUi8DFIEXOY%vpxO5Y<2mqs^2+{^E+)Cs=U&TC`Fdu{|U6_{#3wk7e3cB?f zdolHuV-wzBT6~OLeU8rsvvh{JwpC_SE?G!YGa)D8q$Euc#iqT-v_)4JYZs13UuQv2)lXzf46N>dO--fEc0V7CzUpv{9hVSXw zr%aa^br!LW_NUP-tqqONMqSS_)7*9g7bds~23%}67zth}#8Vk@su+3+=2wCuMGv*2 z1RAy!377&|Z1Q`QB!MjqNf5OY{1LCLngLZ|1J|&!q+U#srNaa;J@fdU@D{^FzzAYX z(#~r9<_lR?01!R;f-r%J)JJX{u3;Lvv1#KRHuOIfX;mOKFxX4`Vts(V%_1wjyE&hs zSb`I@AbkmAWw+suDllMB18{)2%vd^Kcxd$8EzKp@P27dB(8IfFjR3ssgmD8N`wm;s z@NS|H?=B5^H*TZR09I9ax2g~C@Mo<9X%{15aN0>hX_e5SOdZ8WG)9u~M_%X>>mkprZmHK$pGEA0lLKR(o-S||#%8~0zT4BZOszKz! z5`yim5_(Li$s0K=4I@aXJbHyY4p784H zDDg~eqlYh-x)HP)ffDC*AIKZNdaXTNl_yEKnv*(}Yof*pc13teoHGdP+)4=u=ksM~ zHfcSPl_FjNNHs?&t)9Xs^jKt>7?L4_jN9Rdu}wIFSyGZ}CE=zMMb@JHwJyZ z=UL8xAaVpmB$aLFMHJ(-{0-SZ*$OCvmOwlyJ?-XJH~$uDg$Brs33s`G)OtXzk069~ zMHmBNQ7{6$A+$yeC=#&NxCwY7wdtRlrJX?CJZNOK2<3{EAvi>mK#;Jt{<1I%ae z`?lNRFgqwe9>Q#AXtIb*SpxeRn&xqlwcXnwgj74bBzmruLI9$KKG0X6FY(g6CFCJ;6O-#Td`upcU=$ETQ`d zChQ1G(x2J-49Vt*z=>ERC*??ISeZRiV~*{^mCj#Ry6bDDDallo(IpziQuihC)V-2) zL>?R|A83^3%*X$4*#QjC=HH^+dWY+(eCCN|(o&!|oL7o=>$mL^gBOnAU*`?Fw2;n< z{}HfehBgb2gx;Vzx0fQ!%aje_7q&U9lyk_xftWMzFkye-%Wur})bciT7Pa(ZLlO83 zEQQ*#Un>P;f?|r=tDaVez{6jp2mnvkWAQfAISE2#bF!Pls^X;8fkIB;-J2{JLkx-9 zg&p=X&TkS|Pz^Z;iRqW@GJWSc8gvq@qt~EIca)H%;6`ko@uE!$L64_wL{K$^ODBC^ zN->k;X=vN+FniA!l~0b2Sw2Vc3?H_+vu_38?I4SY#w%m?k4R}_{~Z}yns`_kYf<>E z1NFi)IdA3i(5jrnoRP`~K98YFS@0YpH0_apnIfeZvtGkE1p>H{Y&ED$yZl03iUKm6 z@mmkMdZx^=5R#3nkZkNpUuH~+aS^jYyz{SxWW~fhAWP&1)+avZt`|5%@Rm&D-MK>6 zr@4d{#2~FAu6$mFXQ{yA`NxxP+VgCfwq)ViP@g$YHT4R*HWvk5v0N&EI-gJX2Zo$| z(CkF`nMJLK#!^vI#XGt5c;|$ms^oR_tE=KEOxe%Mtzr5TRZr7^T+D_@b;W)|2Vx># z*%68gb0zHHL^&65XFV+txr_ogaAqUU!x1-%s2V#^IzW`K&W_jf)nW}97dXrq7x1;k z1x%rUvju?RpirntvqMZ)*b3rvqrrxdtd?&uoR%g_Z2oz!RrGT~%r3T`DMak^Bv^Q= zIv8w57|3Zx%IEn(VTFpRMJAf*e*vYG1;AU`b*l4}qo-2-3^PLPv=px7HSv7u44Z8u z?}2Qe#X(e21V|RFRM?1AX=odoYv(_j<4lQdcN*@jpup|aBXs#uftq9z%kjcut^2kj zk>@Jbw1O6Io$2jbknt$VYJx`$HrT^AuJa^M?a8SXnQM!>FP~1fkHf8PAw~8|UCtOP z`;hiLz%eAheMjAgx4u`Cc41?#?6OS~HKjHCqS`VB+oFRI8w+q0D95_0vvq z24uDE>)PEYn-VZ<`UfLENw(@Q34XxPzYu)Keg-Zb7e97KF?aW!9Z_WCXUaRVH7dIi z#{tN`CDH|Mk3IlsdqK&x;Lub zcXALQkK141=%%~lw*!K`$Wn&^J&B7ydbmlpLUvz(L0^-hBJrgNK#Ccw4`xGP@fx2x zOgKTQ9+a63kCeEvo<~!v{e9Gabp_z53cwc*sk~@)>e_b!P+3A78sSC;Yln|?@I{by zu(nfEDb0hd=IosYa#oBmFYJpw6Eh=%m?+CAjG@{7gVvku=NgLkOp*Mm2||HVl2KF+ zP~63dQSY)2b(_km3zKhw$EoCYD3%Aeb%4ZldTYnFJ^e|DiidAu2L+FckK;V4LWCS> zW}5JvHJX)LZngvMY(0vuw}ZzTg>6T-&g9BLERMf+r`G-ISB4{64GOwakC?l0xSPFn zZ4B7$luU|%jPzVnwpH~chMb1ry7l@UsMl-`2Sepy-qp&5lvwKu$uGAcHJ9{khv8fs zUHq47-P-IuFj3+`T9XH-VNW5tlkfoKl>#S{@(!VK@%v8LWZOw-AFeV6!m zla-Jbp=jDnR?^9f^O)sdkHS?!CBf=qp&uc!$*6r>9K+AV(U^0V#8}nhN>TR9jLits z-Z=Uizk*7s$mq_1a4~3vp+>Tsmq)nl>{6b->X9-J+wG5bl2uMQ2eDx4laYA9u)pF0 zb8&ZtSGYp52Pdq!v1~F0(U~~;Pbqf;o-;1nI;(X-Ayn|=X=H5z7`vHEcour*W5vhc z%{h+Fr06c0|GN_>BYr-B3sV3?+!GL&9!BrkMa`865vMXK3sq#*7n8D*kRz~i&gX(y zvTu^Fq%O)%KpO@RWHj^rM5X=p}2;AQk6m-BkZb*fi7CUscj z)~<9J+}}?zL?QkWUG)i$R*jlXA93bBXi&cg`Cv)m36T`4(;(KF`Q}Dv9y`Z-N z1xp`X)umL zhL)^k&d6*x+#v=aLh#qZod!LZZ=UWM>bRXk#2o4234my z7K?bCSE2>XQ6~g$AD5|4HZYbIwI$hc6d3jPCP>Uev@&E#aSg=qW}^jM%y11PM)sEm z$!+d;xq*sJeZt&@CJS@J$b=B#NTZt56~tOZJC=A}jJeXSU3r{jjvLsvTHXUadOgU% zg>M_eYDYs*+WO&^IEZ6v4yHD4cd-Rl9hN*_=#T5odLtPU5)Z6OEo)TXwZxWBrRguk zkTepo5?cC1tN80rw6Ylr%rO?rq^=hS1V4?-sG^pAQV|CrNP0HZKu5Db^aXT!UmRsm zm}d5VH2CnI_?0!>RiZ`lE!y~YXhS@UT)*stR`}_-NJOg=DGuzX6&RZPeu_SqTE5enQ-L_8fFV+PA8x&Gfc+*9oYv+xzLR?cxrRQ{k;g%qNNN!%W2k;ny=h*7d6-J z>`18|W%j{U!QvTEMLgl@fJ@mt{nMoz7p~+j)g`;hTdL=5qqSSl;jrCNY{gaCZ_BWe z%{-d@7kZ|2ne6KfPu)_G3s)xY3W-27+jSNUJ36Whsz^Qbaxx+w&>^6`@?+RT!LV|( zQ5iR)KPa~qNbuG`ZZq+}ao(U+PL}`GDw5(DyrB`e`dg}!KT(w=qdn-IWv)0BY0QrJ zzGax46bi{86q0@@*c2R!MLYzC<=W&hVdJX;{&ad6cit`IEzfu5QEKaj1lt70_BA^H zROFoPN?1i+GPbLooocrts+QmsvY2cXn@P5cnfZf~l?nSK4b3GF#4Z0ifROe6rsu=Z zF0l<5fPrL(Rf_JL6mS{zNBy%l02BQ;NXQFA#%u-2hPbsQ#K@7DWG1Yf}|wc_m8|M;xV zd8jvLCP#qmZJygIzUzp-=0$_r!kiSBFTc#~fJXn3`| zwR1)Dig*K$E-oL9^61KVl1DS;qcI-+NPHfTHit*YBG$jIKRap{A5hA0%;nngA204< z2_ZN4G<5S&%WfWO+0CfmjM|O7T5<8vm~I|eVK)z~us445klj3x`J2q%_{~Fh^T29< zv)bPH%|mwcfDU|#iwB6X(F4DE$Zj4u!{3}?Z~W#VyLn)(zgcT<{N^FMdEhL6bC%vb zpaZ6;dcED8<2UEnP3||j-E{q?Yd4dAGif*H`_1{f*?ocC>}H`KRZsbwDZ9DQZ!WZ( zjefJyZZ7tli|uB!-)y#{~zw&14URh6uSKtd|W z+M8$>l8~qoLPW?cV;LSwMDzgKNAz&Ln2LRQM2@$Op&$Vx;0v@5AZjIm!ER^~QBgsH zf{iUIcKe_O7278dH7Y7E;e7uw=URL3N<}z*&b{~iF40>U|@WtF~&EwF_=>X!3G6yLPuE-oZ%HSdRgz#9Nxgj10XAb-i9+bJ6 zJYJYNa6Y&!bF+B7AameZHAbE!Um8J$Wq29QfpNN2hxh7qcMcu_$wU^Ee=L zAaU@ynIn7{7jrtW$kr=M3Z7h4`7=GA!(H^KT!YW%sc|gK91Evpa6g_(qoJIy5urq! zud$$4oUc*r%TxV-R>mTLC)eSer~C^tcdpXIJe4Mn-MK2icjn%s_w#t_{al`UzZXx{ zyC+Z8JBO$0B`nirC!dOyTd^$>b#i|RovZc#n>rr^?_ zpr||C_T)|G6rTvL?k-k2PB$sG53X76ay_v?65vp*7B<8);i>#_=15{;lQ5r781M~| z{B0@Bfor2PtV17b_#-G-+Tq&|g=irRzOSxlB86y#!iWp-eP1I=X{-Sm?o8?sO z7f_N7=)ZKv_6M-ip%7X0r2Nmb^UbF9o1reqQ8O1{9qFtyXXE~hOqLl=2hq~7y2REn zPF+#{$ZQ!pv~aRbL8$=PQtCnA(s(J=2eRFO!FN^Wpjavskm~0twKTDGVm5Mww(Tou zI1+Px?gPlmM{U;_0WPB?GL~(T512(q;fBbA9{?ai{;GNtvg%+dWqZUkf|ljgjQo4# zWVNaw+u46j5^t)&T$Et<(}^7H81KZeVVlc%e`thYgl||CUld#LG6WfL!Qcg21PDF} zO=UKGX)GHC9ClM3r(4u@+`-jAu6Acf-VhVLLS>{8WZ#VAnk=p>iQqv`uzLy$vcAY1 zMZd~j04zAuZvU8*0!Ajv_^oryRW)qOJR-IzZQln-&EvZOCOb+leO`7{SMDJ5a!#Ak zo59MP|Ftj75VbE{^ZUYR+^R>*dyyyTVmN-!Stk9IwH{-<837qg$lahdi@4lH0jw5$ zY-tRd=@{wFTaF~z+r`lUPPtOL3}(RjDol^bM&*ZAl?Q0jTc9)=ppC-Lq0i=MoI$dh zBe4cqMq`Y-opI?fp55f4{>X1u0TCmz#7w~1pW?Vd@Kc#ei)gX7$oZoM0`Sk<7}$6y z$#anP1aAlJD6686g7UiB$Goz(9eD#95b~!KQTeo~YgRP_K0w63c_>yxSc`HMB-K;lLofS zKEG2{&n}pW<+~=!_k`?(HY;%UVpfFBjAYutTJWy3gh1*HXfAJbT3xT95TJ55X~n$=fxD9j=My$BEi?Ys*)ExgQwp;2L6$|5IPwc=0S{!6 zw}4X8KqAUMS=08Q#{LeB*P?8?3$wfwWbC1B;(0`w*g zb}~O-7GZL-Jj7Aiz%Mw(+l14DlJ$^WZQ6m<&T=(1p_P_~^Ij4Rr;S0x793bCNH1l1 z$YW7v%$b8Xlz1X4?aj2gf3p7I^ps+TZQR$38Qa;%8XDz6j3Yp z(|8)x=+rImcaIzE*FniCbRqm)?|)#ovKL(I3cOR;hTu2I13YqGp~7NGuf|xwW}wTH zfGix4AOhlzC243U0&j{`fm#Iu1||SGwltg2w*B)MT?&J|@X@*(@TU?K>esG6%ip41 z5>nnA^cJI?n)?`88~Iy6UFi4A@B*RgliD?%XGpQ*5HdR;WDyKo&C2Y;e{XvkQ0jwP zL?uESUgJ3N49NF3U&ET-`r&j>J@J5zs6=H$b>E}T2bQh@)0mrl2YWtt2;*~M_)G!y zBT$}2WD4}5BU8Gq4y*S;$*DXr{jKx#cS<1>E_##=y3>M=2&lC7FdnA#7gQRu74Tuk)fk`r0YDK{nA!*MS3mlnK*<5sExJLVOcks4g6qvtcVfA zwC?E^EdaWe`d3!jPm*36;#}1xlq;Z@cmivhKDB)M2~FRb=1geguwCWEVXRyi3t{}m zLR{R+M&nj)%-2OdlLxV;a}Gv}ps$7YQbNxdO)Zd!n1bQtAz3KmFxT+hUIo|^9L0Tcc`cVVta)vMHegIg_C=r+w?P1;g%UmEDr^R z#g$L2=kzv2oP-li8Vk*yNDD#ENh7I70cCZK0&){97`9-+umuZ-Em(k)Qw|nG=^E=R z0dJpIx%;Fp$t#LK)tJ%kx`(OBD#knPI21BlV zZGJ^vbv1omgZaaj3rz>@a7V;mV)p+3w|awarLN53Fv8PxXL$Vl^D^VMZ{vuKAWHus=lI~ z)c9eAaf}jztQiYVfgVH_c65E)gDpS^SVhKiR8wP87E=)#T_SBK*3}QO3JY;#9U%a_ zwtZ>FcG}Hr%S9Yqq$JEpaeMH@Q6-#2>N6(_tp%xMNxY**v$8&T10?-ZBXV&`aGzFm zojSG1V9N%&n$!~2_6w`{foO8_hGlk%tH7;^dglcv_Dc0gwGq!p4e|>QXa#A2+nRbN zLe?HOnpTuu7XzC~(b2~yFd zpNdt1#!c2a9<;eUXuy+riBuGV>Owwhx!^%R7VxNR0mFu}pJfCoF8Vm$u+V6S_Dwf8 zw&B(PkIhWx_@&`}>|)mzM&%^5r(qb#38bJ_TA$xwq#O8M09hZf0!Co*rwHnS9|UK8 zI6xz?i;-xn5Ke(PXf3Lw#nH?S2aFB(@o`3qIfw(b344U>SD1^j?c??Yfe;C?pk__x z_dc`}E&os3cxedR7N@7rD5_ycedZb6i5`18&#J|gk0@&gl}q;ZegRj9dsuwaJ;iT| zg5M&r^}keAt>Ai-<9R?D$MFFAYj|*a=V%`A+0{HCM$YFvEZ7x&daJ>%n@H`0(mB+~~T9$XoV18cAUXj%bICPMQW%!qBl)ajr z4mJXLYfkzKQw7X~Yx(IzvN|r7PNIpBMdMSHBw~sXW1I;wlR+0{NGI+*;FZu+MR}rr zUhToFDi#zf3Y1Ef^psm5aCsT9@Ff-=!XFKq;pg;>!lEtIE$kU`Q~ZlN6f~bWQDOQj zoo>$pUNcZ{{DBIf&1A61%eWSor#|?p2}b}CjSNJ|Rg_wm+6vYxT;?krS!b+?311}U z1nw(j^d4Z^hPyj~8t9Od>Xb22zD*4U@U_HU+KxicO@y@)U&TCWo*ME_05YR;b@w5& z_wc^$d}Zsd@_etkOGwQuzbOc~e7JGz6gj1AV1q#3;X^)w!YHNy+TBE=bLM!0k)F+b zg8aohF-O1%?I2yvVFO|%T$9VCkNG8R+c8qcVG$=`F9#*}stHJz4N>yH_lrz$Bvj z>wJ+M;?EpoaG)Lt)bnrnGug&tmHQIj%X`IBl#gSczE4Ymy3}{$(;R!TU<%eRv;$AX zlQENv`mfgcOrBT9t-bP(X}h=P?uKgT(r(`_RJ(`06`?AGUw|E6QXQI{Nur`zuxt_D z`fO5Qh0>(xB7BfZ_^6U#`Qd3F`Wb|b6n1BhmPVd#StW_e26e;;p4y??k=7t|3%^EG zip(gn&d%*{*+@94Q_^iAVN=z}Pfxe#U=W+=mI;rDHM+G57y1gJTgfm^C4|EZzCu7` zW3o7QZPm-+WWlWzblNgA38xN1LL99lig@ZP<#f?>l@jkAXBbW)ZHEfn9pIv`m>WPn zRCI_LGa@cF@`em2WAF+o6QdQSJWoH zKyBt*IW?r<(}(i>2$PI72&%p-syWA4%CuVBLCX`OZ?(+tll&A-3+4{4c#?#$rcgq1 zsA`VA23)DCFM>sP#W>Y<&rznI=B3ndPIrP81Ph#^dqBJ-jUV?`n^joly)J>A5Z{6o zYr;~jBmJx_r!kCpiA8`sCuLqwE2TZF>VTi z6cTReA!q(o;!9ta#7&tgmwLrSOaN9QIj)UYlsBk`;%LPzN;O|o_q|ISZedZImFS8L zR2aqgid44>^|vB8Qfy9(yzMi)w@rv!IJ(aORZNS9PEQ#hqR~C?pk$GMEcA~B{sH$< z#DPJIAdbz~1+9$9H-~~F_ABRCVZR3X#W^0xmkvq0l`VLlX*sQk`Pp!v`n6U|pkhDy zbO|vS;~3Fg>zb6kgdJwgetp@|rd(@OsUIGAV{p4ssuMw2cvag*1p+&@GnrUDyg5lX z;(};O9JxT4;4JSbKn#!pc>$bw%#$+f4W5t@FBatQw=MZd>Sw`Vc;H1Id8;%{1f`C1 z2BM1>_8Ok0iim?#C@&Jbc40(^9t0ciVdE*E-|TQo0brVgR8USK6MAJwrcB4K-uibm zF0@6a;HuSY_e$n@9d|M{jS*6%$#wp5G7s6)R)OC+8RTFhe)Cdnq~M2B_ByIYVbZ zVf&+GCg(8s>)71nxE-r?J8ox;_+)=Vf;Xp_W?Q%Lb_lm6@dGLLLo{1NORL{BTU#P4 z_zRk?HZ2g-;9sI7FbTxjxJ@E7+Z6Z`j;$C}hjqB9_Y^xV;t|38H`Nxp>{4wNz{*rx zg2;(9IO1-q?E*Di1EJ;;0@!Tv49yxtCt?W95O;OBE)V?IPa8`8o|n9^Oz_KPo`y&s8S3IqYMU%9`h8Dq-P}w5>%9>e($#-$Xz5(U+6eCNP9a zqiHEdAw{KA@1uoDU$H_J;CgAy5yE{PvDQYG>s z1~xsa^o%pv~CH8^};TfTk+nr6&uwJN0B8hrck2qB#qj>bwL0E^uOepj^n^3kwqG z+QHLAdj5F}5ChoQ28XXWaxQEZt^EIuR5S9ANAZO_W0xyq~qGifTG5f4eb8l39SdX>{!tnG^h=Cpfl4060rT94i&D z+kez_mkl0p)s0257K53Xp+g~a#q5~&s1o_rF&Z)}uAQl%WbZK};grX+=;M%>3^{N} z{a7TwUT9F+BL%Y{Kj@~B+6Lk^RDG2VE z(zr1qx|RZdz=j{QR*9a}=aj2N$(eGMNXR{#A4D0oRU$`CX8}cMVpmGw?*;hHuhbVo+DER=Tz%fi_L?|{t+<|W$& zt=W*QngK;K73zeS7IVTvoeVQ0W>ajqXGUN$(b98j-OLM3h?zDaY!)oHMW@wi#*zU( zwWTFcJcKquZec7bCj@5-yw0LEVz74-$q}NGPfiHNXCll%5u{uhj87&PDJ+3Z75|^n zXjf~x`2viKS$<*1s*sgdnu~)P1B6h*#0h&)3Pa1J53?%mnN|jgQ4jOuj?$OL3YdWImL(lY0Y}_L zj`T_zHq-p_to+LyX7Y!%oXPjA<|li51M3cNU}ad0U;+@0YFmyqCi{YBzmZP9C2r^L zvGW`5g?2hlnw$ z^OqDq)oU1ilEk*y=tJB}D>Sbnul-?5BFU*Zs^2NEE||BHOAe1@?8^rjDzm+j3Iyx?`5I)7}jd8k5y=AQ^ zNIp}2yqIB!Y+Q3(aph1upaEtX5*r{kq#vC&ND!SWsPm)&_#bd8y6U{b7=l` zJdjHG-%}s#W<_+IK5%o!is;o|qlAnICvJ;-Vu?s^>QleF7g%ID2W+DfnlBtbIi?{cPuCzZzCLZ=3+=3YNY?qk-v_|J zB)aJLI|1THuoQLi^?pfv*e2hK!-E5!Kb_>nf_c;H9yX||Q{uy3XpA&f9zX*)nXu)T-dJOM}lJEoiL`t9L- zzxhH;F?(%$K|oEhz#H}5^FtjbajgN?ESX?lLAU2FCG z?-nhq)jcR|;d)3Djg5B2Mp@hJPv`BxuR;rG??U%h(Rl(O1?Nq9;RrRF<%S|nF9~!8 z%#ML4P7e>g+M-wRHefR3wpa%MLw>t@GPQ}Y(_yzX!Fv?@h5N?UR4R;dzy#JHc4+HZ zF?r*lpi%}J2Eoxof5Guw@5adi&ufDB5U z0+|c7OK9FiX-$L0u#O;8ITZvh3M%fwb zT-Sgdr!l#!)s5ut9At%;yFuQfAM7G;p+v&F%S4aNYr;rP%(C`kC`JmQ4t5wMQJN-E zz(JQ_)to}3usLNcF!Y9n|1L81FL6mJDIzCL6S!sG5-`zOV!=@NT!JT)9x`M=VQf%= zcIh2$e@@S~ShYsi*Prqu3x1kw6a8 zIA1rem#c2`yFz^L@32W#=q*3?zfzmwjjC_xH57McsW^@ROCwz7&UAYQ#*u zEGV?y#{pC?nU(=VdMaZ+TS&eNP9)gkXlZ-k=_-)%Xu#?rf-xGdg=^qPsvpL9l9lvE zO6LQ0yo)71ITEM-Xgd|SPt4%hHnT)ro*@i4eFU)x6bymK2*hREVvDrkln*85nWKvYLq5#J94i5;ULHzo3YLEy zPjHQ2sR=4ny`{X-Izhc@r{R2oyti4zDaMG^ze=7%X?_h}tm?Rotk#dCyDGc^49T!n)?mw{>e`?-M7xK2S&LaTWj(q+-BU$+d@+D$5n$a*n7s7d%=HgU1yW+r$@iOjE5mU^& zBAe^j0-qHi=qLzeTNl{^8H*Ig#)J_o&SRqz@qv)1)CP}**bSjQbxOcc-y^fz_-M1R zu!9Eu9O>bP8{b$%jnP|ctoc9RSR*u6dWYB5*o@Fv^MCQknrdw3c3?ZxMwY6vnFZvt ze}ezqYB<|nHY03#==tX$+kV(E$OO^w>o|Vl7P*QCi*}e{!Tom2c`(pXWQmIy^!0Dk&QR7w((HEPGQ(d!}k?)_d!B1Z_A>>vx zSW`7}FBH|&q%C;RYf@>iRDP*eL0hDZ>wDRo_r+3})ewsu+hkSTNI&=SpZGWHk)$j+ z2IZK}7IkRHS1eocH&Fg0eKpw;y8_LKWHI5V%*ulGPoHI}lTHEXO8}&yg^1j2kxde1Tsj`Yf}m!Ce-6T##60v#XdrB%83v?YS7{Soc&*tM8+Ue71F@QD|BQ^N z?tQ`K)(h)oWdb@EM}*&#$3Lgn(9Ok;%YVR9mkrj^B#t<=D~A$#YL{~ln(LiNhtAsC zLO%Pds7y*y9S=|g+m!!1EX)g+W*dFWGf{}W#uuX;sf_lv=G*dgWOQa4dq``*q;)>8 z&e*gAE>O`kwa(H6j+lv0|Ni4lHWjyjs0j%cQj!#L7N4k_k>-r`g2u9aHARygn_DEk*EALqbO-vpq@Ml6@PoF# zj>QFg_Uwg=c46U^pf=XF*jE57;N`F>Bq2_!l?R!^po1-n7N%mgO~1=sd_jOkW7#ul z5U5Zg*bz^UkSPcfx*NmQg?qN#8|c&_wV2L{c=+WJ$!vo^y7j!)b8sEHpIC5!> zWeT`76PfT~BBXfVvg)9^mPqnU=FdE>&}9N)2?>YPXv}O#8q4^d zD8zt7Ox!akMF5pyArDlBLd|oWo-(q?VhR>( z`M2!toa#IAH2uOivOb7yF&Uu>Am&}o)HpZ@C&TH6RsvR^x@TQgt5AyICqUuGgu*gM z)7eNwGCY7+18GSk)&Na(cryZv)$>u(ZSCT_+SlDxDufy+z++yp0^w&tPZ2My;N@tH z9@S@-v<7W4Lv^i)cd%$nV3~0a%1I!1$tD?HjUq$}0M*!t!<-uGk z|6KQk!7hZI`Vk1AJWvQ}B`f_uGP8kHGj$#)GJ4e5@}r)n0s!L~eQ{rs)=S=3S?#YTSy^IIDJc;d34G3;oR zLfg+3EH6|Wch|x|QqEORB({WFCO}&I3XVob&&)#SOs*A_kQ$Tm5D(zl$~$b{h7^$j4^GBZjvNXa2s7PlOE)djEAW62(bhO1;k%?h^goU0~mDS3KynaOjh! z$s$A0Gs1$b6atXOpIJI03|utQv8VfHovK9}kT$U&2b{G9yD4GTCA zeb6y&iW~Urs9FNCg~tOKd|A#HalU{7dVp0pzqWxpw4cS(QLT}p&`i<%Hu3^ZE((CR zSjmh&3c8-p*}I}E|*h9l5|orQrL@(Z3J)v+>=MB1a0 zvZw&$+5I(~$>Hnx*&AO^R+j&$I~|`MfYA zDX%c8Ce$vFQ4{Kz{YasW&=%73xh7&6Y z!N`ktEJ-5SZH#@9HXNjcAWHI<|6q*2kns$;C63I=9eo+`QV{XyqZ}Y|WaD`brQy78 z(?pHK)b!_1%KF-IZ@?lTUS@`YY}e;Z5d@P=Fibw(LwZ3i9BYf6_KgWDCfNdw~W{o2FJU1{1vQ z@~hOm4zCdXOZm}krI^C5r_KMKPJYFa>B|4Yr^|1bd9o009%A)#&G0n+x#!BNKMK(y zXoCy&=iq7cKiSETo4xuY-COyWPn-Wna2m#oTa8KCZSQWn0fUB@>+&4TBZKT)7w6Li4QZnKa?8OU3@TQHRx9O^Z62Y^MU!=sft-=Ro%k}(@@J2SUl{* zkND8jc&RNs$cLHTRly&u#AAFQ$U~=%Qg*(R15`cTm8cYH2*cHQz4fjST zH*B&djxZ&6b+0bD2dCtQA56(z-K{I5Rn-xz+JaC;`N!*u+tgD`eaGy9_M=;2i6GMS z*X#nV{N7kG2W64tvma{LU<~2C+nkPQLygeH;%Fi0kTp<|TnHen#V)JdZg>D{qoIm) zo_MmlO_$kNSR^cqkYZ*&%1&D=DC(^hjSb3tnp(LJRGO#HCoQBie!)+3BKHI=yo*gq zguuH2iz&U_xoDe+)3l?IHR60$(lQe^ri)Z|p%iUTdD3B#tFmAm0#+azI1zT~zg!$P zMt2EB*TK8wu&KFI2Lc02812V>nFu7=p0sfh+laPbvS~UXgmm`wk7_Jw9)$^@N=!z- zYzkLE$uO?4BGObUc|sRrr}VL`XP$}3$o=Fg?#2^311HXY*+_|UxWO(nDcOeV70P~* zBr7#OmJxvy?g|6#mp;>f21O}8)R~K99dUbSDY$Ra?9UQdaU2s9%+?9gaES3Kx$itF z>@wP`=I(-#J(S%~x%>eLioOpKSOHr#>LM!%553wU7_iZ-9qhCO-87ZKUkt=?{!3rg zONZEu^R^x77iB{a@|Ey^j@@Jh4!7y?^tBt)2wm-}LG{|Sefk=tgBYsvhpd0NEOF#d z6yyO+bT%e@J^%1rix6g6(Rlv_rNmRSz=MN>Sf%uA*Hu9JM>)P<+4!2w7>3yo%oFLl z6&XH#N8yU%^Y>dXB}{2Jl957IPSa>fm0ZA&=+~CF$x;PY!B)g!lHop+Sp@>%LN=o* za+%#V2u>`M&jiV;ZMwPbgOcq^@34}J`5O}u|Cz1=7(j1EaT)-=3(bmSO!r>QbGr8^ zJkvHPoqU>(@Yb`EC6S+}MKmM9GJ~Xo|3O1A12$F^Yg9W5ECKkSyIM+?aUF_PkM2}{ zxc^<8kn83p<%ca34QDs9luV}la48=wjL*~$pXI}z(^YjRANHQ^!_WB8p6<@z9N^us*J(@qo)0|(d@8NX;#z0@h4oRP zMOs^z7e4h=V;psA-?yG7gXduPBFV3hyjWz9%oZc8*i5cf>!ZiEbgvg!Usw*kJ7jl7 zZpE)yjDRIoxsv!1gSVZ902R6Cch|LIcXi!IF9f*G!D+8%bI|jjmE1; z2#K2s-7v8swcor`fWT_7pI`D*Kv^i@zfT$@^j+gJjpf5}+$U3>--~j1U?gxBIs*w` z&MfexquT_LR|N*pA2UoC&J%n^tOiY4#bruvHEK&Fn_gSg6a-@P!J>6`_ns%cLeALQ zy|ghku(hah#P*7a1z|PTYbouEnmQ^n*S_mpD_US<(}ZW6-gzDjnDok0?DbfxB&+<2 z#CHBv+349=-|QkdNO%7zI@p$0=9t13XcQe3MVRIO5&3Ie10U)F0yc_k$9gpLt?XY} z)of)E)iGNcG5|pPU?vLlG=2z{h^A*~u#GJ(-f>!QgA8?>9@Ek6?S6`QTRb_~aYkh` z_+oLzT&o%(Ic)?RE_Fd6_eXM#p>}{%w}cpi?`*j1y+0$}^dux8SQEh^SLRzY<;o{F zSZDuNwjES@GjBeVw*NM8%aST#Idm85Vz@kluc#(_5qCR+0#fev4DV4v`OIFo!Kkmx zeOz_}rha+1Hl+#^&?2R4lej0BUtj5fG=?R`}`PECUy(; zwc=F8BGa)k)H6Xq97ksLsNvVfM6^E0?}0%B?VNoRoe2Pv1 zzR4h!DuGXSHf(Y9@a`2{GQlShM8BxPdJJz4uVO|h#v{tZt3mUHp;IL>cMQUXU{bD|I}!W)h9{e6ryPvFa=sg$}`Lx-^rR03h}2}@CBmx)`#i36}j zf1QM#}CDAs96D4m7GU(_QWS8w6y=F|~xA9t`J}6BGAa_jXM{ zk}zh-i=)Wd-Eh=fIwp%50bJp~#sY^QaC+~d0%sK`tR;6;it)Ucw(}`HKZ~=Xdwpg(2@q&)cQp?Xs@7i6%UFXK&ru2M7V*`ojd_KHCGc z9Qgye2@vl^I-QZdr|Iq3EAf9>7-_ESpQ!(t{PXJr>6xa{jsbe_Wzy8sb(u600uY7Eq|pI( z!JomYUs-6{SNctssb5)BPG7dJnq{|9v0cjASByB3N50VRmz2QRm}8WGr|vD;{pJbd z9UT|0wO)ra8!^RgD8|)|@o2G<2t}I7>aUK9X@z3)W6;&shq+CbqDX8vT<0_AX@-jD z%abnuqbcniqVv5AX4sL|G!0S!A@P`Pr3ASzSp3+*P1(7Hx^K(QQO!1?bX{3Sj2fEP|TUWduubqfi;M0_&tTfY(_m;_|KR}#&yQ0%!A1FnAXsJa18@aQ zRxE~o=KogT_1AkCs-^~iu=TOxwAk>~8(RiuvwTLK;BW-P5tpV)AGYqU#FbM&oQSkB03b5B z?8d;>70eRF7SIFnaN`#I9l6M>{5Dxjf^_3>MBo?7jMK6k0qS!+&;lwnSX@I&xP1`t zbnkvwAGY#gT2tk9eCXc&u1egzyQ;p*hi+?KRpJMH*ryxPXm@8%OkU3Dk;C_LSP*5} zHP^d|b9kw#-*?P#woG)1D}QyI(_M2J_2mDQwxAODB^siXL1Tg9B~X2~s$25`VsIQy zIN;(Ol1@<6Kj?ChRxfDp=>RYyiPzZQr$S@XvPB!UB>B3atFQv#0m$2hFlI3+x%rAQ-rr}7f<&6RRAS$09HSSa zpF43v(~tN5`Je>;BDGq zmtuwolew??pN6&)hWRxnp1f@HVAU&cnN(?mv|h=YqDkF(#S12~!$=2rkX(=vz*ZlD z^S-&VamciYow|rU0FMhln(wgmsVCa?zwEM_gO1!5R4sDHUBex+H{vHHbHG(|trOeB zF%B98e4Z1QlVXBsv&*{avixuZANK41Fc&K0(6*g4m-dKzqRuRqHxS5M1f{!MOX`!IIPUm?Y z5vBNlA}tG>4l@oXdZ~A8-E!KF61;rISsu+6 zdg*`|4@d|0oNl3**Y+@`aPFA?)H&V4BRh%|-u5yQ^>b080{7z!s0dDsZvi6;t6vP? z<2HrmGcTQAj)>1@D8Oi;x4==L(7MXXg}KdH2z$})vnWj1Imel5h7Z(0`Aq<9aII5$ zBUk|dldVL|6+`hZy=7A;@0c{Mo!_2jhg7@44brG%lP17JmK zVjv~b1&~?^V-Pl4v>uZ(I_0pI{KM_Bke9MA`C%)@fwXSjZKt{Hu4bP@wj~Ack%cYP z1NX?oR0;=zn_ZS;)?JPxZLK;5&rpa>l_}4iBnHhFfO37 zFOPqy;rjUkp-Id#b|_LO-6;v#LEU;_3iFhc1|#K>+9#oA=r6GY<1+N5<6`zqHpo>} z0W*-iMOyQd?IAgonufYRcwkFl3vTYzFQ$c9@+C_93Yu?(4flbVYW#QeCZ!zzq)V!Y z{3Xomu9!%Rv?p<%+Ib@YOf>s9`u3Nr@Wdf zG08PmVm_!%MXA0AF{uFuP-PtXuVz03_{jeFK);VCKcT$~h*?mOO%XiM8S#?75>DWh z7KB;f0;+#vx{bv{t-DA6#67VVlz2%z61WfNte?BPz-ugV3CxwB*RUg)J$Ph7)wIEC zAuQ|esz*9Cv*@=|RMhPbCas`i#}MS{(T4j{ z8B}VOXK=JR_7~ekj|66`+YN}~k5ur`qa16cRSwo^*)gt*;;BTz(8TA7akjhaP=9J- zY~(U^Mho%f$yo>q%>v>qz#k|-yAAUU*WoJCi}OJs>BPzwU$~WpnMzI$d-$7e;7Y4_ z!Suy0+a_u*JZ=q(18n4RYRIKy(ka{hVd4zMeMKgxcBs{^#Zhpb{N1)2x)d5PFMwn4 z!j!0hRV@qU+G6QT8GvRfft5~zpCK|%GS!t!oGelyk9CNQKGjL1{+22Afb%S*Lsh&G zXGmG_^OEPGdIqeb5h&svZ>f^e>S{I2d(0GB=h0SZSp9-<;4n+I=rryC47uc91|9&I z2wHLkJypQ*VesivJ8}1zoBBfx`F78plKiGVa`(`y_|9~V$;d-}<~2>AZ-p*6j&|;& zX0bDl_~+jSJ8F0XpOLJJAxdNJ*2jphp@3i(K5Zo7(gxCqdsB!`Z4U^i>jMZ-Y)>;c zSlj7nO7`25kSr%D-^n3uk*SDQ;@69YbHK$eiNAM?1~g{vh?F7YSJ~Wzsi>OO?1K1k zgQaN($gQsKQJ%twEvvWwn6g4%I&Czx8=^-mdPY6BZbE_V-xvx-n@i&MZffPDo3Pjb z|A&@}&?B@1i)+&t&*H>_*p%UuIB4K*(w%DJ6k(s=Y_d#9R2Q3&+9EZ(#gt)ZS7Gw# zxEuKPj&28U83nRJz?r&pv>2=UutqNlH0Gru)OR%Sj@;gf>A z^UGhmEwxWag52-enkPRC--tG-@Mxp>qmjMnm)b~X3|i?k44u<-d1 z6~r1ZqxWS|o;*DGpVP-SYIpr5*5 za)9eQ8Xr(#G&hP-3jRyt&s*R=+xkv)YZ78PbvIwta0d}hmmyrIdRSe3ki( z^SV$0SDHvnCc2M;;23fkSrY$!+=_fVSBGqo=tVg@@{J6fo3xk_gal#|{AsoVjot)8 zQ#$4dF+qj?2JLMuQC0CI6$9=*yBb^bP26p#<`Q~q#plLPu2f=wcqY_^nW5^MA$9`N|qfGes<)Dcg` zWC|2P4pG6&z(Jq^^v~6*MBTgz_GsZ86(|?wD{<#Z<2xuJf+m9OAi&5wfS>Qc4M=BK zD24!N4<1bHH4^apJCy|~o)&}K+XTK;Y#l>zkmL7P)~a^}dL3A%e~?Krt}CC+j_+ zr@zpL#}dEpmXf0J@cT~vp76hkY3woT=r>Bv4{Y;PSVaqZE6;Z($&IdcMObG}D1KK0KLVS-(`cz3D3`gW(EF-@+P>&ysXv!lyT&g^gK!;4mDKoe3 z4qexVaP7h;o4M?+Qj>cEi^V|dXK=HZANK z&D#WzzwP3i0*3{CvFL3eet^kSD=*Ol8=_w#X$SJ5ZYHsA-8L466!+h+wp`crJPR zC{_izO=3qLPj%s`E<7cG64YS=4sA&|e)X<8M~}U9RJ8bxbX+eaj3lReKB!sV%u*G# zfVe?Ast>^R6rU;ZLHTCE2mKrsY1HyttS(5xJKuDQU?_{D3C(lx zY;RiwdOi}jG%Yo?u*ASme=5k7vy-QIsF;Y&MxL6_7RL3yTjBR%4*oJmC$=dpJA$Io zLVrAWmg4Sitvndk_kAZ(FSRa%n;PF%6iz6)dP!C25`B7>nnL9kb{}j ztN7KCyWmR*2`p(Q4rq86MuqCBZyKut2d3_K_cYu)Z8_>E8!a5KW;nK7DFPyBe>0Eg z(pEiHOszjuE=ytT8X}KI$qV3b#^;{67j7*WmK@L7n+lo}KLB7xuaUvjg1MEjG*kmi z^cEwIHR|ZGy&V;3glx|$kl+~d48UT$iV9r5P#>G>W3%idi!}i94?k|W%j!Kde6`v+ zvbP=2@cLCje_6rGamwHR3vP((RgMhhqtlfiQ2BwfJe$WTR}Y{w_p)5JT!=V6UHMGq zv$8xp#;LpUR}FVVFTZ7@2GhiJ!nZ-{TX)_jBvxGiGui?p5)#*TnIW`gCIWw52&dqj6mP99|t& zz8c`wtK+}o)zRgvL0-Kk9^uu?!>gZ0L_K@fzP8-_5H6l4)8Vu0V?!;T7dOX-PSlgo zs;|{^aBS#xJkOsoHZ-C%&%aihIMMqn&GSDiJ$r2Exk~eVz0z~Wh7M4g=SfQMH8!+J zX`XLTdfwR3Vx@V$QR(?(LkH@)U~FiKp3fQ^VnfuOUyKbsPtW$)(DU_NI5zYGJ)bi+ zL}*7sX^suOS?Q6np%?1;+_9m9^jtJHbg-TWjtwo>bLrU7A$mT4Y-oj^%f^OYq~}3n zLx<|Qd~E2&daf87I!w<)$A%8q^RTfYA@uwmFkM~Rb?Qtc~f)nWTJm=QXO^jk$8_KRKtJC+{Knq4R6fchb(~^ zeDDpKyRyo5QszExi3VR@pSkO*GXFVqH`2oEGIvw;j>K(M;Bpb89m9-YRiS=00c%4d@k_yWA4GpYP?F`{?H8;8B^o!rtx4I}$fqA|r8R=Duu+ z86=KiK$cLqS7+`XOXMV8mbnM3GGm$hW0iPm=FUD>U1TR>a8>5sX9?BzlFWU?5^84@ zm|KFaxj~n?PgjYRnY*=09GsA7u1cN2eoh8(r12gvtOQ^QR^uQ9TZBgdFW{EvW9FVym zT0+QrZsz`M2@PTY%w4!eBUPCZz-tLr{a1k35*pldGWVG(@oW%m35{oA=DukORj^;? zerySKv<+QaLPKa}ZsJ{PM~NbH@2?X3W^S7$L=?};-1U}F)%#@bi`AC}nfrz%)SY1n z$r7qbaDlw0le7+i0)pHq~B~)fF;ARQ+dQZs366*CF##5Eq17ul3aGRaE zhpWUaX3hnwTHwu@AC^#UGc)&(mQa})fU5d3l)3L%LOmGF+|Ml`EM&}DOQ@p*p#H)V zRQ;gd5^ARjN-Uvv8kzfoB}B0G%>7GMrZ00pv4q%E4d_@x6{PS!ONd+&hVbqtJ~EDD zJLqXhQ44FEAwIHZ-$1)c3w-n2hA4CP0D+76Tb$r z5UQlg>Wjs#k7Nwi>ctF(dq*b8i&!?&$nnwY+ds?xx@rqEqIV(2%B6LQI_us|0FIsm zc(O#uV_Ck9EcR@tp6b+}8{W-sEw)a7Jj~ktG{U#bUWJ?(k`=?)X=G69qdDsL+0hJ!Q~A2-1YhSEqzl-?;b!TtB2+`AX3W1L(g_rb=f+ zvu@iSFtrphK;JL>Kw||6+^AtJ)nN|5`AB7|cjc>mPPzFmGXyW?T0_ z;zEThmYZuGE@f1`%W?7VuMNxoSt5Wk!2WiOz&@BcyI&eIL7C!YECU=NLA;Ig>!(%(cuT z?Kgo&@3ADp&R!)iK9Ojf?TL$0gE64LFtIZ@%o0|=Nqz&1BxS-`nna7SjO6`5X8AXc%5tjc!BW@=hQ;7&4jBC8^{L9 z?5omj0w}ntPiGwCl)c6}jD~$t%Hchl)fT;VTht)465VFiXD3e#m?<5^imlMjShO+a zzMNhGw{c+UyNGX$;0o+mn(z!=ER$i(gd8(S;M9`;ZMGDw3z}O9`vPGz?j_*E14-Zz zba=}fK50k%)A7_m63I&c&rh-{ys$jk+qIi}=6sm_e|jo9-sU{BS;hu9$K!a&i%|k* zdHdSc1ws@KPZn)0Qt%yKc*isPVqSx}m_pCHw#5SgFr$ZWp@4XnxfS9>7Uef%8q&n)z7Xhe|Y_Ou>eDo}gu@BMCY1xZz3GmwO`}zW`Y8Kh0;T%_sps4hGO1m3) zo4S6S?^0n(<_{`nw@8th&3U(7_f^%WugaTx$&b+=@}>e+Y2ARWJY89sxd)O}AhRF5 z=9nR~%HZw+G~9q#K3qrcADY7$go@hBWp4pUx~>N?bQF$1k&jznB$Cc#x3XgbjZJ;; zXsjAw5m~Z>V+*Y#Je49ue>BI6F>b_B-PZ{-n7H;V?xslcB^im^s|W$U+PQ zAoU%RHsv0r*lV9ge^td+Fc~BT_bQ| z5tiw9jSYy~B=YVMTwUkp2KN>dVS}wZXD0M^pRPl{ry>zrNm%Y7dDrKN4Wy&^a(A7a zr<#(xioqr0@h+JYceX{~+%?iy6^(1xa-7mi8;hegk~wgQ z#nCH0N%C1+{`r;6Og^`p9J;h8rD>Ey)n3>)HG~9n^=CX{9?Ggz`OlPtdeH2(+GW z!4SLi+!p3pf6-Le#K^hB%YHS+83U@}06b9NP&by)!58Txh)9L2(2-0%CIOj1<&to~ zSC@N+=-FVF=Ip{-?xjmX0|%))aoS8Ob|H~-FS{U-eKxVT^&T1geVCc4Z8MJw25QjK zVzdh>yjzbPF_OivsWS@=r3{x~tZZTjqCL*j>>80Q4$-Rdctq>j*Eij>2<)r93ELRO zNsx_fyViuSe&5j0&q;0H0kyb_>^yXT6W*fB)iEG6k`$&j*sRxtO1N*qrd$~SZbVa# zf%z$C{t$w{1(C7C;IL@FTRA=>MF%tKLA^`6>P0=_4HvM`Ep=kED4gd8szsTVzlIdk z3-yG9iPm&@%|rE33N4P7`$NOh7htgL__K2g$RDxObACo>F7b}P0*h+MA44_CuM(sb z5nUPZDU+y9K#+E{`76vlf|HH^r;gsqrFi9#lA7cpFYgjC^;37mdE()zDSCHRjqfT< zrmme^3K`#F@#6bTVGUC#fXT9vQVs#NjzR9v?!2Ohng^rp=ZTK(g)tyD^}`GA`(jMO zj`V>@cZv0aExPK>q`hGuQ!I^Y#IKSy2QyPVn6K1x=&LP z$kRG2=6VRSA7s_?b6D5N4q(W|^UUU2j{r9OB!)*(OphQ&r#Jl+K;S62>(rZm)`?vG z6a$yX=YJyYr_KEbwCF~G9uWg!Z zHrxk1%UySq0b91CNNG}($DM4V7{S>j5k1qr{I0rWqM?aR+Um|i!#5=55hbK-$2WoI zF)&4dl81$p;pqdArz{Jp$J3@3*oPXy%W;&~A}xO^KjDxkFW9p!Ux;|2tmV-0OK~Aq zfEvL>z+iT`E$?mS7ctd6u!5sqXhsvlH`Dc^BTv_g5y!crQ~Eyem;RIu+s3i_Q@+py zJH@CcUEUr#I9*c=ZMVpUbx9w8IUwrblv-?lbydP}mMcW8IWW;LO&%&Pk^8uUiIs z<9IMx*7&O;%{-R)v329h#^}sM}yM?bt_&Gixf5qP@ zW$M3h8c)##89tu_N)-Hn>Uzy6(y9D+>GtS(PXjAS72hN1mcFXYPV-tlCQ4TNf3l=S zGa6}{#z~}G&tyMCLdXqOU6l>MGh%a0<(oGngfHHVkUbJ77!R~c@CQB0+Cv+*Mgu>r z(ac#Db%xa=4I?txu9Kc|do;%1pJ@%Ru@TvQ6ugR-YTy-!;yJk;Q^qQyH3D5{rt_P5q4tYtj)Zn-CfG_PA5jZ zZH5}DSOH74yahQNoB3So&pI&f20C3_Dn0V?^ea#tN;L3Rzc;M}kZ>wVpuqFQ;4q*| z0kELs)<5V5j?jq$z``+Dk(hn}_z!uSkr|L)Xq_nMx;nLxTWT7eCBnd8E@1#wZrq}a z(*^KyT@H-lK_Azh7^+?{Cw+B8)egG8ql$F&(og;TPW^xHV^1e_Ntt?$KaC3hP#kUC z$nJ4KosJG{EV64ypNcna0KgJ!zN-eMHY3xj9WheWxz-lLAB53rdI+Jj^IF7#54*vr zzYMY5FC(-W9MF3KvsK)`yWwaSbPb0-_#WEr07cu-qW(U=8tEkC_jpVGNu{a9xq|oxsXvdo&zvST8j4&=t zFJKDE z?`<*}YQ#Q(8c>9PX5Xx?A z_gxyw?L6y^LTjTl3S%o_6v`h_qVKX224->#bLH~B$cTC6|Hxkn4>MBD+noyy0E0R~ zYAI@z)M}Ug9@+TptNYeA#7xf@|Neg74^Q)c^A*jWu$nS?s3#xceRr7E^8IId-yNZ# zeE((M?=emN-`&mo`+3j#-mVe&cK@`S_vc<^6R1+`)VHNO$Sa z_j!NVH199Ds@W4op@hRVyk9m={`+{pXqxvIeyr&hPV@dc-cL8KFYB{uZjXpY$|idW<6~YbdUttDtNJL?T_4KGlp0ca zs`eHArnz1FLKOh1s?NB8ltr_cN7-D=*!g#owsxS<+L+F!eq@iwsRE>#X`24qCc1}| zX_SuZjQZjzih!tqn_p$I8USsf`$XFHn;B);4}0n<_QR*_jWK8x;ClWM7BJ9n3rsPC z;ZZJifMGN`feWJ@3QAe9v1tu=TDBs0$NT%;*WTaHeU9#%JZd^{%>qrE_L$-PVQCg2 zsM0zmExurs>ijh(Bkbaf6B@smdcB(T=cVHECnPe2U6>+s30I$Rsp^QbNhd!X0z@8KT|XI<)`oS%e$cAHwYMHBxtajL3K9r zDN;{90#s-G)E>q!d`r4#Tuf50fLk-S1EfO}cuGjLG3#lT12}W(0DaPMOg~Ju8>kIQ zwgXdF9HAHqva$Lz=h8Xq)M9cLs$zFah+)dG6ahD29a2U15F2Q`g_&8u2btTCK$;D9 z-4Z`%J?$_bHH>zQb(_V-y*K{$-*3O*zy0IA(Yd4S)@^y;9Ytew+ihFkum88F zyyv^+E8ekrGuh5M7mtv#;8~m6jWssM)wl7Y@weMudM16;mX1xQ)V=LAbS+!wbi+nq zF$GA1S4_qrMU({K>~|_*K>*2bmhupg06Jp?;=?&1;761be z27JGnM$MGAzSy+!4Z)l=k8Hwu_R7z-`E}T`CN`R9UkBz2F^X#} zk+Xr(vfo12CRmg&7JsDS5}Q~Qm?BtDaP@d5;Af2DVYUzb^gzEowp~*<5d!Gdi0NvO z_Gp#AMaW<)b?ul|Ay}LRzKb}V9V9FV>CVJ-rYJ6Au*zA+%!{43`|`F5%Q7_&Gl|bs z(J55Rn4yxxteJrWf{z^K=MW(#|4bu0H%a<7Ds%*UJQKeFT^xCH=Wg3$or_qVybb2R z*3Ee@Ci3)X_X|vpzJp z@J%oHJx{;Mr}uP0#rd^-%jV6StGHgbeWF)Ytv-B^jakn%{3@dQa5p6YcBKrQEJTKPU(5V-CAjKC0`$QE{SiaW-m;9LwzmY+T3_`i)L40@X>~I{ zFj;h3Sdi>7BK&p}{jFVJ9R2tcYyeus9P>+3O@tc&yE4l3^eki1SZbJZ*`Ek1oZ{QuXF=4_w*Q~B^XErNCRiMp;ok| z=c2z+_Wo&alr@J#SS%Zr>sm4R(N4J;b=AkJp@*kw2*AUB|B1=$=9$2!O@;PurRstN zVPQ8Hmlf&hd@p)2nPu!CTKj*nf%LWyt4JA#*2@!#bTPS$tn3LYYk-HF26qwZFr2QIcS!zWfR=_L`Xn&SGf;NDF+hG&s?(Vxs zg;Wduf;OP!{Psp|C3n&?Yo8guenDfBay=CbUtekX^NvjZ72J6cwWQbyg*PVNL6Rwp zBcKq6?GTA*1`4!kBMwN_=t;B4PY*wp4^XCoBCw+s&alqz)j1oOMUA zbR$h5Elb63pjrjBQ4?xFt+*0wgt2-u#VIWrQve!4E=E{QX_o@lbN#V;1UaeW%s57# znEi^WM%u=&=Eqy9o50vxI>0btiP1SQ>Uauqab-i!5p*12A71bz757 zb7LxAshjz(W?19in)+CAz4Il&%9D{+vWln-hD3y_+WO&_hrPHJBDHBzX4KQ|?lSHk zpjk<*e37P^ig7))y^|%9q{fceh8$oD_JNVdhGJcrEL|8-8P&upP*ROb0B2Rf|Gb3r z*Le3bZBa>@c;-p<&>7vcV&zTJGyt;g78PwL%>nDWKAdc2(DuZ$2(H$?!q)9gXk2?< zsNj$?WHhLYqVbg(AKKRw(HfGtQrY1skj5>dj|w0D>bQeBw9t`RVi z8XAC$@#$V9h~syVz&+|_0qSXMI&v0K8J3u)W|WVr$tVm3n%ZqwDPAEe+doQITG(L3 zyX;vsen}dpyIGfD1|qL0dw%6QX$wQWMv?t|u&%JyL5+oDc!R-c{F-YwXV}~0v-&z4 zkSM>!mbMgi8J1>lYWHnyH@V$Jc6AhXJYqT@HnjUq0p@-GYp$gTCE0x0SWsZg29#se zAsI4M6ehG0t-d&qy1h1y$2Dq@H2&kRr3RJmDud2#`FtkD3-xtDDAjvoQX7ldx8#@J zJCmfw0>`hu8{P_Xb33@Z*|8A}2L3;eki4>pdpynC>1gaDmP)CXd95q29YSDwmXHMD!@Cn}8D7FdkNSn@K#27fseh=Ti)qG8(V7m4(XMWcrb&w-NG zaGi&$tF45tWRy<+@9pmJGsandHL13F#BjSle(mV8^H^u8I<4Z)+i(lq5oTG_6M}}E zwzc7>S7;vj{SCNCA8$UIy?v@JD^QS`CxKI1f~cv1FpMhQJ05V#h(>v6L{Q8!9EG&% zg0GfEgcwSZ$KzZ*_;7YxUz%`HrA!4k(;SxI5K9*u@x;WAr@R~ac7&Rx#)ZymV`$a9 z#jB2cywz`5IAM5ZR(+xAXzLXODI_MCDtuCF_>le4!+Cf1#Kcch_WkCsYP@dF3!d|_ z_j5>A|F$s~s(-8c|McgfAHO&`eA}um*T-+Y?>9#rKDlkxi{5+6r$^4Y@)mx+dexR0 zufFxGdi#$5>%nyPcfW0KU#0tvEKtOl zKK1jPjKNhHfxMuyKvtbevo-MXYGR1&AVbhFc||J?#qU2{!<*FWc#?X`@`u8O)5)z{ zOIC6}l+%tbBQxUYRcs<5BeIXhDHjGCDP5}NlV`^zJtywcrd>hOLw}~t6p5Sg-)Oa@ z;ltH6x>%)48ZL|ctf3IfK9dI1m9g^^s{c_#hmGRP$SU49oc#dTEj$h4jlPTZT_nb& zUF}7c?EZJ)8hs2|aF!ADZ%h_AC5)Om%rjr|GP)F%TonH`Z}8R6|K@*ETQ#&>Cad1U zxvywN&L3GTyWRC&Je=>gGoaeuJyEmC9h4K-$y4N96EF1QE&hF;pBehi|?9z)6ggN^KXiyZ~yvBKdYZl zZHg{hc*K|VbI9b}clg=TCc{%tdC;nt68w|fwsL|yUEl6IAieLR{_WMb}Dc~+1vYICcNuD4x^M8ZEbYhsxsQJiLhT>4icoq}eu!>SC$u$q|qQS#5i zsA^^CM?)JNUQKDpwL3NEmdT6zsiJ&sStknOKpmrjO`*>X()0q-Bhe!CN!~VCxRHS&>Ei@WD}5pTU>Qrs#oi5FI|^$M~@lNqx)yhYMi^E$D4 zeG!JN4V^ONhtNB@+v(PJt@F9IwMe%@!S-*}`}X|W-jnfThizN+Utf9aUh8gs%`Fh; z(MFtCL7d994Lf#fzgB&F`Iax<|Jc~&^WSj8T{lm*i$lrHRV)G=ueOr3ghJTxFef#GRT)h4LGwz?#x9Y9>rWh&}SgfS= zE##Tfx9Y7*zQd0y0ZTtsFXG-_2swLqL1^ccUU-!8R&`SE&9}6pCz8)<>f~gKG<72T?^MTt!l!w=d^#j8@bWFR`%zP9 zMYcm(f)5nw0Zc*sTd>ZCdLrBp;f{kWT3bUfg6nE5zu7<&fZgBW%-C@0xSRJDc!5er zM-I1}?v!=%e|A5;xS#cKdxq8Co~>Bek)YQsX462Czzqh98G0R`#`>P4VgvjccaL0* zBdFxt#?x9d3GBeC`2_AFc_S@5$lraO#2+i{f4#6Zj-Hp`u5S*HDNmt%5L zP;E#y5nrIDGt7$DtOn%JWsUNUn`AYr)wPm2N0H7~CF=7 zIUWdmx+TTT@z!2$sUCY!;9hn_=K{TC{aQdIdT_vaZEGIiZUj(!^Sq0*^z(T#uwiQ6 zd%U%vAlOc_GL)VjNms%un( zVX~Yi~K%OwyE^QtesR`_bL_(N$yDuU`C}n~=oZE!&aK)Sv%5CiqZ#$Kl&94?l|?8jCnYOZ!gwXSWJs_U|n0anaH3 zeIQ5R#H55yg&J+JUX1g)X``&2o@aD?AAmr&Q@M_iT)^I~4jb?)Zm~M+0w*}o@!=vl zP1f@w*Bs0Vgq1L{159_a6yD0xOK`3+8N1$#Ao$dQKMYUD-5D2mJJ#(>Q7tl zNIt)LwY7ymv&7CCbe&I&*}yV*DHbjcf?}2-R>b!8XjUyRLuR}VbW^`gW;!F$GJp#! z0DVZE47;3B2v*}uNfS&6^MzDGKk{)}0a*t}bw&R#Yk356N3zrX_cl>`dpI#`F~ zr3e_MT_X$n6dm?7mK?5ba=B45gN23c-O@TW1p|neLPSEGfNN)tXf;bCsX(_Bx8xRv z%yA;UU)lJ3y%NQh(TfeNwFP&J>`(ZQ;?QJAp z+Jxc&w+;pEua;Q=jx~z5*p@jlF{}`roF}dZbNN#s%92@opkovUFaWk~_oe90kO;k~ z88cCOwa=U+J%}sXNwkJiH$y1`dW=}1tm1JEDLL`5Z`e94#29{OcrmQ%#k$T581zaj zuN2I=til{ubf;j85$4dz`Alj;*?ViER^V&tm4lzc#=7_~4^O(a$hpvAzhDTy7|r02 zb~iZc7t{B1;LsTQy0K{NNfAnl+Q~W^jk;S6>^5>5mOknNIXeJ5Gi@0CTCH6MOjs@) zpcKgc5#koGADy~U9ZZ$pWnXc{U>*C{HL2ve(N{B{we|z`nYl1_dooKe^~ixLL3kL;-3@j_%VezKJY}j zaN)u?Z-92&)RgZAnYM-t!BDk#Z#>sRAfF;DrjEiuB&U&*h`?a3w0G~?U8TFKR-Jdl z?wMW1U!+;u?B>dddMFSy7--G$n{T$zXsiu+mWSW}^BZk$U_~RWlBE2iEk&lAL~JlE z_8;xAw~!pnaDNF=Eb{4sOi@uUHS=pc8z(Dtui#di$-e-Nst8TS8z(y)>fES^tta0o zi>pqTJE;lO;@J|JzxmY_C;YeSy8ScdgLUL-|MuMxX5NbH=qZ~z)WP@kiR$?L`Fjui z=C?L*X4!1#nn}!CUVJgZIKcVptW`P&?VT94kcjc@SmUv*{$=#dT7(F*^)D|Q~L zr(Uo4>83MDVi}GuP_#0c1FFKXbMn<+w+pT?{lQ@dX?NIR&pwkBgaCIj)pdh^U@~iG zd_$i0Q-WxHLrNO%j74v=r^u1ax5UJmIx+ire`pU&n8Oif&~WFDe2@jK63hgbpPN6y zuc3*x*KWmp(OOBy%=_29T00{Rf`B>r0lKGHe@z5 z$V_SY_k(Z0YIfFwU5tYRab%ycA1D2;9e=GvSq0;_}*JTv3uhSf94z4-t)PGiq+onl@I>@vp%~-67VfA|31R- zRRnoAJ}z0faF^DBA4k4+bqzxu5 zm^Yr(f{uNN#x%T8L9v0>)TMzk3_o89YlGVESz~p-1xWqAfeP9BV^#KByR5Pq{a>{j zfT)!k7QbXn8U$&Z6uVpb(AF$2%4pc;I~%rn4DP*uk?KUpnhK9t7~623%!J*&l=@*n zDn&ZLW#N`TljP9Iwr4akqrdQ8Gtl7)%K*y5k@%K`hy5)awkIs7&xe}N2jk}hgP->| zpYM#HZ=3xLNGLjCjF~Cu0r@CS23eWJ$2!9K!{NwLtdRFkUpY8Wm&ie^5qeY%UdtM- zMS>lz*k+YIn{Ua_#W}0Q2AD-C4W`!p)NY@`ot~ba$+z{`9n{qItqt2)vbFv<{sg1> z$e)_1WkuFnEhtDfYhwpVJMJ2uL2&UleHUwy_)bOobH&x$_F3}1%&RFQPo(b*GhGqe z$V0{@%tXB0;CKNWVP{O*5m9C2mc;*TF-7 z^ayqNM%SS_AglvM5qQehkR*)4Y@y59zyHLu<@0L%YZ#z-e%8PC&Frc=L>l%@x6U7{ z(prHhSp7=9vGtO%9{XObJ+^EpkqI8moufI-T3vh1zS)|vWjM#+a@cO_4rZ)s(F;;B z(j+=+5ECYk(LZr!iuzkrVxk$#Obj}k6(J*43NHckd;ju^gTps|61nq7SNzt>Pi?+e z!msu3*E6yEWnLL3&$sbsUi`I1|N0l?``i38*S+?vPb=Tgy{rH3)#rX*ukSMB@aw#a zwc0ZkYHp?|c8uitl_OjIC{N9}=FR-y!te>g-HXD}E6>+x+r(gh0qb3+!c~{ObTdPc zYtMuAsKk(pie`t3NhD3hY`Jlv9e|WcXS7&#Bgi?}V;KV-dt|)lu zL4+3mRK^hF{jPg0tP9w!Duy!5eziYN^Ya;$5vEDnm|pWLrWYVlE)tln z^e~lDP7_4<05u34CD9q2{az3) zZxFs99MD8~gNyoQLsg3C7Rda@#?ja~;lvEdbo~Ok%&t~Ea#rxSl%mkohH8Jou$9HTn z3(6?c1jP2M_cf$`q>;E7q;=KFYsO??bf5z2Gpw;pS@Z!N2@y@kUmP0@07tVuf&mZ` z1MT8MTN}&uKa^}_pceMwTCE5fCeuf|7~0$?G@(~nSuFWQ+EmHatO>SE0YIwk>WjvT z;+8bY#wj#y=NH$oV8Ve{YF!{rr`okwLpEjv)-r_eEu%{*kC%>x?=xFuON!F)<|~LF zHCZo_`9`igFFW7r`l}AdDYzY<*dNOfj=aw5I`TU1=YHn`vPdhbWj8!`7x$z(5+WYL zo32g(d#?MhX>)h~s|@M4KduOD&>Ipwbc+W2cYJ=SamK_GX->{RD0JtaN1`b#|4W%U z&VLS(%H^jl0RVmx3g8_a)n9J(YW)*GvOt~*I>E-U5Zj-pg-H*ZX?5-%%7G(l#_G=N~C5^v&o$=sH%63H`oZGVAT*oa7l#&M1Lv9}VoA<=^8K zZg;cE=v$ybIdA62k!!yG*oKSQ5s8rT#YS4&$AU&*)$`^v)%uNCB%BM;9ose2tEnZC zXC+MQulrIPUlUhpDc*w(SIu#--uSXhzVex0`4$e=2i|$w%#JIU$TUcQ>9NNSZTu;{ zuE~G)ullclyH{Sp2``&cfkNr<#RFCXA@?RF* zxky=GSH6A4cb>COuP0o6`1=RG@(R7a>zZGFCL9T2*u zMDdoYtYC;+nMClAiuxHmDcI4zj`5MXA)4iA|n~4d@KKpbl;>RcL^O z|ArWqMGzv*&z;lLH}2Qn8g=W_vvw@RX)>khW1VMQn4N*mWxV63KinXiLcL2375gi-T-|8B*|v~qKxcEhU{b0Sc*n)4AI$WO16 zvwQ8x{R!N^i7=*C+Sz6%$)@@uBsgA+-mME^46x;E6K#{2WX!XQKKv@nO(edZ4m|-pu?giCN|nO(lL!!xhKL+V;D0g?bQeb^0D+ zmxO{&Dwscm&p* z4^|-~aC{=@K0XmFDACriL8(&ip^c#;Kt;e3-sI85bDxO-UHyiFOuq)MrY zV;sn$-&4g|wp|5CH*kXKo5Yj!zPy|fEZ;Q6YjTbWs&qWTvDl7y2#+3Q;z~b>gqumb;XeDk zlh^H5n3ow(<57RUo!47Dl}DZ7E?#f*R33GPyLr9SQ+d=G?&WpAr}C&X+|TO)Pvuc( zxLsiHVGnT_H zB(MBb>JziC*L;3dKb1N?`}#Is`Ki<$v#;;wm7hx8Is1AOul!W%&Ai^=uRQA1TX~)F zR33GP+j+gkQ+d=G?&9?}Pvuc(xSQ8IJ(WkD;a*<%dn%7Q!~MJ-@Khdkh6i~)=&3yF z3=i>o$WwXL86M^Ju&45l}wooDVFmB8=z+Y%vETIJ!&s`aZ{w`IcGPp`s)7C zJB9<^P3k)eK#vZA;ux<{TCmf+93NF%yTe&A;Al~;I&K~I1&;zl$avh5VJmlw*RcYK zZhm1@M`X0@%rve0NM(0q<3eMJ(IK6rg9v1PAo!b{eWJ7H_JBk_(wOX6jl>v=9BsmA z0SH^*dmU1F8SUGVJ2tvt*P$1@x*~&A4mw~Zhc@98NmRpJ93vf8PI7WNx~a{op|qdy zT?V}6S0sb){+j~QR;;B^PdU0`JNwa-uy4DjHbs*V+GU$L2|Z$97&RGA>g7SBB;doN zEP>Q4&QN#`1l7I#|LP}GQk#6ji3~5sK#q(6RW^t-?QJ$GN|d@dKs68P;U?gsS1F1dY6<8s4lC%d4arWjTp9i23;7Fk-N37Dc8ivD^^Ew!;4qOSt4H*o zD62z205UdeOU138qz7Ecq)&6Th&W8b!EGeEh=D{Sz^k_(Y$}aT&YDVU_&7sJRUlO4 zzCwU(S71Z3P+1#sbSxVWx44O>>m35Nh`Pyo3rS!I^CkQ@d_HqJpa@=y)}JtfHHI|_ z9nWiJ5-Xe#B)hDaO!GGaM9L&mj5N0ozDX6w# zOPyK>|4*MA?(i-w`@+^reV=tgo!@5o4r80@*P(gSElcoe- zHl@v94wWEbt%J^dq|&fmM_~bgTyak)e?G;h71}T)7j*mC^V-M>Az4?_>LH+&4q?jh z;!t4%>ku8LVt7%~ z|8I?Tyotz}g7Pj9XXyIhCc0a+|Jy`&yZnbT(YXO^igN{}GEhFECn$C)w?fs~PRbd{m`W;xjfTH8gw1y~RW2>{r=15wOt1`7eFdmy^R zDG+c`Lu5!u&pssEjIK-Gf{Y>C{ix!IK#Wa#Hh?-pEW#Pt!CCASI>JFkE4RaG+61s* zGv}Bp8|c9Nz+Fkhj~s_i>$dT-oVuv|J0a;DpV%55el*H7?Sh`#L8VICzKskgeU9F7(#;X=fgV}3_ zmsKiSlHcQwG6U{zs27s+XoawKYfJyQLXZr5`c#nyPlrGvQZyAyvjTWCn;1#u2G>l# zom5G{2B?%r1Q}@+H(!+Mo<8CWQi1vRnhB$X>^)J1;r8{hpm1w=piQ{FR(f$j}q_Imv%B2u_=5ib=Y zH*^8J=-sI#WxXnN9m$@KC3UYRnd@4c5TWB%=)|h!LL_a3h)D_Q0K{IEayUp_ z31#IMks8}+rj-d$hTJM1 zv~wb~IZIy(h1K*$siaDrz6ca?Cf=`Z?hlJftqS4^k+|@rUf?$=*&D7t%hiZH z`*Ei#RG6|$)S0slApSx3C)o$!YJ~+-G;&3Uw!i>&oC_zJ+`?ZiM@(RvFX6@n@-yZ} zM%9>$q(>CtY4jE%?gx_|4uVLJ2MS-HN}J4*9z)QOUJKG=>)KP0o>Ya(o>=9_>8}6+ z;BuEJvYBZNNG-roNm@cC&*JX-Qh~^|JJgg<_ z#2Tk1qd99(y;a`_Xin|R;xddl%~=U$rH{~@ZR3}~W>9s4HV^1E(i{XJnU;y>+Ah;5 z#Q;Z|E1c#UinCJ>O`I`T8!67-1}F}xvpk0Mj;A=9CLHuk6sNe<9E!s-K9=I-`I}mF zisNUbIO#;^)nVHH|1Rcgksw)y78{aqkTIi}kPa}2ON%;yT|U++juu>r2ijptQnYna z3t|v6I-CLCzXJQ8pbt5aK^W~ku-~M@q!q}4rygx>KUhsM7+G3b9sFuk6|YvaDv*bX*;Jtzo(U~#60H%4AdkoJa*N@assZZ#w}|^f_gwfPHs-onYJTJ8&(!WIYC&k)ax-D}EBjHkt7ZbXDs?A1a85 zVoqr02U;bDAc-#{G$)Fu3KpaAA*90hs{V%v4eDqpt7>{TOH&?No24PZijpzXkZI!| znubt)Q6a-o&qPDC;|lp08X`T4$75*dDPX4;U6zG33}Y*2+`=QVR!h$c6|HEex-8DJ*b)gRiDo*B%R9B4 z3wsXwlx_u!sAlO_-85-icQFrZ-I{W+L|EQKa6?pizrg|2J%9qfvb$L)Tof>a!nLkp z8q8p6i#Nkgn2&$(u_D#PTdQP@^rE<`%Q6VWKQbdNkNB~kbEmmPFl|5FIMDBSq zlS&t)A_O=gb2(R;NsKNsQ#En;TgJ`+TvA)Gf)^TN&-D8xc8CZcrEU6lyD1qpE5bwG zYH9asqG`Lxqb0sgipJI>kX&&J^kALt zjH+I!CkVpk?^n~MV4eC;RF50)*W_XLOx|xkpi{YG zHOfOKj-ouYw*l`LcP2V0x%fk9XWf*t*A9xEmSi86G#?6gPvUTn)WbQ%Atl%^7B_NY z(6~J%y`=0HaYDC3W^2{d&bLVI#;}~%go?JB#Egn`gsp)9B&;~*F(5|pe4>f;0DGlM zpBzcB1ZDk!f4voV7F@2YPiEw8>38VxEy8Ih&+W(7ZvAkQtnZ87P9Z?z32&-%{kT zCYc~e!x9_@D)oVz2K<9Wv;Njy_5kdlsFYP_YGNQPr?q)=x)1VQ4*u{~oz*#D)jdU> zw#<)qUG4EEI#Ty|qJbfggPpX- zsuOK6X0=0?&RTWszp1S{bf{D|R-N5Qp=3!>v+8VHy`obELJ>ldt<8fNkp@Sh6q@Lo zRfoiFVkaq9ov(<=+^Ul;WmX+Hh4N+9B_2DGZZK`o%VGf3%uH6LxIJg>xjjc9(txtI z;jkJK!DYQXZY2#Qgw&JWBjF>Cx2RpJs9i2A4^oqr2USAYjg^OfQxFggRXjci^YKkNDNrM)D9nvN(kT|JD&%~X-x{uCWk~s*PH`ks;54Jj? z{P#sfH>50j5Iy|g@KYtFBB zaS8yi9J{o8c&%-!(s^qovG*!lpN$S&Gcj0EXHG@kIlqRB7IQ>JQNvb_6i@o_heHWw zy`SosW3#VKY>x^R=ON*v9rB1>V3tjoCT^r9;a6#wQL4Lc)ACwPOy%Kv*zp{mH}K?8 z_V6B_r}Mm#=W3q6%d_Hn6HmgI;rDnd7)i}b%HOhE>GSd(wo54Nd_XpIKN*7>c^B8x zgtGDu{qu?ja6;#sC;m_)#EO{5LxZcnkbSW=R=j#GH-8bgh1(oq zuyEU*qhYvwtjh@&qhWzwruUDA5xwmE5} zB`gBj-|7bc#(ZFS!g5t<(-$gmC`QW1nf?*>I=lTDJU>9qf zaQ%G^uZ1m8+nQJ~eBS>hfdjNpGP?EX#nDAv7(a!FzJ#ry(uc_u#~ZH)0NFyV^he2H zL-a5i*w!CLUQg>h|AwP|m0|i&j7dI0hB%n=M>AR`$qJ)4O)SC3+7N`-(PuH7dR>Gg zF;5`^Fhs-GyjfLFCU{mN7<#L_t6{{DLWQEOnmxh|Uvh#BJL{Jn^$W!9w2Im?)=rC; z`xZC_>TCIl*pNvq3F`9FEHX0Cl~k(_M*Czr8|X0!z>aJW3=oZhi`SyKfEb*}!FRq% zK8vsW>7X_=Q59vVp16d3I{Qavp=vo)zym1nOT*1+ zghQ6YK1!1^IjWXS$>26pB zWXU*n)ad>(*5b0lbQo8DN6EqjmcS=f0p;`Xs`&DUOpxga;P-WHXPwI5$LY8UZmD!~ z9^RiK4IXENIn5fDB?AO|ghnKU2T_w5$IV5J7^pC;N?F*LwaR5r)6xgj(nHqL;ijd- z|Dl$;U>K8Y`O~zpM=d;REj-+`@bG`8g=ZBfX~3;h1iCnMSc86xjr8uWu&Kcw==6%> z-MU>kfF;n71rDR)|JGUBFGAo_=jMhJ!XJF92%r0ItNgcaIKg-CF^JG}dH+1S{8y*? zj%pYgjk^?UNkTik^+PRke4-oL7&TCR`J}Aq&f26EzN6ixLY zU4@E<5vpsG#InQY%ovtB<+NYeyPwlrnB_Z+2989=P0O4Y>o*1xPyoVI5Sra@!Cmdr z(7X_&qGkQ&O~{U?-E@y+RyX!cj`e_9WP(Egl<&vVH0f6C$}_g+#v1{fe52X8|E0l) zJ6v;;rLYmMV28$07nvkB72wq!)+}*=aCD^t$Eg0^2!ZgPX)V)Mr0plIN3#kJv9kx{ z%CIyij&P3pZwcJ9TWQV_X<*Zy2^Weph&l*9)xCE^*PsD7mRUGc!o>EsTwU}#r{xt1 zou9TL)i^6wiPlh%3Will)qFK)Yk&Yn9wH#xUn)Qd3#x!zT?a&QLf{3^DULUzc_jE5 z$*a9yrt)N!GAyrFb;z8juWr=)1#PQJ{nstuvDlBbJEj!03vB=il?r~Ub@h`KpDMFO zTwH`9xKn57YAhv%MzQ7YO`u=^Ma7kSqADS*a6xZ`k5siI)0y!a_jb6QV_(@xDXR#A zIr`h6R)5P|@OMp`hKD{!p_nGy<_}zH^cGS28qSel!1E!jm6GQ)zZdhom)|;V`EH&k z@VtxXLY{Z>)RD^vcrJB{5&1GmBjpFX;UPsWNw&$A5kZW|UXyXNyXX!d*~v@;K16sC z%2I@Wviy*a8kcY;^XVj7x9}rI%UkINq>+_-ncG~G9Y~Sldbu+#|ERk{Z+A6s`~7X6 z8Pt_My8bAu(i1tFNo;Df&9@V7*UxFS$JMb<@_RyVT6tP_CSNQDg9{ZG<8lS35d3|?->ow3ZV&n09+T};h znJDDQ=ig1dg|zUG*AF4UfcHS! z3;{<{X>c@AXlJBrGK_zI(FH>TW^B0`_jxcPVnjKz#u%W;+GfwY+W2&kfKKeVrrBRmcBYEoers1x;=JW};DBwjo67;tRx>Zxs zEcnE&41AU;r}VLU7!!Q1GZK*MLdK<-;K~x zk5TwVhz79xwk%38&ivvGZYV%2H9HajDS?F_JhWeZ+M)d(O&VTp5TOE$f*=lyLZ&xl zIOvMIWO^84!&k411LlBg5H-Uf7KVPj&p32uBg`4<@ZowuC>exn2IKf70#Y>qntfb?~4Pn=F!Qf$x*|@aw6Zu4yV-Bhg z85j>G3Qp)aIxvxv<@R2^D@H_)1M zI(p!9-Nt%1asiNsS(;oX)F+A9<&bGR}<+a28VIdG0*}BIANggHqh$@z-ju( zQ#+R>j%!Axd2fPp)QUXs7sr?XO4-*q0`MqgtQsFILt8sTF-g6zY)k{HZQvr7{@($YUs(YMKV?JlIa;ZbR6}a63H&Hl=T0EX$*X2XQl~ z0gDN^{dWs;FZG_Z25qv1EQFoD-H zYQX&7s8ASq=JgVbrU8AXVa>zGa!FcQ>M&_^l!e&(h^b(3?H%U->}wj=)65jTPtu+`OT$0YwMyuAYz^cf@-(V z58wTo`PxOptuM&dML=m*Rxv67s0rR{y#W23yp^`##cwY)Lz~zaLLSVirHh0It>NJP z6fog;TPaYY1Y21+3sV+XNEnDL^r12JN;yRfmsE0$Yv;f%Ia_;7fKXrvoJmaY3n)YO zpXU)Vtq{#z+*^u-206gKVVu#0`G@cz|M+9`nP(&Q0z07!{nppgVG_>NwzRwSIixI5 z3)9!^Z0X>!urKqI;@(8 z&)TBLqO-QJ?jqC72ldM2pbr(&LaQ-w5FlSW>7d-v5R_s3;vx)704cZH5Ynk*XpBK> zO%CREok^b4S>i!~HQB{+(CtkLXUbI1AfB*Hem*SW%pf7mKP(|+5`E~I)XmArzBMu~ zqasZU1En>tGI!#Md|rA2>c0TUN$K$t9sm>CVRHi=Ab)=aMN0RHq) zrBWvIST=|}wlNsD=mSna^u&=+LGoahuOLOJbR(h+?18C%HeL{)D; zPb^+)1{%Dx)8>@KFyStHWpW9N=3(}f^AGAo^1>hCaT54P0wZk#P|_%D*pQ_N2UrK45{qk4Jgm!eTA8 zy1|#NPU7q``cFs%uJZjOBB;!fk{9<3Wg{NDV%EI`Pew7a-^Aex@9EK|T~o#gG~c12 zWXuK9hb#+%j(40;rK7PG3(av+`U}FSRy!*mkP{tn^G*Qs^QtBNQQgoCU;eXv0-`Vs zvdm6@hmKiODDls1G!=)heWzWOG^wmOREyn>=Au1LYRl`DKiT2mvS#vT7G-cty_6%*$cVng`x60i+UN)-z6Q%_40oJqz%sTPOy41R$v{Zcd)s!gS)dZWgZ5f_TSd#{0#H{pbbFhUGb zzrRwV8kE8d9ElDx{JU>$OyJt z!YjhRvU*0U#ry=eQG_W~VT*eujAK@QV=0=CYQV4+nkTWLdkRg!j22fq=N)OYP?}@& zp0t^2@>^t|X#Vo0;{J4|O=RSeej zdrZ8*nB^1j`@$R)=HW;i2`AzB)QX7@Y5708#VI03j$b^_VwO~`kT(CZ8j^*kkOhks zWS>r!-(Oms-%L)lkP4ZcfV}211hw3{+8;m$ebKxqcZ*vvs3X-8lNJ}6-~lg`$M$&bx6TOswt%CNorPVHi$ zY+U>}ez%Nvkx|I{_1~DWG_yqZmfg%VYx&PwG z<8E=Eu`%vT!ZJ`;PKLX7Gr;ASnSEEhEw_uqie+*1OcfA!)~ddv`R3wq+Q@TxN+yLh zvjFMH+h7!-8(U{$-o!hWX}5%#|MbqjTBPM2%lvXVSYy=L&+eK_!qpG9X2UKjnuZTj zERu%p2yB%*DXld7Pxo?8q~-S(t*|RNm5c~oVc*4RR*Bhaa8elo;=s0nhO~e!npCag z7n!|x%8hDpBmDxUm)LmG2i`AYoG*kSm5eL|-^clOjrn? zF5_F;$bPx|3zRc4DJj2gR__cdn0h5?P-vrMmI8d+aL@WQgGd&Ag%73Wzag=FWkd12 zrX^V`YVFB+f>G5g0rYx;1p3C}g|a8o`!u|_rpRZT3zyL(VmXXIqv!2BCDm@z6HsnN z3e9@vE8$g*hAniEJ__<6n276T`MB9kd_rhw1l+b2QD7ocGh<2@2z0^*T3RmITbw6s z+wMBJZUpk8PFYoV%URNN8es^3Yc+49-@oI)30ztvmUaD;JE-4Sk zQr+@zD}OdBn~9^b0iBDK5SSIh^dh-tMr@bh7QZZQ1v-44DrgwTO1SHuCF z#?E9OaFCpfkp?O)@1^|fjq;%Cj4FiVhcaz2{ zUJuTGKdA4NpSFMt-^ycvOiBaOVL0RebyKpU~-)byQc`I*?YH;I5t@5vxIc*3Vq$g{W>uXYYZIwKaMUd1;HPfjzqwKNbdr6VMRwo%S zuT$PjRWZ*4bIQyeQg)gsi^Xlgkt#y1n|RPz(8euNX#!~zpKaMQsMJbp23vy1s17NX zkBgST_Cks2u#*dQ`Bi?_gYHnF)^V}uD+SUdR5m>KN}(c*WDZH!CND~PrQ)!LwTb5w z`(%W2Rk~dHZFK9h8rs2}Me$1s?UVHXDyym!b2#R(@V$h&<&N z>?Lv?zwg&?26(ykh!+Xr!j8>TCPhWc2bwa{_$2&QtPQGik)5efYypDWMAR{(>rQJx z@vsDIm`*FoOS*ER>v8gPqM?nBTOswInztY!V5d& zlx!Zgq&=mCD>EEYSytFVfWSH`vf=uv4Cew-!(|=zSM-ZB z{Tus2vliNm>Nb=l~hFRbGES@ShXtHlBWKe5y@LLKzWM>C~va{ zXl=?G7ybhKE(FAS*<0%Jdb(`H1S@8|LI+Zwq7I{^_14wQBPDclM^4Xe2zMxkrxl89 zgJoB#)v&aqc1gQOEyPBvL(V1qFu zZ8Hh!z>BEN`oU0#b$K){qt`fYmX4KmE_@U@f!{yzMzum0<= zU21fz#ZJaT7Ck4+Eca-`h;f}vFmMHLn+=!rl!p{&PB0V0%oY-P8^@#DH4;WC#(umZ zhzX0hf`r+6iCJmUXwQpHCbCyfN$G_*5U>dZIXagtWFXe zE{2ym(U$c;$l8Ub;qK$Q0YD^ z&p}PA_`S>+R)(m+KqN`Mpd+NUY>~0acnX>2oD)A2)S|$cmWj+yU}_kmXQQKi$kAGs zjsM(H&5#ZPGY`wm(6zngO*ZwhZer+Em?vXjV6174*}RA^edwVcGL|aO`DQFj&9*)o zLyFgRDr6C{JS?>aW0?$ZS#U^LSd%!ta3|ADOmRv)NXZCEZf<@iwWHuS*dK5K>C8F~ zQJGjFv__q-aTF?Np%+s*LM~%k`IJx#qSrEIN2@wjTqM`aBI2+f_%Ufn1J;$qz(L|@ z+-xG2_^X_y%mN+MjjHB9F70d3bC;+&A2+@?5u9^+_cffZon%H$ZmS7`hYjiESz=;1 znp1rETl=6aIEhGDmYV4hB48r(n(`3<-~~fNiF{AON6bgJtenrP0Z}O()hbKU+M-NM z<8vHJv!pesRTg#ZcFDfzSyn!@YHPKh0ODd9o&v8X@_9y}>c~yEQLm%TTO57rS>CAO z~ z1i)s;O}YvtqlZ?}D_jEUFki~h+oZ148FRaG#9$l`2%r|=6`y*@JoXJJ0>}(OJ(8yJ zR)dK_jMZ3N?(UAP5G~mN{8ipJc^Y+VR2N%Ii!=G0jWg>f-SPmD5}K8pEy9(rEt(e? zkQj5># z?9V)=(paJDMzf!$o$RXC-ft1N{ zonu-%3H7cS8>vMGMq^a_C9^$$Jk}o8%agJ>lex{`JllM@#owD|ZQF)%&73j@gwJZ} zRoOMwWp;uAi+Es zwt5@Np(BP`o1B2^?k|xb0sCyj$YjMzn6N~m!xq;`PE+Zf$Sd~}^p*=&neh5+@*UOx zY;z9Wm__cV75ogBJiGiR8`)WQP<|Ixp()w5i6Uc7Ja&-S>vv^xy^Zc-{*=2|T8Ot{ z{;`C&C(}?nL)dB2Tf{%MIIv@cbEkcw%YFt9lfJHuX9TrQp?B;t>vy?cZT^ZhpP$XQ zKf`HzbYy-DCi6dxU^hI<>YRBM7f_^_!H=@0u^s1hgO+<$C&;kxEgBsuXmT+jCmx0P zD(x-hdpM<6YDp;NvtCL`_P+36!Q#{uFja%i5N8rxVVw?gl(3?ybyI>d3@iAa2?=t9 zMlRK?dkpMK0O4=jbX;dM2+*+}>ecgfXqf;L^nSC%C_pe&8sLTHzjyao9X1;cR9f4` zZHpmcl=L6w08YT!+)K}Ua!#MM1I(;fCuk8tUZdi9XYrBhwPsCSiCC%f-U;xiFzq?B z@MDTpTQ76QNlrP>C9KN%fsosZr>PopwgNijI`Yg= z4%DY441gs3XpJx=iF#mjtt@xoC+*U_4J!E<1ks*)BpQbv(R1{`)akOz`tw8Q(mtbg z`0XPcLqH$ZCw@$xk__3$_yYoB8o+X=mehNe&@=Zhp!@?x>GS-BDOdHjI-T}Vx6Axj zHzn>xvrKNXT$puVyyAv3AoIB;J$!o?gN{03$D2l1w;XAN)zjf%ra?P&CN02$XTVps zwVCT-qs{`Q^|)I7Rn@AA4ZqP}`Ic(sw%*FHwpv62*QKhOnrM0}ReMlj8vEfY$Nej) zkN>!SrdkSsEp}S8jkJ@qi6Vit);WxmW)`1KAdlX$Zd(GUmxaGHKozwancNGEER6l- zy<4kQENr0DWBpa-|Ln4odfw7%81`7T2XTde#~}6>A!z1@r6!UBH>?g-DS-nX4-kp`sl=EK~>pS@Y(J zH&__xi&boWZjBPGg>p6~@lUT;Ei^Y+kL1F6i>rkOj+7NC&?TzSoW>`+z24|z0XUa# zb!E3c7>Q6dPpy<9Ov8%1W#p$`=}Rs6P|Uv*o_$7gNhBr^?<&M{pD-&@zUSs3d$QCq zRv}$NPV`mSpK_$3FwR;J7s>m!pT~AFBTg;yu@y_8T|PIfCXo#-v>#Yzg<7me?Bn?b zT={)AfYU9Lwh=DI6SaoUfVi`B&@?BAwBZpCvQG8qk!*|GweShTsLG@8U~it>g$Rl? zYreO4(nM<12_ny#$Ijm8gVa^R@zmb_%4%h`>g8kosygXPe(lZ}`F zOH|0L9m8U^a!pl_gd%W{2x#JJ!lPJB(;_Q#Ef3T~03K3MGRD!bG1}L;Vb)XS*ke)MD?e5KL0u zhgpbC#ampE(&6A;Y*M#nT5O6wVNm6L&z-DeG#-$Qo~0|OScbQmy{YrrK+Zz&#hbRU zpRyl*lr2E)TurswB7Ei>Y*IwAj4|4%1&))uwYPF2=~etgCb%?*bzqoPdys2RH0A(j z)q=Aao~qE$Zh{&2quBSS;6RM5rEZYfs)=-~Fbt__%t#F5=Ri~t6y&iuLw2a@3H$f* zF$qT#|9Fb{NE>`SH?W51qsKAlhF2dg1` z+cGijKxN$TvNn0_v6hNyX+`+)uuiGxk9i6UyKg(3pfG@Dh$aiEV5V2o{{(y0SWnX*KIa{FZHXoE%#leHK=|Hp8AHLwYfjA+oBsQk|Dd zoD9!x`n<9bPooX+a7i4UUqq?{Y{s=$Me1?(PQvHjg4Bfjl@a;Cf=I$O(%xe3Y#g$X zRMW) zC<_~tJxg6$4(-RSG@GvZWMBwDiS(xXEN~Ny-D)wlDneg%r5?E&DFte7Pv}~g$b;a# z+L%K3GUy8c>&@QPb^0q|=0P%gurXYz<*mG3lX+I+A&;{BC6-4!Rk_cF<+|EDpVS&1 zIJ(MN;Nk*rpAkdfb{Ix5$jwRHZ8}EfD0H%oi|OOC@$> z5X;SB88b$r#H1_ije#iK0&x>)p1KZn7&;CHrv^9(%tb-`7*8?`=q`^U429{x_|mDE zpB!_{%Y|cx1+-GeqUA7hc7FxZ29gF?+omu-{3<8OBUpmi76a-foKxgD??kSDoL3En$d`X@N4~*==kv2UqwnxdGSI~tN3U8H73(O z(VZ#)WZ(7w4->C<{!um+7X8X(e_46WFv5OSc~7TW7C!d*p-aPpUx7lgcRC!S(Oa2u z)rfZ8j8`kU0V1E;yF)e!R>eei_11}WhqZ^u-2TSBI|`s!9F^k8t3(~#J)?Upt5qQ5 zp=7$hYT_n(wLrGKEP1wA-aNu`8k%7E%%l!WI45pQ>Km0Q#h6yC4a4qVhiRaUkhKlS zVw_$~fMs^kt_>krXIkexW_a3=BgOh78gOpk<;Jf*%W|tB5#007!5AEIQ=aWB%m9!} zs{_UZYvZTn;aejC&4xWWTaUV$8x3nq<$?E!?`qBoMI7EhK1DA0$?WV7xLQ-#DZIeK0(Bh|_%&p3-2&~4@Ez=IVygjNHL-3RqyxO!ia7vdwp=L-Y5-U| zaq2etpYCeNs9_+MO-_|>8LoP-CYm7@qjHlV3}_=_bd*m^Fo7i<6g4cO8QvexIA@-O zWbMBJxj-HXKd$c$6=(%6xKz=RI7QyWD#p%tTTk9wO&ysX?&w{IAGOe=vo`q!BTj8F z+UUV6vSU2X&(owugL0&Y!V9fY6B^UslSJUa9rr)J{Fs10BY7<{;(^CnYZ5}&m+&sP z8fIFX{CrreNY;tfBC|>74Y7^4V*kP=l9!_8qR9sthPCz=wyE0Zja4hgyx3S+TP+^z zEz+d8#3e*)0<($6Y&(Y%*?C*~^EsSJ#^>TqPWd;{{vBjxj`s!m)-Z+lEo!r~~TVQVsQR$<^cj z3Sm$^PMj5BiN#wa4DmtCwQNzZD7Pq=JykDg!n}kdU%?uPT>o5WSFXu2DsDht#ce zaO!Mesl_1>0eO_`?ZX@rRM-kGD#yQAqRI_<6ef1_N`BkhC~skrbylci?*z~@-P^Xq}>so}y_Z_a_8`Y5jT zEXTl(ulCMit$Zo|G*yPx7D_)kYsng>EnjYVLD~B=nB7vF2{oqX9Zv6%0{AC zw#mp;9~nn44~H#dU971m=&a$vn)73`G&MA3EXShDLuYe1;?1{oM(&lV(W(Mv>0SuM z!eXy9BFRM~@CU(GiAsN?8Zg9$0K-a&T_A+9HO*y_@vm61_$`)JUFnR#z{W{G&DPp6 zC5$E`r?Tb&mKeR0>XS2tlYdJ-jNl<2m0}=o6&+VZUN{j)pk^<_by12o-X`ULr#h?E z4&#Q#UrgPZng0u}#>ZhSJpl|NH7{?);tc2*dBw$NQU==A`m#le_OkJe zo}$ss-Aksz*e@dte(BOa-IBX^%linIv5IsGmZ4S)18|z(*klj1GE{t0o=N{K0U1^p z4fEl-`Qe!n3MpdX^$upH_%B^r>5?qXe`0qMocAIVU`MV~j<1C5z=XecBE5zuQ!vti zjuIsKBWd2=o^G`x{p#*OoHuWi?V5(KWGM8=+()+e{8YzZ>v#9ixwLl6`t7Muur4Hg zw2P?&P>*!a4pO(dRU=+7tcGhO2286}@+2{k7LrN$p=&S6?)?uSe8AcC}-Nxr(;I z%#LA+F3p*rXH}n(u!fyM*JtQVQckX=##K}fddX&C6n_nDp%sCVw#Ha@4eM1NOc9wP z1{M2>^mh2{VpX^%yRF_K$LVXC<-gk&)8k30!*Kw@5+116`u^5o(Y(n+GVS+F z`;iNAUA>g7_sKdRH?kSC{?BwX+$uT+WMW?mDYS~WI#MJFUZKvdO{9j9>SrXcFgT*g zK*|fUTa3(EYzywJ{@WaY+xR&U#0qcMfsy5Jn#AKYs|23rKpqkUZn^y+p5UYI^t)ttB4_p?-?R2&;P0fP{u+;erfcY zrUhf&p1646w^$QtV@sTb9uD@i4x5ld9V&umGHS{pJi;adlVo8!dr_)$^eMzK`b=O7 zx76kwX_^jZy;oCvZ$OHW)F+PJfPYUQ{}~5@~jKIvjeDmdGjvgqAH9t^iKJp z(_ZzqkVPm?*f-fGcV#s527GfK(eDkL$6yUd&a%>mtGKlS(Q<3~`!)dqQfJGl5WC5- zSEC`tkC6Tfj=`<6r|fhoT>#S`mKknyQyk<`1E{DvR7)`HLc*5BK`~9&CJFCMQKA14 z68w}Epb#7LR=NDUCiZXTu=ka>DXiSF;5@z^SI)A#vxG4o`){1J1WW!v7w<(9nL{-d z$I!Aj$Vdy0q0W;frhO!a!K@i<#b?D&afc6>kWkIW>0v6|9Jk>!n|O2jDid~|!M7*T z{!g$$>6NI8+Ppzd)RlN1tIzg1RrHW{%)Gz~ggj_TJ4#tPE-LO)sgT)q?Y@eVLbD6s^JCVz8*b?;RK;1kG7XWPy}VxKJ`@+g%RWoDy^N1>g*(w& z$x?zl+p3{g5ROnOp+@WH5NaI!8b#nNe7#K7#>MT{A{1u};o=&1EnL?McO{y+F<6~p z32Y?Agc(tw$OZ|NV^4;wTATS7d2W%e&;Y%#nxufw*1+xMyWlnkM~mNGPrBUUa4D&j zEKaN><-3Q8`l)7ds(O`e=_3V8FeRyE3UF=Gtyc(_zcZNMf#F73h^D-5c!j%kNqK#o zlLorx&~1YdWAfKxpFo|!9%UYa!N77$d(@)aX-ZmUu7UGu0SFhSykXeJ%D9Ixs%V7T zymTOoZ~3v``cJzKM=7dag`?lfH``S>S@D0&!9*=7nTC?bMRV!|=nx*7Eo|0GrK9ky zhXf2=*;v&_GgS>e7(xF6EgB3&BMfL9hfZ554X6?t1Ha?J6N5={a<7Ko@9~kZ_Qf1_ z$#oX=I=6%I2|`uA$DDb|nMKVh;6w?saYv;q8Tk*`yZugo2x10(L*g^ypiF%R8AKSc zooziJqg_XTDc0bA2N zfBl6v8x}O98;3#>0N?j53u!eKSDHqwRlK4Brtn{so}yu3k1zozl*6mrVWCbzf;aHQ zUx+})X%hzvvhc3Q0FhZnZ3$QdZkSp&^#T9T9Hb(PYf(LjSG9Cu`WHBKgOqg4NJgk$ zC%a&OhI^Nqx0!*nO@d-MqK)vV=~DBQj6!s_S6ZdF+If}+t8oINmzj<7M&v9#GY+%_ z30~`J6c4t&h}t9;=msVYLoX3wUb0&7Pg1nDZCv2%M`|k8>dH1DynV(gDSuZ^t656+ z;1qhvEH9IrYpK0wX>2~up2F}^_tPGr*YTQuD&dyIJ)gLQSSc*A*9J^|>7f>3&v5j; z#&W#Ng;XDsVjW0TtIHMDsG~( z^ktN6>Ex}NEK|PvM4ISttTy=){!sUvnX<3~*5!C~NmIglt-eOVk(&Kge6y8zAhL9; zn}L8UGbzbwj&}QWnBcskgHe1lN zblg=>j+`2D2X+7qZ5G22FZY+9n7Ek*iz%H5GC?3_#?4isZ&|3eCaY5qhLaTq&k}Q+G#|pUW;aJ=Es|T%^f=HEb>-s zWeq1n{y-_v8g_mbOofq4HF3IbidPaYRK&;;ZBmvsL6;(UEYNh^Xby}e(5Kb#MXjwH z6aO<^zsTHOd)Amj>r9d{sn#EPVQz^MG$S__{KDD+W661a*q)&LW|Qbh-gM09LB|wI z{4ocSv$S2&GhL>oHlypTurM~HdGZ-Hn@F4n8*33=>7vukmFJ}LkOH?9o5o5fGGhwp zHr>XS2k|0;tk~1aI@qZ!FaV;E2T5 zdAJJ@!y6e-EjmCnV(Tz(gXD)=dr3B)ix4G6a@}Yw@|FAGXWr5sO+eZRyFH!_(20Kv zEI6UHn31P8wq?QtS0wWv6>5r=H`Qc-QN)1<2H@_uU(&}0(o&j)>Fj!O`S~Pq7CppQ z6{7Kxy;|qGl_^$Rt%1lKa1sYx$*McnAEiiA729~%+snqlE-*>swa%96fov@R)v1PU zQV1Nd3bTbo8%W7`H4V(-NAIryDc)qJR zrsMhbPoHmV9(M||w?cHw#v}BN_fz+`#o5GVK+Kv{+w!xFQ?2; zs3^u}Sbu4_JK-v>o6#lo(^Ih`ay4)f55Beq1%cXA)4tRV^rk(|TFtVA!PPOl=I2W|TbkkVrcy{6J))d_N`#OrZFcHMd-KO4B5)xmE@Clh7zj zj7Dj#v^~x_yPgWo3t+yiS5eq1FF!EM5E9&V&ro41=ixoBu+gh%YZx5g?Zy=LORt=m zMj|~wTTi^RuOQ)c_R2Br!|d9K3#llMhu34hjXDmu7PyH$eIBKkLJKluQ0Lr&qTVZ< z-@R&>2?`>Zr}ECq!mFNNn0duPrlbs_u(9!T~ICb^}MN#Hk~)tKoH24$+mjDbA>7 z44e~@V?uzXn0dY&t6B)dDnNo5_C}F4w_xO~$;PAcbhW&ge& ziH>+^^@%x8Pd|CI!|?LCeJ)FT%+#dwG%;oVAfR9l3}4EWrCDlfn!y`vy(oTGZoldk zuCf|aG$GkRf7bQ0Q`7m91m%Z_ew2GPH6T8u%sP^JjK}q82EFXyGyN7*G8A7N%DA)< znafZC8ISds3HQDMhtUBdx+G?2cz{j=m_tY|@|2{bV4$B5$>>sK(r_=(J}qE-ewG85|hk=!_l%tx`~$8$?6p9$jc43B%q=RZu}=zPu5NcUbT zg_7{A*PVb2Zk-spc3Z(hC<17EuM{&@!!I8b4{JqJrLDySXJ;5B{r2mn?tnse1*q+H z7;V-P;E=o2NyCXKXUQLh1>OD8#!i!kZolnc@j|5d_OZDv0C41vgj9@t@; z@VoS!aX5?yde9xJj0~qRrDzq?H-)YZUh3pKJfTmgP;r!4s_}He1g*?+yEt7pD&q(k zb5lnfn?$213820jHo*drt}~&Xh>(jdvEtK_bT*Xh`z;qC8I@*z-G73CtCRbHx?`0N zsnd3}C$bSFdV#o0*A#(du|aNqSEfRv5f zDEkPrznn#Vid&*eFT=h7eV4gej%2#LpHmcb0(ilTZrGIs#7YxKGX#{Q02tFuAp{#? zH_&Q?kf^h#B!zW!9jE~v>UGP{2FP9a(bA*==FJzy$&pY@lc+T?!>Nc)i)aX`AmfEr zQT&v7u24-cRrgM15g!lX@=PON5%`%V8K|iIs8pWVz~F=pz)n)UE44~|zD?J5l9ffl z&?Gl(@@^*w>PAl9$=}?~86G8~$v=|4Cc7_`J3}o2CsLV_%v8h`pd_9{g6#EKf0wmT zN2r8>fW5A+~=nu-Vdg^TOB7=U29h5&oj z6ofd~ZUPR6-af+dw@9AycSr0dV2yT=Lgn|LwbEfIdBUg>cOt*G{F7nb(5O?^+r~>`t$b~gP+yk> zw%>J_OkqqIa;3+%=!v&ggL7)c2IM z2kFv^BV2cN^9pmnPS27<2PcD_E657A6tV`^3-HUJDB%)1Oc@EO1ZFcKcp;?wRIOkp zl8ygGN1>IH=`r#13n@8Sn(Q%G%Y~M|oUFl8!}`)S#lv*PcEqPF?sjo7OV@j`HvsM z3_Rzir+u*%R+=Js7O=70VM%dWRP0Qwb1I@OV%8{8C-r3twKI5_M7Dh!V7SOx$iT(a zT%AK_kphG|&Oer(oa#h$VlKOqQ9XrP)_ZZOdC7AF-QpU?22zkQ!8>aLPSctJBR0Y- zyb+xJKUpbGqZxq3#Rlv*TF9J?ky#E*=@tQ%FjuJPWXlCnQvUfc#*SIH0HzT!%EWu+ zW(rj;KCMz}$vEn=5num{x_6Jav#RdBpWC|Z_3WLjgn)q^%d-|yf=MK&hf@%(?715f zq>&25}q6M1iT9dGMfkoCLJR`Y|5pQwoTJ@=EOX)D2whO5E})A{7?q`6clbA%j& zWDPb6Kc6Ia#5Nf#BoRmN27h;87`^T7pF`NX?CrNF7xl;0l6vfZ8t1yck*v$uryllF zieET;F#(MG6BEo*>-PWDWer`pws=BP1atehu|5Kf$zK4+27o(Gk0iW#^OT8b)-uY% zASn}PV~4~FGq@PnqX{U6xCt=D_z5oR41~=W!>Oldu+wppxT=n<{4$p|A;G3wdh5BP zF5uBAC>QWRUKM4`kFoTYuGe5DOu-Ck7-;bk(0DF3bQ&L^_>jHI>O^wJ_ejp{ z8?j8h7fqEx0IGI(zPV2g2$}}M?cNY#?F=y0m=jNKbn!=q@W@pG zNRJdV|Maw&srI}JYk~_u^aQp}q)#nxX+%7ZUWWY)j2UZuWcy8+Dq(=>NSz(lzO%}$hv;+H!A=*^g-harp6dX-n2v9 zQxisMacd1VU?5Dhx#c`&jjcw~V))gAdHp|R7_xLOtA1T>m(tIsPto1<+c$?_Kgbi) z-pY?=T%L}n3UaxH)Jl80w;=`yHqbBr=V}Auv<+ennG@BxQ;yd_a5xQ0(B9^%iFf8B z$uo6w%46`^j*1Xr4WpUfggP4&&-|aK?zy zNDq>B@<6n`huYWu)s+Tq)(;pF*jM(zC{YXXs^^|O^$Lw=eG>mXZ?b#Gm_-%HZ0b~?V%&R7O}nCV#0XElyi>j;%Neu> zURD-4%h*|Dba-cX98P%{$#o)~{)52`L>EXF!j7N*gJ&zca6Y44Xg*K}8hGRKIOuhM zauz!>7q@G~M+a`IznnC9mL$)coO__T&ktN<0ala@a{QAK$3Ac$FwouSo{^ljc3HR} zdWJwy;!Wj?AkcBSoOt3B0Etzs6cY0?neT>l0-huO>U=R}w6M2?IvBn2En{dhgl3ZiCU1!#NC%11h_jrj~HqY&Ul&OtMm|dA82X{^ovDyV&63z|Nmqh zjKNR9J#ycd8_iSaE^RQ*8VLQ2nd^jsChx+gW+AG>@Qwb77O8FbRyQmW#&sXHvyEoa zIDc&te;S*Y1}#yPItpblSkR!LW19?H-C*yQtd1Oj6V5R#_krGbQ4BC?_TPW`5$3(`FiQqtB3Otgiq^kW?vkA$(1xeOHQq9e( zS_oO;oJo5h66wZqWD!?%PZ|1OMeM$TnDz_Y;v z9J<989jl7cR#jWHWYy82vG34CYbGQSHJOHS#wBrqcmaUBcaN1AlXhJMqT^ z=9;*)9*!(AaNoBK!TUp^F?U^u2oZDtOVcKG2+3anp&wHs(wZ?L!46*Wy?)iwctffM z9MfE+8V?wbHB=)mKNd2aKdemyKB@s_VN}{OAUjQ3gOZ>GPfQW@aUJBBfb~ML++WAv z03(ow49^%w;$K82>`F{z64pRbTcj_w(?@B)Oo1LNL2b*T3QtK+)f&lU5H7u)7zG3< z76561UjR#wzc3DZ^ALUcAcz*Oq}(m9>tOpi9*U@1`|Dreze3r`zpSWBH}rYiQ-iE3S>y^^IP7iCUu3){FWb#>OEB{##^YVVs4v=xuP_qgq{BEy7+r zVQrBnqiZF|oUo``u2v{B&S1=lX|Kk(G_lXLtqbPFsF^s4e#x9bQQW=ORH3Q!jtQV> zEUoC7#uyOuT3nH|ob7E*Fg%jSStE;;prgraL9Xr1Z0hrlM!2mKQNbF)O3t;HkFwfh zf(nlr!ZHV3qvA_7X0;Dh)YKt9Y;|*})fX9tWFYjj!n&++9Ur=xk&g)(y)s?bSn)wH z4ld@3^uovS!L^KVf@+a8D{QjTr?RSJSA|ridn!@4R`HPSqU%M*{KgSHoFG<3va>ZI zi5q6Wr>yiXk_(s3q%GSJShh)n8TuN+eyZ8_+pqn{)=`KZUfC>6%^)hBXyI~^1e*X0 z^-2F6;jfkra>93zwnVVloY{bbl>e1U)1m&ckQhkLLbE-_O>Ju{XGV0foA8ob8bfp2 ze;ZK81W`Z@G77KaJf8XN4maE`MEcj#qvpluXQ^^BLn+6CbQgBYO>moFg0@?D;MF0dOMr?c-xsB*F zka1*WA$E_DAc!=zjZ}FHkws~_q(JJ!%ti=`8j7)gjR%3w=neH_*7}8_dXLCaxg&(oYO^n&I|D#B*tQ{Bw8^Zs3;}~X9`p> z5F%gI(vhG`4xs&^b^cH`LY+fDH(7i}tNp2w4bjF%xRLpG_ z{^_-typ*^s_Vb#tu%uSl0cBNh^ zX`^yjNchzQyOtIJ*9Iz^q-|}f-xSKa-!Y5P0XuHF21-Rez3!pZ+ZjJ|2X1w zFPExt#E)b0Rfuj29{tTjADyd5H|E&{hjQw=J#ACbKqqBvXNKiJG3gGJ9fVDD)pvj| z5*vM6RJwU>F>8n*+p@wl`3?n?8PF1M#Gf=KuZ$wu_w>7>^rjMjlwPp}&njCiD zgFpkrp4n@8w%HLzJQW%PGs8G$UyxiDGZL7j1|+(*7_p#LyTOTDK{OX0$Q10pYnZu+ zZz(@=w_3Wj-z7Y|_9ug&@3g~wIL2gn!+mP@Xu_V23{~BmskCJ4R-FwjCQ>65btQnr zE*5&KTNo*ERyED$dKA&yTD;e;imJM{1sV#{1C_?Bw%35Ci$;^y6|sQpd?eO6@`{C; zg;qz)-6j+6_+c^aaTqenOo_2-?V?o(wu()KmJn}wYEga6UfCuX)9&sM-KO;eFp)$K zV4@JvZfpxX+vW|6M}in+Ix?1_~0;we}m`^;%A)VDybk zhE4_@=h8{5sAHMzD|3N`UT7!j1-N5ZGBAN>gO4rK1lwA0d1t|Zj(k+KB35gWArJ_C z@3#kL{h0h}tUfM*-B5TUbQF9z8vJxDP)V;~fO@!46~tu_DS=2ZaKkUC8C5bec^&iI zjJj>qCSwKyn34eg-fb2duZ1uZT{X66Yo7x&+)7k6Ob7SE_F7{RQh*R!>uRZ4?PyA1 zFfFYrwyA0Cd4D$9FYL8W#xZ~SPDoA7_|CJNMaLqH04U-vpj8>cs5Js3Brn>CE)YcG zd^c-6wg-7Pe?~utWfr4ya|B>RL8JO$oKRVBolMNBO zKY`YS25I9NygVw)u@4-d6LGjUsYJ`*Dp-5`UAOyFZ3q0^6%T~RaL?|~qA8HdTyEj| zw7-1KSoi)hCy4HP_Rvl>(0jxdsq}N>8uTojj2#UgCn6WBKI~r{#)Oh5vdOa@;2)== zaYtP;eevv0B_PQzlI=5{KIx*}+C?Vu%ErSNkaA zcqso2?HMnv(!bEfUa)cAY2&0UxaX`Bq>y?!D=ipRX_bKqz)H~@T!#HSf(CrB+WYk7 zC22-{0495b`5)!%>iU=ou9RoNW& zX7eCq^%pR%E0$jF(HOuBwQq4FzXA0*lEh+(K!ZbZ2S~+1zyJXs@GJ=7LSi&3dW#rp zj)tPf7GNk6E^AV;4OG^s>Jj55;z?Cw%fXX?k&RmR`YQx5yP_5f}_h+t;F+ zbDaXiCA}yvk{htLvE|D0FH=w%bRM8(4S)IM@WeaVg_C{S7KpPsE%SeWfh>XDjywu~!eXuzleicx9U@F0cB5yQ`mK4n!_~b3a&@+I;bh^_WQ8uGrGa3v?x3K1HxlG9e% zl>VB$4Z7TL=gePd?`{og5riW0cX~_vgsmy{}&CORp&_mvf24 zKlDZNh`d_mkrtKZu}YFjRrIA8t^p1rZfRF=Wvm5B!dw$EB}CBYQJuU;#nBNJ4;g*y z+QKr|2Zlo9a<-?+gG8zZ`P#idj21MFHpU4767GxYg{6wvP{^iQz^BFs%gk-L>>d;9 zHEDwp=SpH9Ad2-t+0@)*{H*b#K4)CFBAgL(tOH1Yssa6O*lPPol+YkeXqtaTQ&H3r zQlHMWD>Ol8!ueUbkRQ0~07fYa3YH70IQi+;7CT=uu(m`Czp7$iL#&so#Shqam6@Ag z>V7w}SW_Y_B3`S;ti&r(9co=d6>ayc(zuPs?HBy4%JR0%8FHPXYXE}2OR0k>p9RwE z@Of;UAO;%|BajB%P*41~3|-K-V8!8xH>Gy6C+xh`UCh*AZc)TG`9G?FY;D9Pvk`S) zL2geIw8BMq#A<9EfRJ8JXcjb+$!IoMWtL1BV=LazV&pTc&0_@JwEN1=$rizNnSiux zVTVGN;VQ&+LVtObMfn=^k)hu%NtREG_3bAq_gv1~a+Rw9NfX zT#kXYNiNkqo67m$y?u(p6G3q&pIWA#W?4J=5>?QqN^IkI0MzeQyGp9|2J|SdrZ}1J z9X_m~wtyO)kO)>#k(vZI`9Vq7tbjGe<(L$`B$!0_lL#t;8VVheSri7T0kb0AH%@da zxU<0z)kI;3kSnh;ACRt~#ChsqHV0*sK$r_QDRn5fYG!?7>Jpfs7!D>G93JXM1tUqg z>}Y?=>%)Wijk*)1ZHjU8H1LChDAz8Zx>T2-x*lAuilP|Fw1Lryw_0 zth;2Tpg4!bgza_=_^TY;=jeaCSX+bgIaV48 z(iDYtNiXF|1B_hjyP9b*4i=}Q*HQ>+Sxdi2GnD$ZNN4E?o~Q3I!jgb-jk@L?6(>bQ z<$ZiyjXf1BTQznXWz{Aw8z81}(YfJ-Af-ITg|tqN-~&&6SDGw!n_9kX+AV$(NW(r?DT)z3pn^p|FiC_Ydypc1wwU zuoyyYpoet}E9UuO#b$Yh+=pE?wwx!?LamE_S<$MY!)r{)(Y6`KW zMjRWJl*3$cC;*nqoYd8pQ`{dQQNht&XXuTamCONxzqCqj%x+NkA0(Pj+0;caQJmSx zi?Sq@Mbv1hR0NB^6@wDY=*?d|0GVI1I5lZjZCzQue=O^4RXTYuV2u&M3mRDC7^U;| zN&NHIqODR*gQA8OR1m?qiyQq$O53r%!VKH02 z7%lP(#byrP1josDT~iHv!|r`VHDP**-o1Iy;WgT@gGNx*o;i_)3oigB6Vuhp`-@PQ z_CJ_QQ(CkeIh2@8epVB^(QgKAfb!u}FQQ7_-_!nNmqu2-e0LeX{;!Y0{T`N`kI-{{ zzg(aE)zwcsWc|ONv#|^qmNVy9yG&k2(jW>VLw7hdKx~boxW_9R`KXvCxyZZSY0Tu|Y zRrAerAQdi4{at_c$1b>a?23<^v}^slE_m&uKf3wTC(Z3zU;N^YmtTJQm-vby*UPPc z`jhWo`oj-X?yme(Z#Zki$5rmxf8W1!7TK=yEuMs$Bm^ ztADuUzF$B4k9V=5I&puoB~{O9bN#5Y-y#{9lY$?pPjVLR{847 zE>6*;!z~YtNmBz!B3J~Z#&FKiUlc&(f7?Pi5j+%?wZ4$&816NL+0iT zty%eXFW(J=JNRHa6Rw`!&S9*2E)Nc-l_by6373glW!Fs|Mm4C#VT1?Mjl4^qZ46#W zt@&Id`dh0{`5dz{BTl#3d2YxkHEt)>=W&7rEe?}mq zb*#?s;Kc;5v>yDq;G%6e+2fb8#Hz<9upTZz8F9^kEGIp{D8g)n)cpn&;3O7t&>*PjF{qmWx-Ol6JB+CSI z=w40fL@(!+mINaKQ?ygH^*go)g|s((Myj;rNT-*JD6{?vS3P~#hPz*X(k@F~y#D?V zJ@1IG{L|@Ey9hYt?eewj&-`U?Y)bT%Y&Bcwb!tPzZ}d>Qde&e)75OO1w!A0J2{6U9 z0k5m`oB~YzrmvOaBc9#QgETUc&?=XhX0zn*g0CU}($_HMC+3!#l4AWCzEMafTGa#1 zvft>wu>&21ePL1%CkFy%7?kV*uTnx!(?8_MEY4H3gRvYLA^EGP?epvikCX|Xbl-&N z$YU)aa+-pbJ!M`DG9$qj%`OQ$-FxPVzlcM(GeV5P^x2W$<&#iHLprcqY5ydo#IKSM zV*UK!(}(t|t$&6jgTJx{M)7Av7Ye3|fdyb16#yjN8xWiaGPgxNBHYsYjl(S!+(zx1 zTc@`@vr4OM-*f%y>P*%doBaNJa`vGP9+c&y8M0=ZR_SvD@!*~u*=P|H_UFWa$2{RN z%=I5?b#Ga}h2^M)r=u}Fd+C3CCFjamp#XEr%s~-S_y_o4r(`5?lhP6KS7{p%VewU=?vQAX zq%>Vkle|eY*&t7cW<6Q8l>j}PuKs*-HwV}zaxj{dP?D_-hvD4v+!?-_?@XBsi2aG9 zL>^Oc4vWI57_qXKFL&fdrCo{GHd}?+{Y-0;8vwPONTf|CW0 z5Kym`u8lU2c!<#)rLyppp}UnZm_f zkOs-S`3vkdej+FIlP%%m*^4{mfD9{6GqNW$Ch5vQPB!zy>CBc(r@{Gdi<1ct{?bh6 z>DG`6Ju67jbE`DUCTR#1Ee6P>Z#@g;4J=ECl7CECWKzqJzD_++#?fe(I> zgv#ih~TitF+DL_CdDI-&vmg?o4RkIefx*ex@Pu7tv&V({ZCvBFc(zwDRDwT%MySC z()SplEK%@B>Bk1CXY+8B4c;N9OQ%dj9MDX>k92Sr)l?^B-2`MTSSQP>%=ye>TE;@E zrSEIj%UWr6tOnaMLOfkFXx zyzXQmdPdwm^JL^m_aE1?XUv;;?txe(0Br2;?zrzElZn1?gcpPWjs@Ba+CWFkg!K*s zCEyAxF)->IaD@686T_}*V7Y z5l7nBj?6;F#e)jCPd8)e8OINNv5+vis5dSQ)^7LnwKokieDCZJD&2Ss@(fAGY?IK3lL*8=n}jJ1r`;2+G)66%!4;7o|Mflndd- z8i4LUkfp){L_&VscF0ygKep?&ijzzjK2f0qGRaBc!7_f|s05g3 z7w}>I1nZ3qjzRGfgGp-!aMz8?1v5}s+QDG*`e^; zmSH<;0x_K!e1QOP7%YOw3)l~(V=YR}s49sCL!^*}YFy1zvESjw8ECaG_f}~wVSh1; z*oS~ZLa5E50W1L=B9K|Od@4@`1)U%u8iHd6;3fZbHk{Y}K?hP;x7^dd>V)yr%!R;J z&}x)@k5!CDX`@Zf@v?qPDpF3g!9Exri?oW3jM>|&Yh(&pcH|N<^KsdzL}leQVsfY9 zs|Ff|YgkvvugCcY0-79q!ojz4%!*#R2Rp!x_Bg@Ntf>}a3w}cd_%}l*V2ZIzXuEf^ zY1H0PQrC{<7}rsmSXo1PKfSRZ%0u(w(;x)KRJxeVIGun8XH|B}rorCGh1d2>1Nc6z z(k69UfJi7Q|0Ojzl2a3jmR3*HvwjWC{heBNmvpS?5#j6W0C+(!?#S+s;H5U$?v3n9 z882?^2+yohzuFVrDTS7!xMPW=vpY zrmZ1Z(5}fiHInA?1r7F%5M2BSXb`No#M+`_Yts-(o2NlSmRL;X09S>nd8SSruVP2t z1*mn(C^o6>_4?-&uxVtCeeuoMld}3SLv_Or(Y9B)SzSGdALHqRSiqd$#&DDHrFS>N zkKT=_9=6$$>+5h_XkbDoF>C%cu?@*H9$wT*Z{TPlcOOZ;P4N?CTPh+NAEjzKBP=qF z5TN@elqW=Qk@BtOwaH^`|0q^KPYZU7WhxM{mm|;k#pqil!BUI%eichYv4mlxs1Ot) z?-FwJ^gR}jE5Z2){DD$ODAWB=2h#-0P2O1cVfbXy{bwMUIb_Op%%Ms&XvH<)XK}kG z@`M5$qzq^)@lr`}xMHz<{m85xlr|QSQDez{EO{J@L^dBw4nOJc#q`UV_+JVOI42+z zVvVfhM#^`ofGIASk$GD zx{7^}y60bo@9~-RW?mxvqvi+^mspxDfK9?679C@41vVfZc`g5bvtgB8{geS+6KGR# z<_4D$sUq|-Twa?T?l4HgmDhWi2E!wmp2Sbx{nBAL*}yTb*Jogq;6??Lm<2Z^gOR;J zg)i99DI-l0-A;=lNXHOg=A|aOH!%*rm~EGdtYid(T_n+UWtCc8orFQku_m63YR-&^ zz0oCciHE{z2h<{^Z%lBVGHjM%84WZxf`kh+Z?{7#dK^#vlg+6P(6w|g@eMl@6~$8q z@n|59YWDz|7q||5pr#X=8WWCc%Gv}KNC~GP*kBkE6D0Y8hDyg*HtTz~*a3x6L~1f% zb!*Cayxgj-vkW|&K~rTMGd;ZjqP(IARR=DVO`!rq0%kPZhlJpZoggCFUO;TqG+7sN z|Gc9k(bobMP~x4Y^c%nkNmy|zHHw?;%o32uj2N!@e^D#v@6#gB(nP$(nqX_t@Nf@E zy1+23a6|+>-+^NlTu~ejA*$Yne66=3614;`WM;P_A!;Iyp_j?tFhm~dkeHuXJ>C9> zR&LwVrDmqm!G+pFU=s;w$m5L%_ql)~&NG2j#+N=QA037c>;ajN4K3YcG{(XwAP^GO zX}H1P&wae1+S2bO!sxApcYE6mxB6Q;_D4vDR-A1RAu!rmY*yKsREkP#lhsm(O+B+F z#A0mq05j5*_Hq&4dj}_0+oA(3XuMIr!LM>ro@5D~Hv^1aghMdWKjTq7Lf)5^22G1+ z$|Ypqu&^JeIgY^aK&R3~F>V!iRvGGC3F(()9H zES(Wd$?#^}DV#8VF!ek;z9m>58K;odQ#VYpA2LCi{Q?N0MG8fcb^p*lhybHCEfw?B zq{(Cuu&ol<@V?MrK#OxkrlDdJFv9l){h^{vOg5q+U4mX|TQ(#$N}H1T2Qu6+8C8oQ zIR)q{P8jwhAr+-a zj;L5?!nc5vqP#z~i)oyVrgWtw1<1Ms?j&+N*t&kOi6I)tqLlapac13poxPf~4N9^A zh!2l|GnjGIsFLjqcH*@60i3i2aauw>K~eMO(GemxZLaKP`BXL~Lk>GZ;a9XI=t9Kc zs;my>xB#p(fsoJ7rvng+c#@42BIu#s z1LG8O2tGzy__T~(`ew6GmR|9Fms~|OZJg*9#3)JYWv$Q%=t^(ImI=CeE=>cRWxDxO6Gv-5&f-SDbrRA zZEPDu$iuf=5;39vGA0;`rtC!@lL>6YS1<|k(?T5yjvDW`Hp#RwcnlIv*Q9Io@wu#56V7^aiQodMkw* zrifDzD`JuZd1A{6TT}%r(Jy+C&XLQ3$VFX_6Pb$DQjU>04IackV5qSdi<*q@8H4xO zx4qf}vgU%}l9l<5$dtp9(LKXc$6Nw*zu97-FZ9b0*$Yp_EQ2Kw;g?bj5j;l*^V}{P zA2`b_N5&^4b{JqCi{r~Sz^CSo3La=NxLA%Dl>rDQhbe;LV^X&93^7P>)o|G~7&AH| zU5a*WY>bVpBm7Vd&$kqZZ!7L!k$c4&0#1+L01&<*^Az$&_$bV&BU>4Zq5laA94rVd zC)Q%7`~O9x+UBh-c{GGxbEf84Xm2!qgz9HZm<`V=+Wk&+sG<^RX+d8?U(-}vx(`yz znuH?_YzrP@4vT#P{PQNt@YyTjSA&_1VG5&0!f|+qoDPaUbpk6c0P23ZGeID)hBvkm zpb179qY1YZp5*0$l=4-47Mc< zG?QMAi?zvto7h!!LrHUA zgHZ?u-LG~y906yd6iuK4N@W-r`jP)&Jori&aUQsRJrny8OKYff)YFC)D2{wb+;Vwp zqM@x6)ERj9YS9p4epl*45`ukbN3*fKkQohmC<4^@$$x&^6>93C8nQ;6i_wn9F+Va zB{Nx8tlqm{M_zl?8!7P~o~fE>i~v3%A#Q=eNhUm=N8_2db^RB73{jACDVyC=H_bQ9 z7bM9Lf=Z^BuNaz>%OYR|#dR0a6chbZ_Us~lECjk4_l5>FgT0(cxmotWeOkt{r*<_K zz0#u(lMYFInx%K6w$WiMui7|f1T-UyjYNbX#&eIeRBn+QTBWSYVCU%<*kW1*yU+>` z#xpowFY#AHG7X8Z(NwoF)%{G^=*(0j=CQY5Z1uCODTal6WsAswf_4F?&XH6Gc7Cf; z6|&-$r>h++_n&X8e5~0(nGr@tNWJa!9$zxJBM*Hd_`|N;5HOnz)l!hG zLymhgDW!Jk&8L}!pAz9lo-T!nB;o6JE-DG@6kdREzY)@&g`U2wgSS7Ydo;)(NQ+@Eq@Zys*3Dk(W`gqtJV(jodwcOz+IqCXgsVxW{X zqz0l*-TEY$m=wkygimTKk{j|w&2IA~1Jf7DzG72)cCsp-;ZEBTfC|ekek!)-8?5FD zS@@L(*MWgo~E5`NtiEUw+}vbbC)hQjUJdZFA&8 z{6N3FqLQf1g~FVx(2%lej=gTH(6;U2Cu~@J-$;ZAqN-goOCg0Z+MgZR^)zz5O|esG z(}~AO%H{~HQIosPox)uN8Cdy;lhde76Y4QF(nfqJHPy)zAxCpbj72*r34znbTB zB-ef=qBR!5N-fXX9;;=vyP(=IcRN7PYuXt$oM7jvn=)%g0{+M_zN7FaGmr?M`{tu%3j9S85IPCzbVweS93=iSU%mFzvC)= zW-#qrkabR=PJyk@Z1L0JC_017sNbRrqZN%h7PnD-foCm4~WJV!aK zrY6y_Cw?o%+249qu?6je`Y}(nRqCHo?v+7t_*)W#Vx()12(gkjX>c-0A;%#Wr==Hi z)?;9oZovPik)qT+0C=>7bcp7ryTzmzDlM_JB+c7MtVInDlUt}bs7wpWJX9yq24iCb zir@ey^0^2my_yfnW1|304?iqqu@&>`Tds(qzUj-Ao4m=!1_D|xG1O1U33ccpr|2fJAV)%4_)I0_~Xi^glj z#%Guc!bWu1@BBs!u2G0;YBcG7tiwf5nNcUWxWdgf#<-=2E@o|We0!9ThwrLZdW3v6 zNy& z`M1;+b~1uXHO_9o%X4LTCLZz(o??BYm^-$X42q4D2YZaH*^B^V3^a#m(p=G`z2$Ig zvOE$`Lquc$i0UvYYlz-9EPUGiw(j+04hD%ncZAeKTRJbM)r1^|c8Fvi2DZG9+fx~) zSuJvDDxj5=3k=OgyF*nhiW-mR`JoJr2)$-?IH@g{SCiF>O(dgUs&OujQLoTI3_T3i zL1PvX27d^3$*WyGS{zPc&9>UiTs(;<^1Q2O00V~hTiSRw*-9~DtF$!3Miq~nlD<>7 zb0c;c(y=|&G69W`Qfw}}w+<}@klGf)5JVa+94lLs4_ZPkiSH06>^0%HmRr+zueS@U z8a4?^J#v$13!>C&G)f6tG*s=uG;RHTn&R_%ZOnd^QCR&{>ufz@R_E;ug!kOiTA<$H zYAHN{L#zB)7f;IYmbs!+a;4+#<+W1;eifP5vN zSf#lnm@hhP1CjP)dU!;wHcg@)$#P6P*v6%&<>5PTu)_uBvq(6!N8WHk|3GzC{T6JV zYRphTc1ao8MPzb`K1dI5&(lV92{#}!K%(en8L$ABfK*O3-!pOufGb1w*6pZKLaS*W zH7%CGHA=-Y)Tt>fz*uqY5(1zqESMZZxh$^e+1EDrG$hmB z!oQK`%52Hf*A_HDu%rSc?=uSSiWxW`M^LM*jDY_b$r<3|f(Cp9-ap8y{-J25y!LTa z*_cN_VQOD^%NwFSTik=8QjmK;XnHVIF%TL`tsx{^w%CG>=Z)OtAW079;7QA8Ch!A^ zc_}NEx;zI7k|RT|ewcA;D5bNJX&rTHc-Xa2PIkj+z$WM83uo{hwGP z5_yJ^G7KqJ7;;GC-Hu=u87@wxxVeKL8cDhgo``>5;TLHEF%N$W7)k1}%FCj?q!hD= z7~7^7J(J-!ccZAP2ous2A9ATvAzaoOX36VlV}l@=Mm!Fbhk`&vdh(k-{l1ux_)m+@ z0yWry>|tUwcx;hEYyvz-t_c`uvwrGl(eEQ7pr?_9>2Q?a^_UU2T1oz* z#B!8+ThXg6cb)7_BKb1ux_wAyb)*6Yt76#QTyQhtoiNvTGEiEXB(j^H7=UGd2tbSL4Y<~Q^GboP)Np}Lko-gJkzE{VIGQc zQ!zzEt`Rtmd=I=gaD$IJ9jO7_MEgiCECNI3Vdzgn52n{EYA@c#;u0Dba%t&;_H;^x zvc3qFAw+Z&q8plf7?`}D=w8{B;<|nh1Z2EM$!!hPW{re=vj}i-PbjGO{>`Gz3`3#M zD5R+?q%5gO!MmUtV{jn$=54dqw4=pQ1sS^py=#Qbjn9WQ2_DgDCa;#jq5=9 zYPzR@@FQ3^ln8B5Neq!p0dGJm5ruCc^plZ{?3v17dXa#lC_K>q4g?eyzAY_lZL*qn z_{Av-^Y0H^R7DVxcO-3d+Yc-0)a>ul?FzM}9G`oT0Wi32@~gy=)#C%$buB(R4w@ z>1FDOfIpkC?PWC~+X@>=9m4^joo-yxq`o}_FWcui*lnQDBy6T`+q}L z=gm>{R=A6wrBEI{zPI6hG?^WfjdMY!z8Sc?Avb%QgoVfQ6f8%?fe0G3?FTUDvCiOOSzCAv$WUms zkTUwmg~kF$`%S{rF+ebU1}Jf-jy%v}!lPw{!8njp*zGa!VZNEn0Q4HBi4TmuTqTEz zcl0lMo$B_z=}!1Qey}oYHk(;9OUIg|eqkt_WyEJT)@&B>=dnl~StI`VI#n801pssi zePd)D*;hCOuAc4Z*Ktpg`^%>Q+4WzppVt1WwyfSNLR=e%#!9YbkwFs=AF8E7jZk*< z+Wf7x7Z_Gm;Men?*P9M zCphm;M0hP+Hcn`g4^9JO2l;YJHV+A}4}*S@b-LdtpN$;6 zFi;_!_Q{IyVk#Xn__dW^TpX_|Ba{3|dd38sF!JIWP30KL_lI?-LRBOZW9$GsLCl1B zBaHYg#yZiSw#Sd^D*>x&w_Xtr;RZrVYiJ{fGeeQEzE9Th-G@?FQGgvAyFChj%PKi| zq9`{G$XdKyz};9kuhF-|x08+h%OQl#L`G)eb3ejP+I>fx9}ZP;SXC_};WwUGM5$6!MZ8X=FQR>;CD*GZ#@(`A-MV%L)*&ZrDE^jxhU;1yA}Um5ztsV? zxdnS9X~IN^L(?efwb5h6Y#IfqtvJ@v7)kb`cI~mJ0>xZ|p~7G=*dTl>*HvtrE*y+@ zTE59P8_e0)ef0*MATw-^rZ%1Yb;zsr?nN`n!-w&QC{zBysC+`22KtGXI=!~1F zge~SSMfXdL_3fGyjAapCPy?llqEr z^a2H4i8j^vh5B*g2F_714+c*3DY86!F6TLWj*Sub9AJr|sn7zE2GKe=fPoMM_DI*p zrbUf3Ns=WX4-ad!gNAEmXt5;W0jq{m#;V!4;Sn1q77s@lNV-C=kq~2`Si|tZ^Iw61 zNNqL@W`y&C=H_vQeiR~9BjvSfD2E4alR~iL=8Z)e;arX;4mYA!!1rt-op4kIe>8T` z(-3H;dp9C-5>a(Vh-#3@Y-b6xMV_t60cH=^gK%0EG1KnD;QGC-(~uk>c_#NJHYIW) z$DrY;Q_3+TyUZeuOZd%NYiLB6ppzN~NGxKKHMogOGn`~WF(Ntu6~-yBfTFCU0Z{X( z0ho!cM#%{MO(jhs#c>)}mW+4+FD^<2>pRyTm;};`pvC|~?*DwKxV8-sgZpHA;@3la> z??aj;y6E%ai5II4g<=3%8Fta zsSWZnOU5#PmPQn0S_C=ZEz#MSP3ed2SUC6K=%H5LMx;+k*^Ivi27AARpqPy^E1%;B zRioF6N;>x{&pC9L<&mmW$W)HF>1n5s+AMxq*rS7nXdlpf<(9%Uiz?^_1+g(pot&0u z0v%>ti!(BxJd8{VoIQOc^ZxC{%bl5-#899Nh587Uo5I*8d7ju6SgZNbJrGZ@158H9 z;YpD*f!oqqk!>@Sf}8}>Vhd&&3?acREpQftDQ`|pm=;LqF&jZ=>M0(>HSE1PamkZk zm%P;*atE*)Q1o_Y1x$$yU5a|8<%3EGyi%j=rCKMO6dF8`W`*snoStT;1cI}GRP!*Z z>^E7K|5pS^p=12O#WN9T8vzHqx#cLA%(dl^&s?FjvScR#N9yDJ@r`vrc!<5Zs^F-C zc-Q>Df;FRLb)gz#GoeAJ8m|<+MR~JIuXvPR|?( z8i886(GpxOJkMdc=nT;->SU|Ar#q;AV>TT zoHZojRa&K6!bO~tS3`rAucl>8BEycauo(_JHt)XyUG|be=W`Te-Rns+$MSLD9>deV z)zzdzPv&ZxD(e(DJIrs9ovgen=SZWMJqkYp$21L<`%=_g94gprxEPKI7eGUb^TRl4 zu0nR2j&Aaa_u$euj4QvQ#xSmee9mUS^zU5>Ommq&!wcvDO|L#FSV0sI);< zNc4V2pD|$>lwGt_$fmXn{2@I}!^jYPh_Cha?~p2;pFtfLT@jCR3RIC?vKp)3fRNO2 z+M_Z~d#sGrq_6m1`T@2tBR%@&q(^m}^r#ND0VR472>j3vQ@RJ@z;Et(A`+Zm+|UVu zNvn*Y6*9E#RO&xQ!}^l$D3B47SwOZR%VxEs$>W6$nCyCqI}s9c$JW#yQsnkJ>$J@p ziNOh;rJXAfq9BVAy=ReEDc6h|@zdfH14d`1C2~w)Lm~|tDod}W#q^lSxF%hr$9P!= z+oX{NSUQ&bQtbJfFcC@pjGI$o6zSf6zID2zlvDXLv4XJiBEFJrBcwv|!<7U)Q|dNP z$ZF3}_u>fzBzpDlZXFJBgqHK2ZeJOiXhCC)>6sFl-#Gyxn7Kn(AR)B|Bc2}H8t*-V zN*kx8jNnU-VuH@Z&6HWkVgJadD5#(@`}jf&4x<^XajtCg%m8Xh=6{9(Aj{iRmQyt2 z!PoC_|Dr}Ltk{H70;ljP_yb}0+GPKSJNDen#{}x*O7KblhI){S1#Q8@*DZL2wz}{U z8LU`TuB!dfkL)&5gQpLMEt#^ZhOc})O*{3nm(}+3W-jeyEQUhSZ}9)Z?8mO>=nazHrM9jW|UWF9Z*Fp zCW2KLa|YMlGN`e9%XxfvyC=0(Jtk)mam9rTt(G+@_F&(v5&LHKCbA$YU9M?{iEPgW zwfe>rKgem_dFBT1&PEercVh$$Rr%hku@^e9bt|WXntPu`>lW%s(8-K`$?QlfJ^F#6 z$y5TdFE4K*Apu?fLWbbwk}NYFE!KN{*)Eqmv5x-xKguQ4Lg+7>|<1ZSTi3k=d9!NQoxa8E)H#UTnC=4 zwLXoGK88!Hb+xKfztLCWN#*(o1qyiqD>%sk?wKJcE-z0#M;oS_#!Clecc(7k&x zuSVAjxnri6`a}SZCSvwbS+YDu2Sp)K-^Elf4~qo62w7VfCPvZn9Ah}Y0k{ibUTg_O zCkpQALzjb{syE5P6;Wg*B;&ktHYHdgU}MjwykPF~B*ts{(dwR#rwz9Ax z$kG@Rlq6NrWtn3P%)(ToyYwX4)8CW$)@eO_KFW>Q^vlcg45#UK_P$XWbi%6G3*Fzw z(TfYt)CM&brUk|-Lm)kHB(C3{i9Xe#YYTgd3DuAv&N?OvM!||)HMfK}CdCC z+$kCRt079?3?;EUm@V*3+Wn86>8<3E3Jq#|$a_^tck0-T`W)Bm!&LrT*5;-GrdjSD zXrV4+`>}X{4Zd^HwMx%?AjQus)YL3E1G040U@kWlQfXb z&Ie~81;kX~m$3^1O+3s_=A@II)(3rL|7AUp&c59`*>a@U$5->IY>FTciN8HG3!#DC zHAi(rYEjq?CURlwk^s>J`z+y$MnMJa$=@Z1sBcR=mPypi?12!nj-RMYe`j84ct^|> zz%;CT_f?HrC+GTeNx%N_+QpX-K6XIwSowI|4YN z(*Z|0t(1{kHYvHVAH!r_t4KCVj1<74Y7uV1R8COhD?8`eqJ} zwMgtZ~g0*_~`2WTa`G14d;V)CZ@mO;0?D!$T*I32A_hgzl+Y zY?Y-W>?2xMftk&8Hwn?c-@>LQcTPWv1u8;=Q!cP6bu*75Ue{TU2&+Yl-JhCq55uEY zLa7S(txx!K?&X&TyZx7STPJjS+oOs_+$5cEPq)#?b?jWl&E{Z@P7y&*!4t!^?o08E zr)sQwN!in_S3pV|#==VqdbYahXJ+U@|QY zT(TW*i`gG=M|NLPvQ|xcWg<-R#N=@jE@p9;DQua5+ASg~_U&Bl6w2u7+tNNBA|Jz~ zWy$Hjl8X2a1QEg5W@a6re`~zqUoR`V-;3!3Us}rW1t}$I(x&cJW5IMLx>=oHAh1A* zi}1j%1{DmKzW-W%wI~9EI_oI{w1(?;PE6JaGqeVNtzjzkK7EpW9ZIy>TTCaj;4NXt zy-v>R`Cs_XBC#J1AX?52C2p{V04L-3PmUd-BilIoZC&~k!0>fYM?A6p)|T)MT@hGV5A z2hfzP8nO9&H56YttX5S`D9XQD#j37|%@Tw=7qFM7A_?&Q?0!4^fxhS6Un58gYe80M zB0GnVHa>nKZf_h$yMyst!V+Y9SbHDJdN%VO=NL7RXwC>t`Kh=7_zBL#rlTS|XC{ol zfYr?*0cuyIU&L66f6BLm09H-G%oNwsWx_A8%Ww#}w-VSm{+ z4yXvMlbVr~D`B{JdNUUqMw(Mk&Dgc(N+2jJSI&eBNV;vKr;dgVzPs%^Egm-I`V}zy5q#GW6ioR*2$FTlFGp1FO*5N0}@HOlV?Pqu#kt?9V|W&uCI2b>uP>M!g> zCWIn<;)fa&VJ5x+hL?jjZGQ{C)cJyyqjzm6I<=i&_iY3qUze<&p0z)3{QJiC>FQ-9 zx$%t?zV!DaQ-`Q9SMDz!1%N2cU}#XoB3$+o z0L(Ic3#GIxZi#YcJLLO6uz?$V*Fg6i*jj@LS_9Y*2ey$N_VcYtLlFqmgnaZDHw5r!c6e+94b$bX6 z07*z>Y^9<>%QI7i_mNbE@Y^=T%e>ePTxKr3oX(?#1j@lQo9y(Zod|)9m6<-A zAO+2Kd~pE^dCNDQ?K3vI@4Cn71`vk(3*j~3U~B0VQe_{w`)$9CY&7lKdZ~&06OnxD z_l0B zS$@Nf*9;wfg`pea!Brtjg#vi%;$8-6qGQoqDL`m2J3o`mgqOW$2PshWNa5RjjF&xr zis*n&5FM3|Jd`hZ)YeG-aFmekFh2#An%4F|hS)fi5QIkd5iiE^Cy*v7eC`WN&(=Si z)ZsZ;C&J#Vva`bj?|c2{QB&?oUH~7~#|$4k^A~IikKJ^=@3+stalaEMDJHwPjjaU& zYHX|qpBnA*aNxH5Y<*i6X1{u8diJJ06}MW>+pvlL*e|L0%x(bqS(dER4JzpfxpzqB{bBjhWSVP`Nh0++1E z)G>0cZVo3=)QJfsQuNu7+U6KK=de1R7t*9|83)%%^$t4nxKTe=QY@wsehU1ooM@<| z?7;)j9$?eZk$=}it5NB{t;|c_d4U=`H>?9f0mcg3<=krcK4Jc;mdFT+SD}p1f0o$$0q9RZ5l9z@MfR78z^h*7f4D;drx(r73=UbO&eY_Ug?#@2QeY1jm- z^L2Q}aGR}7o@56G=?V0d9M~LXs)_z+k7nQ)oihT*SVl-6_ax`k4-8iggXHl9r)4<5 zw{;GH8Sim_7FHD_ZRfD=fU5{NoT{e5xO0wxg=%fv1aQYbgzY0+MDs=9nvZYi*>O;j z9^sDmxzWJK0ay{sn%46RR2xC03wJXYQKob36f>Pdu-K6w3Qu~WbjF09=iaeZ`yo<2 z(S|2*$x`|L0<*Q}Na7B|`4pfUp#Y}&&8GmBQ2;HiZj@s2)+|`lNC?&~SW^L)(#IhK zu?VdiKw7TfN*^j&AqNCwqDGkk!6=3V17QC?1j7i^kYF&t6M7(u>HG_oig+2-8fW@%!f5VXdu@E~CK!ao*y3_JFT=>wt>u*m!4=RrQ@ z$-h?{3ic}w%Hc?K-(ilv}3xOa%zgG!i?9Nq4Y6kS(!+MQ#xLrhkFT@Gp_|t zX1YH|>tGY=Y4@d;g6}sJO!3}Xq{&qFJO2e$nJW{F1Ns;`*dNNJ(_FT!y=>XoGV(eu z?k-ujblKp~p@$~fvSp3DXzBg3a@nF~i#Q0AuYdGkob|-JA3TFutj_0hyxwDjM`ukw zLB~inkMLFuQlh@ymyB`h2AF}xFUOEw>FY7stMlV6CxVJzjnyY70#rp06~ns$KaU=< zCKz5jiC_Z}{hC0JR&@h$X8N4XN9fLu-J$iunILP+*@}(00j6cL0ynSVmIA%LRXZfX zYe>;L8ddMtTJMu0Fx;7rTbHa`FCd#4log%8*9AEMuhcD#dOh_ z17e@;YO`lQA)ghk(9~Y48;xWERbj&9ZA`Cb{5$IsKKY$q0y)=8yonNX^GnQm38LF= zR$*L661|hRyce%X@5F2`Grx{xS`Z0nBFGl1tvAZS!zlF8|L{QFrX(Qq-LkJu^k< zaRQVMlvOAxv}lzReF;Ab&Jr(?2MKk2WX|Qg8VK#y;GA>^*J(fr>5=T=epI92O3sFK z=$W|ws9PcDWU@YPR}beZMC+J*>e*~O!oV)ju`*rPXpVFn(%&##fN%twpr$xPR$>HVHFV?!*M8JFIV%nU-SnHhD1VUEs>SHIuP)-p3dw@qd^Ci2AOav&&) z;?;ast?Ai|Z~VnqzSVT;;uv7LoL{T+ohg^pStIF_yp0ox!Nhnn zyPuQb>-dgf0!Hc_7Ucx4f)9vUn-3s0D@jLMRvK0jLJ?jCp-kBoNF5mxhc*Kgo(+Ie zO?qO|{UsR_>hHfA`Tp}xd69x5CU%fZ842b-4u?YJVU3EIxa%Wm$qQ~ilUOCZvZE+zd10C%hS4G5HDo%x=`Ig)@z5ci%y);U}E^w5>!GeiyA`%mHD zu~QW|r05sCq%?zH#T=Caeu4}w^pafckxem@S|_E!e>sL|n`0L=*eX9HMK6oWdIp`< zAdaJj%D%1R+e(6>ShFC%S0>reU0-&TkV$YMh;U2w)=(M;FJ-xq=UOZ7o=5pgi6W3A z7>QkJL+y?bzTt5naf?l;E?)CIuEW8TdM!XUQba*DQEy6C;(sY_n2Tjk*+0jMwBj$` zG1;8S+6M3xkLy>z?65C==4U@RNz~vPsCaAeny*9Bk1LCVZI_eW)ndV=c-{HmGz}m*Qm4GOb(2B|;J{-QgNC#mFpt-j-OZeP(7b#8Q4t_`bDJ`PA zB=nSq&%MHPK}8#&t$-pofQ_hI5`#Mcq?>+0xmDSEmQh57uTwzXwy|=I95-FPLI?Yp zqln=H<{#59AK-=i)<1CV4?lm#bDwt7F4bLU$cHPE0ingfOU@dNaT2x}c?f=Ii$s^f zQMG(Yl6o*2!(hLv;FLWRE|DqM_E$8D%PvhR;{!=K$$;?8kX=e3h@(^9Q+S!)4c~6J zk#jFxDU&mJeyCn9A5_(u1}@!wvRSu#tgrSdowB3JXEI^z!BZFyQ+(gKdsoCU8@O-+Bka(O^ZWOhaethXs%{9KFi@y)ds1rm?b%w)|<>a zT0gb1+Q7W6Za<&So?UQEWgkY2blV56X+(;Z0F65s_F+Rq!L2+n>{tko8rVZ&G`tgFZ&i>mQ|el-I4WjSo+VieS9E_eMhm*FMu!*l;5DX4 z*;lSX&9AER=R(Z$2b!F`_XQd(XkP(+0`1d9?i~?;5Hd9Y5r7cug*bdXu(J8Om?NaqqV_CT)-;R8GE1|-uTsCpVWI=_T zyHP{saNVm+<&tqb7FldBG4T_>L>$!O7TbovBFRJFSw zZj0ID9(xOb(RzTBk5Bu5$~VsS$k8-@w$5@(P?J2lUkV}XeG8xZjqwW8C$+3zwE?-< zimW8054J$KMAhBh{U;qjah7h?l>Gt+sVIIL{+UnYeW?UD(;82(s!77aEXf)$`rN&y zg6*ZGl+oVR)3wQJ1c4%vVl6v5#v7p$0hil3!ysv$fueJSmGWZ}-HKsAZLCwY&7YH< zn&>PYz`+6F2843LfX0P4fTU{Ji77Q^BUI5Le7bKhErZylS&w$C zReIW=$yBUp-eE0?h=aoH?GnMD`;NM{$$*kB7>8!+P4R1$mC4D^8>L;Ee-oXl>_dwQ zix7%gW<=|@g*-7=2cIo(oVgh!lnw-0yt}9D?qwWJly4+;(E{W`Ssl#sVEqk-1h5B# zvX_d&y((*|hw|g{H8UvFYyhLKZ3o^G6^yB>s z3!s-$3Pvbgu1%8Q>k!PX1{MRezuKZL=gmJ$%C}$3V!51_hv2#ra8XAKu2H>-!w>V9 zhL!MUY|L7l?z=7eHAiw8jvt#msHvvGDtk;UCJqDegL#en)?qf9#mLGRTK19>lxEL? zWh#k2BA86ju{{{6Fkuxaf?fc)P0%z&r>SEv@e&kuA?S1}!b5g<-@{I$58ohU>`LGo z?PK!Fj6&%}()V!nfW?G7;f4|3LY)gzvT4vliX&U?#Lj@1s3I z?}n`Vep`uKGl@{|Wg|+*6$g;P1{%)29IPq-pE`+arPl92Hj{N#LL4x+WO+nifGRn% zlEiMuJQ-c6RgtatTNVUl@!?%WOORSi4eYj1dt57;c7Kj$Jwb1_#$W!Y1~&&01)~M$ zs58`s>kn+7{PY^dC8@hKT=ACS_qTu1J8}H=H&oX!y(evpB3$!DJFTS0Am~WfW;Wp2 z=y1^asFGUTdB&_fiNP{C)xBL2Cgu#?lJ*y80eYdx_~g?tn2H9>Wgdf3$42H2D-Aw9 z86MSx_8qYsUo3Q7r0yjFR-#gQ`8kO(I#`KJ_!%A`q}Z|EJbd!frU8%#W%%MdTjA$- z7u}x{7|D5Rj@H<53uG6VI{EGBOW`NCr98od``(n_7%LFx&F9TibcKo>w=x9Hwl7w6 zj*#lJa<`$m@jIjAiXV|M!}?iAW^gbH1V4_S_w&h!zB@7^O(m1ELce@vLmZnn!(5vP zGsRYFgPdSUb}Jw)%N^z}hGV*G%1jhs>f1cIb@&YC0@rz}^zrEm{a_eLpMSP!XN)Vo z0MPYKVkKZ%YZGpK*$i1w32l#63g%SBo{QW-O1qx{d^mM&qpCfAD#f%#EY>DPAm1Bb z^YCsHKmdYSn${+h1NW%dW***SY!kgqXaoOZ`Q!~$&h7UaXU)U=21Ov(R!BoyWfXv+ zc5gQ%(eFIGKi0WE)|uCRtgSAT{-ivRd9F(r?I0IO4mmi@3erIvg~+V783CK%i=0}# zf+E1f1QvZ?0k5dPS0m5GL+a<^dG+_>?0Gh&x>Kf*@tw}b`&qa_2TcvycIGJq06Obd ztOX?lk%GQpJT*&@(WLGj3dHy32%eGTlVNcbxzVB(%%%u-147P28l+&50s}-S-2ww}G}*nSw3tj3OT>s7*+D@T*r>^rk2$qqvlzmKjOI&ooaZ`V z?hI10Cm*8Up*^F{1if-8&qRVOWey1YrLz2&Mmi<~qepewr2X*1{ZQ2^M+C*3Y#)_> zfd6{P4$+jy=_YbDxGoX@n#l6*jU0{@>qsQ_g76%^6zL(R@^Ir|1C`~=9G*$cND&MM z<0d34nLQ4QqI+j)6iigMkxym7i-{qpp!HEN(-Vj;rX1lHle2orp_&H~w1%_eth-b@ zp>lnOPJ9_#DKhIP;yECZ_22GSnFT9D8(bCDy~~IomFdVHCH9Ce zCHHRxZhA_26Oi#1+vjri*uASAgj_b7_GB-u+9y{H0?xt9G#qCsptwnysmP_0F{oUk zKSxK#!jYDtCdSo`QHcKrgBaH#4|x)y1z?W+Sv3Np$`q2+muZr07~Oh=&SLxk^DTn9 zpvs7_p8<)KK^gcnz%R(z56!51H>+V&Ucrv8Qom+c&4C-@L<$C-Ma@{LMu4SiB7zuh z-(70y^u0|x^>@IH?RnI>v|O(pK}M#v$zmcl zftX;@N3s=q($$)XiCS;jhovS_&qm}#1XtTGcm_St&6o-bOCuzeNc~NU7E;`_0W0YfrT`Ol_#WcuEB5go8Zq1Ma#Y)m5 zvCj2ZZ`=wLsxR_RJ>Rp;_c%HQk8|i6LMC1RDWol(kGG?7e-QT~&SWzxO`p%$%9C=Va%R zfdsP8F>Qy-MAF_ABB0HVB!KuptzNaay`>Mgmz$wpLurd`fB~Y02Q|urh^SG~pc0IV z&j=AgQPHLX8Z1iCpj4^SiinDG-=EJ~`^@B_{oUK&zxNfGvme%8>$|?|`@Ftut==e- z06MTdkh4ZEmndhn+H)00&M^+ft)2cDOvYZ(+B0dioX@T;Ytl{qd21TyX(Q)hz4X=; zE3;;A9>Rz+Bx{e+7IOH8{(|!q5{g9+qbved8?k98Wt7JpZsu)+zR2nT0D=egD&(Q< zq^y%qe0Gn+E-d)_LzH0mL2eOc_-Nc}B@8!^3x7_)16FM-hSNUNw=2e(H) z0wt}H^o64Ma#Q)+6l+1OA=uCEkRsECy?JGW2bde zG;|`I)6(J6g=PLFYzFydzm>s?OGnd;9qW`@_7o%Nu!yS|(o)>wn< za)?FZ=a%y~_UCUS1P^rVPFELimz_{hCn4Y2W?ZF;CAEjiHVvs{3~*TYrZvQ4Y>1zLnQFZQ{+G!I6Y@>jrDpEE9Q0R=FL-lfo_!wOg1uF|vB}S~SXtze-%r`B9 zH5eN;L?LFTZ#t^XUi(D0%i+O&+5(Kkimz4ohtBPS66$Cbed5VIU=+lVtRjHHJkMUl zyr94u50RW1SQ-KT7QJ-02C*)qi=YwOW_H9&%cg0DszS)CLJP&P9BHNiC{2XilS$e~ zcpe00?xkyxbQ%)6p_W;i$@WJ?Ca|9Y`CwJh4`X~pOH>XT9TJ5d7#+%IqC=-Mcn3y@ zK*j;#Aw45L+;AI<&pyeqifhohg>rr z=|B^Lw_Ub~+lr@-5U z1GRFIu4=FWl~w)D{p3r4BghV0kN94aBR1~}$&t~Lqhw382#rJ`6TJ`%nbumuE*3B@ zCf=uyC?y|U{>qDpjesiX3ag8-3;a781};0^||svS4Ol=smO+&;WtyVyDUKSyDlE znx?1E3q(}N4nbv^h%%8xaFu0Q%i^W`Y+5T5CF`6$`22jr6qu+UA*RmDtU8~4bofN3 zI)4AzeAp0{Gd85GX%K$0WE1(aak9dcC?%F-+R8+{2_lk@XcIulTyZe{im>Gk>U+n2 zwB$Z(5;9BxMM{c32~S68U#7Ecl%j02Yhd(Z!4{>E#L^ns%w)-B_0SXY?^B(kBqMxQ zHcv)25JvmXM`r?ifXA2!BfJM(1u4}U9}52bOum!qu-p7TNw~!KC8?v~#FO{ozED7Z zIX|wA?+3*+lSGO!+zZ=U*m{iMAleyUU)%x~iI95c2CAtJ^(gTgV8UbQ2_?0jZLee> zkO~OCWyz8-W}2yP`?N*?n}zYK?Whdf33-cYsoqFFMTaBCw8_{cqIFL?IDoDpQ(k&r z$xILrP;t)|CwF$XiJE``m!djvLx?+3uas3{CTv!VfVxWM2G}|tB7lN~TSV*!2owj? z5rl)<2H7ojt&2R1nJLKX?MWNeA^;IN3!hCa2zW@2fO7I_rNs*wk7}-lg!-Lx(DIZl zghu|GJLL>5K#=Wq8BU0cbsleouJ1n!X~Lr~1927!5;-JOsNwlU#J8#PgfQR7{0lC> zY!|D11WjYY@}{AAIB=?TK%~!jxGVY5G@zyfKgDkWXp*}+h0wEqVQYGX$qhlV2O%ac zAW9&rDV@FFjNi#JW*m4>2!Zqs`5T_@!!iQ%ak|e0!5tOZmWVpJ$)HotEe?6AOyo&9 z&cmdwvN?OIOec9-Y0Wr@e7PgQs;_{M`or8Eq^Wnc;UQ)tGbxx3>Nagq;HC`oA?WQ1 zyb6VQg4%|7cs(J5Smu?SThzbgIr}4&2Ws$U)Zh?vq;wh!!ZS?SJU!9KNhdx|%p0nR z(Puei*0KCyoUCKffuD$xxYb_rILoY-(q?2&2tTW+)Dy|l(G7L*vqVrbG8ps;+kp8D z=UrPrb7AYEn52Tl+ol;JC>%mp+)Fgayn=9dLN8-v(ngqC@}4%>lmM4&GizDOK7q`A8j><)Or^pau@(lpm1;Sm{jk%8!vv94WC@n zStO@m2^g1)I>`^ZfZ(pA>oR^E$Aj9dFCgU`nnu4BEFtE@8SIjY8bIP9Gdo9w6uiH| z`W%SCim-)n4w+}G#q1XK9$ThTW<)b>NYD>ei!_%o*C6V0j~bolVPgzNV=(NX+vvOz=;F`2& zGdOV6|KGnQ;VA->%aa`(n!WU}#Y^TNam3PO?t-y&w8N^EtkG-@wYzhQc}o_JAIh3B z5EHzXVS;c(T=q~gk1VNGNSt0XNdn-|m7R;^5oYkv3m^0xa=RixF zhWg0IR}A&VWU4k z4s-g_jkBQ+9Z)*~`o+5Zk(*>SR$3hcNNY0mg+?o`;$ZU$+3DU4XnODQ==2&HhvhJIV8S* z(x+Uqywsn2b;6^*geKDJP}M}-E@eTVE?MoAvZ;(NhdK~!Kp-)mZR<7GXw4KLktFtV zjk11tSK_xxc+L!cMiA4WJ?{EEmM5rYkEP;FiO&X8CuW_tBwl}=U$P(OwW)ZV9}26x zNuP;}lXQZXz6wWMVY{v%L^IaWk6u8QVf#D|B@48Wi55f&?!@3V&^4okOohK4Am3@5ZOb$t2?yA-?yW>N zqjS+VQ1Tl=(YM(z+#Kk^sPSWlW((tkGxRW@Cns&!cNzoBpGt=W@Z6%oXqp8HKrn_8 ztwaNnSGE+eRRU-Zu%i?KMl*2q;9TSrnSXL+04k8wfdY%~f*LYW*$B}UUt&`icX^lg z>a7PLN%U*P*od43LQ(@ci-x&;p%Zihx!MAmUFAW+tEu^M?MCc9_=50-!I@w&4YB+@ z?2^oJcTizNYfn&M&?0v&t*ywaYp?>K&ubxqCRUbwr$D6qRd&%Vyg+UN!Irth+pxOi zpRr+q2GxE}u%=q1+N8G07%s?^nL!Gx5Uoo=)U_(oCoIhZR7zh>%ih#}6k;5O5u$h3 z5IgGmLpIgHCKO1Ws)ZFMv%1R&w8Cb1S%OuSK{phti)bAUe4>sA@o`OH%wf|9@Rn@+ zB1#j~s(i4vB;e8<4HnyIAPlbk8QTof4i2g1+MuFYq>%ENM|?k$Ff;2=0sdc;b&gWH zHZB9Gln07nz&a+Ny(WR+)VS z$MrJ~(y(l~AwVE^g(!*q5at}!0p7~yrr}m~2&Oz%H5NXjd$Kci_^ggz%qm?!t&jW0 zmtv%3=Vz%zVGA56RM4qz{^b7lB-Drr$0!7f6YT_r-@wvV{|BZroE?H8Dj$-%9gVOF z1f=vuR-K@Tg*(}&uz_yR&tYP$o&iB)SojhX|s`K?AprU55N$w`GJCP`~(vZo= z-$hZf8DZ30+qd}87}L%=k;^7@hPMv-z!`cCKRjU`Rc*l@V!mm_p?wXL&IZ+&wcmy# zT35noK@`-16{%pD=P_{Fpf$pXl4K&IJDkKQGqiI-mT_Q=j@LBrZ&!GzBQ`C zaGSmQRb~3BHBTiMY^xr>Dfo8)7Ok@;nkiiM&)@ost2zK8#r6@Qg3+rZJyF#hZr(j%2V;^ zX=txe$?#DtM*@Xviwdk2A#;^)3o>GT7%{DC%N?r2ghZrldGhFIN0H-8VNfEUzR6*+bFUhGTM|sBPmmA)Ihbo2=6Sdmx`Df zJj0-OVLG&PD%ykzAiRnv6E9Gz?quhoq`E8I$qA%{25=LO;8wsmW!c`MO-y>i|}cC$kV%$y&ST`zA_)jvXwdXTHY zrmS~3^R}T0%v{aXjp^+PjIv z+Vz*7+wZMOw2ULE<^^s9un7=?Zwp2;_AVtYyVWnMcRMW#NUnE)QilTMz&Wcpa2F0b zW^PEa8`RI5q_-3{B&DRwxmv2yZwu53(x#QA$;_j7>rs;Gz?CtESKESK->OdF` zPM~WPzoe_gAAqJJqNCe!NT860)E5#d@rwq+5as!vRR4h_RX};Tc#(IQIKkiT(-M?$ z2zsa5mewt*0UT_sQGk$2nYTRoNXq_m{4T3@VuiTjrO(Q4XbAwd&aWF&UQbfJ`Sr|? zD;jX=2;W1h_$^~4J7!vWOp$%EE~%1O#9`rjjO4OFe16{2vx3D zXnTE)B5-=w=2rFf=BY5Kn6}?OI^Fe^Q6vp1$Y%3aD1^j8f}D-&Zo$~Q;YV0@ zjBKd7G3@zHrRszJtUN_Gnkr)RzA-h|D4rSC5 zlaFkHE&Bcc`Uk)NhyU%5PXFUSdD)--*`NN|fB*CUamN2#KXv9`{K?Dz*Nb1V;jF(r z`>+1`Z~p4VuQ=zG=lt#8Z9JEsSDttNbb3L0VR})zDScJ?>hv|~=Jew9lJwH_vh=m- zs%O70eSNwmeM9=j^z!tI^vd)q{=F%^I(>6`P5PGf+VrjI+tRnEThn)>?@Zs7UboM` zcc<@3x24yo?@iyAzCXPo-JagazYnB4(hsK3_>lg5IK3(TNcz$AW9i4!Po$qrUwO_s zFaA_|bNb>>r?;fHraN`7D5p(l4ejd)b%LyV5VGUrGNy z-NnCuNWYrio!*n)n|>|*db&HkFa1XPkLmsCH`8xX)dT6C^uhGoF7usqZ~9RB-Sm6u z!|6Yz-%o##K9c@(`or``>7(htr2m@!IDIVriT^#GKA!$GeIor?`t$S`>67U%(_f`e zrPJ93*@f9fsLxkrug+eRwce3!&h-Dq{C`PyX?7X^UYorxdp%FLWN*mcm|e~)u6H2n zn7es69ovdYr&Ygqr}@^w!?)`pOA2`Sc0F94%o}?f&m@wFDKbgPJ_ivvToA088JcdEgh$o@>5mU}GQ432w?Gccs=n$hZgct*w|`Z3HyqgI`L z&Tmr_5&}`#ZlN{9ZpWQgs5@rKhL+Ckj|^He(h}|4PrevUk^7tn$aKl-({fapq!BMy zVn&i+ZEE_3BG*u z=!9&D(DRe~BV>AL2Ii?>H8vcdEEZW3?9gZk1AJCGZ%5vyr~5T=ikYtdf>YcxxdS0l zM->zDU^~cJOvotwL0B2su72u4;e?669U^-S7AAenESP(OdqRhB;0hI&bvr~z)WQ61 zh>a`^uOx;#Lne*{0gf44l!C#EW6^QusH0JhV5|m9h;s$#hR1Ay)3RU-?PK6Ej2T*( zut`a3OLKX`!duq;5JVnf1kJ$oN9+_k+fZj?S{5A-B(8G#TMZ$zT19@XF3XJlXUJ*D z-{KdeQ^78+VZO#0P6jq#7n;hip(G4INAZeZ+s)tX`*d)BweOYA65hW__eP5!@n=%` z80NaHTHxG_*LCx&RIGS?H@}jLObk}!SNL)Ptc_ciU#>f2RU&q+pnju1qV$3({G>E>_Ho6vDT<(WSXH{WXKTU@uBdA%?4GlhMk;2obwh>}@05p|f}77x-gPVorcDP|E^PWnFV{Rw~hHGrx>#nJ@ylBQvrk zR_Nx)tf{;SL9{|S03@Qlp_^YS0F*pY>u`kAQO0Zt+_bc~%ziRM) zv)?!U-a#?KF_BsP{cH4I6z?&4b+{9JwEZgX7-zwl?M(Ac;g*r9YhdUUF>R`~^~D>z z`9+~fU`F1U&R`3VrwUv+yAHpt>)NND3tX&O-3WmP;<|sVuI`{8O!GmF&Q)Olai~Ny z)R#-5HYMs{9cl^?LsbS>Up0zr#yD3+9Xa6rQwpGJWuhd>uZZp_yQ(2As2d>y^>U_^ z3`coj2B26fo&08)W7+rV_~cl5h+Y})AgcRSqgw#gynRJC|E0sfX8G9rz?u9_iz~bNlX^c3 zn{ha(rMRk_{~~K?1*>aHqB{Yit4C`XOcLV$p5o#G$H4n*V( z9!HEpc1Q>#hNX75j$_;xk1~vya(-eqN)Typ?&d#@k7{BCG@d>#1V}(O^Pl+A?DLo} zp@mr@O?YZ^u<{1Wwjak5V(ujWSKlp|3U~kFyFw9u38x@m>7v;D(fZx=uaJGSUUHnE z-v21p8_uEpVSVS*CjZ%Yqk{DjU!sVb`48$cl8gEGeTU5V-9N>59%H?JxGrp!@V)xZ zg}=)kWXKK*Y5tI|f!y-RU%ML+2grmZ_lB~wwUA5gOPJq@wbUq&wFNR4ZGJlzxIK~N zaeH+;{MyiiN?r5+4TU~PwVBT4Eb=|Rl!A)`@PHpM7R~%yzC=?r^KWuVe<>pli!EQ% z&F^;+OJQj-wqO;Y7(|ghF;L7tq~8j2H%GhD-1Kyuy2|HIHLH(pqhhJ2&<;ABy={fw zz7oSJhWL7&RV*^9Hi#!+6sNEub()qs%hNOtzUR zj_UENxVlMKEH2*a-(#|C^{a`~t99{JF0RnU-CS(pLLXnI-(ljecq<>*H9WfRxa0=E zrrQIHSj*w)?GBg0awfv(Ppi#}PF}}#bs2{ywzbq&ypGe+)N+WFP7tmeNQ~xcW~CZc z@a^afc|gUJUgwVMp?sp2Q{aEeBd~;Tc@+5IMd9>7s%|7o$}LO>{t*-$xP4mq5G&P_}l(?YsT9oQkBq-`lV2Eh8i8{x!rn zB>0YQ{ln zUY2UdGYsR*k~%F0(?Yv?vCH3RKNy(9DV)_)zO{#4Z4N8NOF1GOGlDx~t`b@rS_}(& zY6KzTB)?bf4XpHgV(V0)mv{U2{>1pH1~SOp4z6rx^IJ=;T8ah#vz7&6D87lXN%F6{ z!iQspK~_+~|7Xp0jes&3T+n3&5pWX44PK-h2{aI`{~xsTe{jQLpaU_^ARgJlU<3en zQ7%wQ@y>4k_x{R_D}7x;Dc7(-jvjUt`(nrQVKkmQGQ%aj@h&NG2m^8$=Z!c~*9-b1 zeuY0Rdx-twf=z_s2#3new0FiYdLF;BReV`7Z|_(_WA~8YX8;+)`X63gYy{d6xxKSX zhZyi`+Up1>`JVl6EMW2t^5?#(EYy9;rX`NT-dC{Rgn?v%1|T^qa#z)cr5Yt6AiPi9 zPI;d-9QALgzGl{9z^gE(W)EeWjAPb_zSI=#qG6OIlTld{xot^AqNKWryt-7th-*}K zq&i>wFpgACreqKFqmzY<1C+iH6bQ9oQylhnut&^AM+~r7h{%o*4d*v028b(k3t_I1 z*3ew+XvK;~gk9H8FTkq|QbWzicm@q*VnS3vF3pFI0e1k{kARx{ zUZaap)WTNvQg$w?>)jj=A%r4+kaS-pN+tqe!qi(P zsHUc#hR_Bd)q*8xkN0@s>>-R)PjdDTC$$`(jO1SoVNX?b6NW-D&2$|^FFnT*^&y=> z0$O-`L>uv9Aqd_+Auy4nq~dKpxBLsCCRam6_x~SMBsX_KaC7M@d1(AOfl*~M=ga9W zb=WKdPY1Mir_rgXg8j}U|NIPoKR0-1{UdZXif!Hej`$p?)ydm!{3~4sPfa(l?Fyom z(Tf)U-tI3nPV_d86>?Be^GAUOelYd#H3mLQX^rbD;1y;x!u*LJ%#@{51P26`{YmO6 zuJ7iz#jg?U_2-CoSI3{(ZSDWPN4j`#H~);mrTH3Mk*Pm67y+gMxH~;y23K8kJUn{o z5P$|f{1CkWH<6%#Iu9QrytFh(R;K6>-&ev7JUegSw4nt?2rZtOR1psG-!$i-1zC3> zq*}N%8%9wywp-nk88pt;VCsMeZ$h+UczC1?XILIuUKn<2(Tr>|X0?tps1fDx<4HsW ze5ueW{`11>7jTD!A!9g6waXLSZ~SmN626wDd3<6Cv40hKi)zc8zWCjQx0+D(6Ls}J zt*Z~U$y?X40xE+Hk?c48Y$1`~s_~RmEb?1K9kK6y3K#EF+_JVIPYLS<1u`P5`1Tlr z$wA&E|McKPGp%sjXSJ;Y9{YbSj3NB|bs*6E=E&5dB7GkkwuG=st)ltrPh0v_f|C(b zH$09H!WtZk5bOKYtoqE>t~SC0ogG-I;raI~B^i{qEO0;JOS{&JxdK-rC!!9VP z5yjPq{GdIh)tNd-F3CSQC<5o{^Bp{hY9fl0ftnZy5b;|og;z1vnl z;ONKHPr>;8mXz=DD+FVB1!);#bs-dZ_r3*q0oVG*oPhJ++_ci}nt74)66ZSTnY~?!^!=7lqCY6+61x5CG>=_CO$pfRbSRgVzss z^Q{qT@s*)gudyixV;=}#5qpe@{OuIC_b|{F+(x58Uw+7Z^EOu+2ue>cK&?~GvcNV; zWbC)PHuRl(dysmrjlk_=mAImS;OG{qi4S-4x5UzMGGeB}=*P)NI{>=LngsMA|Y!Q>_7 z$XRR4rE(IlP5X@-USh}9Q(e=t>2JPP%Ydf=^3u3%OuM*9_ zK_z7EO_^+Nzze!$a^0@8pWqfYd)A&&9kRbUoo+=rG5Xqc%fdDt_S7C zajL@H)DjhhxUs)~yDci=@* zjod#&xpoXfUAj$l)KFUdhcFR0Q`S1f2A2j-dlFcu)iLPB#|Cv>IQXQF1EOEtYS85R2!P^8wZc{xfG3KL z3ql=$5<&=uM|JGm)U?26Kq7Jj(9QgO;UI!I=&Y>-*Cwg@!NCx!|6cyOpgDum+|^`Ab6Hk17CRIC{+pJ4I8;7>g@Oq>_dtA8GGwS7UDBM!FlZMu=&0Ok>{ zj^BBVwcCX>$9!94kaIA=2)+Dmh`bPB%iP_+RntrUy{2R1(SUXP(MZES3*%-u%Ks}k zUb!C}uMC*7DIoP`R2Cn%Epv|E1@EWA(G@|37k@MB#qwFm|AasLYrTz>|MUai{*~T_ zXhU`2w`c2Z;K=I0Z~szn8CKC^7A0p%)3gLglfln)&~-oG`)68nf_?w14%RlvLL! zVvsQrg0Mi7woEus%K`~|liiTRgOTahj=kK1k~2i>&(-?NLyujjV<|!Ukq`FnpEj%8 ze-W70M7vl~I*IuqB_$}RX=pU*p*19I4;})?FyJS<2BJEuB1p{p#n6o7YWNrU`mtfX z(asSX4z86;!x39^B6_>^;ud;a+$zcrQ=TZbIOmBdIhH8^)uZn1R3(pIsY-SZ zD)9t~wof)ZSY3aXuj`fML{U3?cctmTwB8{vln{ z0E>F==;nVA=tLGpeDYssp2!N-_n+(LzaO5$ry42#=#^f)EWDTr?b~|b#Y+$JMXE1e za*!|l;=eG)aX2;2W6{-{>ptUQ(RKE;He->;5U4?d75s&KCuXu3XYH5PF6Scf*0aN` zL^%h*I3In1#zp=wa()R2Vgtf<1FgY`V(Gv~qMT(Rb(-;jGN*@_y<^W>ImR*HLlYIs zW~g@Uj(H!rM|I-Hei+@hR+s=;lM->k|kn}h`o%Nqz_0aoLwRT*3az{l2uaC7TDcCy=kTWpI71 zO@+Chqj_aAfmKwS3o|RAgBhr88F|d8+K%!Ac+W8Cy{EBf2PK5|C(w7!b}mQ4q&Mt3 zQrxLYZ{P`9oQio0Y`OQh9kClM=*iXqR#ld?52jiz|yVip+ZbyvkukrBN@)pMZ;gkAO#~!nFPf~mlIb@`Oi+#Y!QCuY+ zQ1K12=NYV#;u6{kSOf{ot_$T0*6|8$hoI1H(?@`?-B8%?8`Q7T_^=8P_R!|CWQY{E zxAPZ^4e`HVJ?CBMs3i6#wPIb!Wl*u=&ixinBhnRj>le-AFISh-FhDgNX&`a&wbQ+v2 z74QP;9t~UfaZj!_sIs14DYSc%f29P@Q1PX1FQ>3eQ7FPt=3b`F0tm6n2||<(E+M6f z{V_rdfK_upp{(hl{(``cGcw>m!uSGuzkIJzivhqurdbS#L1*WC^|>SCOFiYkrM%RbQzIDLW4Vcr~Gkv$>I1jYjGdp|xX0=^a~bCgh-s%)!YLPk>D z)g5~t6*TLlQ!0?SGMs=WDr`els(E41u%KqE-0Yp#D<-m)lbspa7ia)Ri!XQc=OY}b zcyMToK5AA^zABOcvf9|8b7D(TLk zs*@2Q97U3|7)p0T-P{|HIC*?tRGqtnDEgpyTSjoYz)K(h)f|R*u0x z`g@k-pl(TQDn746@rH^YYLy+jhvdze>*G^hZQg6(1h6c$3kL2Q@DdIG6IYHdOGk^@N}ftu}9#~Hu{#I zl_qpLCbMi#wz`3`!wLxN2T6~rvQz#NLt1uSmy{471z5Zu$vpQ(m~Y5p*$$CDH4xA%7QZS%Jq?8AqDVNNHt?Of|aj*W|AP0%p*XtLVdtLn9s(x;IO8Ryy z{luRguCAD=X$zNg2bEl=kEm>O{M{5QnN}qqxK))*he}4P>t2ie2GJ03jg%*_4fOeA z`UQqZMVznwpgwyPBz*?Ta|Ca=+M6+Z7y7b?OUkMnfPa6iVz+*|zb96;OI3aCC#s5_ zVO^DsZU@nw_DR_l7y)sys)fH-sCGG|toFpiB3%)rPFUD2mmOBDZn;pV&wW6rgv@dj zH{>B53fjVY&7UD^C5lhXiz5wiEhgBQ!{pu<6Hnn5dITrNSiQxAzWodqHNf^>s330R zn$gv=C@s2zgtcyxvn=s&5TbmAv>>Au@`!$@DjL;s4RZmNEx6}XzTXY!^AI=rE{q~x z2ROcVR%Z3BnTN1Jmq}=cTpI-6BK3Jf_2a#y>7T*9XoEGSa_Z)VVYQ(fIz29*Y&QDif9CM1A^Mx;%kdqistIvo|H6Qo>v9aUA z^mB-ibH=Y7KR5B-cd7}=tZog5T-}lCXWtENw=F7lQb~eG=o;i`mks9TrnCx{UZ5@5 zXU+M>;OKZ|%y#SL50bp&Wr`UoWGoaH{&e5F?V@#gxAVL6NdSo$h*4) z`_0piml*b(9eA~J79tKVqvqsIx@2rCo(FN^Ud%}0C|QG0t9Pd4#KtS5#~?Spy7MYN zo27q(7?otB{j$MHwn}BJC7bsSFni!ft=W~xBp{#(6-pJ4^9B4&zyPBVuIV?9Py1Yh zG|l+8|AJ1R$^YxO^PPf5wE)_}cOd!~URJvWWyCh?G984yCPr!`@5qU!CaeoFXV{FE z!;KVx(fe{llm94kza`7O8lE!8(9sA2N{;_ zTY;n6#C}HT6QBqN2S#tRoG;;abjm`$kO8GdRCb?868Y-%^A} zW;t6c37_*h8Ct(k*zNj-?{})T?|)0$WhbqnBq1bt*eZfLs!u<}mIJObf5g&?OGentyg=IL+sAx3#0 zUG4lKcp$s5?T4DKw>h9zT}~A1US*DIs_{attcSec2&_lwrufu-(ICQITW0`Tn)%j&d z)|Svn;b1qoU2rBmSG2oO7r>@Ib(s&OxrNefbVb{X2(+%8Dm`AHvfOK@kw)bOkX)aE zNQgU)WZr3{TTM`WF-^wd)Cc%z{4jGl*hR&kE)Kh>K%(fF5m_a3ZI1A>NS6CCh+IBH z;55NkjQAm=g3!hsK8g+wiN!^NVh33KDCr#b_>dT~EnS~5i9@0Z9|8@dF$Q&%IS1Bf zrAc6Y+`E2~y$xcf0x@MtY8z(8$Jj@q7-u*i|> zVUC;z++oJy!EWwYXdPnk@Rd_~{+}hlV_*j5EpuvGLXuytvt^7Nf90>1!+zUauKbpt zdx}p*WpI6`JI2${q~kd9Eo`;6ri#5?r7H}g#t;O4C?D>j>%v1oDk5^1(B-wUN;A1f zcOZYae!;>0LgG(v5)O6+B)TR+xRaWkZP*W1eRVqztb{FUcf=a>ts;FN*Du(!sLkOG>Nb%Xv&w}F$<7u-7RHr)LVA=Zm;-PMN2vo*{>K|pY zFPJJW=;pr}Y8u3{Ca2;MQ11V{mKD^(XnwbwA06t061aJKxG4pk)}-@KE!)nTS?`R! zNOFoclsd}8r@>;7>^?s@Ry;hWr_>%UkaI#Vc1L(1A6$+&GYWioomN0ypNe^!#rI%2 zW%o~KqP-BpsCS~8W`|#gdNmkCY*mBAn5XeHJ28btvnk7uau;Z$co;eNpG1=Y+RTsC zl_nMuv-yN>=paj!ll(VyvkKFipHiLz6ctCp#1^c~kY?F$tr^E?v-hE(3gT66vW#*U3B)ZCfT@X9c;WdMc^IroX4NnMH{1ZW> zX1**y9IR2$IrGkY(;K5=));LqE)6L;MtwNjLFiO-XjAi>227S3l_|kNbz&@v(I=*Y zy8=e^Hh8+alH;W+dZ7xyR;6w=d|8L9z7Nbvjyoa58s>u)#0Lolm#QE{7wR)T?_1Lm z5oX!qs%fUkppL^;Bra14MJQA^u!NVwD4VVF=WF_t6h8pSLfhE~tWmXl8;72!-d@F} zwNZ1^A9yqSL9gNmb?j-0xp%(`W`BFA8ryf>OUTs{y2iuH*bNZMLy@<)EK2f2^cw0d za(2E~8?IOc%*g}cKBRW(5l0ZfBFhs9ys1+N!uv=8^PJ$sP4H;_(#=hXd`$_l55!0J z`*v;}^IEglUXP6?HX8*l!Ri)fRl|L#L!{o+?(!b!SiO2ZCo~`;0ftzsl_L(7RdAs7 zGUK#Q&8h}*ViwBr?nTQpJxJI_KV+gyE(h1+i6EM=hU8w6kB)pMwR=?l8Uea?pZU?G zS|J0Dt5iGtR=bmR3UZB-hYmXB7DaEJTD;d9-Xt=4YixZbcM0W*7oKK^hZM&J1W44WhQjn>DsCdj7cqj$h)1QYn6#F z!8WS4m_g9~I;@G7X~QuP5W$deVS=-G!OxJP%4px|G%xZiu;beygZH z=p|e$?v)3JP$g}InRXdMB#co

7`RirwnZWxgPl_*|ZMGoA~aJ_iq)P4QT!_6{-Q z77`Uio|Yqcp6Zag1Yvn~dz43`#rGIA#_k<*QH3h5$z=p&<45l`T+IC7aA@)ZWMEQ} z)sg7vwjeZ&e`cKZx#}QcPa|*fEomTGB0;oYmSd+p1ZjHMWU}K+qjB4*oW&j*;6}xf z#CTlVZ=9h2kY;~qi*P5;o}TWw1)W_x8NpQ4!cOtu_kT4lw&>NB54m|iHEX07Jb|**_+SR zgvlZ8vAmdOMQ}sa^r*(TZcR1l`4}HJwrb~RezgNM{-y1lavP-LaS8iRPdnh*s4lvu z2~lY9n244e+F&9LGJ9v1n#QSQK!r0|%w zrEV2(Lr!t%RynWw_`OX44F`jRZ??xy_0jTV5s40E^z3qL@pi8|^0AWesL&eIC0c3DFEp=Bt$=k5L%V6`|dO|7<=0lD}% z7J66bZ|JlrX;b|SHTYO;F|4p5F)8r~*6)za@$v9egrE2jc*0qSO{_-+M-)NSxyCRr zo2h095K!tM;lW}_9eUWP(K@Fi~C{FaO#4Ov%`A03^Pqal^-jz%b@B8JM?j! zKr_Mm{85&+;>lL6&|?TntPAiHS{uYwFQYRVk#mbajG%-t)Ks^bq-g@3)5>HoGvmRh z3;4nkG!rngWt(R!I?qOis)n%HRKKb14Df@5A7wuDsFNU-vSA2$BkdVG4$;w{#+uu0 z7O|!SL7>TfxvM~4af^&DxnL4s4FNEyM%pMK0$f0jTlnN?!iAEB#_4$6KmxjS;|-E^ zHL@C%Ns1$hb-YH{j06BB-)MjgiZW4|GL%^zepZEdD+TUk*nB?~-ZDixnp3GQC+M$9~q81ne5&$5zChDO zCz@=QhZT2@A^F+crLRDxTATf^JWSLw$J_)m#?XbP4;dJc0tn<*`)z^ia0C2UZ9I+h z35ZmfYAppCaZ-VIa>)o)@=3ElOjQI;aI6I~ri~QRQ1w-kliJK2$X*g7^s(3`IpT>S za!3OIv+AQ2BwfFVY>6BQna3c>!#a}-Vt`xgPK)1zjWkHk-kF742A)o7H?^;`B_>(oFQJ7<7JY}Y zFo&k6OYw{Mxac!#nxkOQ*UC-CED8Z%0xF{VMXpD&B<(??!})Z%LPRAmF3zTch8B|m zqNrhD6vgRdVTTg7AG_7vUqvS8f%M4=X9h}=KxhnKGaAsTQ)3adSf}q5<06Y!QLhiX zgNosI=rev>4hK!85KLrHt`R*~GZmJwk!5@qoskJf zMjEI;mwj+?L*`0HbbE7PnWVA0WD<~8rb?d!yHsPz8_lXcXE9AKRpG_vErbt9lAF>O zRrV8>+zZ{>Rtj#^QHPLQ!7CoD6WSRXY!fJ32Ay6u%kr#VX$c5K3po9yDa12N;p`;* zz;=Hz>>3A%=tquZaYwmkI)K3aZ4z|fEAFvcS zelDGYV-3CSA0`+aK_C)<33MNJjH;BTO~h27Fv!d(q*#C+WF_VSgDlm7TRg+FqhNwr z8ksuYaBZxb-lZMa5S&s~W2_rca%}^8KxDQ4dU5X%_oRn=7`i5QmoV%dft+Sfg87Jl zOzAT<4K9Fsh!Sr&PeKj1&mU99$zlQmqgoxWj*V+_d0{xMKJ<4SQhJ0ye!F^@zw84y zR*i}1=)2@xyBsHWjSW*h^a(L%Sidh}y@;7H;s=kgnW%mr=GfBW8;S&IlXGA~16?(y zEwO=tN$~|Tjw0qJKp+ti2Ej&-ZK|XIb83Xa}00ttxtVnIEDN&n-p68IzL^)o( zaZCqRVf7e(9D4SLeoRbRJMZ)G?HF%_E>25}WpdS>A0tknrPsq)R)?YLcN4ipyvj$f z3-#u?+xvAGtxs2j)J0;Pprx0AL=MtXD5C?#3wuW}=LyD`-r?dZjxoYa(h^A$u#{*S zj6TGRWCqV*P_1DwwKdB_tUH?G+a1Iqj=*xWEEtO0#`*&t`tRs#Aa+se# zg5#|vNZ7`jE|^2;yR2_m3BS$CrQJiQ(~rLM_Z;lPnGT+YX8uL-a1?M#x$v!|B?p;7 zEL3Jgml=DEP>qH_M%L5nyy|;jQ^14@Jt3W8d?>+Nnha^|vnY~I?@)?{LF%*iU0Z6> z;Il;7D_Y!PLq(FW`wm2^! zn{SaF?UF-?ccpsc;pFcW(S%Qa#)g4c2x$nMzkvkt5OCfE$@0R!B1M8oY6XKSP}eBR zjYVQqjrp@j>~NQ9Oe>WjGL#U@Lf#w`tBj+$opOPc1+j?3+%rN9dKO$u6TpC z@5I?$o)CJ+T-HRxDCC@NK#8WnJ*^?lm4Uc~2O=FO$eO$?#wxs7k=O(==^$0%6OS)L z+j9^3$i$mf8MGuy;fNB1Jt(3LWSt`??mK8^pA7V&7(}I&ZkXK|Gy~WSvkqqn z=5H9yX1yp^+^QXY1j-yy*(fn6BGEup4hpeACwg8I^{8&|1!R#m`1nvhj-RUg4hLWj zMq)(|4Tv={vEp?JCs2K%0OpL8Pyi;3IH;t(ndL)D)tLtgj-hMnT=Rhsj0t>a3U!hO z!TytNogTPT-Mal2(fm%+JhGh=CM9x4s*MabX2OVRblu{!`Wahz0(cyF9;VRrV3s*@IVMcUYe#2bc}n62n*f|Y{`{P2o$>fDO_X*RCuJFUebUQ zYzrg%^)Y^|q!UE#D3=I4#Y0}6$UoMdLKRe{8esn7OIWq3`beefW8%T-;LX0=;W_Hp zWLA~T5lR4FALm+N%LlslV%vBc~>hPIUfmukjB}>LIL^| z{Zl2+(Gfx+Sl)yH!sui`?UJ%!9P(NPf^ph;(E#SpjcGKL{x3AHU_7*<#m1yZNKb+p za{+p=2}4vVy3+EL7n7<42B43$#|#!HM;4z~3|HZa9+DGO*j|{ub{r=zNK<#WT%zGPft0()ASmu|B|LVlzUnw5& z(X5A{By?a@@u84FU08l_0T4W2i(b;_OLJ&aO5^qb|Ibvhpq8wFXE zF^i#9O8kF=t2}w<~R!{;1k*KgA8L>rr{vVPz+B= zt^1H#8m=u{FT{R`6@!8CfE7u%EpdOTD%ES|d!Hj9#E?MEok9v0d$%+>$~iqRTODVZ z)NlF9oQ$(w8?I>q6R~JL?K3F4*4Q*CuQ42XE*Kf&h-9$9a5KNjNjiYR6YV=bq-O8TSPy-MSDe&v>KqOy;+bPl^x*|M|a{h;vm?&|ub9&E&33q7X zi?To*+(*EOO^F0J6y!8*Kz=OuZl;dGksQ|5GDTDn^PsOmMVl)ncd+9h8d>R3fF zNM~iZV@H3*Qsu5A2$t~!F_-*+WL$Yk=5d4BM}&F?nukgw&exw*e76mK5whyg*ZmrxjMU+TWC=?b*_!3<{Rc%}5S$ z;Vjt^B}K4x4iqIi${qWumK8Iz!Acp)R!bHWgcSUiEyI#6JdP~}0pm><%ZATErysQl z3xI?gefmYtr`IR2o}lL*o6fEF?-_j8Uu92f#YZ~4t+n28H?KqL2kFnN>`0z|UlTASvn5dF!r*}Np z?>VaEWEbAhdCS5q5A(9(NgYTnKs4tuNU2!n`gUREHCKy3`+Ux{gy!MW;vs1l)w-MJ z^{b;wNGSF?cCH;d3CsY%JT$@t1l;!~~DF&qw ztkKLP34rfu8V}9ZW42GE5oYj16||%;)dQ-YSYM%0i-$4(>t~v@GbJg>g34VosrIs# zAruAOLiqCJY79(f6J%Z%|D+fX2jS`s7t`l334@28mq>4PGG)j%qm5pjs zv-m#0#SbW;Y=Q}%kwN%kS|v5XVEJUsW{;cVk&YIB^xAb8Ou)|mCv)wQ49NouxZK3F z5~IeLzgQjS2|jbcfgmRjqwaZ=5By3Xo&?+)>wjkLEO0>4MjM}u&8aRQ!fjw`qCsG> zKjyvI%Zt_|FI0==*c$C1`9~KW+bF9}EGIBLtsol)I%3Ukt0sqlY*}EQl6!=Rh_OTU zlFXY}zFrqC(=i0I`oM{zbg`ELJ9p#=$lP!$Z%p}Ip0uk>NhYKAJe(Y7LlSE7XZV^7 zD-kC!jCa7$uGAzImpMO}Im?q`Es-X9+^Co0hxA3niQF0xrxag=LX@nTmJlfR%?SY% zvL@%Ypr4Wea`B^s5jdUj{$KQDZyFMdxZDF=e8`|xmoxIpihSZml$twi>&@IlKjy$f z=j)|s2-vcpy{IbsP?3jckCDb{00hlKq|?3^lo?i=x{wbey=LQ+E)2qupz;)uLW$zh zgP8^xSxxlVtXtl339gzUjXg8GTf0-R$cTiXm0SBg}xN|(1YllpC-R>T60P__SB=8t?AgaPzZvFxF` z{k?!&+&9Wm%C6pfemjmocSz$WQnvI`El@LKo|1Z^F6axIxr@p8tEM6I;$J%W$&EEh zkGV$(iw~aLLrx(jYdTNOAd$*7D$f6cJR^ZiV4L1<_I`U%nP!Sa0?ba0d0GnxeQrl2 zsK=(&#>w)MF7UX-M@otGVJ(x^^=%Qi0GQ{xpq&n2=D650EuP<%eO#+~SaHV!(8!DM^JK31<!n?!kej9^wsVC3$erI^k zkwyU9I1aPcF%ijb$jv-jwb&cYQL?1Mbz7z()hu7|n6l31)JDBxknVY6A{Gjwaf^t1 zDI)5)xLSEkc4oN3O9vLcT_w-dSA^G34_A}&#*oJN zej0*Oh8#sq3=@zZSJ1*4T&1&&((vtsw+8gMPpJMCoprc34QK=22{MW+8Zxabj~=;+ zxvckkDuR&;mhC){Aqeu$l?}N`Af$yyT4?%pzY#kZYUQ36e>dU zHytVJ_&p7vvzo=v^^IU_&w?$X32=vahmV?Jl(}Dm$~>&od8I}Ln`?W+i201>1dCVm8;CM9%`1*Pgwcae zz#_!{`URFb0AU3oXKdTiM`F2ZQoDi)2%=9YD5=>5?e!#&ieKtQ)L8ySW6=+#NIdn_ z13_m6{E#bSQ_E|&0QAMeIIukofeF){-zGW{z}?-j7Yk9Dns%$oG$?I>LFoMt-DtMA z(V6eoR&%O|W;1~3SDnr>$2l|(>5-_=HGF*}TI1XoI_&W)n}~LqGztC1g_yv zaY3%BpLzBg9TQp6Zx$DVW2v5)=AyF8`lByrB%~3mPIcw(X7#fNn#JSo3IU?`BzUZ< zmaU&i6f+$^r+VswCx6g3|pDyq5$&sH&jNjX3zH-vH`Vu5^sQq}+pwL+&CND*ae4NkTH_7ukfS9$YyOH$Ws((_5$lmWYIIf!ahHs* zEnrgEy@PB6%|GQ0cTm-L}AZ06UtKeoU5jXus13aS6N+~b{6qL z7^5=10lH`w*Nmc7?z2fn%?pZu%~dg9HC9XeKigCr#z$#6Xk2iFtx3~awI|&D7~a-F zPQsp->e>CsSXgtwBM!L&?A^(e9esaK!bFcWPRpuX(~}5cEwTVuhQY(HtBK;r;0VBu zO8`q!ctrpcOG(hK3O{HFl`drq`+3>+qe63{XOP6SseQ`FU_-N-*lX&E89Q>l}|V6;#qiBd7($tnY> zN|1yiCmW?n@mQX@&#M9}@N#8P=jdo@Ck9r}jKbn4G$0ldtm87Z6d>^sd!7So2Z7)* zu)9iLE1Q_foPw26A#Th<^17>PIj}U}L}6#QjRueaS@F-gT)U`LkjWYoqzr=rY2=Vj zaaS&0fO3szlBiYWs(sO{^4>~ez*&5)B}<)WGC(#?7#57*#s(9D1_G9XHiAzRat)bc zAy7+srax?55|Y>8VTe?y&j!V1Z-|EI7uU6SUW=Gjf;20B=xH&8f~J8vel&f)C|P!u z;8fm?Z4T8iUcKDYx=HaPL@Sz3H4r<4awZcXVZr_tg6MK4pqBCbp*JB~i$`)in9I>c zlgU+3{qq-hj$sUpYY`+P_y`dYxOcmzOzqET*o^TfN0rN$1i5pxDOnxkn;+n+%D<<* z5c-w#hH!q3BR$im+uy2Gf+m>HNt-I+h778FAk@5m|9pyh%G0(2Lb( zHdQuwIOurf5p;rg4*)ChSm%yLIuz&W;FD~1sNTI(8`mih+ zG!eBx_e(Va%RyjY!UT~m?xvc{lcgkdYrRF1X0o)t|Fs!Qi_fKpH1uX1{xBn-q0t^t zCDban=F4%qSB(^bHPr%m0h5a{%Ar?XU|tFI70Mo=DAT#Fjtz2L9h6vCN26kEYS!-p zOr{ZgexWsheB0L#0MgC1PFbvhqk4)$StJ<1OtL77J-OnRsuI`~O64Ijt68K|v7uN4 z7zZRFLJ+zcN)a2^#-+?3Tuqxln$kwPwK{*_Afrg#TM(iU+TSc>X(2?H=*74q;M|;{ zp$UfjyF@iSie?5^9B9bj>r=WLWT)0bs#cM%k5gv%f`zV5ls-kT+3|v}@P@BX&)>$p z;s$V7@rY@Gs=p-iXfD}Nae#3#uLr!Ll}oI%IZVa&qkmcz0wYN{X?CFrGbnZnd| z0;qd)9uP+`TaT_|?nC^PV(m&I>)HLO0@%Ym$9&F<>+&9>%a#2pau_LHTnp)tw8RLo z+7T^OhY?@|*oc~MV4~mAF(yzX7PHmF&_}#&XM?8rAKQJXu{vxV4dtoY zf#hCd@e0QG;T4!;<^P-fyACRg-R-HLg0zb@GKVlF>~pC;8FV*5V(V9*9e zA~x8Ehq9qC=Q_xy{(hGQTmo*_pPA@(oR@7rm@E|CZ7aPi9 zTP3O}j1iT2!XC#!6^H8wb5#gos~yILJ{7m1b+K5f!@evvGYu4_(23p`NY0%T65uJKj7=jZiM(ZwC%*|aa zjQqFNjZ{_bh3g6DTmA&Iik`4ko!l*31&m!aonkCQ@(%hV-@FV!Y>e?(@Vrqy>3wG4 zdouAoO(=7JR>TdB5(3+%FQ124&NNxV{5XmxCeAPpY zbs;3e8~6KT;f-B%SVv_71sHK|qjTX<)(lL8y*UKWAdAH&DLZC}?1?yN3Q}9*C&k~( z@}(UHMjZ%q9n}->_J$W6%9Lvo9;C`#2@)~pi2@k^q}{g3R3+88Tc;E=hWA@L8NC-2;#TF!37) z546px9wt@t=P$trQ;w}yy*Rn6YCPXpp35k+J|G*k6s<<+O8dy_L_)T6W2Kez{nDAl)1ici{U*CdUR z!0fS}FVn^)qFz(82nG{TJ;Zh6ObyZm4rf`bBzEZQ=C;RqW}D$-f(W56~2d{c>V|`O7IWW6>Xc|j_I)6#z`5c&Y>(F;Fq>m$fi}?i`gVOD)U`Pq5FW+YEb17=dR3$4)sh-RO-2<>?~!J#fpA3!D2C_NN1 zp(GP0B`2tY%QG;(Gp&z<9f@$UfqO-!OL0~&DLLjt+)}ojUp|rjSs7v{=Ef3lPvgvR zxtbVJM-wtF7U?v|EPbd-S)c_YDd&P_$(<9P6i=lViDt&v5|n9}RT3l=I*}qw7}>>j ztb}KEU8$CEXn9km0m~Ua3Kws%?{io+3=wHlD6Ds=0AGOrS;yG{W}>u45epy7CgK{H=W<}G&X(ZYz#!|C{{*4*)AxqPce7SghhBHAxI!ve zwCd4@GFy^PvXzF)j_?JjnV0NoAHDH-<`l$?4uFh{E9niEk6e#IWJpuTw(}buj`0hL z>QqA`%5a0gg);f5a%>5rh-q1gLl;_gF$`H#rMS0FxB~SUZv3g7fa0lf+!#W|SQ&MS zU72wS_q5&~!>oW3h3EcPD-r#*!$m7B?bC z7^$SXp1>3oPW!xvx|I$k5M|1%R2n`MyCD&Xz&6P}+SnI0IM5-|t!1hPPwpwq528Gl zOEmGxxXO!70Lw#8Hujq4xkgt22Abz1#Z7?7YYEKXP}DV?$A<*9iU_gJq2eu;CZa_! zEg1_f%27|Ao?fNaK_&}8u7Z4rxg3&m`-apwVPIh|+Aa(kkE2?T=Iq_#WT5)QqlbQ2Gm$Rbv%|^*h_dlW4K+=gIVPG8bLM zKw;WSkSLR#=^vh)?l>&yX2pH+R{4`gA`u)Lf@*Xji&z|mpp1k^^}k7b6DYf`@<8_t zcc`j!OFp)RDJ%ufy-zD$1PLo?WBhDCs8d0b!I&X|2U+R$ntq|z%2GKzB=l(3im=LH zjKdU@Kx~G92oM{vz+ecZndBkh00~YYOrgPm31bqP$Be=6`~H3It&#^it6Q>l&#=!P z{{7GYzxM`hI=eFArA2<0Z0kM)1aPDJH32{pT@os$VFJ-HA3OPuD|90IT_kU$-&yl# zjF#GcrMU=_H4n|6m9CZ^nvtIpExZ<6za#N}^;$Iai>D6gz{t>_C;iUM>~ENv{V+58 zn3;W)RA+xszy*>KO49ce17SiR=OOd-_)d#dB^Hq}8F4Lbpz8pxVJ*$@f!W;ZXWw$= zEr0liufP3CzcKxcEx#}@OHpnhmLSP^G&&qOcbBKN_E;)Nr)d3glOiR`^4Y8NvHVF7 z$+VWOhlvYD`IrItC}Swj}G zqQZ(`Q&Hivy?ZE4(katz!ZIQGFsr_q=lw3RVRxDp$Q8LdM*l-0=^Q118@0Rr`Wr*; zXNH$<>#nKT&hH@S(|vBaIV1mN^>m=cf?+o7L(xawv3U(sd^B53c>C*#6Ne=YTS!sOvBDCfe9 z|3VO?{vaaRf`(nbrd?)hqG^|7rpqMgv+5Ko3XIUzhNk|uQT3}1J-?bgx7aNbR4Us@ zbY~n*>9VM#x5nF~X;)Vz4}EY2^iAr4k&X*_GT+-f);+%N0TPV;lz-|M1-MVsIdxHr zE-16&GqE7vB9gTT16&R zL-nzFoXm#ZK{+joRf$5a{a&0a!#rx)W!_fwy*>H>S{<&z8h(~NZNhN`M=sijcMy+H zk9QYhe;TIkBSwR!b3Rl9VZ1SpkdSc6SD|_yOgm8;knPlFn(kG*1C5jS4D6@vE3Psw zsx^&2$mzH}Dy;t^F)K}Z zY%*lSSuVSfO=+}{6X1M_s*CM)Sdl(F^02ge7sy_dKdb;l$$fLAXP}97(nzI@9-8EZ zbfq0C>55OiDd@_kOF>Cl0+T&`>s%MY{Bj-+l3m$6Je1y^%>p1w8bW6|#Zo(pc?TfN z4H3>{UNBwIhlJdka`2NI1uu{TP%kE#$^o)D43N_up zg1LpVI-gK8WFby+c1$00hgsfFgkFY;-hB`ZIoAnWw2`VzTj!@3M4|}B2|QnO9RF`Y zr&6GT4^9Ikuf0;%Ko#wgs=iLuE2fT7NEzpcQxW#ZgfbNmF_UC!j!t6-=yg$4p6dnJ zXxJef22iG!ibVhzm1@;e=Ip>yDg)8pfpP?XRUhn!`I&^>EWm^1pvO8AP7%2~S=IqS z8y>QYtk#5b80`wlqQeo%flwzdCuI%ci~5KyuV0&n;Zf<^Uh;vVre^Kga^_rrib5w} zz2o`o>rZDhXrPilM&M9g0u{RDYntRMleLFKYSdm<12B6aBj9%>;GfogmMsn|gp!Km z)TL?9)i&G9UB3eskK9BRQyv0Tl2`{mUb_t*=FCi}1CAa#3BNHD2^M1%hkc(OJ{fP{ zI{`u1I(!^X;Em!yCX6Xl%h_9q{e<=TTh31yddw2p+6f&=6~c9q2pwcsZ&kBP7GNH> zDb9(^k9v~<#vs-;cY23Sd(q?_Y^!kx2R;+mvv%CF5XE`<(z9pf8@|_C|gm&96FgEbYzOo4Qvw`^o^xT^@0dzMHpI z`|1x*69MGq(HwK&A-Nl^vbv|26-6GcPOJW_jjBJ?wf12j@o{cH?p}DK(Ra=oC1D~o z%VIgd|5-V^(4>$q6#!)(o*oaY%X%S-o7~7KUg!J(`agXCG3LM!b!+d;cmhF`Wxx>j z3EQwRyDqaU1M5H!9gI(O=%7Ba=a-Cb_{fGNR|rLBGuTgfXJ^KW(qpT4=Fc4ujSx{y zSDejhc4oz75HAVr3@>|LxQIeH@~XV|SG}&v^cDW5OD@U%n6On>dl9ww?^>=a#nDL7 zF*p;yokTyei8 zHq$>PSsrk!W25S(LbV~VAhydzct#R`hX-H=cmQ4)HISB&@+aT{GY%f8@oNL5A<`}J zfK03o5BlIi0Un56^p2588bw=@xdR3S?y!A_bH}|{7~6Sp%ANNCHn;a^nGf#4%I-ao z=YBA|2gcl^4|MMrTAY(RGn}yJZv%K7WiFrDg1YanvA|>o9|`orZT1Q!^NgHrmDhrm zsQC2Op3bPpXbeIV&ck0$CBPEgANwO8sNScY4s1eYne;m{SdsP2m^;75$_^PbjC4Y_P!sj)cbnF>q7w7X!*;2?fkAbMX*tMihu0ae13i00Fk9 z?sM-ba6@R0n^6EzPZ_~R$Nv`*K>WbH5?#E@1fVzq)4NPS|HKLCI{~OTpz}RUz@JZt zpyenpgAauM>p+4pB{W8rDo;S-;BzzJ2NE+iegvks)gv3e(6UP12B_u;YAG-rmEph@ zI;Gf|;tB&*e+3=*TZky&f9S>4uOKr6%j>s}9FICMbgD{WCK51+4XvTkio=tRFo-PB z1qooZjjv_FX-t^fk8FyuDL&vt^+=P05S^pcr}D%;m1e!refk9_081XVZIasd8xRL? z+8V9f@;A%yoI6*$6b{E>`9ZklvGH8%iW}28F=x}$BC$KUUiMsg_@xv|&aRjVdM%Sr zQiD@R4jpXoT#wlZW&uBtc?{WwfQJWdA@;c8qq6&`*z}PR$HYX^q*-27(XQf&;~@<& z)c$cz1Gx>umPH-o)hW+Bw%SDh!px$HK8c2)3q|~luwtqex|IO=T8Y?LYKaxZxqnp)G&AQkBl-&jQYQ#exJN z03G=M(*n$V|B!CD z^xD=+;ugdh*cC0S{!iL-|v)+BEB@rjG~=)+?~sy8S2|1__77MTtgB1YRf4G;Y_LhE+! zshY*4r|CaR`aJdqS8B;+Xvc--q<9ThRJYG^{IIBe?)h{2xn;cz}x@9-t-Z>)WWa#SSVV#MIPN;W87G z#q{*RZBgyFwlqWab2w)5&fH5S1ftvDU3i9)~tFb(K7bH(% z!KjQiQN`x;y_K*N(^(4Wk60X}9<=MGt%BtRi(JL+jQ|Gj8quH8r5E~df*AmEa>82G zo-^J4K>T2|XW|!|%`ahunU9wFjRMWR7EqrG=-q1x%t1>KzPPtWmQbXyonU$WZE&~k zIAVY?d;D+sVS2~LaIUG^owRY}jMIik6ohq(wc;d|n8&cacoZQt<}@ay8HQD+sOsYS zpU=DX9R$39J{Xfcf!OuTDi{LAYnKNBdF~H|sC79UW$7s$?+iJvQtnCg0?R$DI~M^w zFeW3s8j2fsqoOKWBh`(bX)fX~U6FPR|Ycl_bAeCjDtj0T9)*#hsa^B|~ki~?$_bnBS7p_yvJOW}7BbnRkmE=`8@_jaa{ z?5yfP2bE*!Cjc0y*}Y$SSZ*)fa2kg*CHPxbzjxGLp5Ej@Omr51%Jyh_=u%m_7h5XI z9nO@6o}NM4;W2D^LzKx0mugr@9YC`lYT+lee3H$hQl?(V6a2NwFI!#R85 z@n)K)QN2n2i(J=Lb-NzNewV~V5~qzW;c=mTv<+^!!WQ+Gw3cRgK1t$?aJV+cH*xJ` zks#y6lardoXHQ1-ExaylQ8~PJjA0(fgtG-ibJA~_=c|$$ep(K-~w{u*{KhL9aOl?2<8uU-5{~VHVXzsCjCF^Gt^NV0|^4{YPXw zwH_rgmy+9e)cM_ERG{T_;JYVt?GoWcblmorJTai|Ztm}mfkxjvfE%vxrrDtnxI#|z zC3k3tyTff#k|1vF5pWd)&5e&6zj+-QOV#Ic+!V+$C`@><;DxKDav*kD#{{ z00$ruxB*r;DiTn82KeS9R2%8oLVGufM;<2w#kqt zWZBqjn5V-4Rw0)h5N!fEfwQdv)W{!5qYP*!tBu$0$fvW7%Wd!=+*<7mdR6t3K0BF3 zf*g5a!yTD}qGtLvH(5YIX%nvU=5Ld04u~EufrYAMsk+|OpKz#cHV@RJeEhPyit8h0I}2{QUQEqEes@$ zHTL`iJ_S>kD#7Jm1bzlc@X}s;$)rPC_I^>=4XC^R-dtL5{DLK_poihr&iFF2>SqB? z{11Rd2G6fY?&8HQubzO0QgRPewLG&FhziPf>*r#iP4C(eQWP}y%iCGpa)>?$W1FkC z2v4B`WFfuZhKaU+eA<|2yRzv{DvoDYmZgvYr66&~!4;FlZ+){4aOc(iCSn^E^|4gN ze>eYz(s}i4JORdWc7>qkb^TYPhn6h=PC~W*(CFBt!{lGqL|9EoBG2e$J%V%AQr19heSRj|FsxG z>a~eQ(ZC^f(-pzA-Hjwu1y{@JKK+W7M?Fyfa;$fjF_ZsnIs*3RE@rDg5@8U&pdWS} zEy9V#56m947i@!-(=aN8;G}BOYz>(+uPYOSj2s}6Zev*wm#7_6>uR0t>1jB9Z{6vN z6lgLk(2mvIM@%(X5%sCFu(jL%rg{i!n5JjS;bVNJ!~k_p*$_4Ec^#-A1*m5w)&Ve7 z8}@US50$5tUlLX0OT;QfalO7_l$f_LoQtB1pS4zk?Lp%t( zP7BC7fkE>Mo$AE}4uZr0R3kx2z!3F89Ff6*RSr5KSOJ^<+}MG{P-TBZ4P?zR-DQw~ zdN=8#bcYY94J0t(A+4mPdQ58!U_M6uLO7BEWip+44s-}|mx%0kQXxU=VjIiXmGR)H z^Bk0@JA(4S7dGb{+;dJ=(xOmv?2kSpkdhmrF=TbCu#PNe1RH~4Gmvf7r!__sIC}C! z2L!&@C&mqgFsGZpjGd8bde>eg$ZTBG zQ6X+2VOZ@9!@E5^91)|*>c4f}$gG;Qp$>rDwE06-?Fp+!qQEF)VAujw2_Bu#kJcVP zFc!TFj$G}hY;Q+2JLJ9F&dI>~vU-V-$5WM;0rcN(VzWR#cq*V`5>lGAE*>-)9d^Jn zSXatw(0AZ;;cf**aIQtUv>HP0R~XD@;^_)~P_LsC^ieHF4yyswllecerFG08y-l(gL3P*uchs5G$Z!geo+4HRW5PNaV*>Z_h49fj)9| zyY#5Bor`qwH1MQ;=ABVnU21AluQT+@DN3%mEJjPh4n^!bVZj}70y6r;HMzvX5Do9C zE@yej*Wt|=E82bo)zl&ef!uXD274<$Udn~-vzPR5ItGS{6&ra6T)RR|1ZLGID<0XQ z$pW`o@9^r75JfwaXMWjhjYOzjBcI7LBj+X_Lx#LCPCv)$hUeNpls zpe}H`-L(ob#0kl{A}n(*1k&!5o&DeIGTJ@d!`=w1PB7TX_P}5ZF?u+frSTPShrD`Ulv`242u&6H z20Fbpkk4V?BJ1*;AeC84@#qbFiPH|bb=i+MAd1J2s*w^TVx@8&A`kZ9bapEFlw`n^ zQt7T~Nnu$WJH@`cz+vJ@ZaK;?tfur$GfPQv_5G`2M-(pJ!39(rbA$I2q78iPfu}m| zr)vFj@v$_5kD-@2G#a3USW{YN`?dG4zP2glBpsOL~sgyf^iJ^~giZXTd@E zdAeLD0M#Q)<}PXYwj3RseI7d<)n7oXF_fCspT_Uo@uz;p4(j-g068ySJK|lnl@W-S zSViG7sy~B=;xgT4VwHb`FKii>dB&e4Jf1&_KEWcTU3A8b?UrK4b4W(Vv?PNI7le$t z1!u-lFnps>6{McA2kp5eYG-({KAY0$ATb6Chu>+(KR2qbpPxDqcH>A!tDw^GS2`dZ zlKT5}ZU&GaR?JO1toImZgee^!xx}{IC(zOG(tmDat$WmWW-*6Pv;Id+ExJ9AD3wv; zDNHD)x2sgRF?|g?Ex=}ZIZsU%hlixo$aKCwj={KS4UI4MNnk9pCyxtf3lDh^OTHrJ zRH!7dZJ8Ej#4G5nA&_BvByb^pRvg0-7zuGM`kJ6}umaM!#dr(N;}AFq-W12;U?qNW z!%_Olhnp?GVb4+>Tkc*3-)?D6vP|^9MiXPHY>}KEo})mQjzX>}B^QD{3azmdMB)~) zgt^By0$}Apq9KW|qz9ekw$6mJKt5e8chINNy-I7Z*xUhLzDNSD1RJKJVkDju=y%QB| zr$h7m0+BmJ@sZfuO);H=n#CKCkHQ=gTrhfh?i}v5)%>!wuNq&>HTpO%S*N60dbpSb z)ga*%k9zn>5nY}eqWtX;baR>1{Rznq>(sPE@KB&xxXWM*vfvjmfwn+ZkUIWb4v(j@1mhs(xVB z+X%8IUZ~9WlV^Rye_M)ZV_G063~mJr7T)ydn(ZsWHh z{1dzwym)oqEooBAjB||?<*c-@#*v>1{3P!SLt3n$*cC9gYMavp?1B81Dp zg1aZ-QXWk>aDBSv=so-4CMUzqQ4e48&8b@huZF(oAhVMbxdDnEvL|i0g=nl}>klsG z+lmQFUIq5S*+tl{VCzqL0ESQHiM_E3t&eoHD-GM55IWiL)NJ+$W;;NQ8sg_f39tix z83)3}5Ssnfa&~XN#L2>Ai8P$@SiMvXy_S!hx(A3Cdjavn1uph!7P`$*Q+3nEHrgjPBde|a=Pq5CwjD!9 zML@5`f!FnW!Rt*Op7nHE8s_h4P#%841aORI@-sj0TT?VjYBv_F%%p=hhe;ta+~_B@ zPG5BZjW|_G57GwM;Aq~nl8o)7Hn8P{eO>WVg~QiMQU_B<(KY+sVqEG5bvIygZ;00j z^1H$0-b#by-E2-_gv3R1gjg2^lV%nZNas!hbdj`J?!wS!okw46@1@uYJ$sNtCh=66s&aSNpqH}}ldv3p`C4JrvwGXv1M z>;V~IxC#=5VejZ`iD|}02WB-$#@K5;yCjeAT18l-ilG6Kq-!@+D#@a2H6)#Mes3i1 z*3BW!@Lt*n<5XQAxV#$_W0~FziqUaFhtF@$4hlF=$4kEE>iHFWvnPEkg6me;n;^iF zK)ihOrjikI;{Evw5!#&~>R5F|y`)kMR59mVQwd6&jyK1elvq~fL)TA12yTrq6&Xt& zqo)Vyha{b(r8x7zki8btzFxSIR*T=FqS#)rYJd2=DN_aM2cinW52}t`2&p)-R=OTB zO{jgqzy7E2a&rJEg9h`LphIi|9@D>AS`>}>nW{fs^p!0VJEHA*WM%9W01iy!E?QVf zPz)LUKm4Aj12C>`%g0Dp_;omrL5+LN4n-@H&J?!zJ`l@T7J#$M4pr;hw2ByKAUUG{ zg_!V2BF2ZK=q|doyVNvYx7%~7{TTXC%9i{eER8JEy@8cV3ibhV<}gc;p~+8#1tO-B zsuo|bnROcd2fN}616_>SnvEoI;ezDOksOBwNgbN2!JLD$!2t-^cXJa{)9-eYmUN6S2sT{^RU*gN}$8f_M>Kx#x(44t0rx2r9PZu>LyS!GQ)^ zLgcg*68K|Q9SjObSjT>xP&XKbg@hW!5(`;_4W^*lJ)HQo`8j}x}!Jd`mne}B?mZar~4NEf`k?8&5n zy65mBkg!tlzF`3uJyWA2**dbE25sq0%74Omzq)2aFedfyvnMtU(-_$5z32xO zqFSuOEul(4;a>=MpjY5VB76g#3^z{p=8kws^1N-DxHI~hNRfqeur(j82vpXd?Ln~aNd-qOU7tMs*}}l z^&jG@UH|;emtK5U_LQo6cdzKrZ0vrD;c&)X=deQhAx!}l-{b$3gu<}oIX>CFyqbkR z@@3{C5m);2UNuZOl0QI=GX5#IiWxokR8xWaeenhEXBsAEC zFI|4=?u5V*?yL|I(G!Li)&G@((R$H!Tf&{FpX@Czpk#IdgMH9zdW6|6>eMWLD3a!h zsb4pFdYGfF(KmenNSN@!Q?~j;7t~C5!Xx?*pr}7NH=Ivyz(qh7+7JhMekl_0?thR+ zbT}NOE~%u~s9<`1q99Jq3n4ywSO;H>p27p=7U36fieqfZ78(svLL2GRn&HCf<_NQJ z;dbFc2Ta!n_RwdHo^ViTHMUHz1x9K+_O61@Y-gV{89;<_$gP4=9=n*-B83|Zu$`3p zOMx;(O7#UgZfaE(Ljqi_(z$`Ga1cMi@_V3x9DS$addlg?q1qHCHAOdf*j;M7HAR$e z1!Ia&ENowJ}i@;VY(qz@)&auh%91+Asf7u9M>YI(2!!%u-27ADgO|qLVXS@hi zF21EBF}fv?_^K4x95O<23jQaOFL3sT41c<{##KeqcG>K#*b(2d08W^knNorzM^&BZ z?KD3^DNU7_Bw`KqhN#w#%}Zvk8R*5MGV38=Z<-rdym6<9B&tiQO&M=;4i&gz{Y@rZ z=EBFg3}y89d>jslm=qdjNl}+(Y2(w>s=}-jW%rzZ64lr=(g(tW@dW{<$>YCVn0ngwskeC>JP9QzfUSUS*c!*`T z8la+vWf{(58G4IW9Kkj;7`O;m=5ot37((P}cU!k!QpV9%B9P@a<|5K**xDny|B+j! z&`ZnCB@1TXS=b?txNS%GweVEbxWmUIY#EI&CF&wwo#C^J6&};yZpPELA^90uiJ~xf z2ppMv=D`W;GmIF{t+j)v@yP8)l7{t}yLKgQEaJI%EX)C-yRwPPsg9)ZgK8l<(&6^^ z16jAh@M4ezj6n2LCgS6iBwYSW>~UPSRl)u%vSpPNAPLDlcCk3Ued8_3K5@cei$M}l zckfX!nc*W5Xs355&Irl$*l$yhCQ*Qzn?A`3Rr}^!aAHnViqhd&ugeqW5;5q7)R0xi zyqbf6223TP0r%(z;03X_8kL0iv8O~+Tu4(aq0aG}q{+HzI$8986RNW;uAz$4@sF%Y zt@omqh3qk?BboI=c2@s4x?Qp2#1Xbe1L^N;Y&c}Xt}L=f=p>@f$MhcIWqL$`QRx>- zomjAo_6mS{`{KitP11&pF5(=pM80KWY=Mk%4j3Fq?iCLhHp_BeMEHr_PlEP6PMu}h z#hxy5N4PQI9NCZO~@2)UtQGX)N<@cawWhw)QpPo|mM=AN8{L(DDj@**owmkeXKOFQWxG z)DXQ_rDf2Hq?nbt`X)p-5b3m~oVMeW(sl?G`9r5z+IF`u|N1Up9!+1euS?e`U_M_u zc?{T*c8rQSfPR3FLcC~y5N?vkFzV}i1PQC?b`J?_u42+9!u-PqFk+dJiEuXXuP^6^+DI zfEXaBQz^mgx~i5^T`6gMtDW7%7bz53-2 zZ=Nt+S)y^z)I5Q%yK|RKJfW%{5j@LYZ`yCZ1x|FjvwD&~6n*704i@WPMt-08QvGj? z4Q3WogRJX*ch^6H9YMVAY81&sLIFT~c3Il6mb^HCPQ>I(IPK=DpX@5{cO*X?k>M-Y zmmA@g75OurONU`ppcG_UBCdX$-fv$>Ft9gw z+G#HP?YCR^E*hWsQ-bYomV7OKtNR2I;l)DOk+u?NuqJ_;nY&lFSLgeJyWz@t0`>q) z#!N1aU8tclz&sIUNI?%SPd$3oTyup(x_-{$WML8d0l-aWpt=V+ZUIfy<`Kz3Y?`efubE3Me@{bl-UZ>cg@H#k^xjxT zpEs23w8_qfw2!0uSao`vLsiIxPeNH2EYgPBUsgYNk?Xl}1zwE{qiNUcgYZRU->kP(H@B&$@Fc*ZZFyo(fyF46AR%Xj zTULLFN=5n6Xglrtnvth@m}Kp;bB0K7e~tqPpw*WyqTE;xy{ z9N>x!n5~9yfr|^)p(S0tqCaF)r7lH8go1z|IeiiQ|LjUL9KfS7`5f%Xt>sc6kMCQx z|M)eyHCtp+aIbS^YY1-{v%{%8c%p0I@53(&9-m9rg1U#~C97=zjaFo&Sza z*8mhRJ;DG3kr@jSMHh8f$BVs-9GO_QpMDu?ME$~KLo5Hf?^e~MJ@9Gd(7e#**Y>5SpMm8s>-fa(b$Ce$(@y zjrArfEP!~Ysew~V25YkRmEmCr1MnJ1=JlV>vtj##%3Jfvwnt>~=Q|I7YUHNJS=mbg z)MonbX!~b(6ce<^CAke7078F?^~V9`D?Xu1l+;B*9qUp!9~1RuYjhJN0}v*GtjGAy z^J!z3zMt)7lZhn($xzn2v&ds8w7^FNc+UUed6B}%@(#USatwk{B6NZo2c3qtO8ljO zH-W7zTrP@kc}xJb=2-I7H6D?pSe8&_|=9vTds*gOtiJeZJ7fwyU& zu7l`$O1gsOox*I$UWEA)))dj9>F|!Sp8#eCyb*Z#v%w;vllF8y`z!*Z6Zy2OKrG>c zd}E=$e9`alFPH5ngZm~PTac#QEa+q$FNA)b-f~NhUT|S}51h`ZTPKc?SAcoM58?Cr zEgj45FV2lliLOOeOmSokGF?jyE2bL~Xz#p@Kuf!_cDLE?FZPf-fFvSoQ`#V1lMD`o zU_g<1?j4xa+xBq&4b_88>+AV}gWE*oXu-Pw_2s?xdq2YW(bNaQ5J1|#|GVHAEsk)K z5!p%Nb-p1-fG|`XD9yuxVd7Zv7*#cV_gTNg7Q1_A_bf)#xzBeSLOQ-PHTa|dpWl&k zOOrGqwDus~nTYxi7ULiav41xuFF9t(-FI(+Qo;KXoV7nswYM%yg%OEr#yZQyx=)iJ9sc&(AIYJ=?zK~XmPciWTtLaBJAs8>;I;$) zv9LB_=4N71j@+g5%f;pB5_}SfG^jrYz2ZB$oqbmKz$zhiXBc$EH*Qt2U7m0O^-1;x zOPEyGA?EQm%I%N$mRRa|$WetwdxbLN8F0v@#PEU2Gs_L zD_xE@E(7q4aado0z?WVDvKW_SF}ShmA(=9l$gz~ic%%wM1kEfXqGU~*rQeLill&vt zaw>SVegD{_(B|2pbj&%1<59T+8Mpz(X$CUH>ek1*^l%9b4d^Iz-_dabPn2CCPVv=OnrjIkR{G27 zWUhMBxmniK7Oa&m9ZFLEh0(Ao=Cw0&%AA@UUb6>bka1*%>BaZX&K$r8HQ&ce( z>ey*n`zzjYl?OsOW%`H~l6pOr=L%cILe`v$a&KqJBLVWL*TIpQo=9x)F*zqM1KyMw z0?L;!qC&eh8a)AhU9iNYlC+EVp!Fr;WZPWNMOWK*y|Vj=k+ZpTiKHxW49UX#Nd=TU@`w*6Z2uU6eRw^V zLL1SPsa^@VPPPARJkqcmqJ!Va4b=0_ss^(-HurvzEMl} zJ;R8PqibMBGk?*3e&pzkA{7Vpj8gYw-rkG|H44^7-r*r$JbWsL*M9!*PZZTft?K>6YMiHds-<5HR2|3 zmg-mF>6ls|;ZH{WgDD~?Ph`iLrW*LOf4n3-MiZ7D1gvHT697P4KsQp^%eY%q>1W`J z1c)cwuhzeSjgvBEzeOPYEIVx3Kh$Pj;%48X`$e6`j& zE^x&G-XMG`WroRL{(WqL*nkOdM*TJNhd%z*Ui;POK<%J4qg0cM{=QmG4W;HPf6?Mz z<}VEL3Mc6J2b`cQVuHkb^B3c*%e${G?Mkm(`ugLRUZ0kxm0&F=ZGbm@e27wX=d9xC ze00-aDxt}HQ1PaTsQ#(^(1)k+{%s$#(%a%agy+!pw@89|OmJ%8GC4s#68YPoWaImo z(8)$%O#6mQE|LAG>ZE_*e6W|G%m5zVsT==; zCYTdS02$^e63cn9;eLAMmnfyo=Oa$BNf8>wm0JnR>}evrc+SWJ2`da#_E0 z&zhG}op~LwF?*MFq>>h!Q${AMpT&av#*=>lg28d5)dSWh^)6tXgekg%0GYsY=(g~g zZ+EUyF-vL?r07%?`Qwc9j(qIg#olG8{b3VzADgSDnLk(mgBPd9J3)vU77AS5)aaUV zWK5)SS#xvBqqeDdsxhGDpeo7A6f`oiC6!vE|^7gj2OIzg%FuS zZ)^QUpog$$_{{`kUB@gZOvB(^x(mTEKg{}im_r9S*T9*=L38KD2gLre{THstJOfuL zl^-Hu4K+4hBJ)|9=Z#Sgd1(6uiMj1 zhaI;}4?cJV&bAy=xO@*&m?p4nPhE5RLNSY`WE_qf$H_f`65X4i0va}Bzyp=CoMXx% zu;}cBp~^|9Ls0C9+3?XEkB}Ty%1U!Af*Xi|fg9*4NIC=!Ud!91?OK9 zuW7EB(x1JpU%z@$tr13w#R-_A5hsIMz=Lq(bmfQbb=RlzsW_eyhXIG7s&MQhi?IMr z$@WM6kwKVn<`NE!M|#7p}9r zBTleDXNK(0SkWxQGjCN1zSwNBMwsC8LkPUk)NowP{l z$nCQ+@$v$cX@=_AZX?XRQag*+)PhAmV?9X2oMIYxfE*@q6e~kCe*}r>#wsTnyD|bK zKN?69kSQV?MqIycQGN3yuKtYZWg4W5yyvOr&e2%7W16w~B)Dl`o7#Uk-ei#w+LZ3C z{Lv`RCzP3|4A#wv!XH9|!p}^h#ga$qGY^#K?Eei99Jp@a8zq|VlVPN#9J@Pf2Ip!sLILkyoWOm!kB2gPba~`ix8NTZ5-l^QU{!21NweoM z->k}rcswIObfdATe@2^zjQ<9{#4@1vZ~1k$k(jJAvYU2{P1H(*d0nL{rClYdTEjhQ zP^4y4^b1%zVj7>ii6p^Cyy^CuuiVoFKVDPPW3l4-MZO>rcFS2PQ~%xEwN7%3yXQB7 z3UMV++c{WEgrsp|a2`<8qrYbJpkg#=`rV2jj0xr@phfi&n;Bs7isSFd_d!iOh1ap1 zUTWZCOgCc-TJJ_E9xS&Az|;GviWmT%{V_fqmJtSZaLPcCc`{4uJI$WMYf28@JpXhZ zcb`7)N7H-L(8et7m6gntRw*d!7%~~WXKDG(reHxnqwPsu~a{*S#AUyPFt1sx{yljx8;N!3KVk$w>@6XV z1c(_Ng^y$AGL{`#_3%isS-mBQGa^wG(~2pmyO1_eyTdR&9;PNuh3aQ8i9*%Cn5N7Z ze>sN>Eo)!I9KuoRACsFOU&n2P)qWWX`@! zMyuf&2wECKD(O?z^nL_9kx72ht{g*xi6LMkmm))6+p=6)X11tTg@< z9G3wgC7Bv8$X!VNg|^eFl`Nmmf_FWUE~XDU?FE4O4J+;1_2Z$lnZ|+H(5S!iW-F8v zg?7P)zDv9eNWd9FQg+}DV&b!IR<4T#hHet5>+O1^^&P<82}m7WF!nE^oTgeB|8aqh~R7B`b`VDB=rtbHEg;1cl3-Gk80rAu|YoB)4L zD)$P?sdm9bqS%GAV&#rZJe#H#@=ZhpA^^LiOB)OxV{O-ia<9iNF2(pMVdTKi~==Kv#?9t~~L#eg03>gN4AU_{l!;m0qP( z4)hd#LX6v?Pjwx#aBu}`gz=@Qv6Tjv_yz=z$Dt#M=4O)OK&86rNUA+lxw>0+9=O$J z?e56s#X!J;dVlIj*tZYqQgfbr`@P zjL^ifKX?UNHD45Xq&c*utohLTX6upF*(;}jh;^N?u`prfxDbH>A?jk|3s(zd&H3FH0eKyZ#iFn4#eFss! zi5osm>T@h97W~}!Vx(Dsau*bhJp@4pc6wb1|=~|(Yv)(T0(OrPv=xp+yIDGP*+1(AAO5O>&&ZyRaO>suUTJ~KwA-mjZMwvK`YuyjtKq@sufi)5Fyx zt0+#tmQN>N) z+ERqc3NsP@{)Sj3MS(Eh6AnbqQC|`+q=4>!$s9=ANYsTb0X8d<%svH_0uwv%efFRV__7(2}RngZ( zT4+iX{1FZ^H_F0YeIcBOpF`}6`6nD||MlzpVIF7g=h5TU7UdM7F8;re>`be)jZg`k z$Q1t24hP~PA?cpef+SaX7XU`=QC1q8%9ESIGAs$QBK#gx_iKxgnYM|%_*Z#jYE}!w z);hWEX9$rLKr2%sL0}L$eL9Vk`b;7E6{f) zyj&?(1hrg&fi0a+MaC)}WV@SWfF-@WkFt8Zf-vVT9za+(&zaL3j`rHmqT=#;&Q#I% z^RG{a4vJ8zDBy+Lbh_|7**JI@UgLtYSW?2G*HFwuH3zKJ>$FBdBDW3Xnt|YA#lOiE z>4kw<&sSeJ$l1)`px4{~3@PJd7d7Y-aT*-RI3d zkH#6$PSU${4o2}54&TwO5)(--n;WeuiBlDwozaht`8Mz&oM8IGri@m6%i6ae#RsaX z6Em^dhpz-DkkSWu=6mN=Lcra&h{1b~lBg_fWQk2*2&BJ> z2Y`pc3J$vdA;Mu0`~fc`ZIr$TGhpPBgHW3=ss6BrmgHC>ccHk1l0g5hPBvOPFN79T zVQ{;esS(VCWo9K3NMl0Y%!Hsx3VQik1pUa9R*bzlM|Yxf-0!?_9=Vxg*N3por(^21 zZ_?dVfV}XNo^4V~4-d^=qoDFNcWuBDrvdESvM5PFaY}K<@)89&b$O`1CTL_da|cjr zm!7##_7hyqH_Lvo4E*1O$y5lPt_m`U!f15Iwr>sD(Io1paP+_TjP_D?L@}a+m{-l= zBK%u}P6?dCa0;YIkF-TBWd9Lxe9r@76#i3UwRIKRX`nYjy~n4f&cyz8;`kJWlWXX0 z39ol$0c<=`q2>RY0+DK$CNMLvp&#zFhoD5j2lj*kP$bVREeR&kR1FW@ZNkTxi^e-r z-l3jIP%=zUXx3lt^nb|nK_?^!A^-KTB(mzIxD`w$t$2%0n41OPU(eNG$tXxn^HV=2 zdq>BPJM*b5n1T^d-Wpp#J^{}AV^6zvWZ)ECyopuQx~XrHa7=QP-^)iljWBp+=>88gRH;LdbSv` zCD!T>vU&&^!w9OY`uSW3nczp0IoN{sGI)3#1r(D9zDHjeFimLz`-IJ27h`QuyBnu9 zGo#o|)5#g}Hr1=Y%|3tp!FI>GReS5PWVij1^6!8STu{-dKxHVvmA3I2)LQ?=;@CJJ z5F)=(RJdycDQtwFJ?icH1q|#9fb8MS_|qfyKxk-0anYJa(R!0K$=`f5ZfZ( z-SPv=YmuJ;d#5mGj~P;?F1FCln#x75+<+D;V?QBBf)>};`v;nZgRrx(W0-JC=#JnX zsTXJ)&bllBTy}M@gNmn8q~`DRzb(C1mRMnlBlTfNXwDE zrgy>4{)lC%xbM(UxB8$X7F9aN2gwk2L)C_@Lh1>q1U92;rWJq>2`Nd3-6>JQRk&+& zlxyV7H5`{91T-jY66O&8ad1iWoEDNYtVuh>ufsl^eP~%ejHh(e)G*3nbI>Q18EOg1 z(W~B>gWWA`n4aZ(P}YL6ur@`8U56xP%SUfc?ru);hNN$rK*jZDt2fbNoOiku@_YIS zA*)%fNcR(Gi3iCm6`M&5Cm)R{^JW|kl(Mr?wqwVB>~4T^ELd`{dSU0eqR>IGr$yS+ z|7;IKu>VSOo6?;iA;B>Wi@+g#gW#d|SkYgrMS+#vNbcN(!yK1UVhBtC`-B)J1258V zVPD+!3*DsjXo%|TgH3ye4Uxwfa)d>-q1h+w#Bp_&2GfDEcBC@O}AA=`k2c_7>u_pf;vSDpE;3qVz=o=yhEj8SnSFE78ZP&{?-S7ak$jiEz-wmqD%ck*?Lf! zz4CuAWwvod#bjYsr7`{-dA#jR@4s_Xat5I)9KXFhK+>Dz@v~K;;JBf|ziu2M8N11_ z-B-n_6Iu5p>{ZlGVvivP*2chsj;oHooqW``_`PGT`iM}qfdSXS3{nN4Dw7{Aw>|nG zpqMDacYBV7yJ`x|V0V#C9MahEB%MLy9lGP!Ffs!;UXsl$PK-hWK5@MIb9Wj_`aYTm zvz1(STyPqOkpurqj>$Zo16nYRQOtRndO;#@D@S08aCc zr(AOJz3;sI^+T30xJxh94Ivc%3Qxu$O)T%-#zrDh%eVl^+<-XRLT|M`9^RP6dPz0b zL9#tdp1Hqdj%h??aZph|u6+di>QQylar^B#>5z>dfj`ExDf4x`aGH3f> zti974dclj(Cfe_P{*9>VAWuxf%>WtxV+UKWdE5{(HD;WIOL-jjcw=Tn$FbP*_q0C? zfG_cssF^3|`5Id?iC$imuz76lpr4FNAi4BW5EY508Q2%d%|i1WK?lp0SKkW0&+eh3 z^rF6gdhyQgf}$l`t20zT2U=Yd(?)LkVbsIWI|+;67`3J;#!)O&(grq?HiAh*2C)*7 z5kko7SI_Id3u*QHNrB9PYU~Q<;PLHhFBGhykVh2ZI0`eke))pY3nnSH(66sbw>K)x zrrq1gdIbBB(g-kDu2k2gUR}$A<`c0W4*S1+IxjgVHEj-pC&x%^HoByvd-InF4PWF>hZAhSNDK8hY4{w5E~iQs5iTs&ny>+5K?k5DDO^Ug2c z^gF~rrw4-8&M2nXaSpjY>z+nz`?Ny#|-`RY)Zlp z_##yR#tP(q59CgnF9At>^yov_%w>=cOc%32s1S<`C0SZxdvErXJ>CwV+0Gx>PjDlV z7<%RA$Gt-w^9jU)j%*571+xuqa+5z3jWu!v%IeAz9e^O^ihmQ6DcfD)VUKSSp)tM^ znMr5L_Cd*J3;J}<6r#;RT%s|{*l=*AKUy&4?yN^0umLYp>fLTsNHQap_hzHhC=AT1 zgHx2#uSGHRNimTDlUJdh8or}x3Am7EF2p#61~EI~!M?VK4w^A$;~=*`U9gkhW?=A) z5wV<)&ZTwAyQhPWekO1^8(kk`SB;FBunGbNiI>e^xJVi(4kU%=!Uc%&xx>ka_eA93 zDk>F+k)_XZn^|^%q0z6|7C^%rc?B`bR?FC^3`?q@k(A?R+C>4EY_D3+^^#Fdx@lju&QxNyu&P!I z^QRqWjm~7Nw`XP*#pblMATdw#yt><7q)hj+YukNBPGg)X>DA+6Uktx^$~oCncpH^(ET50t zv~qq7+tAr?zNO4Hj7m@CKpiMNCK)3J>YKt$PQ{L87aWCrG{aANY(j^RaeDeVVlcBDIu2|$j}hdSZ$uWcLY@f^ zqZ2xieWV!If-N$IOZ%!{$qIVUSFJlE@q8oBW&!)Y%*t!64H}JtZ%W=CZ$j=Yxdhw- z|Mr}`56p7%g2((_j#~G@#XWIKt#cHv0@^oN2*^r<+EWLI6Ar~_rmx?eHff2(gX-52 zc0SKmNA+b$6uF(V7kPoI=uF~raX7SeT|S2+@6QjEF(>DEJgckn&gFt9BvMo4LGW~B z1%aku^{F2ypLa=%apON`JGdt7ayNdONE@W11mwwrq(9KA&2^`nJ5cL}DubOjK{dj7 zXRu)1BXE!#(D^eaYBuUi)*Ji>>y5*iOH6sP*GL6)S^rH{eN>7JRVly$V2_?8 zY%F8XHTnV@K=Q$Y`ML;zao0bD=6-CJyh0I{zlr=^=r_QRFb6gXnrY{`90XRY1W!Ot zJ@;ObW{THd^sx_MX8Oo0x2wx$9~pN@A0d*{M+C|r(&=r$RelI?WP5oCB^cs%K_G08o zi|7)FHNJiT!;CO!VAgA&eSy@%{$9N`at5`qDCR1{@_=%vY>R_@LI>GCKce-pTz#I^ zL;UPa`dh95a5)B+V%RIOEY=4N_N=8$4+^1AcG)2L@or_g*>DFh%~6g$Fk&0hcAe^j z(0A)w)8t4?BrtWgA_;{=?kR%{Jv!`GrpjYKNi-vYYzW3;wK7vJJGH1-dh};BBm-wv zfEx4ac2C0d*m6*fOil(4k8#%x5(7lQfsB#?*unGa_Zl&PxZxx*?ZHpTz6PLlis@&t zenod&)DT;<>!;%3qb;>wMjwVOCpwE83gDbNpV}cEj*1`@t#f1|9L^&Wbn*hxHruY* z*%8srj>z#@pYD>y68ZtX4)e*RC*e+=3-(wz-13QJx^wTyqF02Qc=Mir5JGqX0z%e& z9=ql_zXP&bg_gdCa0VyQ=MW&7z~|Op0VX*YDX!s$3MfTDLMNwXsq@jZ{xqpSpR`r> z$4RXY*gP^IEx}-Cq$^Ak;qTXVQ-6#t!6JQGRh!~C zuCX%3m-dZc$Bb+MX0IKjbRVGxX9!nwqt~9hL1<3{6~;!N3t)xxL>KU-MYY{VYzkx> zHEjMv1LRubUe#RWfDexOCUF1?mQg0rvQ9xRhXfBpbUgF7zYOI5BY3w3~0xEI6VtYk9ItZDxsQw^>!GM$Vcf@Tw{=^de>pl*i5Xt{X|E(qvG#8U$b$th0(4RFIax$!@7 zW_Y$-X-k7nQj!ZR5~jb<^@8?7g!H@WoD9#VDP`TdoKi|$*GO5AasvyU9B6x< zbboi6p>J|ou|PA!&S8UMEC9gR_63-_^n9Zt-E;7z$p}3O4L|w|A8>7$>QE&4hPf|u zP(pWXsIvpVkGBAtXZm5V%K!G9y^%@mNT^y716Uv-x}sXPGd)#q{NMzd+er46sem?3*hSLrKi&agw+%@zFsumpB z0wotxGFMw0m)x{jyTcr7jx~aX5H*o9VxPrcgb?KEO=YYys4dP`T4=wVxzQ1p3etqhh5(x4;5Y^Xwi!7=vHa|;u0SsiQ>QHAUuqKcq{4zJJP z?PA6#HCyX)M%HhS>h+>gpxG^Ufork7Tv)uos9w+D%jFvZOd@j^93+$voRou z@buONHf7cJF;!4lA$@ERh-e$5xY2~J%=iy7XzL`L0@^QyH8GpU{o$r=I8H^n z7%iB|3o^W)jJ%mzATVA)1JIlP!BAa57eQ)515L04%a9-Xk@JH$px_6sxHs1D;~Q{{ zsz*FkBH!Ukx};*1!7t_V0EV$dSIE{06_&gKzC*yu2I+J!v{s_6J-2d_4>5yrW~W~Z z4M!|nkF_H!j1gwk@hHXvcTEk1e#V$mXhKyV`<~Pa5DP&}&6qR0vipRBms70~*qsj5 z{ZbQl#Q>cXScFKUUV(PMbs`8o7Xby(2l8{Ski)UmmzUwP0>v8Tb3#m>!n7a(#*55W zf&c#7U{E17MRo|Bgv$&KKG6x3)V1}C>Zb@~5j880RUpusUs-~!LT(DsJQ$cGR{KfV z&P@S!pNPQ43OE7d??`;1804Ta+i}k*91LO?Cn28WyV)1<62$%!oE*FI_x7Ji4L2xv z_|PEf?NkSW#It$=+W;adRm$&mS%!-DBz_FNGp|7(^o^c?S7J*EMwRLyu^LI%g9gdx z8MPItT~bhL2(F$OG0ZH+J`%Gb1M!r?|2RObtSeD%1nMIi${%+w@DSUR_a55_>e z6QguVA*#3z$Gg0aXEa=y@XF#lzBQ?a~-p3&3OknCkx{d5u9t zmavsA=ilt)2L%Vlp_4@qW{IOiHDcL0H4!L&;5W2835y-DcgaW+ROu32<|Pk*O|*3j zzyl9U^w{_Xv|HgVTi4VBAfu}Krvlq9VFc=I!iYKNE#?kZ7r8DtUjPk8;J2>t1AoYl zX4)73#WaEb(!c6q5~RF)NQi!sS-2$;W1x8CkS`l2Ur_;>ZXSapBMy(41MJhMO2h^=>r13Q#R+kNPFhFILjCDjex}OT#j}Q-u$aaEaA_N;@MML* zX<7E-Afn1t%B{pvi{mjMPf3{$+8a9K%H{Y7ZHmwEIk5|c2xmRXVePXIqmP*4@dW^{ z!Z4qZEw9c&tx8}vItr05rsVb{hJ$$9H-AamdEtDE+fn8bcv@Ch=Ur%*<15f!Q`%%0 zdqBEJ2%c@QPqHhBiJd89PecY%EW(DJVFS5iK72w9^i2W0?ntUEz|G^~Y8}h^HJF0o zrGhaK1v-3$F1yVJ{(SrcfuTBkK~v5bXGKX>T_PRO;_i!7Hest-xzeFgp(UcPm*(-| zm2R@hT(|^VVTzTY`>R{g6s#K{%uikHLpHet!NXc5Er(WttSF3_gGLj-2yKIaEkm{M zDi9!B`2BjTa}lXZ$Mear6x`~*bI_qYl-}`}=@X8Sp53(C7$6N4C_wO4=jTg{adK7E z?VvWWnV#@I5H=-k!5H)dp(D2$AUCqXV53-4L73ubNg{Vz;_!*^_TgQK#esYs9-Qsr zQ!kRR0_f?uUhwF|RZHa*hH2GOQ*5kT>czUHp6iy_C@odzv2IJNu3?hh#2O}dQ3JBo z+nvmWvHj571S(eAwTzKm0LEeh39bP5m=$Wkh7!X5+it7XOaT-+k{^3s%cB#-PT>cW zCZYXj--6)-+FJ!vZ0^7G-fg2rvWqYRo4;ZtDI!;0fo@awZaGP>`psEXDAqJf-aP;x z!5b4jV@)${=##WVb{SN>9o$O%Q1;9o5%I3{s#pDI>r|--0g3mFVKGi>;h%%;3yC)1 z_BZH2F7y-|BBLQ{VXj@S)>t#YAi;}_&t&b+z?ILV>pS#)WflU(>yYT9}h^q+F0-E($V^#HcrvN0~h{YFr;07$3iF}Ut~EMBdyMVox&VfTZPMQI3gX(JfE9%}u?$=; zgwjqmqOH{6P{)WhN7K@7wyB~I}d4v5-^FgAuFf`QLX4oc;5wJ(oq4>jV6@5Sw}B? z;v2d^#N)o>LEb+|o&ea=oa>EhU;Za-lX@B6K!oMCF*+?|g}cikhkh74$!n`+u?c5H zdy?`JrF3i47l0SrX{tr4&SF~|IfE+tl~N~EI*B=?Ns~W>xp>X|#JDW{b6*!-4)KuI zL;xhD>_*B&v;l#K5-YJV^;g7rVeo7z3Tw!#XTTYX61eoW3pqn_ZmINNekptmTU*tt`t3A2Mwa*?O!7l> zjC5vdaL88lM7GmQ-^Q1r0_SPYx)L(WO(m+wcR^nIVk)rINoDw1_t)P{-mY%W84HGE zAkuPJQ?r5TI+jKiXb2*4K-W`M1Z3owS42l>X=NC%99L8e*JJ_58L~pKzzk&1v9Sj(6e=t zW5?j@i$Y@pu${6D=5xq6l`2&qkTKsUG??s|PNFT;fyZqcRr+;xEldswT=( zui7uzzVOw4{cV&(djjiS-72XEjR&h3hTJdc-an%d1Vj+PE!{}!CgUKb7jNE#ps54~ zAn-(U0!y}+^DSsu6~FqkUJ{N+IfN}t34oyL&}-vJBkBjExJRL5q-5>hQeACm20C_n7qHTwHNw#CV&&jxu;e`^r!C=-vm2;*83v(tw`@6StrM_)?PILQ_EAPh^VC0GJR|=#1dhUh*4UuN z=Q#fi>&NOc=aecTL|97acL)q8kw1}Fu7nd)w}I&&0C3Jt4NKsUnG?1{L@VxVGmVyPn7d#CV}&ViZf#xPwuyFtO zBw44$%hDoB8GdX#kM+aW&!y?MpnPc$a36dJrFB4vZ5{tq2P!pfIEO2WJ;>#Y0@GAhRBMDhbNzdzdWl>rM>#Q zNFLT~P8|`xEEomK z86>!70sZE1EYe6>Y>0`jVcEY6M~|So#JbQ)QPN z79P1h_z%6g215s(L!h5@psgNBu#s81k3T9@vefQn>Mft_SX~OuTigrxe<|kCi`7dY zL=Gp8W)%fg0?k=VTxnX#=6J||PHg+}>vcH(8>SPUH)K`=3b3Do7@OKy^`71dD|G{< z$Ab=W9f^oke}9poM<1Xk#tMMk5j{2v_{Kuop33nruC*6GlEt!9e#lm3_Lf}&IB?vn zCbU|wAX9GXNPY;tZ;~Hd{r@R@_h`$`s=#+Y&pv1G^H_E2)GMcwti5tbPROZL$t@FZ zQr*_7K`POtAuaZhzJng}hks<$$rzV}XgjU}Dh)z-s2Ku^8YNyMfskmthKGO>5G865 znuj2uM0qJ-3pUM59{2Z~bM1Y06#?zk$k}_Z$M=}ue9!sKZ~t@D?64sa#4>=A50(ct zO#RA1gK7H>mkC)NCfo>f6W5XeBSqu(;_LwO*zKL%ATnUl+I91xR zCMt{fgBpoW!m=znHNL+S{I5H0h*lV~`@um+%Y?VjapecRsDT?fe<43sZW8OIM6F_1 z3XtQA35>;#02Ce*KObt7C0a(|8)D?L3+5gp6RgUsHb*UYTa3oPt1_K(Z3VkfT&owuDv;~iGHJp<1;r2Qp&qvaD=R#53#BDB@Gla4Zdj}h9nXg1 z+|odvGT_{dkkPqc=DZ}?Prn+isrZb<&4p>F;z^QhtSQ$qFY9l=aE^=~>bubeBROd3 z4Lk#zb8CndP+1gs1UpE&@Kt5hLvLkAHDTpJXcR6FMiH-Q8R5;!H#vnBy|iqLwB*0J zu*O~oz(5{^z9`TA42cx&AiGh*JrHQ|DWlK!NYab5%uzS6k&^h%{#$mI7u14!{kg$H za>D%~M+FaON1R*&#>Lmzs~Z<8DyIykh{mwMmEP>6`DiQGNaRrOfC9H+e0Q;<3e2ID zU-734i$tOnViwTj;znsLE{ykVO=&zK+cg||~=fPr!v$1t`e z%EM3|&U|usE%D#%Sfq(DBKT)B9A%V z3C8(lhQ`Iv(Y=am&0!TEG_Pb1ohSG?%f{H~)dxEFH&2Li%{6YY#5oypT_qtGKOR5` zap^uD$W`DGkTK@(LQRMkJ?SkCu_dPY))U8VX$~bqtaOF|qSI8c1>XZ=TSQ2}cQDpP zl00|^2UP`%7#T&M{AlZHy1X>%D9%MDEmM`dsTjZ=2|w!W?$$qjEghML+DH!&)9%Ta zaK4h5OTh`Iq@J{8`whowt5h;kd|R@B*64|h7X%)+Vl*_-BKVc6ZZL$yt;@-qtRVy+ z#2*}PWt7wPMjO4W(`-@tRnZSl@}<$}+0#&iK4%*pcVGr$PIqd;@?Bk+Jp73X3plI* zyba{|@>@_8AmrPztQyI*ZmA!5czEh?n-Z8hQ1ueoscJJV2TyH}9%*+5wFYN#2W$Z^@0|eZ#Jdyf6{7I7X*O2+tZ~Dz1m}F$e z1|3H$4zEuMq;3w@#CYnT|4UdnkJ3{3@Za2Noy|-qziId{FfVV0eNX>u)A6V?{n^Q{ z-Bt^4WT)Hiv^%4IizY|it`BzS_{%ju2L9#&evd};qXlx&;_I}n{$Mb`vK$SELm6yL znLk7}b;wMAhJ$nG=c8xd{nVL1KYQlS&z<@6U1$FMhckb^bM|MCkmJlSdiYsKqiZ3R z4(nvfc1o>>+d)pK&#z$}CG;O<85!*Z9cWi==>e`snsy6Dc)0zB4wVrf1d8YISlPa( z_{-Ix)9?XD#%^o$?;47n2O%fA<1@2Ay1qWE!0>v+YxKjc1Fz!q%Ykd7pJ?8`5!H`@ zJTo>O7hhN=NubRY5LC^uSNzO!)Tj>GeOc#5pS^(}q#;yHBn$y<2NAqc-J#Af8Ja^1 zB3kE^!GCf#-1t>PG`A#1->K8^&UQz1jm4K5OwiCI6WOT0$*2pM&U@zEuP}x zOWPa83zyFU@tW)SwJOLSImmiN5t!nimPem!le*W5$v;yF+=!afK1ISBXz+#Qr2x>4 zrYZ;+yfXo$u5(XaE_^g}jCp{r^PG^tm5x{9d<0LCd+33o zZe0US*Toieq&H(=kBMxEx5%nPG1_URXq?Smnoe2RrOC{!?kT;TS8Dp+oxesbxprY0N)!ehI?metrIrr4cN|8^C=aO5@ zyQQdz$GXq1dhuBIsnz0oMb5{MPah=r-v>$CU1VJMpBUVF!ctV{Kd$h5+kali$ zQw)#YT6HFsw^5c&oWUf1r0p@-eKVlXUsg>pFHY3r`rSdj&AaMv+OH!`3jaB-!PRnGvTD^X#qReXs(KdBE~ z+3+ryl^Ha#@uGPCawb>Okk=np6n0&Znz2Bv|4(td#Pha6=BVKhoS#@~cFrk5#u4n7 z`0)b>0L|<{eaDA-^>2O%7xAGM8haPHuugEgae~BZ&ugmwDw>#%Ro5$0o=Cew0jl|6 z1R+e&@^Y7N{iRpx^C3tK#7INalx;H+HTBs9AoL*;fX1n|MO^o8)}Il&B+Tf`M5h1z zMQ~kx|Bozt-8(jsv%rQ;!DV7V%Pa@>On?>(1t|_gh;fb!EZHRt5+36+%uic|-<<@7 z6;7|?I11};Ib?kp(ZnCb5nJG4>TF;569f9qoTjM-5_}E4Eizz$ycvRR(|OP22}BEX zoN(@rv?iMXA$VEm3p%M-jp;nhC18rNQcSVsqNfw2hIJMXvA2O9t(xSb17rDRcFpD# z7`OsKA1%;B4+rtyls_=783lf8)N(Wp9u3)Lqc2$IDz2%fJvnLpDFqGupI)i!RG#QO z@A~z8A#agaMv`t`fyHL9fra@|6q>s}yok?36=0}z1Dq(`ObXHF0f23aS%R+!DqjyX zh-;h-iU9_}{0zsOJ}7-Lb*2G6@(=t(NF%TrBM;Vn1+wto3X!ghbW6c{8nD1Qqh)^n z1zUDwUSEGbhwn-rT)fR^L=j~-!@N<=Dz*?gWUMg!Osm@#oPy1_>wmFN{>5O$jYF+B zwCg!MG4Q8I`r!T3?EBIUm^a=0=*<>q^~H73fvFVlW=6%^P&QcEoxC2=Ez(ft)Ae#y z5Wjpy=ewUSs8a8pd`ig7Kz-F_u!dP^A`E5Q$*AEX@gL}n{(QZ&k}jJE(0Pa9wjywE zZ8jVELOX+Tw&wwzL0OTbwv_oPf4VIB#02ZprL*I_Q!1pL;xJ0~8Ocbx<&um9{R#v7 zlE~*lz_YN>nPe1?(OvUeK*8O%05i#mXuTwfqe@oPbQW>#QRg_zd^&Q~r+4`2IFJ%p z_bPg^>80Ha9;YKr%=0y|=0<246|T&mT<)~6d}1%-n6Wuq)_$5bj%053|6bO)9f>55 zc)p(y%W0rp|IeRgzZiHGtN~X`JaJ1tfSOl-{tr3ec5=cj{*?tHY4zLDzgtcLp?`{z zsF^Xrd26`DuuIS5MuVAW>#j=P1kPq@0B3SLEtZ%LyhuPxaAY$~f~sVh5ZkJM>&?9> zJlQl6k$sMyW1>iB2Kd{stzrFW9)T-cqS@2^z*g3Ne!Q3{#g@E8>+nucX5W+B0>Y&> zJN-}LyECDMnp%+nQ&6wCIc;GwqqFCL4RQ7)=^c3^J7Yi6EbHKBWAm_E2F?l~?cN=v zNEI!om>=xK^iAHi(mVWPNL<%g5X#hYeT+%JJYqf%QN1zd$nD!AmuxD93Fh&Wb0vWx-D zd4l0(AD;rVA;XLR`^Eo~$~ZsvaQi5x>6PuH@!4@_qKSCe4eH9PGgE`m^z_MLGt%3a4&1IA=7v zK%?v?&d>+D4xb=6SdF^9Ql7 zSoExuk?RwP)bra2u&)OLAdkEFx^6#LssVd&YLZdh$?g0X7_!+-OhhTivx1iT5HUCh z1`|`=fGtoJO}t}abzCRck4C5oQO+H1m-Pw-%`n=Ee_3YF>XdlIBnZbwif^D2CaCG1 ziLTO`$QG(2b$m^5c5KH|*uJTl!XcrxT zNNB(}-f}#Vn%k@2P7EXJyqYJ=Ci$x3#meshSTERI$pw;^)Q&1Yyhs|n335aAs5=!B zsMtt|p`mz%hS%?Q#TAoCjN(-d?$IkmRD2VoObvH2&O@EiaChdwV+Lb!E4WP^w*bh( z;ITPIq^NoAE;`z@8+bYzO2R{YF21GBcUot)VF*>yQGJ^~5v9#fSt66kUk(MbW&O_{ zN;j2lRDOr@FMz)p$2my|G+{H586--AaozjId%7?}Rs{aRek1u3smaUb+N~%0w~n=z z!0$wC-f3=CYvrC3bI-Z;#09sS%^Bw$E#W4GE!H3YTEBkytNn8A#1o!c_1DKm{g%J% z*Z=g3(3mW}1yBa{$6r2oaf@9saM1cA&3E8bV>z=V)SYo5q?=Z8dPT^+yj?*br|M0l zNobH?$OeTPl$U}asD1$7!%-2et{Y_LZo%Ema2G(`o%{Se!FM+pJ=0lCeA1a@i5`c4 z4dSam_WG!uQSGuNH!Cu1ydUM_q%lu@sXzgmOLzfiM%67Bf-jBAf;YKhIJeD+T%D7S)C5F?f?hQ0n-9Q|md0FR2fYc{m+pk~$Mic@} z+3Vp{kGxexrmBmj8+aZSy+9ud_b#F!pvusKGQp%F*4Qb3y$J^38h9!-P5bruc9!mC zW0@)L@$p2iPt0<=Jj!L%FIYkS!C#|VFe%@~+n1rbL8LD+C(LU8azC~+!3_Q=xV{{` zDW%xh^E)FI7U&eDf7vN-N2W#ncYkdM=xJ}qgqrBM1yo9Z^&{W#CMNvbWaEFfJe3iE zZ>MG2V)B!Bfs|8IoV`LbvjvGP)t~wqRDPRDhe~o(T|OY>F=i1UMB)3$yiF(GGd%{0 zIlp9Q`$@#q2_y#BcmHf0Pk>0_b&dXOh!-+5Z6-g;HtUb+)>`L(SK@9$b4&7DCW)ui zWWPV2>n!BaFL-sPg*33?Qf~5M1xt{PAo5I5rdBT()45@SvODVX2?^9^_^#k`brH>4w4ag%wp&fRQ!;s027S-KGXYvi!7p`bJ zuuf){MUkyUE-;e}Cf%zS?0}S#?fOkG@7D`?EM2z-<-0JWGu!)DS~K$6B@7!waa6S# zDQIU@c3X)_z~!9RxaXakk`&}hRBN*3?)717G8LBK)yudk0KQHoVk$C|UVZu@DB9f+2y4%t z>pJgXvv3h0jc$rWnZ=)W@w^CI#bM4p7&<_BsGZw0m~_y&H2-~B!7zf3hZi;mvc&2!@j4h=S1)Ab8p2vQ#;4I!$zAVH^#^4VWN zfHr_f2(o&BHmHWsPUy=?58@d-%S;9^e!h7AN}b#8Xel&Pv{Q{!JcAgro)R+Gh23gn z1O=k3psB4?OW}e-3$}W8r_*V}lwi?|dHKs(WrY+A&0}VOt1PlEBwPf@ygPnjCsHq$ z0w<>f3hQG#WH;Q$%rX>Ec+%)9$_w2Lo^Tco@4<wL+T^S7#9Mx$K_cX6tNy^`7xw}t<2Bwf<9DgtV`!|(Kva3?_CJOiIW#6?({)JK<)JWEc&z_->=@|!(T{;su^|Q zapTxm_IKn9WWtRX;0qv5t6E{S=`Sc(wA8Jtf{a)_v&@HRp0|{1MlK@rtVL72tYr*G2&*K zr}-Z)*<-?FE3AfPrX)~EhQrw03&A4uK}pyZO2RJHIrPPh#WiN9wv*#6cO!gv;Afj9O)ND$+)6*86Uc&`4Bw#Px&T=C2|Ih68oji7KmW|Ha$w%^qN!~Be8Gs zQ0uyhkp4lYw9>aR{4J6b+U&LCy)4ig|GSB8&^^wG=w=XpN69-kxf zEK=WCwiv%vceoY{^DXyCV^}3Tzw%GO^R!h%F0Y?6w)_U&&RgJU1tCW#AaWVk1Ftdm zR}gpAGOjvLIcA{qf+^PKD`rfvAQFIxU@QmcFLcs8aC(2bMw_9OE4r7OX-?qg!~ocQ zwG4WR5-QjW7?O>4O}R!&aEj~A02juf^f>%^d`;?ejHf8Wr2ln6&uF<|jjd!i&@UdG zSDN^<%$B3Oqc#&mD$6xoykT^vTen<=MqJHSxmN-!*R&qi%3V6{cX1aXzpBcorLi1C zXt+nnLtGmTuB&ZTvEHJ^ctz)$&MrN2x+BU&2Jz(FR5jJ{Z>xp6M)S)TJ&exXyag!J|hBSF*Q zmokNd-6`kffK1@I=}yiu59?j083v>Fcfi-x_D6#(%IfO zD%UR4fc@zlVvi`}`{LJaXU26lsP7Rx{IAm)aOri%?BuN(uQNYyu%)eZJTu_f!s8Hv z1wtJxrITV3oJv=+ytxf-VuYyhdG$-}z%n8ZA_yGY;~C0y~fw ziTw;m2N>q1ltI7^T(0|2;K=*lZ8drnmtg`-Hh5d#H6xz5tm=NL(tKyO0~vE&b|Km`;hgbV_K3fv(7Xn zyzP_}pKSq_2VR&=L?*yu}!@&_iRV>U;L^XZD#0a3t$f&8lHfAR`8mIIPw9L(zCGepaV zfG45x;?pnt*dOvIdW2sj!NB5RLEJ;I0Qqo}qVt5>FG{}gQLtGt%ggF$^T9MAD>w-* zGe1V@iF``zJ7HFm@{f#hZt4Hj7=Aq^?W3wE@FYyY-`I&x+ERn)mY6Id1zu@K)ll$d z^vTZjoL@K+C_^SXuO>T+Wtp%e=0j4@gT2(^X$qRc>pd|f@q0lMyo!r#8zlu&!|q3w zbRaxQJRno5>KDBNmck73R0NKG<5as*edp)W46g_H^?6a>tg9C*TYv6F2uc8iG0%Ls zY0$WhyOz|Kjk+AMfLu|t%zQfbHL|OnVZbKzhgw}7rkJsX7M%L|9{0(53lB~ega)*j z$?H4R!tiBpliD#8zP}AuY!x4~;E0|L?`y8a$=ZO#w6%fqJvS+g(v`}%p+GeQ{nH z&Z&4uI2yurFb2`W+7k>u$LnOcw4k8|O44+^^|eJI;Q2%uSqkXrFX|$E6F68aglP}8 zs)$9(18GsdfEzM`p8c8vCP1;#gt%+sX6*0bQQg2`6*ySj&k*w_rY{ES`n1^q&l5mH z7|o3sM%fngeHrMDn#D0Y#f#1577E5~Uc!$+EI3rb_+pB~DS&1WV0cy{@Bp??#06c= z_u+y5Js1W>uXyQ7f`RcsF2(znrL?k%&H+v1v5u8@=rpRELAD#YY9-zO@dF}26MsGH zG3ug;Ck2nah)AsGO&SMwk(x7vJU5CuIo;F)+}F!6S*NwJWO(e z{VD4VrJo9)%O|cy6ltq+pL@4l{K0it>+-ceH(OCJCmCQ++m>{ScS=ok#+cFu!a)jx z59JQVVIU{r)rv%60QgJhdsouu3=67$YVW!wz54?*>(+iI`qE93NHL) ziY>d+K0Sk%PE3acPh29kpenKOAD2cEIcGNF?vp3S@wQjCpDcf9tEA55SuUgJOv~Av z5#*A4ySx#{0tM=CeXGY@U$Q_9qv6G9h+<&7_}LX=ZBZdHVo(nDUtY&*O`Zm7h^u&7 zt#%A2Z??d8Mt5{4dSk|CT!a0Q-QWTei^OYGyme&Kv7Bx{6>Yqg1;h{!p&CSNE;&Y~n*?J^2R3VBtOVQIo=5jjn8=VaU{5U_nJ$50!4I$1;B; z@MtR2sZIJIgD|fb$y}eORFS&?dlVg{DkCfIA9=z`N7jVhqDqxV1xa}T<%w|KdTq@9 z=uCB1IbvP75hw6?TsGn`_h?zry-d_CUaMo9dJIt!xGCM-9sTswT8RQNu^d3s5w&fU z$f7|$f;wEdAM0O>00PrQl=7!RoHogD%+1w;lVbiki|(3@5-+qix@yNqGxIbOi* zj?C(?hhu`9&n-U=wcyKAN23g0>sRF`J3uIRGLAiRCH^~p9n76{`=JzuZzvakc;qoz8W-E+T>~U3~^JeGySm%DFV2_$}fM=8S09a~$^RD~H+WAR%YrcLXD^CDy zzWz(T5anX$Uc|R30}ARhTTmNt3qZnLvF0(Hc;`d;4o}Hi0%05*1m;O`)yUq^y!`+O zpw3sm{kG>?hW4R2C6N^e3}6t@$BHOkwi4?Q->R^3ZP(NORSmYOJ1I!_7(2G_@6P(rO7&fu{ByyDZK@bxh;i#+a( zB*wlOB_AC%mO%Z zqtrwZbH+*;Vd$Gq7z~gk#?>@$uPd0nB=hMq=E=V%ji-c!{lQPlva?z(0hCKSXN~W6 zgqHkZJceD7y_1^iy4Ermb`M22uViBmE-tQd{u!N_UQ$0a!8~rcEta%ahSy{q?*_f) z#L*#e>r(p{4q-Ce-y2z68BNU5_@8 z615I0L+D9k-8fVXHy%=4K*bx1_?syV~EeG{By zzJ4h&0D~s|?(wisn!i4}6+)i{SFXs;sUUn~wTeiSyvZ!*HRrr10wcjKbu{v@qvPv5 zY-x0;wMdf0!;O}=?B0gAQHptTca`qM{QKbC=wklsxdQG5A;pEDrJ4F6n-Zb%!(^EJ(FP!6ww%VPepXE)sffRsd04xWdbufuvhSu4k2!^%K#NBG+urkt(LADlTsrVMdl0y zag6on1HQGjgb9OXoQ-LT+?GY^rDBo#AP^&y zLndFzi5=q_$5y`3Ib=I=9x}_qoa6YM@F|Ct%Ew!Bsq|<$MsQwSJv#?!Hh$u%XajU zh6LxBj)onX8Pi8?gxBJ26-fZ$NcbVxe+{#gyC75MwbLz=>()ONW*Bl?Rwb4<&oq4A z70Zf)0C|Fez@=UM`bxD52?Y*J;Y2`n)hNN73tO&f49wX*;^0*)6{#JqvXH}bk@i@~ z5ZE}uW)eBI#9|ObVNZK8M70doXETsJ@j4g6wMuGj)xY^N#ZWp!C?B1fbmBZH7!|6) zJRSKkhfoec)_bFF-mj#J)NE@{lD^Afn#$PfJ;$u@Eh)~_saZ2L6L_o+>jhn?C zm~G=-s5B7w_BbO8tvkhiJ61BqJ@X*HB6i?}M&XtcQ02<@8tSMw7!pGTT@k61J;n9y zkwG+c0CGFRH6nf9QYx(T{$_32!cpTkZxZ*d; zQ{VUsvhakwYs)dr`A`fj&eKNU^HK{o7i^ZN^eOZ7K}eO4qkcGr1q;I1N;N0*lk;Nh z^@~@pgc8p1_v_ed$DI>TBo0R+G&B+#W!Z<>=GhKrABQyG0{f^zayeMu=-OX-Hi=xJ zPlwq~XKSI+b`jJ=Rt*y)sMqp$$@}U5HsrIx;TGra1qBi}Kxru6vRn=sna4SC?{h!` zKF6sf;k${YP+f7r482$XS8a1~^GBB;zR;cZlOcsvV^DKDn1BWneB>V+x z#pQtAODy{?*t7@mEqL}!>xTii#;0J%ZH*=sSRmxQzJ;3;e!jrzLlwFs_t@+%e92bp z@!h1gh>oI}T#qK22P2>!go@3R8zKnO6S>Hvb^C`@`w?{x-%RvDanH&*rm#I;b1f&tUbm+55*ApJK-pIDlh(z2EI-($NcZPC)(vjFDNfM@dN+Ydb#<$ zT#bwF!MBUAq?u_0y0uc@I|a`VK?h%t^HJy0Z#F^9Z@=xw0zCpA;eiV@4M?bGhN6BE zJ4417<#*6ZU4il6RFQyPm$u3Q6#t>T-D=Dd_N8SxsDqe;JDIryLDpm5N1 zIS8Y=Ai^4h&XC;Il{WjVZo@d)|ra ztjdUtNvygxClcb5bx*+k2gZJMl8^33X&AYw<@;HF*RZ- zyHC~g+mL)eX%kwEU1@W5WWF_Wir9yPMa+9TOq9-A==5qd|n>ZdEnpePL?YAWFq{VH?)iBId-z{)L)(4r zwE8AK)M{oM-I;VO`|P{HmGvqHSB?b)-_tm9eWr^^eKy8|gAy#Y8_6k7p^Lil@O$NLwv;XCG+L#R_l=TDZ2g8x~@~= zpIm0k7J>tYy-J9gL2+S=^P|yKy7hy7@&5yHy4rd_o{&+;hL_N8=8SNrLM-Et%`~(x zPcrB3KJ2VW?Spejw+HL)c6A_8b3zd33gV<{Y?d*OR(({dX{i$LA7fvFjmUR|Og1Dp z-fJ`S^~Yx2m$aoRphG!)7;x;_C`5VC7tc@UX)*-NwJ-R_B9u|#koqdAcM!K1SpM;4 zoxcx`rQW;?E5vPJOB}-Hd0cFusy_l5E;+a0FZ+x8k;_Y#t7hGwwzN1Q<>VSt81@%P zdlJPF6h7%TF9^h+5Xi}!Z(fRM{>1T37pwJOL5~hU4E!kCsqgv?}PTwB|xZ%RZvxCZ@Nub}` zPfa*KKN+jntT$@=%z9In)xBRZ&K7DN4Waj}@d3r_YBVH;l2H5(9u9`i#{otm2%};B zr*?3=7T#Z|>2z z{0i?Ah_;jwOMu%GLbIJR*>pZRcXc&$&Pw}1(2;GF}3QxGfKyH3)0LZ~r*0mKvr z04&oFrXbOdJp+UdOzEhohU8yxQ&5~`ER%V?1Mi+=P4$P@diC>9o53gA zhRVsI89k z#+eaIt3PqaoQND@g&X*QqJNwZ*#0wMBdz))e;|QB%4UgQS@C=s$qaz-Q37RFtjkuz zmj7$=)V=YtCOfl=Q^6!fOSqDKiA5QZ=7KM`jtc64AZ ziztxT1;d3)4uNPY!&&yIJUBg|o+u-51lA(L#{7M2R1;xC*Fkr6mAYqFiw9R|cTVAg zW(Eq!G#sl`A{_<`D0GFk*}51sCcLD<<@NNCMmjzy>+XeS!A;qnF84t^pGB@T!j6G2Pf|a{)K2?AZ6wVhU^9%kqQ?92d+@K zmY%GSNTho`_1#V8^ZqaznOS+Tcm#)0zEywwTiu)D>6`L#(w}H>sUDX5%5$56B-$*O zdsSmAf;~8=%E3BaVA_Jf)NH-E!KKLITr2ky9*DD`Hamu1LvEI8=LZ9BT4vAG>_54pG?G5a1-Ae3 za900zvJ4ah2@1xjDc%h62D84K9YkT_*+(}(N61+%k*!OD5ga5ZzU5-D)d0HU2dFS4 z8yM*Z^bF!RHO3i_tSlg#iTq2}%yKZT;-h3=1P(6MFGa(X5}0CKVWP#y92id)Z&@k# zON_yowkiTee52h)RkpXMX)k6J@&!V=P9CBTM}!<02xd$d)i$p+ z`d#;S^}D|b{Z2puiCAuAE}Er5AbZ#V8caX5yq?@UaB9N6(~hRIsTZzuye!KN^$c+3 z2yQ99=^$a(I12HE%PghE6lPN1EzkGC`UMS``sFU?IV#7(SKl0tEp(??us}ToaW0u> zw_>jzZc|W%-UI!X3>@wXI_OgtGoyyN=l~W2T`J|yOP1xr5jKIYtOG(@H6Als5*|EA zwKFThWl3(41w-No7!~Os zkS`?I0w2eaBs=U{x&%KujJcF7tOj4;c-}O-SPsNKOl+gWN5lfw2{oO z_(S|sY>__?W}Z)+$)y75v5ybIJ}r98)f+@?iyk+;-2i>X_P_ER1RYI=1nXs+JJ7x` zkrB0G0_?!&rLu}8P9DeGL#8+xatN}W;fW&*C_C8U>hFY5f@%ZnQ78$^Ix%9mf`?t7 zJL=dO&mwscY^?TyAX%Q=5o+L}1==R+21aHiR!}?ajLy!rGuuHMXq1{}=KSQ@eba%h zUR9nG1tVJJjD;Znr1ZDg!%K`6qM)EVh2HiksDoTl?4e7Ys`e|+a84ZIHXoloI`|y}@hu zsoQ&p`50Z}pk>E8^;qUC!YoKsLOBVMu7`@TkSBD6ZlJ+JePf@mKPmVu(qmAblit1o z6xnr&sD6i*!iGvQasW!xXW%_O3^*==@ni$gr_Z=s_cUzHuJ-AOXkNUn&692;$#%Zq zw|=B1)WC)DMmmES`q*Xa2@ik)LT!coWshPiJX|^2G|5*zqE*R*st8S1sheHR!-jC#lvU}g$qC|)ti#=e>>jWcMycpIEGGjMo{Jhyy` zeEJ$~efBBmp1;K=zV2dGQPS0z@hrmIxVD;m*{`TM#2(@~N!1K`8@UoH5D-K0kOhq~ zp{MiAB#^gznfdQrA4Dz)UV~g%`5Fo!>oLP<`FR8V2cv~DiInHSl7wSm4g*?r57T4< z2Bik|=K%&%WEjo+8ge7IgXkKEEjY~aWSIGLc61vEkjYvtUsdk8DvnzT%tfmU;%_jQ zmO$s_vU;qG7d+NQ#0?XjL44C<{tckm*a1cOp;{Om4u<3D!kz0hk%EE*-7+0mA_PGG zBh+(OFaiTRpb7+nxkhve5Fc|1ZjAWT6-cvDxem{Grzt+}-OOe{ziE@Xic@Nhe4x}| z!g~gmpFgApa|{F?yun0 zclr+XfTqh53-_;*(Cp-Q+L`IwG}FCG^v0pU{$#{N3?|G;*tnxvPDFX^{)!zu71Suz zcwwcti7Mdc>tHd-c64HYS4@b+#4|8qX4cc`e8=j~Kq(-Dj7sK`sn!Wg_L5Iw+0e}n zBNQW>SJ}Y_oT;inwILQ$z{QU!6f?n`oyVsOHF-&Et2242=b)zyO75wE4B$#)@lzti ze-)%4uSO%X^Jr(%D5hy*7ju|>tC{EIhz@>EwN62J9i?xr@_e{^tF(WaDnf5go zkVM9(MJAWE?*iGDw z?ru7kP*4bI|1Oo=$s&k^NZ>@}kR&mmI|*A%QUK6>2ZY$#lv->XIKpsHwHl<7$1Ixn zVVYzd_}k&=5-=NrAr;p2m|lZg+jW=@N5<^f!~W zaL{bq@H*;*t#MRqtTFB2ob%3AbjNITd}HC@R5$g}s^E_>| zoFz!ysc-WqW#kcj6{$KZE*%(I5qVa!k2Y?0pOe?_?2(1$V%v5gx95fMKvI zq|fslyTl?hen4`flk-l*Wq7OYTl{E2@6#u%4(Axu7uK8QUbdxA*;~+AoLLv_ zDy2T7_Z^Bfu5c(QeCKh#4PtMyfUE{yL2Pi3N#@!&h!@b24xune!6go(-dUu~3naU+ z+8vgTF$dyQt5vAw>{B%tMb?vQI|My4xmzCw8)FxacO#$x z02p}WG-7liLzrJ~;7PYNzukA8=miuC>P2yFv5J#LmQ3bgyToK!S1oqI5e2c^nh8N= zR7qc%SlZ2I0QEaPJ>-;GE+pF4$3sn z9jSFNYM#?NjDf4vghJr3nL>_;c*JfP3e(25(&Vbvr+cx@K9I6pxtK$n0Sayt=Bfv| z+YHlsogg{rJ^`&T4G1sQ-~`JUzO(>r`m5tM{`a%M#gP*ME@!d>F(?R#y(=I_)r9X2 zv0O7Kgb2zK!+?g}HO;Y=Cil*BT*bcz6UT}jcQ$BnnIT&uS=pEr5cmeuW?!D}V^rL2 zgrWpEsjAb7O?tbC3sFrw;zB~`7=A*OmdMcHr`iG^t#RaHY?_8_{;?S+#57vf8ad*y z067X&Noh>Zg9Prd5w}i}d<;^uc@_xsKjO%OqKKnTSCI_@>yn`E1Hy!rM5>T(#tq&u z$_W!gX?iI&wXRy~ljg~+*x75sfaNwhficn(LNCFn3C}g986#bF)rqdUwu&7&cSqSq zGPM^LA`PsiF42pcGmE^=K@q*IqHRt(XKuuv-?PTgBE|T@P(&k`WBi!$W^sa;NL2u9 zeA~6zFces#!)oE`bp(Ag1ifdCR3x}+R}_S!UP0ZWrov>UTF4wWR-u7fMj`)ESTdju z945FN`@RKrl}U4rwLutCDF!Irl^;8*B0$?=A^1PyXc52RM-*9vXmqY9v|yI(s50Rd zPR;mFWmL!A{Aa?C;jq;#qR~e7UHu;>LLRYbFMt%3S83K7xko@(8KNr6{w==cT9n^m_$+lvA=G#$gFP0g_M~zDb(b zX3Pql+^oO(J8Vv1KP;WgCT1@Xb3@ql^d0E|!RFvT~-{Y4l2 z6qs3X$fb+jN#N%L9imu`1PBs}%|uW|ZZ>imtGz%rW4@fW>dps>Kwi<>@a)yg*>V&g zxOBZpAtNtZ1Z%HdLF7bh`OctV;e_OaED1YPq!ZuXO5OPnD6xFpf=d32i~aiVf6w?M zlYwL1&T$-l*cEeDy)e4~fd+S>Y0!tvbSfIkaDDho_v!=;-iA}Q-bi7o941Klu)7A0b*wEwyDscODZlGE9iucV+QY`aV5>j=X*P!jPFVKeBYXfr1 zOrz+9p02>}HLKf>Y)$K@HJh-+*I`9M39Y_361#fR3->GVnGeH?=qrkP9<{GsjcbtS z?91wHsfncB-XPLh&-&A6Cj-f6CsD3gi5^p%gR{=gM^A*)X&U91vpM6npkmb{SR^ip z_$QW;55|m6R_OV`fvUB2z>5~n|6Ze>-S=901yeKThA*8t6b8E#gZ)S=RTk8pn^QxM zs1Z*Sn!tu9A6M;p6}vq5mO5F<&GZkx-@xRs;57i)8Q2OafA1EAB{k^CRfzZrXch6b zU@2BAYSkYHelT>db`tQPMH~USj8>07or={0*L zc(_}ie&}?n^gMY~wrFNlgtj9sF|zS;dsght0*zrz!oQynBP~tu5fs@la)bIeYWyhi z9c8{8yIOHCOYBx-7_pbd+`oe2#bD*pQ#|5RERXm2j`^|&QM}(f<9&*P{e}wt!fr@r zK{H3~2}#BWzj~F$e(2ND`P=w2vuag)8K5urDzv1qSa_dWeNh9x*+|6;n-yn8_#E3- zk$F4c>9*vUEs7C*hzSAk-fHQ~CiR+w0~4fqHD*iL`FD*i;Bx;s4y(KA=;; zNeu+8Fr5a@iD%g^C`?!k5i7>K#;YVbKu=-fX&IFxQyd6t-D1raSGjoi(s=)5HcEbB z&LGz)X|4MF?xk`+nhXSF<1n%jAPq$?*D-egfH}{oztjcFB^dX^L?;~rQ%gk#oI8OEt#kZc{b|JO9 zEnwlKDh))QnMXZBIgmSwrfAxYUcfK38!wu=807@ecD-ACgX517A|dx-8nK}orO@3P zyJSp=wW!|$go2x6COB7^I5)%6CsC4TLK|f6QT7m3*L{$Gf@H+{mAgf#Rc>;`OE4Ec z*$?KeQOr`HG9)KQYc|B390Rymd}t{)sXIkYV;%q-t(26739QRz-ehmRew2lmV~F;; zpH!UpTZCMwi0~{R5zw&;;X>Dz65XWpITv9O0KwOC$14o2V_rII#bUI#$til6qB&w7 zRXyT7FF1)fM(DoKNr8$JJ6;XW!eiDomH>v$@1Y6!frwni=CL>xV6HxA`w+wuH9RO} z>Po+hkE+Wli~UP`7WF2g?I-Ja7-e7P)rP0t+1_4Rv(bU%+x#%%&`OoZy^n zrU`dj1yiC?X?I!@T9-v;o?JxC(F$$qdzY@Fu@F_8ER}2+Iv_v{KE=<29K*nRAo$zG z->{d#UP2W<2qm#~uV(RC){jwRql=<=kX2@J;ZAq}Xf5_wu>Znbd`aQGev+>^lY5Ve z4>{&eYdm0oiYdV|hyOrXS^ODj7ClmT4Q?wpj_tktI2x0V(I3s>9wknR7b2=Y^cjSk z?|`|(k)&cc@PE+8KD^|J{CpUE4XUH-X>qE*1(hAD&J4^P%H;%-ZDS$NtOX<40_EUl zTTQk8#$VF717 zi1Is(C;U`mejdJxAOH)^Wngn63I|LGRm14pT^+M8yOwrt0}(V8t10oL_zmnr9-&+8 zt>UvZKl)5NkItf|AI5%%$C;ke!saRGg1(VCwO!n#Qa*Z0I5jDar*bhd;9|S0B^#ht zd45N%Wa(&XRDbkdb1a)%_5UE(5xnc?L?<>07L+y#A7|Pmkn8owPHUJ%m09yG z=9I?HWwUd8I~=~uwfAJtr=b$*>$FRJwyE*D&FSk-FXkp|V4^LB8B^Wa0#94Xb`=_0 z5;80P+=wpDM{CB6;pYuP>!{eOez~@dJ|}}3ov;dU93*9>zi#Kb-QzVZ4ah+M_KVKX zDLi};Qt5$^nA@B>CcG#N@Q)oz(qgpq>TkAh$~p>&BX|x<8%XHJ)-iS+!UpF64sQH6 zcCbz6()*$*tUT)`+Z(`k9#KtICIKZYKT-T8nsGaH%BTo2(0v~QE;Jb1Oa2Q7K`f&_ z!Vx*6#Uh1(SC4q;EtV$~!|IObrsel|$Sh%(o_q1Tnk&AzY%wJq1gwV7hF=SNjiEFw zg|9dG`^E;WV;W$MT3FVN@f}G+)awBB5UdyDhXuRh3EYsbt93CVAmqU5G>5Eq4^Ncc zimI~4(SMr7jsRNtji}Ppj&+a4Dm&lb=@q}eU}hNv<~-T&!Q`^CwG>OI%md=`NVCz8 zQ?V1E1Qby-xAjuAS%681B@lv%&6a%c_;_dMA-MrK;>Fp==)Y{=$6!W(BLxG?s2w1V zR>(W|+lg=BSC=vaCFblSEy6m{E~gL(&WeL?7_Xjj7pH$SIIgTkvUIKm4WT03s}%U3 zY%^^dqUe?Fzz|Zd#^4x@1I^ar1@$Z2<@R-)f*La@9hHT%ri00a#qBve76;L(@e@J_ zqyGXBJHSkE%XzY#Njn$Gwl*104ZGqYhHnsG%0j%WoCpw><8P__P~tPfn#bsAV z>u7Y8F$a@$-C}5)+ZAtwd)b`$CC^=IE$y&PSnXTepi4A8VUP@uR(jO3xP75&g%-?Q z8CDEaeM8~oV5FlSy!caDhWx~B#55?*)dJcc{Mh8)*Uz;g2F|ivJEzF>0_EVUvDEN7 zv?M1+M|{soZj~*Dn!i}jP#w3p&xGj0ZQ`R}Yif#9y0eFjpcPZG>F`is9mIwc+~xC}g|8uEp_RzJii z($(_eEDf0$6D23z&v2Ky`k(XJo13J3ifXp`F295O@%M>0)#XV($2Zkuc1H?xXgLK$ z44wJZKle*>_0My3N0)FlY5l>JXD)ZRjG|e;aZ>rVZ4W*4382v*UE5+NNldX|G6FCH zNb)~tWmTb8!`O{+@$Yb}9f1~>x{SD&&vnG-(dZXfI(zf11vXh&mNc{_HZFz=mKcN| z*DssNJvgr}H~TS^`ZhX+XT5s`wUeZiOE^NR4d2=$u6f^^f3bpRVa(wgT(D@kW9sAH zc#na!$mNMGYuE|)LV-S4*)TA|2zx=dA~xq-*j8vb=#|kpDgUQnSK-r;kGMSoArod3 zng+Y-n_`t-kpr$eD71jXZ4Sz&Kq4QGi88z%&4MdZjBscq))en0z>!6Z;v?!lsk)#_ zZiJo&LaMG|zr_bTGQ>+lQ&~fH9^WVk|52vfIbJSP^cI6UD* zM|EaZuoXatsGOzm8zkW}|2WWz>8zXFa8SFZp8H9x1-IeNj69$fpotAe-At_@aZAwD z;Vu()ND_HPpPh@E%7t3EMqBi>U`oCj z65W~Pn1Dr}6YXj<#;nJ-#!nap=f&RVpxk_joc~1O~M}W7d>!_8t!<2C8&_QfnWGX*y| zc?C(=N(#j^5(xt5b3KE&_A7!MR?uJy$T zty;WCznY~89>GR7XN|Lxzul%7Hz97UUR59wJ&mNVstmU3H+&elh4siymqHzIogs8j zytJ(o(6d|U*ArmUyg_+-u#Id(N}+HlrFgUcJ1&Lm8>f#xH5dao>P?v7CsE1Y^D$Ps z_^aUYv;Y|g)%R=>hCOc!CX**V?TFeU__^LWJ{scULY zck})Kno4;mWNyhUWfi{9#$S}(D5`8rOdmxTl<@(S*~m)Lq*QUoTytJ!J5T)y*BItT z<4Z<^qE=hBLDl{)i`yQ(|M8uzRH!_wWzS^oHpGrIbf_p|6K@j%3Jqu%&=}PJ%|+TH z|3Rx-Pa`6{gAQ#0q#qH0hGnN!u{ly*Ej+0j36M?aYbOXEd) zVX+R5cXu->v(hPth$)vij53r-R5psb*v(dqbc zer(D^*i)ejw?TuXP7(S+Ph)f5TuPEAVqhcq`MvGk>O#8kDT4LqMQ=o}*`-1!Y&$Af z6f$rqz0~J5g2q=FZm6s{?K`)`VXssymDSH#KnoKE&S;a0y-~SGg^fde{kSGSo(OvZ z8;|l4$h$|h-U9xoF`=K7dZfWDyd>u)j&F@)Al)4?9jG;@!{UJB-J<|eQC0gx?H#~K zVk;&`esZ`rBi7f;=*x0mmD5MZ7s1WTa&CMs0f3TCY2bvLKRpV3`EYw^0_07x--G8a z^_hK!U(6oc6Z^W73SRu;Ipc;4i*&n+Wwc!U5?&Kxw`X>NfGEpLPmM25{D_87OC}R5 zISmOg3EwiNwHqM1Mw&}`I7bXGPbx>Z9p+f@>z8i##6?rw=i-*dUqx!*eEq7QMazKs zhk8Egsiee*(m0isOp0JO;qR}1V4pi+@(YkXzG^q=bh63yRbVd?(G3K#B(WCO5XncyzQh>a!*Z z(6IvKdU^iMPGA?t373%Z=maburH#zH;Ry_Riz>MO4;ua6J1Yj3UO_81I=?DwVlaiF}Q?pz&{5ZY`x z#!8Zk52!5!)^|mDXkHG5-*?XCm2OhK&?d28L^I!36#|IpGGRk->0@!s zIDAN}f#7rx6&mQWfR41G)umE{I1bVQ|80f|t8;yX{Om>xouhUqgWT=$19|+bPDzngi`(#37LY{uLfgo*Zn#f?IIqd2-3n>*Q&y7(CGw6oBtU7m@f3 zVKv9Nk5#o$gLaS(UdGSA9<#_IUW7?>b8yJ@cON*}H5r}SBKVY02wB0-Jv-^wzuN|j zWcEv$MvWl2k>sLqUGx10UYkj8IBP=A0uarvT3=%XwTMxww~+;;F~eFiiHiU`{Q{Qe zsYW568HsB-S;@?b=2?8sbBr$<%XVRrSaWbkkn#*ej;H?)?T?@!To=zn3_|}7pk@a| z^Yb=;&Xwo-6OIJBB&NGbNhFIHsS!cqUm`X2S%f{0hxrD;lv7bCo3onuH%j28q!lX6 zcA(45n;K2>T zT09495d_+p@v%&uD{Shz88tCA4*!WLF5y2E_`cLEx)~jFYR?-g;}0QQmO&5B{9EiDPu{o6t)>Fw zJe%DDr=on}eir@!Q3KjeR8P{FtGyH&l+kD3yh3iwiT$ZfI{TNp#jcV3s>Fz zoY=XK)W^zN#&PZpDChSJg{c}6l;d{`OIx8sXW&k7ZdB+VAZ4rf7=3+ zIdjC{F&&U{(mXRcqy14?hAI?{NijZd)Q4sg=W?OpdQ0VaB1puMMv>JB6By?Q?OZaR%O8olEp%e83k{*-vXmbu3M*Ijt1rGS1bv7q48RJ<0}ERi2J2@m6YAO5~Q4inV|qV z)?5HPp$z6E)@TFM;rD>yf?G2H!o)y^R(-XEx5W_kqbvY~v!^@tsyJ&J*qLq$_-4L2 z52%gVQn06dTsa2K(%3bB3M-3w?IE{4%B4Tb#!_);qnsm!6Ez`L^Iw_#gs$WcSiOge zzl9&#q1|0bl<4nP%EQqRkqvfnPB41t2`-CR6g@W{$Cxl(vAWljpc#scT0m$Oue+`AMb8U)Dq{Gr&B2!>p%!Ue5olcA89>#GorNS(*DSr;@O}Jn+ zX>dpZrkA)PDAub?J&fR6RA*Y@AAY=b35N{f9pNCRpeT4aU(ctx4-P^VD49=b%2;7a z;uumnP$ASV+pV4w_9`1fVN_5k4{gHIQ0BR-7e?LnAl?*gdmVg1a4HpnbByFW92@06 z*lpYJz@gS<_)bEz@xW&Wr|UhPaJ1UEJKW#~?~{BjMUR*8U}`EH2;k_#8DeHDW0;V6 zKCpq)o=npX9Km?RaTa60^3l~XzNa}@_TDBT0B`tU1BDO8TZ2o*o2f8*_&gBqTtxx` zkmW1xY|qe;0Cmq*dWT=gSsGp#=J6RHJjNc+R=0%ghSiX!|}Hy=u_3VB77q3t^Si*DWX}}k3#9X-rI~m zM}4)}fdwp&#h>Q*k>dJPBX1td+B6ZyQ@mNzu|p;&G!y5Zwnq;h?Od1+=!e)3Hw0Wm z2Xkk{BcpPvgWBkh#5#T4=NtXKWc)a9O+4E3)F_Dj zfC%Jg@>athK%!Z98E@@wTxEOX4DSw=2|hA%fP`OAlS5qy;S;VuJy398*$JTWhIe(Yd~gzO5VZ z{Y#z!#7~Gqh^(lYaHLQ3!zeT>6+xWIaGc^yCslD3^fq_KDA)+my85Kq|2$0G3F_ca z@wq`BeT=4oZY`@~doW-Mo5iR$3D&$BhvN_*SsgC{8h7}5_&3aOm*jBSI`;=Vx3C`w zLNT9DyRDOf!TPjzf(a7@0~O9%KL8Tvg~wCO4xkr=>li8-%lpZf06`GqxqN z?qWU*b$7dd_O0AK2qMbPyEA?$el~lg2z;2pN=KmkG`EFO zp|3AeFPZ^G5Aeur^1SJol$CSZ%>{DBNzr>V@?5#|L-*O^oqZ)^1{{AMsq#0n1m+?}(pAuwQ`%C_+PB3+mJz~XenvIhH1<{?W@I}Fm@ zN?#f`mg0s}W90I>9h_1Hb>Cg$=439cc>^FG-bcI#HAc}*QbzDhH3jl1R^>Acp@eUa zvA@Qh`X66Cj52<(HmG`}}<9$Z`)G3+&yMm-9OC(J_cFRrWAY0Cr+*vxDhG(JQ1f8W7Sm zPgSp>Oyj637v(Na<62?6JB}4TSWGviVhHg}jP80>ulf!5TWlhWRsjl3YF(t5ODs6) zP;%F8Vvmgixs7-DzkPj~ebLRi&|WV{Ah= z$luCO`C;@-`Y&RNa#ZwvnU69r2KNmB{;Fm6J~4+)J}{zhZ!;@8b0>(s0YhW`Goq89 zH?Y`|-b8;yv?l-ODsv$3&>*MbTIkdCuXVi7paY(Y>nsIO za0&S1w>~F!ND%(ZuEjOJ-T(~fwyBJPb_XbaI}c`raSKz{XtCVee4DQ#(QEA_hi9`{ zup1njJXBrMnL|n}-F#D+$4HvsJAs0eOE9WLpTwB}FMFlp^PSD-TboZZpoV66Jb(oP z7h1XwwPsDrvJiD<*HMgWTy?8l}i!M8Gaxa`pI4aL^1;0T&=cYpitbhoYB26mGx6z z3Ah$X5xz~49dyc@~LZ_E9SA7A#zi%f2@ zBo=FB0-oLEgFTp(>op&%WGo@Q!cx7!x`sB#aio_+`|&ZK9{|}G?lOF(x1$3OS*C3c zj3iDkBE&{+RzmX@B7@E~8@bgpX@$+gD%Tgrv*V2rGh8XEL1SisPa8Co5{GZi37fA8 z6CMZMK@a^5<2*PCWdCT(Ln?;lg&xtx)L%n3$D{He_Djl-z>h0h546h-S*pv<$CT~i zvt{liC|o8c+cP%3!Ddw_jEjwG0Pk}2xlb5610ycf;3i9$=f-RmRzn(7k$t=lBzBeS z2Cas^UE(cAGoaPWI&W&*kp{jI$p{e)>TfX@9_I2Um&4URU+tAl|JhQ-w8CE6m;_vN z9J~J!XIHkrF+0Dwg>owjX3DxUly$%`r|a=0;3@7vnZza7lMWugG2GpeoEm!=l$o-t zkt|C5@pGK`t+q|R&=svc>dkqudOjEam{_+901=QW_`vmbMeDBc%ni#6XqDu$+Hxg# zp*w!?4sn?jv%eaW!9g09SA(YNUw#$rL99ll_Oc+NK57+6 zlL4#B_t@dNzZ@u21bVyzH1LYCr+XdeeeLclQN zV`T;p%;}(X=U8dmLAWxykP{Z=40-M%Oz|crL!d#iKN&2xWl$$qIp~77kNRr79Et=& zXWCfe+S2z@4hT;hVGGfcBd|@dP5nA%1xo~D5PsXCeZc<@D&%81xMk2y*(L)CA0Ht% z!FoPA4CBJGSDYjM3Byw;k-SraZ(vLIeIZ8(pCYjy(NGE84FSARH zpJoD`-QnHG6E|5-_;Z|dY`6m0G;PPqo`xAIgYbcs@jj^LpjHGbUF5SdCjh)M0kFE< z*{>#$(c!@3vz6=6a=}r6?D~yrvjAiO5Y`T~Ex?v5=&ls82U}?}1l>4Z<>iQu^5|s3 z-#M25pS5>^v-GM8J-^2}Rj2Dzb$!*3bSKqGzH=y`I;l!pY?6@VO7EJyfGAP%In0c| z-*9I#-O-;NBIEUjbcYThAZmE{0xmaVGy;i4d<6ta6g4VpoQr^p5*{jEL8AhOXV3q? z_IFNIC&AH~p-G){zQ=y7_gZ`HwVkIEX_?61hDWlAt3YT**Cv#S!fVNsvCkLs$%Y9+ z&ZuG2zoDGG9m6qe4H_Nc`~UL`EALrbqL11ZP;LC+L*P7xN)mHo!J)^^-R9+EBth2ensK&5&uf)vo zTg6ym^T^u~OG}nttn96hP=$xnBa2FU{q!u;Ckml+z&97x=#P3Yh-ALE9;FleuR;CW z%5JwuZrQCe?8Yu>7#nU^Y*q7p!_QIeDYm?XNGjJ0m?4}M9$mG}{#`jCjQPKO!N`ZJ zKl&2KOBtg`pZ$CfY+@U5&rOg)q4zJ0-O$ZMfPhEkVp+}e%Xy+#@(JsVpS6PKZwA9} zE*u-pF;k7+*hX^|zZgW1N)gA8-xjan<5+--rzRt#<#hks^9X2<_C?X(Qq$po1O}}D z<$y$B#>T39N=W>8*KwhKZkLKwHSe~dpbeDi;1$%Xv^__}^rDW6L6fM0 zJiHtTbX3vZb}sZnNofUWyatVYIJZ&frqkCVSIe}{sT@^v-}Xtp4@mp{Y(Rc)ey=$( znMT=sl4)E(TNA(tNFkDr-_cd`U~5ufzid9fWyd?^O8%$mexd#fV5i%{B%s}m5E&Cjp_ZnCKQe^9{yz+EhXS9^HUQIsPk_N6>y8+%*vp=x=JOQM@@K6*k*iJj zhTj$(!Z4#SDP+ffBk0a)81EqFOi2wvpkI&y6;7Aur8@SCrJcNq3Yk^ea>e+D!>0v< zi|$AfjLj|)`w4C&dqa>N3xQVvx?8tXc_3afIhrsLd)3Fb{%I9EtvoM5C^t}H#iPN2 zDX4Nk)bM>JrA64d`zb={armSF2#1o(20iW-|l@#q{0awi&7YEQ3%!{mjGd6Dhj z;NCJHn_E1m6Ysh!FeU<$)VX!cs2OL@q(rDQc=dmwC=N5At-dj-IyhL_p6O5>+`Bo#KB@tuLdl|CP$cGR zUl1=ne7KI$VzgYvfQU4EZcHv5*8F`W;NUr|Rv)$^#uz6``=)mTBHMBXp+D~+fieBb zTdgv22aUZa3Ky`^h8I|3$*1VixqOXGnNT z8ryBlv6l&Fw)rR0m5+@UGJyX7_-ltfys7Q`m-fzV-#yrNM!S3WX!q{reP{2SebV^! zUVnF8&7E~xab~$^*TUk;aDK<`2;BpIh(NH@Gw^bqSz9ic>j3(dcfK20SJouD(wd zLY2cV?I2hc>M;#}Su^4zY!rLQ6L+AEU&J{9(-Rj6iZ7=V0QdKS`_IflQ8W8#cDzC+ z{z9?w71zz$m3Ae6`_ARNM$l!S92*-$+$$RS%`pf_%C7Mhue|HzwPZS1@mc{jU|GQArT%LFyO0eP)xr09`R!Kea4wGaxS zlQ-`MaK#?Oxm7DnEzf8&4pAi*{emhqYyzd5Qh6SkwFP7l$;M4mp8%j=7pgP;Aip<` zO-Kl;DZY7mymDcEZ^R0V{d^fo9qo!#Jn9$;Fv*)jM{E`WGxZ)LzK^8^ydE_tmscyxZV;Z=H1NUMQok~BdLDY@^B1pzcZt# z>mArUy138^8ESwTIBm5;uY;UWdg=am4x3kOcZe5M&u`(qY^kBZv7^*lT!?JEJp2KG zW`POt7!nRu(Xl}XvA0M0H3Tj!SAj&-1`+R*Cuc_xCCmzQ-!G~j3_9pkWIsJqFFt<3 zG;*K3H@*8oe|9IpaF;-uO@(%isbHA^=PdyjeqPHXv6{)m2J>g*&Sr55*5?ma*N84J zA|&b2i@IXh1W|J9U8K{%kLC-Z_=&xQFWY<>Hbcf=Tzu>N@?=cujt^*bDqxn? z9ga#ChN0<%u1SuS)$FKUnhEV6Fw)*&+35BjP{oO6eYtDt=ZP562oMbWg|d4en9u6I zxffzBx zYlqY9Q9fsnrr9f&tyUkHpw>dyhZX`nKHOh(3%r?i7wU~iyf{|xoIH8ciRgdX^&>k2 z)eL22qdaj}yf%JvB<)2CpM`s8(N|9v^O{hZB6#}POHW7)D?HnNBk#|uKOe<5^z)H0 zw`}s7!26r;48;W81506|LS-Ej#Wbi(i}E+4t=IfI`d7l>>%){H@bbXA5RJ3%Jsaq=@?Jg}vw)Oj%16R4ycd1D;_K+NlYm zYnX{8y+(6tu_{&C)Aa^GbX_PvER@YwR%`;1QO|04+AjR2^06&p+KuxBfkmJ%?v4_|UTC z`+JmF^FDT1Glhk=rzyAS;xub6RBXw|Zla@Dm~oRZqQLwN$&ytpV(loMyl6!tgF<%3 z6-}aFI3jZteDz2KW6~~uHD4z9p`!z=fq`Lwu%U>ne6%Cq(}KHXtxe^{kFCa!gmhPy z#z(1NOPIUg&K}g&fGKwWEANNtlnwPe!Bf0-XX#tJp75=WW6f6_Mar8_4x;wy7-EG@TvFH`t?9%3D+X_(PYZh9na!p7JAvuB%gN`HV zj~1V$etu|iL}Z;J4ApQn2hkx(bCng>tp_Yj^^`fhMV2E4PNnbTWLUFu6f{ghi0h-+ z3d>{o-w1~gNVJnpVjaOT*;$BW?236!>RydyrYOY<#PV^y8Jq50fUF;u{_JPi6dr{N zxp02@lo7MWCStMByyc3Qq0h)$M%}>)>lAS#7j1bYn~@EG`<|&y3yK!Wq{T5hEgT{i zePO!t0ajx4xMLNWU8}h~9>7u28-;Ld;V`G%GNX7NAoSCSJ8JK!z(V(|iyy^686Dw! z`}Js^G&(%lPuNc@Tb>ThZ;Z5Y8#O{nh*0K_0Wt8r`a;=a9b1_0xaj`Mt*qjwoQ+;B!GGW-nKaw@g0}yH>;Y2On8BQm8lmXxVbK+2$j|%zPjR+qE+;artR&B2O_z zEDG?#>sgriDUl~9t#0Y)@@5h52{gxUfyK&nO}sq%AxQ zQlOH7EVo(3%2bW{K#ax&v1vDustzWJ5w*owFvoORrpmPHLXLjL1SEbOi$cJnlqUip(xPoM} zFq56egc_mbN63sJ%Rn`AT8)fgD3P#;-(9q0`Z&Ek^U7s}0pB`5+LgDHU#X8$`71fE z)0HEc20A%1o;y_-gx12kkF%P|q_~pU2y@++YmoSD%=Vn);2V0aAwh%+Kzd=Cjm@3# zg|sSkJQGPNf-tGYGIxSBp)4iVY+JSzLx_~=dn`E{h5NV1S_H7iu@pFXh%V=#dY@>V zWj9U|cpN7bOf`5R?zeHhWt`?0gC#2_5epu;l;lIGT__>ST&r;7diFw{0OK@@$#ZM! z?An1i6=qaMLjXl(jDnWVA$FxJ@F&t3D?th&&KKy%nr)(pSr-;$eyvpD$!~p9zmm;4 z(|kD0g!yel0=CO2-qD^ON$7o+O{z}hFcR{5l!9so8h%9YPWcAwpr_J9RFS2CZDqJ{ zD(4<4XrNB{M7Vx)bHkEg=NZz-W8JCf9+hPnw`<$(|MW*@ka|{CoMRNHsgZ&xfful2 z?0gx-nN!>OQHdk$ttpQDHo*~r4zShXNK&ay-l;Nq^`dOo7LuB##&6PBCy~Q6n%gW} zX5I(stb@^fM=A3`DUt|@&1a$7C1bvx8|S&1xxa@e-M7WXyn7@B8;kDp)x1T3Or9cp zW4wp_X}_b2A}KaL+?|B%;LTCq=0m*{P!|K3~#k zwC$tDf#zR7or6K(F71Hf%iLfCF{3ai6Tu)3Dt`DeRQ$`op{w!mDO9{kwytYJzd^Y9 z+-FU0^{ki<7DD`!%?=I-mWVaU9#&T_I_|z>wL^&bPIq zNDmax9*)_Wf{-Q3w-yGE>XsR(CHT z4(`_eWf8`$cj(yXMMn&TyLUsFcQ2g5dyv!Jb@x`yIiNiL{y}=?(e%ixBK3Ai5!y|X zVm2b56`_wrc_;GItdKBdXhYBWD-?j2!rJUEwe$|GT^<>=(1BAPkLaD*>96$D4vf1q zaFG_V_waB+2<@dWqs#QeNlvD8l4TqM>RNteNthOqgl))r?oS2f^!7q0D9N2#z&`B= z5{&fYP-K~5o{Ab<(ysFpvfl7x@bqxbb$Xi}9yl9+sFKpc|f|I0c2Qa6Wtox^X(9PW)d zbQ+kx+ty^FPW46s00k$qR*@Sb2geI405nZrZ-aYBDlC+`LcIAhsTZ;eFb~h2&4?Tuc?*C0W<)N*dGMQ|TnH$YAVJNf%u^Sc%5QESHIa_rWiHy(f zfKI;bX2neO>6sJ9sKOCwGxbU8q#1MUHMcg(5i4moNW)Ggiq|ud1dT`jnX6W!x)eEE zN1YzJePYn`*@uKattWyb8&nK2z3S9wdQb3-b`N8MoD|l!wB;$37WEG%%IcW%07p|J zFEmtWtLu~pTNF5r{0QbLU1gh~pDyrwH;vBxICUz&aOyCha4J8$2vOJurjE zT7jpI53r9I!)T&G0qE1(>Ny~V*|%-2G3q`?(PYWOEf~a9rWR5d3m{_(;-|xNk#Lcd z6cC4DZf|l!>$+1&T2Zc{1NLx5<)f*^`)@+x=cblx7To{7nS0*H1&^Ci2al=-0uxwN z0Sy9oQH_JtKZQ^TyWMWC%V9$!C>lYKrDeqf#hSx-dn8LtWdYA{RN9uAhsx=m&x(^w zi?5EiNWaqn!_CQ*^Rz7BHbM96j{{3bZvo^?nvCg*I-yj*x+^*fKlpB{X^;kE8H&a^ zm=c2tn;oW@3wi&W)E0|UPcZn%pPU{`WLZ09L1#bynhB0W06xc?V~X&y$0s5bz+IOk z+(Jc|7z<-Re?2$v(I_$8uq}A-NfPY=8X@C~9yH`BI!%z9%LmObDD+zf({G5;?i#$! zD!6yQE878Y)5&H>l{P*Dpn<(1m_69GOy5h@8j+n~olL^$vWXr0IF9T5F5s<1$TnyX z1Pn~+Fi<_xl;uOBV+an5+0?nDJr`E|iL`*4JeUPE!Zb?9@DF=j@ht|DHD5@oDMNnM zVTI8$6;$d|5Ic<*>Vn}@TaxmTZiSIS6zNMpMjFr!d^t&Q=RflKg}*~Rp|9e?em?PJ zT_Pg@&hoI6cC|brJdcG;*zWTNh}d*_cyxLO4biCiM4_Q>VTAxNHu@vKM#l83=Lq+6D9dg;SEo30-TpDV1=Rqd+Cd;;2b55%@xs(Gh?zv zxcs1=+twFKs50je_FzUA#{0X=4h2U3PcRkxO2P{yXy(2K>B(^n~@6C zDv=6@z~eb)+tG=(xGiu`I!W*}PaM#3t?G{M;5K-U4QF8|kieE1Bs`Ev?7*JLW02rH zcm&d8sU!kuwEOagSY2R0Ck|n8FpesZLrC=b$~6LzYm+DnSvmhe?2c_4Td2=yb?vm) zasUQ_w1#CwfQ>+M#;`oFlYxhEp-PE;hR8&QjlzyGPg12A`;wT!Lvj1NtCiu_Ghmi6 z0`0N4gW5(>e1#JSo+tn`aCHn4gG-nsp8IKeaSV`*sks^M3oy;IK5ZEg0UcV}frrPy zzn1cG=-Vt(UfFbCmP(!0{FZ#HY~IQHO| zm4GDKkYv@($pkBuI0t1|=Lrr`u+hq4c2kK^G#~x!5&h@%;{eXp}Q*ZM!&5it?wjliY*CPk;G~eZ-J|k=*O78_nib`h`?mQIO;_P7b{L$L2|p z;T5Qnhfe>h4Av(bW&*RFi+#juEyykGdLUnKI8+!(@ zn548*r#)JoSWq{lJ3nCfYSMV{hT@pij#p@rz?Rv?siii<2{SGEda&}Vy5G0rdh}rZaKwe5QF`|MQARTP87D-5}1GdC}(Z91J zy59wO30z}t2;9iYK*$v+b6j))a^R>9!lZ5&h)!Sva1uTk3-MZR zGrS8JZw%XXV7{!dmXRqFI=v-^8ds1ol2Xl~Qt-q3-^oN?=_Lr}c%Hz=J}nTBXCRX9 zWB*%W$@mR?IsSL&bG{h!G_4`#gH6~4cyR}f>k>7?uEH;cq|l|vGQ$HbD!TeV_Q*9) z-Fg~37OG{#Z16CT&c}MMs+NgotPQRJTp2ISf-SQo92k3b`3FTxcM1bg<_gCJ)%WL_ z-Qb)pg&~m~4{`ZO5q#(Xa!n`t+t*eG5*ZyoTH3F2qPaa{gyDKT>w=1c4r2WUx#jPo z$zFPCH2H8esZ2G~N+dvWA?g_^P@E8Vmn9tuAu|+iLiHmr z6yhgYE$$&ON`kr!8_hvy!N5QPGYl7_Rmws{v_!cBF6X(X&;1w+t<@p_DX?)b+!)X4 zr|AuU#~q|f`rME9((k_=WxJcyWAAXu^Xdo;)SIKOK%5SMrBnEG>DFC>~@?7RMijQBK3B&LWfIY|u zXHXmn%KKsRU1l)vkPMj?Iz_CA!jPi#LOz`51L9eQXyPPhPnco9gvrP5AR|^^yFKFT z2+enUJ*pL6H0g#Y*SN+Odv9#Qh>-jWbH)3uIbK+v;F-L;VuKAP0ak>^lOp{hdOcnX zoz?2WGSYs6j6YSSs7Ar%Ti9xBe1fr-!ykx)Y@1K9Is$pu;D|do)>=G}JOQN^!AfEq zDaO_uiKv?abmyrVdc-42BRI|##;uu%G-I&t4~b?1=kB|wNht0EI9WJo!xzQ&nDk>M zG+HFP%XtP2F&6j3L_KAFuQo0U&G7{PC($Tdu|&1<0d$d33kGB={(P^CjU)_=>3O=z za`@fO*#4Bu8!^1|3(m9gfT-PVC5oh?iQkHx+-(jF zR9{)No4Zc_BMZx-YWN3b-p<2j682S9dF?%K&Thd-YKIb^G zohrh8&F}gRu_x^Ut@P+0;0MS4me?%Y?c%S$mqImum7>0wpv?C|B~bQ?__SV84U z5KEybl{Ao~WSKMN%K8vLa?%7~2R)2UQ;jSx( z)Y0(Vm~iLZ(h^zH$itEd0MwH_6!y>m2A|yP!#g(TuFMf@IPgGgms(xzyI184bMQ6n zDZ)Nvd&fp2Mu5Qg9H7pThVnZ7L;D7}Xfgf%7Xd_UUb3NBdFN^rVg+x%snhm)lCpy9 zRbJ34uCtShqsukT+q#;_Qi!E_ zrL`>3`I9gG{KYw0fk<;J^<1=e(-Xgy@fc6>UB?W+jPH@p!(h3<=2>V!!)4|O&r7RC z^FwZ}zN-|1S2#EA29qYE6t5mMTYoW@{g_3CApd`&+A19~2ciEbGmWHQ8~S0FM7dr` z0RRj+91p{OY=(*yH)1fZUjo$T{bt-K#eUdz4cI*P_vCN9C#`{jyg3XSH$qnj7j=#K z6Ko<~5O#s?N!&z{HZCK85nWXD_9N3y7?dxT*pwr6gFQ=t0M{nLG;tkGWHa3v25U+$ zkHXJI`k`0TwDN|51r7Y`kf{#Eb|{sYtmzUwWgZaV+uZrbtLEv4ohC?ZG zn+d;)LGl)G;&Z+DE`}F;mc@YE<#>wxcSgNH1zCAzRP{6dsQa_|se|Tg{s0fYfkOT0|dW%Yl zk?aGo$H|4n@&GxI<8eh~8Ppp31|pfti9h&J&+ zk;afB-UG)>wfQ~Wtk`$%MLdLjaX9?XC@d^2L`mM(9EL#aS&vW}=UCtds8|qOWREJb<=4-bGQ3{=^NGGL(sxe zzQx#$kAkKwD?da6rdw43RiO7jgoa%&b|U+Oj6lhBw^!>He&?(%GCj)g>|@AY&hc`1 zfVag>s1X~%w0(|v1-fqd^^Nd}90ku0|Kk*|D8lrnTM2$!kSfRvRC2-%17jmp7)Qaw zNLKw*K)@WLvDNC1U&m3Sm-K2X8$N-O`?@J4=tYXpFL%bqe>vex6X+J{-dkp%c>$TV zWTh25R812?n+zdq!Z!`YNu$@H5Wj_OnM}F7|IEX`@=GOAzSunj~Iaq*q7#yi>gEM zVlQ0^$E1r6Hy_&`78hVq)dmvQgKh?AAgY_l8S^xO z-f`ffCB6IIihTTibLbhN5jz>gufZhHxW*7*4P!{f|KRYh3>;cOV6sgJpS-S$no-QC zGS=A@q6gH+$n8FJdL84++A2oZR>QY-Ol7?S_kF{;)F!~*qs<@W zk3Q>|f@<k}LMTV=-%Fu?Qc$pqnEJ#niW99sT#=nh0hx^>zCB;X%Cn>b4H&5aW2V^(?cOK_ z{b1ZfS@0|$Mzs9q{fLR;yJQoMr8U+GH%+vGHkUoq5tf&Ig|I}}x2&q>6G3#$7(Ayw zBYFF7QwO&}ERTrfT;$>)qDkqn3I*k9@D(bX*q(tA_`5hr$d3R{^T~zbP}75#vhk4q ziivQt_4MskDKAj@%Ak5=8gXZ(gCe3D-nMzhh~`e%z)XdBCXM9_wVOG!aTb<_H|)4VWV|!kT7@Z0~ifUEI^$ z9{j)Gnm|lC#eI(c~Yy;>W-r zu!db$AriB|#?JV9dy*AVwOR58z`{){GvpjxZNCndw|RMe_~{2I9D5=EZxlz&`F=C4>>X5Vl<7#O7 z;nuZ^Dn(O1YTolkf?Vwxjm*a9w!WU#DK`JQH-SZ(Be3Z};h1-VZWC?QGy0e5@)V=|yLP-1Y`I@Gl5dswl80p7Y^?oj6+$8YP3<9M2L7)gin z2dd~_{nrW6Cwc}vL72O$K-AT04Jg#*Kh+Ke&FHv2GG0I^TiVI3vQdw7vF>`zswqxF zIF`+^sEa#igrv-^UK9r_w{x2{D=2CVjyZ}ZTerLcrlNc=SzHL z3z7))7i)sOE-8A>PYzyzX6BGRwiotGX1T@8Vv9!gIA_}rGb8>b*qu~*X@Nuu!Wzw= zl8?rYi@i1w=qE>lKrH=pwr|e7AqC^Q@f@)`cv4?zfUlJAFnTuu%4a-^&B^I9f@^dRj4uLRNL#Gj})3+l840s}BE(^?Bn z=EqAoi-6w~{S&zsr~4a3{&ROv|9AFa=h}Ohy{({6P;)MIcTPKP@7}Y|o||7<+7thqzTuZTe$D!KgvRNmoh<3?SfKO*e)L&6 zoV+qd$?a2cB5mUQ1b!o}Oe9$4oh(GT)}{6w`tIP` zF;vvL9n~zxjS4{H>th}_R2JUq>X=JocNoTiSiHNAeJcGiXFNDeyZ9)gD*{qTwj0t{ z2-bY+mxhOPKD~o*T*_S`#f&c+gZ)r9_SIdl*+Q7X#?z}~k6Yu+&Dt_9K#HFci71Uk z%%+EpBM{_#pM~qn_V@!~h!| zDv^abzrd$d@1kl1j|7c?g}_fdz6t3b3puPEY5iL(j)M-aQ=j=I7&UK#G8IUU4W{$n zwWN{3wSbpf*pk=?>e9Aec&c@n7{D%`b)}cK7+@3@jWVMGRQUdFkMU8IgQo!!PF*5C z73yeO1CTiVbS*L-!tJ6G3}n$$;rH{=Gqxj#61xFdsBi!V zqRb$m|F<;FyUp%Gi%6R3fRFSxkq9REYggaL;T@PWNvvLX)|%+=UZ?OYHy zQBjA{1Cmh-tp|}HB*T?{zLTB|8S#mOFJ;7G*nDNs;vF%Z)9;`PCm&8Bd2%HZl^y}T zk_K@~7j&O_+KMIFw~ZF;@>C&!M2?AL#Qut#In1A@1Vk(#$7tz&B|GbNC)RKRVHM;a zu@W)Bb;K1mm4b|$XqHa&M7s7=sM%~E!lp$wX)zuM+el!sWm|Hdu3{*(&h!@0fLn7I zTk1x}0hKLEh@aRhIonTdNkIw-^n%PbU@pzAl5_N_PxxmGJv98B9Y^}9Tb0>dAz zI`Ar%7AP30yot!?L<(?5{HcPM`i~(ZVmD4lz8$`PSy3_(&=dlqyZH#-2Ay~Jezvhp zP9_0asN1P?Dx!o8+j4qzt|XN!pm_9eb2>3woUc|uvrhojB?%)12QTFp&kIl9)=l_sGLEw z*A#yk4Ph=hS{*|qXJjp$UL9f}Jx*^@<^|B2*!l-tIAzC!b^ri384~vyP_)5!t~PIL zF!X&3Z&>mcnGsH&r>u@$j2BrFG@;1xFH14fB@GeUe97YE`?2PIi!{qS_BtNWk`_&z zJLC9x9j-$98g_sfNchID1U@{?$KZ0m;k2>tgUX5OF1*4ul9X~;Pi$q!W5gs{=GSBS zo9lRWQaRo`kOGtZOO!28a+Ec*V_dUlmjSQL@WEZ3DBw{ZV|->%g%LF?10yzM;3zqz z(85`p2rXpccY&!l;$K}YOz2%z@3TxQ4<{~Q)i}31IuU}>Y;Gf?04?f&xJsJBct=Zl zlNLio$gvCw5Nfuw2E>)bU@QZj9UpCd!}XqZ55Z|~T;2|&bVyce%|i=nIO7Remw;kZ zkj1(_Vv`vXa8@-ILh=A6*EZay1M##{jj^$wO>B=*h@NymwSXc+ekq6w*@bI)ltVS7 zJO&)p&BTQ+sa|Sx!A2|l37c+b;)OrCL-4BTO3>COj`QL@nnv3YU`yz$Xe2) zxrK9`_7nFZ@{!wwU{<55Em{|2T>aO^ljAv`C9`Q35PE#@tdGg5Mr^~Y_S=ny6?j(w zHtuT>w6np|Q$w*&@I{dZISkSD!@kE1atQHFIj5mOcN)utcLZS-Spt{0&GosW6lh|f z&K~3uh=R$mnw_lb1da+r6WUpADb-Ea0qclVf2NC61M9gkP4+37HLoQWwD#k~A?C6j z5Vg*hgd@%>=P`TgQ62vlg@S?9PalT-PH8op#Z7YuqLVr=-K1Ghc%j>=JPw4|?c?6b z49%fboxE|JIq8gw+uQ4sr4C72sZce)+NjJTS`7-ha9^W7o7p&id+);P8s_(gj;AM- zvS~SeYRh;pJ&ZlY0JA$lP7Wi3sX!8Yn8SuEB17N+4+U(VI)RKOazuM+jl=5ZG8hB|NSML`Ks%A{H(@X}nU*7) z$|jSHW+B>4SLW3OKn9wv#LBn8C`4@rkgZypX##WG)3glQjG~&&sN*>p414TO#byUZ zb4~x9K*W`BD_%qp`srMt(NVzAgC$?19YI3cF6;q}+Af z>|qLU&vUrW%N7FL{0>W-6(!78ICmic*K2q2L@p(<#S9~OG8f7D8tFTlcQhi9NBP1L zfTtKV4Cuo240wn2*I1OlIQ~A}^95xL%4YGwmM|>5puMs_1ldL0B0g@+zplz3m$gbh z@^Q8rjr_b8;0J*C?zTGFZU>a&dK?RQ{6+XS?__tD1LejXm&kwSugB|gW5HHENDt2d z;q9NxP#0EPtdWHa?anA@@G~|b>*N#H_;w@Tb7YOnKYCqt=nZ}i1DhL=wzOfG~6mt^2&*z%Wk z&xw$U63sV`kZw~Uvm>66r^Jn9n4g9(puoq7{Xm|NP^Aw-??jE-T$L8I*Zhp+e5wu* zEPWwqcr(YvB3}IeiPyFYEZPlrVOoc#1aK+ev>X^4X7fCrcR&3RQ;e8R2Sbz{i67KSpYzjiRN_zv{EHlJ%D^OaN$MYCcCkly*u4BX(C?1?06vJWq5opEAX3uvwFm+<1Q@WXsZA$zmPNH@I@s^ z49`o-yysV;K|%>*pi2l0DQ8A%J$#1?d3N7QWhzOEPJF9irdL3Hs7 zAH5@ap{q!PBO=3VtKFj{b)`7KZ8*Hd>%6kU2iDAw18Ab(^(sGK@)cfttru>_I$6RictQ-q_DPmDgbjRs*7zgNy8$347AY63_&niMxK+1hPZC^rR`Yj0~ z0)ZvDmSbf2%CanlprY~^3m<%g&;TDB5_MA|-ck@9d%8<7)0z92eiu8*%|vbsvSy`m zpO@VX{GoS;e^HvZL&y;quZx9p`<{($H)+osK#t`WA7J>|XO~Lbw$8RYgAPXoiU6Cm z&7)5<=8Bl8MTt*i&Tn}dmy-w3ORS2-^Av2lAyVDqVU+Ebhc#R4$#a`6?%QmUIO@SR zhOv(iwhdf}*KkR3p{CNSOSTS_H%Yw*fg127@K_%bPK@w~>=D1*r2$U3kJk`eqiWm$ z!25O22^X=8zgQH~ps$%>kzqN-2eYR&q{-yU2IP9rt!@97Q9rJNVQS=FSRWd`qXbi| z4n0inj_Hwo7g#RKA6Ms(=fS*y_4$6Ji(;V`P~r$m%~XY%CQKSEQ)cKfsq&5=Z?5-3 zSV7Dt691!+a$h`M%HuxN!>ghXBd%!aWUT&D_XyV>D#;E|JYocbwE}t^IjOjPI1Uzh zb*7fca;VNP$+tkST8^O1TGgSjvPt=YfHmmUMW;2xe>wP=Ph#@r?%IT_BgY29xTM4y zlLPAJ<~Yg7Mor!~eY!jxw;5Q?IXR?19vuF#;EWL!8-wqJURN{gZ=5W@y(e~yjplin z{l!Lk+sF~kX#$#l9R^X{W%cFq zhV#qgZ=0h?*^%J`!|#eicjTAy1~^obgQ`MC15V7MOl4(IS>>t&2mlozCe~F@e6dBJ zDNceazyjvFr}^UcOUeU7NTy)zg{O+FE29sTPG-cY;5Ynak-5SXToxPDU}nXtA2Iwb^I{0tI$ z{bu%#k{~F(j;4sWIT?>hMImZO(B@3uk}_j%x8@bi#nL5AOou$4E{?6+>9guHnOcmI zQt6>pw0KVdpm`sQwis;l>C!9n{+wPE!ZU4!K|*$E4D?fpTLj<*T%DWmWOPkBYDSL( z$Q?6hKmtpNJYVP4>aezPWfNI48swa|KWRH(2{b`uS-?ex?Ds;!Rpyp4jm7>Vdgd&JUf2fF070hVa3jlQR_$w$QK!E~+_H?2 z{w913_{D{9CUi;Z;%TgZ-vcPw-s*2xl!wpH$*D9EyL{Xk6+eEUm<%*Lrw9TH)m@Bo zx`shS^;Hp~A}#`h;SZG>A?iXbGYkXKhwQn5EuQK7kFtjc<^rG8nfLc$UD5IEJ(veK zhH((sM8d^&lE`8WkJN17igDB;$xX{3JEM(u%OjUijtR)Ctf0;e&7z$Z4-V&Uhm#lt z76i(OF!pn?A00BnB1S%D^UqV5{l}t0PL)1hTv9%Tzj7CX{RqS5=@sj%N%lx}5X0>SV<oJC{5u0dN zh4oJ^4j5ndGQuv;#a74ohcChfc}ll?5j@%x6DwO6M?uy^<-No&eZ0yq+K`8>Q?)4 z^-Jh6ri;3ruk3#`mms5ss7uX?R(Mt!&M|F9J)L%xZgSd<1PmT}1_A2y6q*ML(R>UX zx$^`Vh~yex69cu!hE6C#4D)k3lKgW5Nd`7#O$Fijzv`jH?mzp>mD#tK|KL zt$t1sQzg<$32S(56*`Pf=ysxf!hEGQrh{Oj&@=B4=_|}S^ohq@y}dLVz^WPASGK+= zOBffHszY^93U9(HKTN7~qn<;|m)j%Qq4;zF-mG4yJ9kw92Klku$=bEa(ZIY>Y5 zVwB?}?Kkqud0KtQ6-0F#60kQsJ&;EOvH7v&9Jct*zWQ@$mRH0IH($zXwritxSYdGu zPn*lSV{EI?N%rZAr}+<08J1y}%BYNqk!i`73OJ>;BP~g4(YRTr5m+#oNN2EIS~buR zo3EKjna*YdFn<1I9XCc)tM>-o@+)B4+x$ozX;H=LUgHj@KqJQDa0{6HJR;LQ<%V+KSDaPFOBm+}_m zW3)voDw>nAjlBeOY@uB^ywc-EBXHq z*}!D$Qr?3y8K`JwLZZ)Bp-d8H_$!5Pr=0{*IrjWb9H%T&a5HCDf*_btH(WcP@(dLHH;i?^I-Ql|szn zWEs(kR+wo9g28S^a0X+w(m5J5-N=^L>tS?smOqB(>GaT!{+mdiBis8$RaK?hCDR@G zpAp;S9bD_1BpBJB>2Z!15J^lhv3e>f!tjt@SvAQV1~`7wgrXpy?DEE|#G5DHdj=X$ z2{D`-umA5E1Klpc7~OxvuvatEiPXZwvKnOywq^M-P?~Gm_0zk-f)6~5fMD3ULQ14O z@QU@x6jx!>{Ox=>-?8J;3T*>K_^BL`jYVs{5r@jG7aS>NYXePN9spA@&t>yXhV71s zFvijYG~C=_!~obZywMzRrJ4D0oajY1Ml5AzL>&-_Skf^MmU3GI>g7?333KVdVX%V% z!mcy=!z9)+>A)U&EA8B2wE-jH8_zM`b`IhN8>BALZ-u&`r#S}8f9UD?nxVIxipp8^ zCfTVNOn}LDBYkuzNhQma+tct0{!mJ;dREMj%t;o5Xg!MWvmUqOdhXBp(EJtG$0JO} zgeJt73m5d}?8ZdT7^L#@-Re5SFbI2B*r*NlaE^Qx?)b zt;KwNz>eB&d1^XtE!G>+`0-ArE8NWW&CTFZuvmE|`IJD>`myt*5(!Y=_B^fT;$of; zz#Nz)KZ{b&{OE#D-sd@fKZOuGeXT7 zZ)<)hk^u+JuP(Mjp4>_{qV4hj05I7c3-xR@GN2Gg z6nM-lkJ~Aw0-OhysP=vsC)Ww=3bkMr@`3IK`pH(jfFOv(-vXMJf!P&9!Nvz;avaZ6 zvIwo?C4>DwlaQ*d{3B@TM>Y0f}yO*V+_D-W!D5K4AB6NP-S}XqY1X;@fDoc^wjfVY@C% z*n5x@5*9MlT+)2v;YfNtfwHY#xB_uK;M5+!!@b?QsjqTF){Lp+A*;seI3~~Sfi*DA z*3DO-QJ&a4CbddCFx-ALFTi7QXI_E<{fEaY!Vy@-BD0vy_alL)Tx&O?>CTfqF66XOqx8$}CjP&l%EZ-3XnQEk7Fp zLVO$yMGDs``I(kQ_QMy6>{xdL_bb4neW6_kChxFXjp+v_=CRg{KFf2D&51eVpy~$2 zcrmnG)4|xrO>`k*Jfl&K(MK4J55_>k4vVrA<(@ku4b*&bP9vO98NZg$z+KbW5gIw! zgpI)?9T+|a%{+jpr8B@thA&KCP8vgtbc5}upVExSAQE~LTXOipvK(}w<6;~Vl5}h1 z(;!O=8RwIb6M-5CSaHi}{0~|(dIfPnERxg=v_$bZ^i~D*(twm`RR~@)@q(P%UYRx; zOZ$Wy7^E^C6{en&?fxq=HMl|T4;)oN&AY68s<(tG0bKwAVxWu^(dk%!4f&j2VC?qV5$yQHKqEoQV^UPI&;Ny;4N z%yqTkIDmZv+KHqU$r1>&3_D~>3L}S?yb2%2{&jrw`YycaJiGdK{PO0mF@M6} z6e7g5J>Vf$vw;_qrXKF)1~7*Vi{#=yBMT%B|5HKx+|>Fg)7L<)v7E^Fvk<0ZC*G z6h3w&9ltM56!9YbQ_|ko#DnGLh5TkIFuR9%Z0nm$@&Q7+2B@UNhf6$}2=7iaJ|@vf z?q17S4B~-Q$teuNKd5-2K2#mb4jE6aN?9hXhPMtlNO_mqAo(IaGlEI$g$Btkg;lK= z02HJKX62Xp=BF2-txPttuY?wSIkJbb5HOn!1k@J8>-i1xJeWV$@&`wcIUUxK{8%IM z1QsGyHwYhh-K&$85OOlyM$BFlJ}e{%qs3;EiY!l-mM_G+sv^%by4q{Nk`(>ayZ&O* z_<^XHXCniDntQ1ehc_Y)LCBhSa9NrUL;EDnsyp{-MDxjc&KzQgPJhn=PD?P;(0XZA zfD?db!&~!*Mq-|&fUC%y6k_QDt$J?F6Ybz>B?=(f5(WVe5Z5I4J}9*LJuGwvf67zH zp6OFsx1~>oGPcp?&Pd2-Ei^og|1@DmK)BbmAgZP_44YhM1t~4C1eeMYJ(G5xZvIly z#i1N1;_LT!gog(fi-j^q&+g6GmKjt{bNL|^0uZ$S-`|m0@LYQKjkU07x$jPRnMp5~ zw$3jWs0!}^dke$g=^+a9R#d!dz>{Tdi0k{0M~MW2GwA^Fa8~N)IFp?!myl9*D1&lM z1m*-ILj+vZpP^dMbmu#B#B4%}U21MEqv{eKVi$ZF$D~mp5uZV!;8CBEC#komdEfSf zrPV?OqI9fnkHtTqJ_V6u%orUYO^$2;2$xPP9okE1)`JTrtp{?~B4F4C zJDGLNo|Q!`LFByrm1zxuB?u_67AH9bGE`>NoF!d zjQbfu3E~fVd2hzDx{W)njZE-iAc6sNl#=m_viV(Vo*L1>q^7RMj@UFnyNV5|B0Vjf zRdr3?odR3!!r%hLh8P_hb2#;iCio^yXve1q&HHxHW(?VCc#glDD(rOn>6?SN6kf!z`+hmysFg}$9SPjMLIaWbu zBsbOkBh|(`m0MY9anJ>*XdbPrRSBIjvlxCJQ9mklzvIoHw;#qed{UyY6pZh{BzAjZ1`V3LHUCL}hnLsB`|QWKxtoC+l%bVJ17pI5k| z*gQx$`EGWN94GsaDiIUbRCPQMKKhPyKMmOM-4vYM;Z_h0Nnyv)2+STV9$m&x|4?z= zcs`xP5@vaK&UIVO1-RjiJh3zIgMKTPrSMBHF4hPHAG;8_8z_$qYpk(Lv8iV-DbL{p z>(3Ruh`w!Wk@)Ub!gw>CH%7Eg!lyu^A(z&R=ap2mU@_4P`{1p0wuhi)o}{U!* zqZJ%S7n)TaD+zFL3#VoShCKez3ooob;{o<_+y`fuVz33C|rQ_v=q*w8Z(ZwB(<^UhtEb%+0bm*Etyv2(ad4|H$TJxzTaFln( zK3X3h5!h0kafD86vgFZKMi<8A1rC$NlvwK7jYEZiHIl*P3)&#Vf1z8L;9|@965yS0 zMuvosKj(~(QBfp+SuZtdHn)MzpJt@;AWBT zfwO6hy=qJHp6x0W`Salg{*RQfK^9N>qH@`Lbh-J zn~gM%&7iVH3K<#lk-K{dE%DCkh&mlvK1T4JNB53_vGMttZZV72=SXh>m3$-efRJK- z>NI2P`|IKwQ+CiZ1utPHOLLjlxJG=;uYUs}b zcND=L%6dzvEC@#e<1KAi1?}KS^eUCPsjPAo5K6bY=!9)Z3e5P)umc$OGED4Qq#4*s zP#gqgaQ*KU&F#ch0GZEVXQ1_YildOdNa@FT5?V~Z>yt5Ph);zHcf*iRNdUDaX9Uiv z9Irs=PQeuLEhbK#&dPbx42>6SfGBkMmRA%}0&=*LxxOZUT$Mk#sJQrcKJciX$A<|* z+)SV5^D;gUp{63<&cU6}15CI%8tLIGmg~hX#B|Ju&LqJ$@2y6q>n--k=t0m8M2V(% zC?ZZZU&}lT{2oit3=^4ie!1FJDRi0SB-;xkwTYePV;Mf4+c>7kXdYS8edf_tPRRAq*tbnLjMs+4sVAvC zw4$RP?{=w0iAtly#ZMVGr_P6n>(N?H%ytgQ*KRgNyBiyUR@%K*!xDvgI!P= zpO)gLa;S?4=kXIAcW(>9gKm8yiyZZ_v98dSD#<`wB(blu0-!XQcLv?R?BgRg1H$XY zYF>W^481WD4y$rq3FGQGgMzxmt}1Bs^d zrRHCjSou{@IPFWn z{|`Ml=Ivay#xU4K$9e!$wq3HD5JVc}3H6P`c2Z5O0W?1Lrcva=8j@ohZCpDIjgD>e zUSe|_C3scAUr>DsTJepiiKM(#z)BQgxVTt{tk#SDAeAZcP?$11){E~*B*$vNNuxTh zhGAEkD#XPEcemS{&tqXRGpym}3rk_``-=`-^kPEWFr~$eEYog-jFrO}?|KY}R~chx zEz!Yvds<-Y{C3V}IO_S?_}PuSAVVB80u)WwAYAugbDmH|{Rnjs{EkCWLp2rkY&#{f z#kWT;o`*<*D|i5h5=MVuI$);4$eg~~BSh%aQ09P2>&3}%8C!3Z{bgtZ7e(%@O)r9M z9a~M){P*nTdq&EG{p~s5Tz2@paqR6lT&dnm!?yt?GeSU~N z=sC-hcK%tsiAAN2iulM1~S&t+Z>AKokW%$$u0I*MxnTC*VwSb zu=ZzkRrh=&7^>R+ca7^K;Yml$0Q3-}l1(Ptmex-oVF@r{4dFsZOLEkB9;0QQpDr`` zAO67n#&xCb$#ahw-B^5Gd3z0G#_! z@;PF(9YIB!lUmG~wyUkGr?u!I(T21!C3i|P}J67{k3 z1z^mCFU*dch|)A!VR*bcWr0m2qn;p?zwg)vhmR5hUCv^pDOiV6zl=B zM1n{hHs*RBj2#gXL^uVzmE>W;wR60Q&Lb_H@qq2P6`2An098UkzC|)44|0>O-MUhQ zNu&)gJUF?)Pgr!c8|;DOGsArVH^7G`Dy=_Gh7VW{R0#{|=P?AJ0{wbMPbhyOeV^;l zwhTS;M$j6dYIkBc|PD3m}j&sej~Ed)DQURB+yhn9q|$ELRlE z7z}7%dN>5w?(-2A_h(T*AY?GwuMVoSj;F&~+Nm`|ZX4|n)>vT8fwG%!rD-WYa-G88 zkTBn`Wz1c2Z_Zf819%p~DWHoa{MRUTINvF6lFKC|kn!v{63H zo>$tS;P+?uR4BG1ufb$(W$H$B* zAuZR7Iaoupd=HlfE@(D)4~Jv+Ah1T_KzVm#Z*yxLLxs`g1!}mMC+e*ha1#j9Ugy|) zgW!0E1_)!#oH&@vwt>kH6-TigwR_{eAxDEsDGssWz|nOx2$zN}!X_F6k@lWmR&g{Z zOGbHpoDkC9RB3UW%`%EZn1|3TprxIa@m^sY-Nl**sO9+raXx6iNaMkz15yAOq%W*t zGnp#Da~$ZJ*{n*75Jr_=a$KvM=AZzwyg`TG&d&@vAz@P&P9fpMwjt~u41d=(AxRGB zlZ?7CIXt4K8@rk>E#-oyA+HIsF&NjtnKF0?qTm}>W1XKH2N*FQmgM}y0&y({^HP4i zJbBD?fOvWVl40iELyTZb^o)xtp8y5)5wXjSh?DriVmU)90RXa!uu8}ogZZTJu+icz zai+}leS{CE7orWGHfe`dDv!q99l)FjBKDyLJfwCCP6-ks$w2)&xC`Xz|;U?}6UlmuKL z<47t89uaAQsaP|~S_fb(DoJAkX0I%yIUj?B;gfD!VFNJ2tw|t(_bv$}4<65<0yQ@q z31SR>*NCS#&QKh!^NGwJEwM&Ga>t@e1fgxV_P?u*0AbIM`LMR;{K|v9@y^XTRm!5- zzz#;a6CV+b?Ls?5JurtD07WR|E5x5$P8}J zXXA08CSq)h^d&v| z_*lbZdf*mZ_EkeB=fbVMo7#0OJhIu1ZTof0gl;)893ceIajsA3+cKdV(1n4uQ`;h_ z=I+sXX>Qa5i`ktx?;9b}m}KnVL_9!}`!}A+#$@ct09F9(meHE)a9FSH{#(ZfHi~w= zJrHj8no}TP-&0H8h?dmLPR~R#0oCW?x2#V)TbJI~+WoiPML&f6(IMSCn6F^Hlf=$% zaUU%LOWurWC7o$Qvh-YGK?f(BzhD>SsO$;Y}`e$)-6+oH-!^w!9rAbQvkMuQw zzL?0fm&fO|`#TcZ(woVGZAa8Qo7e0Z4=`g;(pWHbJPgV@?-pp2ce)Yf1Bvr^+MTc$ zWtUyMXXRy(7v-;{{@N*&6(B9XnTa|_3`eI%@Z*_bKT0-j9FrIWo5MA5RM8`6-gf}N znlXsLK6s;9Ikh=VB4V0{!Bhk%Cg;JP0#XBN(+0N!mFzM_$J#Xit5X>L^jSdCp2p8p z#4!;J8Ps2uPlzv>tajeV*n+ zN`?szJyGzWQb(M?C#It3-@Qmr=t^<@>crmTabW8|$L74>95&TtvZ#YT5!6l=WdmvK4TN zVF%o&*@MwoY)&N7qpc*;0sw*%!i;e=x0!s3%A9~5i9Bo0$9r}?lRF;*CMc4Mkj$E#ZtV+|!Ioo3rEOl`9rV+2n;y zZtlB5-0^!p)(dw1Xpe#g8~)so1?JO>u7hvKgL{6HC`nE*+^toI!KUZB*@a5*~?%P{%*s!#;j_$!&KCOeCOh*$s^Ot?lTTppMUHo zJ7GaQ^g`C(TbqWtyQckSBJC{J0!6yP@SPGi>~fvZ${a9mCb@fD;7El`fx%$g z9U$vxhB^=xOcr_yQ5bGQMUw(_o$$?^pu`9m;E7aWSHi%YIL}P*B8VFYr$?PnHQz;H zU_n2I<#blncsOe`Pl7bRh&vbUIh_KFQQShPB?nw+%~+T57T43Jo+EMpsD2Ub2*5P0 zVorOUlQwGK1^a9GpcLNKjM!NYv*}Eh7V#BC`AqX=atb=TdL@`JyO38?w)!cO4^l_^ zCl-d83}NgY?;;A1(nP_iAK~6zd_MH!!+7wa;4iB2#qOru3R&5=hjXLGL;SW+Rru(Kyc#zx>yM+g%K&0DCkW zJXWX0=Qy?V%d^_rH7?l+dC(k>y(Xcx^2NuafhQf5FR<&{V3U*vTSGP?kqokKJ=*d# zYH8QoItNd73+Ik^uObHt{0HB`VYu;BK0-W(NQ=5}5$M2xHVMNb4MvI-iGF$&sUdw4 zxyP!(i|+edIT$Dh;S`^Q6Ud_BwuA1t9U_{C*f5lh^+GSt#==Exab8ePe>=jsP;TTU z%MGVNNwjhtSJzamK68LlBkh*E%w*qG=7S~nGM$F-CQC3}DHik@j}ff7EgTf5Q0^8!*LW1Ej1lw~kaGn&<9}8{4s4(}LdxvwEHh!w z4g%$1RyYe}y*cg(d~^y25+brt;hEk{UhhIW3nF#Jq>BK~rk5aDUlPYwVpu|AXi!2% zvpD~tx#Vj%Cd+4#bK3FnL9D^d=jvcrt=SmXtBBh0W9M?z6739bpY6!2sEC^_pA0}RiCWGMz`rpWQDBSb}(ZvEcLg9=+?jB|hK+vCD+B>#7 zZ^pJlGS+Yr@F86yvj9kF$MWnx3O2pbL=kcoEDI`m%$Zum{Uw<-+f4Jj@3_i4K=UaQM>#Spsu}hkKB>fdgbf8=0`g zVOeH_;qeLq*q+NrEyE!I%oY^3q5>WZP|cql&zAf*GK zcosXOJ)6A>bC7Gv%dGdY9G)y>f55|nv}v?O`t9Bvr8h4a0l~%_Uk%k=Xzk=Scvahr z2Mi`nGI?l&RCfS^@(i1{bFJ{9v2oMQ23s1G^#kqfNkt}%Mgq`NqzNiq73%{vW32ekM0*4Wn;r8bEUIYIo^=H( z&@iA&pSPNg=g-(*i!~feX*n?1TQ8zWH1{DTl>slom@jlXfj4L&b`VG!u&OLg0%-uZiLPJ$CbLNKG+xIx>7Xat;9uMBYu>tOt$vgx zt$1(FRd#y#az~zG8rYhCQ^inO;iVAt?o$xo39>fu!r@ryKnvE}mc+7`dC)8RX-7Uk1Z> zwn(dJ&17xf#dDelGeS_;+1Ik!a751YAxQcP(ZM4v5|H@^Cvz4QI z8FS9N{1&I~h^>i@yZ6n>R&~5q<+N&I<+bTl=mcdi$Y+{Z%rGw7M` z|8yUBHVp#xa^js!cp$#SZ!KOb)xh6@ZmzUx>W7;?6 z4x~5$4$I{wJ4XA$WEn4A4&O%v@icVI(qYQGIH_S;OQ`ncP?Xf89F6&J)NTi7gcj!0 zs_TEF5&DHIVZG-oV~68YILpu$b_q`SpxwugV-SS767Q4E5NmLA&55X0vXH)(k)72I zw_W0W*7NgtPGFmmXJr7c3<uUWNLt!Sk~?d$d2&Rb#VH?l2XVK6g!X-r*#%E>2*mi8TLvA}l{3IG_PhY;Cgn;$p z6ZZ{SGuso{APecsg{A>p4vMsm;-=r-(EwKLY`Z@S_32IAibEbJWp7T>cF(c)1-##u6%0^CMj zr@8}+0Ne>W+c9YT6apZ_GRMp!vFeGWSo0Y1oLjbT{bOLB+un0(+wGpi9k?Yb<0&;3 zE4~vBbNWib5C%}8%jn(&dPAc);x)3L?`YMg*|MeqjbzgfB5y}d!k%GWQuB~K*jr>f z*4__?MB>N>8?TUGlLp;r$SWSNO8A*ojOXPGLCN#ud8^M>w{G-q-1rhPzr_>QvBXrt z*D{fPOc)Bqp>d2uTBq{s$w1_Y2L_o#65G|xSSX>DCr17pCwR%9=vnUa7OENdo8T(G z0g>pq6eg?tRJ5XT*VFag0kw{PS9>O`O)s%X@4~sc&BjUb?xX9-w8qCEWfs_eMA$}v zA74Y`p!o>*uw?@tXuWJBIFRUZA?>4&C&{*fFhM4h=s|!Drn8dcSX^-bFuQ)({_@ft zJhKI}qKltB9^kpl#G`(a+uGZ1I%VV7FM@VqqQmr5HJxHPtxj}aR)0$Ba4rmBHO7E0E-vQlqIPW7cKl7` zGU2b&tF|$EkdJFeH6zg~u0mijquMM$7JLepdS{}6{q;43;_DRoizjmSVn>3N=7rK; z^ApN6o{7~>5(^_n-t<^CU=pc-AO=Sv0wQDAFOhNNiFvOfU zX7n*Vt!x!}doP$m=CM%G`wxha6cnX*dgS>N+hKbw#(_C77tICE(i+H-J7OP`eXoR2D28M`q*TUvOj;ddgmdSs zamY5JBcBwzeF6ZSZHu%E0EFw~0U)J&3>CdX6gl3luc48FQ8E!iX!#}7bCrX_FjOD* zBT^CAa<5nyVA?Lzx_`y+GV8&Rm{t-$x_C0{2$KyfbMQLSTDFg%b0fwTgGfFyve@)7|71r(8sJhTM`1O;-xZ_c&%Ih6$MzT*<=?7i3H zf6RZr=ltidn63irXk^R?{a@q-cEAA-5(u1P%DjP2H=&>4 zMgeGY&x4EwI{U|<=;ihUPD%FOJs5g6R>Bf+LtDUILBTb=A~Tw;#7=+yEPEm8JIEH4 z=}>e8(Q<7@vQvCS_7xI|t+cq|SkS&=!#w8gFmax*SS z<(xxWAWftie3YzLGA|eEXYE!~rf?q{MdE~yg|50p0IYN7V*a}fQA}b|c%Z0CE*pzM zISdgk)^~;mHF|s;pPl(ZR+g^PP$?Q-p^yh>ICF^{j$0|A5Q4N|O^W*Y3;g-`@O7-Y zinv>uc)2&aj{tG=7rTA)+h+Go18$AgLm}A+H{hP zZY#4$&HgiL98#zO;??@+O~*{v#+4ejro-=Ty5C+;Vob)rPDgGl7dKrwUg7_3SwC-C zOFIDS5C(z#%Bf9Flw0)uk(N4ZCIA`O;q~=;3^Yk_IB4@|2Of= z_0iu8e%8)Dg)5p&2X)+;6phIJS#-u8ce#~{Pj#Gq^-45;K#Q+hr9KW|Zr4Kd&=im7 zd(4&$0FP?#zDFabcLpe6~jodO(T0*pL^yADw` zj{-9cn2{Z1#)%8-Ow;#^`a7`|lu2swlZei-9<1f{_&*6$RP{%-q(?WcSLac+l^gCK z$F*#@wv&*w(FkAS&Fz5&!~r1dUwbwm*{% zek9!DV0&beDojBZ=Ha*4^pi0>1V&@NKfp*iiOjigZXXAE+&rG1=XMh>pd&KAIIdtt zYCLnz$hGXNK+1Vte9B@4pWd+xN3Vph9^+k|m;LtamaSM0@`pM8EO|C>j+92Ia=1ST zb#7IMlasT-A<`!lr@8SgBCJd%&%?7`XDkJGBey;uQYs(|{6{!WH5vOYzArO;@;;q{ zOVcbn2_jjt{#hT(C9@lY#SVw*(Z%bpQ=WHD8NZ*jY2)$VRAcalD(`1WWa7ttOsYNEd`}Mi^PXLAW&} zf9gjpSe%fB+|p@dc`qq->bv2>`{F{JzZs7J;OU~B->}Dz`r^`!?<(%%=o&yX4>-bA zlfsm}fy3tfXAt`zv)^J2v2^GK44!160zMII==q(4`ROorW}t{tEhUOT_*1g4QeHRb z-;lYUFb_E6zM?CdG(@A-G~l_Z)0r%WtlP{GS;e_7EO|ufQM-!{d*B!jh?D{tpfgp1 zJFp@gBYeT=h^aIE(+zFo`ZejC$MvL$A0}b%ccuH8EU;&Mu0UY5Cu`gKA%I33B!-z# zY|D%jQH=-V6qRsYCc6?;gs0Ar6HF9kj%b42 z92bmxsxh{}BP=eWg{+rB9R^ZXu;4W~;# z69%*er0yI zWtCu6(=8LQ1VF7U;fx}hbqw}D)aoj&Zi1Z!Sf>0SLjItCOe4_XI52}}z~|{UV(@(J zbZ7Yu593el$uVdg7Qi34wmzgbEHC$iy}M z=44t`tHlR`;S5-e;1y{hcRq%sf~*afSp!uN)|{5G;{nH@wW;^GY=05|Z^2NVIm(O5 z`1TJbfiZa}P6UP$0ZX(FQRIwAV!g=2Io(;HT}lpS?(04QN>X%&ue=sjX5IeA{;{8l zT~~<{@D*HqNN09~*3U-#Ed|^sdGs3VEyE$cD^)Lt+oJE1Hj3FrzlCm%HHL1@1tJOO zHoL`1^&(M0feh4@Q{WX*W>&ZwM{N-FKA*TGq?At_3Yn+u=Ft%Kv8X{GoRHunj|kT# zZ*9>|bg zaP4ycJc^tWU8x=}2p*%KS$2_j>;l8`s8y>u!8tv2 zUxFP`*3%Kg07#a-spe=u`_y#kx90`XV5fqer|V0P!HBx4<`or_eoMhN3CgZ`fM%nz zk&d0L5!;RQUmPc+bQK+A_B)+S8j-d?rix_F8{{4t2L8dV9>>7Ui3>Oh3bCJ(B4E7K z!I*=~VzMY{5+u?fG_+97*1LOZ6X3`UVa8Tpo6hTS`}$9T$NCLf2ss#ct`aEgt^@jY zXEJOtB)>+}a0R**>txvlV&wQPw}pRS z;m;8IU8`hyC11q`IP8D^B#cTYU@VSK@vK}fgWJd?y^yNZ7lX-yLG(vJk`Irh30N#* z{J5cK+MCb|D0Lk8M_}NRAVFpu4BAuFb{>;XO@qL@w*ra4z$s{VqIM8L$}JT`Ruh+S zD02(V5Eza+PWL;#a-pksH@8_hQH>D(m=7ST!$!1=_%LZWLOR^_sAf=q3PE4k3{D#p z-%58w1#V~gMdGQpt*(OJ6^CLuX!low07d&JHv@V3Vb7ELBB@zg5j?-PiuopMW)|$U z0xnzy!TRi=x{3W3g(`W{F_1|Y04J_}9)EHMt(6B5Gt8LRBDbVHJL>ioM=`HLePS$2 zn#Tw=$jKE*?T`tk6X2YCy&h`q8de>{ig7YUm~62PweYEVmJszsT?QuLI20xzIY8zKtvE=v2>4!;`b7?_;Yrb6GVZORdtaN4cvT$;n>9}PPn_7cf z#o2RG*T&iiD{3U2-OR-;{y!e%LN!NmXXvlw4ZzV4nF{BV1_B)-4-+RMnHBREPA1KH zikERe5^7i=_ZbXOFC$HwwrP(P1{WgQX2g*7np^AnjqZN*ScH!!auI&7Z5#t}ozl@jfkcCS5dcTxQU5VL%P^V%xmUI*-9L%L_*7VO+`1 zXpZz3AGN)6ByW1ku^_S>4F9mW%x@;OzTacnjfl**Z#7xCpI|_?Lv>h&cI#Dp&$U~o zbD&Qvak}X$pQfAfVTy&Nv~?^_7WARcrpRF7(Zq|TMEbZq;)zi?`b>BmZUeg*9GSnU zNWZsRE?OPl0eNP+gnOdbCqTT8>aR6v zR+Re{L<}S3UHnguw?3A3c@#MBxBr0+mRfqVgRJ;|6(p2#)@C7;m4z|7kj<=;q7jjE zBR*_HNxr9l2g)-t#jMBl|5!3M@q+ZmjFa5m<}hi4T$! z4-6APsAZ1*n1z~w$re~NQnkjN5OB0ds5u-$v2l`Pfa{N~jyR?@s|x!P!U?Uae5}+e zU?=jUTTzJu<-D|0z2fIpV54RYyo!Sx6ge;=-cah1W*d@uTuxu?mJAu~OR2!8vfCn` zfdWziZZjHvt5=Sq%qpTF2BJF*c;P-^l#kERDNL9$qgVq?ocxp-;6Q;jMGQ}O-WoyC zd{F+tJuku?(CFNg)@wJrw}aeR1o-Un$wH7LcqxPF>9(;8XaW0`B*DZGXpt=4ge?^A z{H=u#q@&xmgd5WrM=tOd%@UOK1=bIey?etc+k5SAN&5)d9qh(bU_3pPSdYF~@<`R~ zCGK)&b|-7`%yhUL0;$7YMBNN``Mv{pDI!^*7PVVBIlN@kPJM??m6AI&>UcZ0TtR_q zdH9KNKbn-pDP5rO&T&wVA*v7+0=i>mJBQWDqHXWj=&uGGy_KGJbckGe_+>iW;{&aL z!V%qxw_qLoiy@bszJXqn8rI$af}$53U#ZKo-(Ogm9}Wg{@jvF>Z_m;T&~M*Fu`gM6 zo;%Y%_|i$=(+`=A(wvXlL(C&>F*pTO@%B}ZSug9?kawKEH9>(UGriL;wG`oqHo7!E zXgwfQcfX-?6NX2V`ApAYZY^h9G~G`Jwp~Lv8V20>z-N5~ghD#97D`86VIAtH)XyY- zF?$rIL^1ePk4xYY<`hjlv-Ze^>2&xt7_|>?FQU_ZB-W8`NW$m5((@!ER17lDZ$ z*9j`p*vh`kfK0>h?{B5}mgBGJafFDB^Qsh86JO>BVm)sH<--f|4RAo7H z=?PX_huH_mhGV6eYP6YBpW+M0DK)$bf89h5=#JiO3Lo^+{_fI#^vm)XXE*_ej{lc* z)V0^-x#n75yz7cIqtP`OaR{-ZI_$(Z)5oO0hI7M+T(ygjQx|JUUATdAcH$bxj4?8J z&mLKS*}kFks0g?Au6O9^KzR6oS*zN9j1XT7MPeQ6617#^aty)eF<^8&I}%E-vyP(M z1FLB~HfQitt(OsV?i3$1LPt@^hO=sSae=yiH10|Y>*rORl18H*3!1sXiUChl@izTc zZO`25Ui+=0Q{$d@F%i_G{1K{$$%x;)qXn8 z8+4nV0K%O(?V)Y_d5KC}j$KilSK_(ZgnoduN}%ZjP+&=JynHwZak=)w&eJJqgS*D< z^b&+f6L4M6-rJk%U{6EENBy%s*6HXEO#31 z=N_mL@-)^pAR|;x%ptb8aHe|Dbht3oXynik`6X~z3`D+w$rY+gguhfoRPy+M)$l6# zrB39=r1(N|My1?TS;Lb~L;F!XbFRTM_LJ)QA@=TuJuX00&{!V2?k4D0bTo(0P0}vm z*0l4)m?w*UHHKj{9u`(!W%mk?JV~p-3rtZfijluJP@rl*5x$EOSww11K12twjA)y{ z3u*;>-~uBs>f@NNHC8MfQ(%WoE#79Rc0notop|VqGI7x;TG-7_x-78(vrQ@n-0bGJjV=R+-@r$BtrE{bn+XgS@0r-A zegT$2>@)o6NCLRqj;-XXK)Zowymuu)?%)qXlIQd86oxWTn0dlQ$bYqvmqE1C)dGY3 z>HDbf2|l)+Vc^bw(N9=(SNdd-oe=D`zxmdf&fEB~h7~vE0g@8@AxP-VsX;6`HAfvD zjZs^%`TU+!O)Q!9E3=(;-4Pg6*e$j&880#+O!{o>lMEfcioWa+FWtxoZ8Q2u?~v`~ zl$>{WATh9M{~Aorm!KCsiUE&A0z1AhOTP^z8VMtF8!kM9ajtirSKa@|s3qyb3@8rv zmJ^{(B1XFvd5TWhI7%3EOh^bly(kD29)TjoO6ZG4OX>yxi=?kP=nF^&ePJomjN)ic zW{ND;LW&?N>cbgT7Dn$LaA>!K2$z#8FRTr9EL5r~gQOH6hF~zxKCnefiE}WQlrCM? zU!_15WPh42Qt8f#Za@6VxKXqRZ41lDj|PapZ`H)W0!bYg;`&xzR~G~;xxPQ2@22Vx~m)ek(&^m0gJ zBWKZ1k_$P|UB!W@sE?B9G z63v$v_A($osP)9U-kBRd#HF~}aMejq7Fd&h3%+B|W@jqSKty5W`|`+ca0l|;{oT9M zH)-cdvzMJlybK`oWw||HJ^Ru(gl8F)L=vR)pTdM2Mxew79=ZzX!L(JcqFh)^FpAms zVbtiusy(31hl)!lJ^Y6WryhQmbfb zOfu*dotBos{rSYD_;8;e+-c^L&7WaoE~fp0^Vo3!GyHrX+ReVyAAusu_F~dg%Jz_d ztzbYgLu@teXeP*BAO<&REhU}c7isf93C9C%s$KyVrOneR&LV~{SqwM8EpP|yF_~SM zzOdLWN7#DH_iF2fVbH~6!I>O0gP1a1+YB{bN8z@HK46wy35oz8ovL#J5R7I< zCvaosRUb{670L|!7!=H{6YO1Zg`Zaena~q34Rs9fXo_Evmpr5-dV8&^iLFcko;rKH zJ3W0Ji#&984!x0$OS6zF{v%2C!yaZ{1(POdg%+7!dQiTwGVh=Vp9x0Y!woGEWurl7Br*8tm&JV(9dM)mOiVvpFp+ ziyD;mW#dDC9l*7!!fzP@fuW>prerWCC1>53SpyB8pRn@z$^vD>>!Oa3$ zaS+ZiPV%C+exxV?(?R?DVJ(yELz_bMKfTsC%k~j4WI6nP*;KbS{acTeH(rCMXr2o{ zy)l_Tvj5s6!)MelSTOh}BZwqj(uNo+&RWV@(uZBKR$UY#oeE7kI+xY)@d-27J1JZ?zO9Wujq?rZ6r%pB#<6Q08IuL7WxMx$we3} zX-DyYMDhEfC+=z{vo~4h!~@)-SG7bp9>a5(Xk=l9F&I;Fv6MGC2|OhAX!N-BpO-ii zIHn<}1C-PSfjg0JUMbr>u_?WBLnyB|Db4RWTkkQ?|#jX2s(p zcxI#|{AmacAlFlHl{|>_F{T)^XjvDUquJ}~TiB5YyF{y`ni)?KIZ4cr>#v54jF2i1 zo8J|9fh8_D$matiSv-J%D1Q&9XHS<7%i+{9q$c)}AxZlE2P$7N$~{lzMO*}O``nBp zKtqmToDkXrGpxrLPeP;F9B29Rls-Ad+8v4lD&sh`}}Yt5XTG833cBk{+`Gt209^r`OHlwSc9vW=CYQoya)A2^mZdR-6S$5r`TQNdCu8eVi`llXxBv}zvdsRN25B3iMT~Aa$9(&iw-Y{rKeptfhuU_i zP` zeLzXFplA~GH$>nueOZf}uR@;Pvx5vT5$$CR)8@V`jH5lK2{bk(M%vt0obL?@M(UmN zT{_(FmRHOsyBh@h`E73WiV8LY(ve+{pmd@!S?4|Jb)H%$gZ!jYb*O@L$w5nD)jT<{ zoq$%v>S>Uk4q${xSxuI%d>jt=eoQ$tB|h~$1(oqfGyr_`a1gd;Fly4k?N$Tw<1#j1VnOWBFeit#XEwFs4Ctv2ya z`^&G0ZQRpANd)k{8MdXQ2QFq(lzghDNHCQ)9TrGy^jh^Rw)h9M_@zG0=~f$=1_ z$+tK9g}j%~Q)oSEk;L@_9rxs2p1>}M|4yGC(#~f`W7ZQ51sy(A6IGH$4}*=1R?gu* zcvHVx!x*OZXFd^1%3q`ur9Vz3pmO(I7hVFSWm7(^Jixw~`Y$=Mz!ky$(+(gp2yGYU@+t)YXbK|NPTFTTpuss4mgIl zuDz4T?!Zr0@$CvK^6=%o#p5X^1>}`CfFCyqoEV&*Dt6P?BM|Km8A%>;iwpEi6?-TG ze-cOM+gChI)-lM>;HI%)#LBt_TW|&0>`PxEclfnexehw|rt!gXSgbZX%~zds*82+e z8u^e}2cQ%+)*TIL%ReKwdJ*k~S^Zscsb8afIhu9swTQ4KJTv4nV4m+C_Eyvr6{TQH z)!`lG5|ZoW<~F`#Q8PT=>nVyI&peysr$Zv1jrjnr@9mN)7^B6o7c~OFqwruTv}Rcd zK_y`apfFp87{B+F`3RC`m`WqPQss7jbcqolZn!J0Jl;%WHy2e`!(_M)G0wyzKRfFf zSpnXUUJ;nchXDyoe>_NsCvL)4HU}d4A`n*$Q-cRP zkA~wuen7XZ-dewKA>UF};646}9vCdDhwJ z!Z9-Uo2~xF7-dt8(TfY%ty04NH)w5;8*~fSl!d83%>Ez?_1hD_TUaH+>$8SA2yyvA zGZZgnMC^zpNHt+(`;oOcv#7U73;MPwV#OJICnu}WQ;hGlub z$DRxaE6kShfLP7ao$PM5l*8xn${CuEng&sE01r`&>w1S;@~pe;m+L>L@C8en&EIWe z?R9Q-XY3Bm|LVU8El_6N#-=atNwPS{>d(`X?h-SdVMyWT49tZJfWV|?0C9lJIzc{Omh*}6K8l?l7AJF&&x;Rpy~9jFM9O?c zzF8+06~s_5zpSMj=^cvugCIICUNHFT))U0jYsQd#kp97OB?1{Im%~2bEW9(<-u&C6 z`Z+71=kW|W%cJt+fPCZ;p01k}Y2Lwt zdau?$FR#ObxFv#uCa_$Z$Ye%#h%|FZMOuQokhZ)KD>&WT|EH^1KQxDOYrZ5PC#_@I zLMdpurb#F-`~|{y74D{u&r9cXc=ua02~YOQ#^>s3o3p$ZJ?+sfvdT7h0!NesBjNtn zGY+Q^TQb~56A2U`EzCxP$Gty0rV%v{1u*rZx+x%$yOd9d!uHza17s82^bFDEvlR9P z7Lk-3$sL?|%bFre)mKbdV2Slp&8x43W#MVcWWq88KegD zstOuJ^_sa>oZCIra&Qs@ykcfSj4>wUu)3HyjH3$GyWK>(`3NH>h~7dQQttdI7}otB zvw=myvzWfRXSJMNJKjS6s9?&p?ym8tS*>mfUgbpOe`bRKoM+b&sf41tE%e}yyNY0s zIn)Mk86-U&3JO)ISi@5w&ul1*24BW6<2Ik`Ru7gp}v(Bg|$(KFe?|K*z z!Utu6n-_3Ew(a6{-|I1V-=Us`+=s111%#pI2;h0m}Upha@dLm5XoS;83x{E&6AJxJ}hN0A-UcG>xZH{x^A zx;4<_lt^V(HZ0;mQ;{2UFv-Gf!N2S10^Nx7ID@_P_>?y1?~JADjnU=+EoIwK!vvh2 zf?DLNz$zDEAMQg!sRv!%Yg@8$$Kxa+FmM)tOYw}3b6}Fc%3-M$_&tuA_t|~LdbB`~ z;Ccj9c!&4cTZ9rpDTEinvb&eQA1}2fTOmg>w@4#M4oK;c3G9jnhBH~LHTF55W`E%@8lIdbSK@SU-XH78AhZy> zU^xSUq;1qgnqMO=L3f)KxF+gWTw8!FP$u6Tv8`3A@p$->LQi_)D!+>M>+f*nEDDV> zp4jY)0s-D`(YlT zz+Y6IVo}l1SfgOChl=yK5Q2gFRhUtm(Kb=IKOfOgw@K25&0)kcKrdNTN1_-=eBR!D z-odBs6B$@+{~oOJf%Z4}@@Bu>4j7kd_1Dp&7B4K{2f1+7=tVsDjeM?q!u{U30>Rvi zs(FFVXBS zW~r_z-#Fz5AVx5!^pVvr!aa=6{LOu<2hCzK_lKEiLoqR+JY-x!LqvP^HLV60MBKRp z_&j7ZqG2@N1RuCFEvg5B2@Xo3caEeN17(sFT zVs#kz;W?CxCi9~qZ0IMaea#tRq+&rdadt~)*EuxrX8p2R?u^6neQcEtAgQjmjUj0w_v1A?73j+99E|XRyCU~ZS|j-Qqfk)2%pT^ zIBYZC;NzTG#$g~tL5=+TH)pqt#~GDo#{HXf6hG_jYwzSo{q7N!Kv&xTRrw>wLfR%)bToHP0E4RX2Cxn~M??hM9f(Wc>g31`0k6Iy1C+_-|-%As%Q zRtOOBI$Rh8zbA zEV1cE7RXdu6c@MJt{Dp;Spg=*mw4hVAPH>aodd*8V&{ z2lYS(woIQjiNSC~WH#Czln^dHCOmBsmLo!gemh%CNd`7CC_JQ8mKu%5XKDVe<29dL zeB|^UAf+w^;+6T!DLmY!R&6 zP;jhd5_6Y@#=}!oTn82HY2F0}^7QLRa~`1DekzW58BRDX0#pRyN{U{(T~@X>UiSSc zEdxr_UtOKxQwm(!4X${bCEkteIZ=4Zs2<>3L(rC1<{~tNbEr7lJiyp!Aba{Dc%OCx*}C8T?BDKzl67@s&Y|=aWno zc@`rDrD0-UT9xx{u&99P$rCS+@PHmFlDm#o>g5QG(7)H!e+*AD1B!O$aI_x z3*go)222w+ut~2gnFNd9aJq;^vro$i^_3?mI{a$X&M=m=bCx}&#fK+bPV*e-35z9| zxY%cF(*QgC*Ea@OqePRm9MOV}fIV>vX z7UZ|uoOfAs;G{-9WpFPthF&$hsU)DJ2p2FJQ| za?!@*LY8mGxLC)B9b?*~hI^n^^YCLd8QWo_TQtweG7BJt&ZqmmW0BoNP%~WiPEOg` zcrER_Nsxfr$D{+bQ(?9ExCeXKpF~C;FPqbTINsdVY@oQsA_4F7SY1ALFmpU20a06> zh8bd{voOOM%zQnWIjFDG3RXR3Unq~!iHgtJ1_^z9}&iik;dDy z)X>uwn;$}Qv(aoyoV2qiWJOUFRj&*Vya#^ss(Qwa!IS&llN-m-nANr@i4EMHDEVPY z>xUiXLjq2(S583CF(e5z<;;Auc_)|Wn!R)pg67wDJZKj`@Y$3p8!0z;iYI`vLmEJj z0Wcbxss(4u5ohf0Va?Wb1xL+;kHZ!o%rbjCGHgQ33RmGwUP!;OfV*HF&Tkpj3&!+| zk>Rp@VX1k5-Kw_Qr;H6B6kFR@+;KZTnG+lSUh)szXKzu)ZmPhIdlU4{S_F@FsaMIg zuxn#4cA$q#6a?@CWZ9lX)(~V2=)Re4d5(r5o<%s2+hBJeZ5~9mgRE}z<*ue5~YWrj`;PtWs;|3l}v9QVxsYf{ie^R zI5z}=3QVq&jxOA@Fx7cu3rhsD*v^M%?P~j9n=C3sqaO7DL?+o5R~nY@!$Q7U!5Ri^ zw3f9@nhfttY$mt1l5KPcIk8Yo**UFn=!H=5?xtqIP_{~#b}=$@eJ5kN0xW$DJWh-O zj>ANK*mCTQOS?+a#@oY7%$pC>6}ZIO?d1{Uhz3jI8M|krF^(wmJnAp)lI#pD@ew-g zfX4=u@P67PIB%?1%wdnlC9e7NT8`bT1U7?gbgy#pd@lxvVHn$8^ja7%swZepP*Jp9 zXkX1T>a!;ET?U4h5R-{n^wIv17xxmO83=jB=H2ina=-|_1Y(#(73PpmaR$c9PBM?m zsc`{hS&M}ineb)rzy~G=HgImaCU(%KI#aM&GJqDjjZaYI#bfBP0K#^HgLs|qLfrTm z`gR9y9F-A$1olKoc^P*kT?XvKhi>tK>*}ur_+z^V;KV>Y4=)>4a2qof?(qhM#v9@B zZ+7RY#w1YP|EAXg)$frgHM|V>i?h@g!5`QxZ191{wA;d|TP?=Pm-_!f?Bdf07Y+A1 zH8#8_GZ-c__(K!OGh}BhmR8NRnF>ngq^FIec~~wS16VhVogLpSl%zYsdzoa-lXMdi z9hM%2(-;Ha0%3@t~T|JB9{=gaj;9L2z15@9q!DCAiy*OzdQzebEdz@K+D;=HRV5iIj>N#F|=Fa&A0g z5{piPJ>V6b#jkM)03Hq%eThGoeS7n*bhN|($qMP2v&K`ac@6;v{tRk*L>8^UGKhs@ zy1izFiPAy+6;eWIK~0v*?EYE`NbH({fzm3X8YRclxaIp*u>Le|o5hfM zBMD4NBNw9u;Ib%O81Zy9lQwn(wuOi|VhyZlKrt=?&;e^x^qe}tilmS177Ed(j&5h$ z5?B}D+Kyo)O>bjDhVn_)YTj?P--e|4mPD_p_i@K!b?$XNlkTZqykNefhz6Zb6E@qY zU)Nqwn0bRL&?NyTiaEaEw;_>&GrMleFZbofC3t9fZ$}y@aIUl|&o^kg;etZeG;h+V zet0EGG7Ns4FYSRC_B|QsXu02%z^(*W+WAWxFEqMX$jhV|&VfHR*`u2YHD0oVw_6?y z0wZ(FkF7>@22I+%mLJ+u>cnHqcz5P6!wKj3_`Z1A$rBNmLinuld*S#PH5dzk@D2ZT zbS))9a@e5hLkrD)FYYyb+~;C)$$|aWgzcoL@<5geh|tRWb9jP(GOF%4ZtCb9qX6az zk!1kV$r8?R>+Z0AJKvW0hEMeB2^@}_G%|h0Qa<^Nt_`$PjE%w*AqK>a7Fd@F=4*Wi z8*t(`MY~|s{3gqzyHp!Xj|&!urhpm~(LqtP`?g!!(a84uJNjAAMxx2t1N32&q?end z#3D9lO+7fx*zWQrwv_XBe+%a&5LjE(X$^i8y5q@omd82eAwBfUD%1w; zKeGJM&d8kkvw%oU_gC@e;xJ0;g9tGTB|>vY6ripg+}CyP;9X31vk>Doyd~rpH_IYH z^x(-?1=H(oZ!36u0oDvghmadxOE#T)<4^JESS0Y*%hNepoGX8x+* zVV*VW`p*2-hRR=^!=s??{q-cgz)iY#=;*UJBVAD9({E;rr%l|h{&x`{6%zW$M+*J5 z*!R$Ts8bWlckSW(&avodbU+NJS-Y)%)dF5(lnE||HRxs}EE0fw5xeaDg}XVTu?OQ9 zrf2^}j9&;I4dljWB zKh82hMPbayVL>OztW>0Q{TUZ5(tq}l!w;8sb8v_{$eR6ZRavqKO$DuA&pnop0Qi{N zZLGeU9-Aq3EYCOVq|STE?=J6znB%%LB=m?{Bn-W!5mzy(zeJIODwjH45**vF&i>*u zcA-Q%2<+T_FaI^SKQc6=tz3;5W8kMLSGZo3oa}Vm!$)k9f=^}Eixf{%kO#OEr@^Ly zkC+7uih%y6dKTkSE$ZX!h{Ov4s&EPRDM&r91SvPC_>KG| z0G|5^c&A5sQqFm=>zfuR_w#O!GH{&QM2P;(`{E4|f?OFi4c8Y(GwH&7aDjR-=ogYS z;DgtR#S)!}InA~whfq)uDb;Tib!O;;w7bA4_x(gxOq|ro4Lc)~%$|trnGib{;l3W> zJ`O*w2wKS$9VcrRFY}I0&R<&N;ePwuFCa6H_1%8nc}6Pz9uTGtCl{{|+nY;XsK8%M zkei3oCBcbp!UbKLuqvnvyv>85N=Q~e8?5M?>Odf5 zY3_Dgnzvttn%?wBPwJKdqO)-Ei%2iGudV77p^Oihgy#3KTNu?N$`_RM&gYB##M#$w z5jF&!>Hbtx_-c&3tp9#xG`c^7VMLmF(?)%POsgX#Vb%Y65zsu47rYs}fm_yBoQVZD z$CeBJ42(GzLzRoo2hghxa&9YdJsS7LfF0SlVl0ngb)0JN`0Yh_4@{-(9d>Bu5AtYa zbJFtHth>A30eP;m|)JPM=miOt;Uy2TFIrsJ`I^e9jrT#9dN1fQmDd02$Pr3LYJBrJ_f?` z`nt*A-Ev=DT;`Cg))cvQDP)C(6d3h)SJ9(KN0~m`RTT@#3HvEHoSRvy>$aTC2mEQD za)Mvgsgn~^?P#v8o{Hf%`ceA{a75yy)1<{nvrK&*oW*A$DO8spD%`1B(fgr-koE}Q zLHBk{YODG}IRSb~bx`gAex-LTwKu)oF!`>s9R08NczRsSPzSCsHec1|FYg~dLOZt} zS-kP*Z7C#9bL`gZUZ=c?9?YJmb-VMzV>U+rE;hhP#Xr@t8CBSEX#);K~cwPM-k_{^TrcxNPvqQ^gi%QjDJEB0w@z}EUtfmczXK z@<;gyjWX|^w9%z}f_vqO(OVo!l7K}7K}|>iD(m$Q(;*mWXR!7!7nV%vcSAN=(cTm{CKW~~0IGf(?(Vg?2~Ljw_iPAF zJR8dAWun;-zq`gKG|q7N_Y)gUaV7uQuwMKW7P|es+&0@UeSGQRRq=0p`KPF}hQ?5h zP6oT%sy^2n_bQfBCen_wkj?GCAB!32aZ7#=p2s$WtJ^OPz#G1e=(##(F*g{-@erL% zqNCVi!Z6w|&0j5!Ub3qw36GB+9&|0M17x{8436{|9}54A^;Nh;d9wH~;QIvLieLki z2lU(A{;GUU2MH<35!q8zQ89!up{x~z^Wv*K16IWz;u2Nw-#7|5l~~&yGd|Z|t4ox( zK*Wsl@&Nj0r!KqV{q5LmG9^&j-pJ179=m&9Xx?|CZ0y#|K4M26@+3^o-2MxG1wR+> zNXJL5xD_RfQ>YxY8>_l=s6aFhHcEwIu6=5^J;!HA@pED@?@psEO!fogr#SB>P7I1q z|Id`-tPbDXe|93F{f|}O)Fa0W)&EfSP0CgNfa;sw9rjcF|5kldG=$^1Dd&tLn{Qub z*+ZsTZxiP;0U?LQJY+LFi*Pb3+^gKkWsf4EJF0I|OqA-I7qt&(1+BhmHn>UIK0cZM z|5|-B%NJu+o1`oeYDZ3sTZZ|XBl>~WH;G5!@L?)}kL|4bCPpa~0M=vpOH|Xt0Qh+o z=6)mzx=-+5sJ^-LNff^hI@1Ay+|1ri5bjxhvoBy1PcxM2&u_wdyXu>TRDJiQVgm?< zP!!xWiaBjyx?bvI5Hx6dy+t*rwKeT`=i`F!ZWTJAB3#N2dkR!Rg&L5o9m&J^EyWVw z&M3e6=N0tsU~x28hx5R8MzI+Vn(bL&`&KN6UA$7Dq%44hwAPsqubxsP#; z9p?f#Bp3cvFe{>VQBykdfOMrG_8IPiA$oB%Hs6Ur{D^9Imgo%C9SDOx1oua8so>pA zf%3s@@4*dy(hMSeSeIQ2WWPLrynX4@9(}SI9%U2bQ%vyIxG1q?J4FJFX9kSV! z27LW)L@U{iL2gWtX*LSlFSAvUg`D?6gyLpYHZAIpv6}z@P{0TR476a?*#?lC1o5BY zCD*4EcCfBp8pP2~8;hFLAS|mghxW~yfU>7w@VWC)gmdI4as8YwNo*y@tQh@Dzg+1g z(;~_gLUL=omc-J4`pfjdyE=&;=Rm~pB74I4%!+)(6!2;39p zJB+kBr-}U+x6O`M(O|y#MB!Ba!@9uv%w!|jyvHot2-u)Z=6D!U=wkyQgVQd2P52M+ zEE&;*H*NC4*jTM@v-W~WzknAH?!$dhcPiCcpv!rBp5aYk?RbGw7xtm>PfSMj$ChCl z9K%PkCuc^`kQouTSsjzA5IQav?QyXp-Z{pLXx^a$?(zj@I7k1Kgy$Bo#+(^}p5emW zW5AKNe#lTBMQfVaw1n8%Z?S zOfvA@1qNxjOH5$9s}+A@%Y5)mA9bHdb<_B+&gz_mi)RFwFWTjtug|8bxa z$)Q<^q9LdQN<&#Oeo%Qn*tSS34*+}Uaq6uKjCZ@D0l8Bst?$TPicsxcf5Xbu--Ed5 zZmA0s_R~XjzSIKQKpsCv>jbi*>H06~5p*CatKGDhXhZP3{+goT?}y0J~%>cb~>x<%M`c6!asqZE{3EKsKYem3+HwkYXaX; zs4%oxLcD!xCZaYbfBnVGp{abr{g$9`?6(}helj1;=Q*NeAyPZo7m%6xQ1u22oXBvV$nb2ijt?E(Vp%kWoRsC#W9mv?7nfkQp zto2q+U9d7`mNZ@hUVA6Kwuf%NH!z|Xs`AU~oGiCZo0d{}Fn1LTpNwlsrZg4#zxB(z zOe0!;MLqhtK##kPlPW;logzl;6!2wZwjvK`zx(2T{W2S4JYHmySZiPPBESn@fM^jz za&R#;y7zIr{!3TIi^L`wV2+*oLP+5ZhyWn?L3!Mvqs)w;N8kZJo(8|T{L(rX&gfWH zcVUt5>s}RCvi%&dwvK|q;RRayKkx|y-|&|@@`_3$VDaVg@L$Bk^j5WVWZ8(y$s1Yf ztHmfLxI@k4=T(evre+4u+rmBKbjW;WSN&*O(d{ckoH?|N3k|O+7+x}$E^^UP?hT-X zFS0I#exix(K;G!pZds12R#)1BC0T`s2*sei*}`EK391e|&9%;L-;&2x^$r7nDP?+q z(Oh^}!Xyj(GMuugAF%3Yl1Y9h?a2PK(e*BhCRWJ|9zIehAqah6@N!<@+t9~Ii_6ZnNlwu<~N{9+v?hE#fp zXTZ$02axyeRVlYwx5YTfg!kZ;?d#t)ayS#RB^=c)Ba)VQX1={jspmDaq{#4+rFPo1 zhk)LiuPH2w_P0Kbzox#E2y9y$mjg@G*a$rew5TY_`Ai{enG%X9`Osung&I2T06)`S zUc^k?%I-F^yN32(d^_zUS?IzBndI}N>Yamw=9I2_bYFibBZa|qPY+vs+L%E=S9dci z6$@k;Q5I#9nSdKaBI#gX@sP2~(%(T!D(FPn*xX}w$W$_10Z%X;&`y#`EEigItQaS8K*6$iKU{=)NaFXNiAZig_@c%3GZbYp z<04KROkPZK!*upbM?M>YVk^}#DQb#5uqZAruH^f7y6->5_q*<5LrKFTEK_wKmRLBh zApxuO^lBBij0lC8)7YkccL$scu^BZq z4Y7jm=!xV(OSENJL%!vT*r6(<8?@urV(U*$cJfr(-1%b$aD5Z7h)f%64SEn{1NacC+WPF9GxH^DU#b zi3TbV*_-I_DFyB}t7r(gameMPfe4KF>ewE}f-r}$_}!TVHy}Md4GOW22C(?M_RjBK zqZ#skwaFZ1_brSB6sqq?)L2224LEM zp5?>p7(0GAi43fO$)V>(p1nBiBqn4VNnd=wid(6xp=@HS%@WxFnOtER5$?sJa*Km=b0V>VU8>#A z5jN{)MK6(B$YJ(yGIuzT`_g`-TVkLe=;NeGp93zr*f#H0=I$q1mK=KxDm3f}bi>EJ zPb{im?Hu7210+)_M#rqK9(PCH21pLafsxov#q_{Pp#w223ZF%cw;1}3G2u=9Wlw_{= zC+sR`Lj8_qbf>?80L2=XaE3M00$`R80%;;J{LhTeX_-}sIhOfH6;w~uB>4ECg;M9H zx%*6gm)~=NK-k>Z1T*Od^*=uk2M^bt4Ss5#bb}|cdFMRG5b(WVbBC75=HY0x0S8cZ zC|Hz&a+C_m93aKy%Bpv5go4N_EYUX9p6d@TL~H`Fx7UXi*74C8`?xqfCp_0gyn@n5 zS$MW;Nr%4Z*=XywqA7aNcFEah`eOeFSKde?c_V)N0y}XE1#&J ziS?h^KkqK9l9|pXh|-1HT|a-po|=-5uk2rY1P!70x(P?ooMJpkl|4jvt*kaj$=`}7 zh%;d9+#%MAe%4T-v0(HL31yHcKu}#6&;=?mt#Xr^K|lkt=C$=nBr!A9TNP-Vlj^o3 zcUj%>nrkg~�Kgp@P6=bYknpkJrCT)wb8TR7P%DAp|k{8WTVya0uIMD*CMYM|w9N z*?-NEZ{LMdaDz!^`o}dr>m4#huz$*zYsny3qx~#X$BJj*DbvRVksWU+L1N# zNb@k7-Xo3H3SQ9ZTV?&97Dr298e^g_INQ^!S%s<|h}AO{ZjT?4=`9{{-bOrc)BqJ- z<(f){YXFL2L}d_%Ge++YR}s@h8o8wll+QVn>ZHw^8JYl@kpLVd;l(a|eQN<9J&STp7_L86Emdm^=P zt2tP0TzcC@dkuLefaPvZdL=Sg*T-_zcVDINp0p;|w(PUw__Phs;!MB&WdOy;r>=fB z3IYmVh{wn9&|#2v+Ix z4jWf)aQhEBI06qgik)!_hsJ?eGSU&a9uHCv)I-PcRDsc81?hzOKKmr1FAZzE&gP=E ze%AzwI@gVBiyU$O=X*=br4KJiYAFSgIYnq?VP>4c(h+$SGg zRj2{1IKrx|zwW6o82L?)v#q^ByrV5}-(hS4)zMQ6X2B9ti3Px!?FlR4sn>Vz3$4S~ zbLv}os3|~F{~D|A3zAF_JoC(*iC&!W$AXm+F5vrBELti}3}ODH7^D;&eZNfdycu10 zW@f66A13(|nC-V3j0V<3{jmG>HR=IjSLV-1{aa#%sJMVJ)0wk#?cPJhlO5Pa)fF^w z=umMue_2Z8;Gu$0nk%tp0u`e6%KV9L+VBnc{Ape9+_(lb2vjg&`-ZxHm)Sy*-lTI^ z*TJrNcP;Rk{D3@qjaSFaD1q76H3Ru6I(TLaJV58gTzhjAOnJ*9CrM>IS+6~_(yJIB z!TFc49(DaC?$y6u2NPvoZl#Wa;O{W)S<3)Oh5GZ0V|l!H07a5$a0;-3dXAA0#<+Wz zO?GdfLoH&U*0j!_JjepKBww0uD;?^>`>*!+ zU5)S6y+z@|_S=VH7fhcvN_NkuOZQ2;TVgbZJBaR4qnyOTX6cSl_c8^Cbw@YIQuP)I z0Dmlb8L(AG7zgDAP}tx$0V&-f zQ;@>AKt^ZW=P)JEpzo#)hNE2^5be0I%PL*~BD5c3N9hY?Vml17SVquxE35_RSB2Q< zXTQA@q_kn6h0Dg}*c07tU&}?hPd5mbCZC0W(kFo&SAhZ2W_n@q|G*);83oFofdQ}= zX9e#dLP~d=7{B>At6h!powZdS2RWralvZJ-3Byoxn#W#2fEH-Self5>@W&Ou`1o|yUj!zW5j&i1KORH7zRT! zx0>)+ep`+F(Trq`9$5tIVe!zLknK&0|C2>kD@OPCqJ+*eG0RDr-z#U#)pGP>++Yze zhxdGl6nnqwz#&rO|L+7v@pwNLRVqG0U-cS~ZJG!+-WJm&85q?ZU62my=YS=I%S zS}`VB!gHtfn(N=JlMHFeENmnPdhLFJ=s)PTB3V<*l3l@bp=w6?bXrDC@vvmqxBgc( z)$qZ4S~0xQIeh0b6<1zMfZ|!p<7yjBSyGFxg*w=0tMUJ00_sgOTC4g^_*`NZnd6Mg zd3#)Gi_tF|J@myTR3%$?iU{Xe0NOGU#tuw)&3zpV~{sXmz3zNhL8nG9&0Eyg` z5m50%%VS1_Efb;@4H6e?Oqf&3$g!r`aP>M>#5NG>LVyThu*^UnX~=IMRjau-jqv6w z(o}mh5S~Hpow%Paqcols#jtu}OgDp0WfLNKqyy|l7Q=|yQ=?Rx501(X6Ner93RD)c z)Sx0&a+QSPWhB;0`kV>Q8xRm9&0U(Y5R3!;N(lDbI~ug$Mn2$57_!*Q_W>GryP%@W zg#-DqCqD=<0NZ38s5xu|@-eQl<8ohdLGC)>t!@ISL=Yhpm_+d-3=K^XHh}h!pOr1DSw$}EWu2`H;~Fk*go>LYO0VN>E=1<1^WRRqT2 zA>;CiqpZQw<0a+5QrP7iYpO<$Wx7t4!KpYJ%fj;-MHXXvgy*&1K;|*F+VBq4kzcDJ z_q24XM+30;6puNH5Ib{WwXUOI8gy&gcyQ#pX6d#Y@l;oB|Ix7(a0t8=nG{THhQVcW zJUWHR3|zV#EO}&{;|@qR(Y`6OMRRQJ!^|{EgJ&LR`{DT{!KD~49x1G(!;_0qfQd^m z$(-^cQx4TFw~0;G(Ap_egL70^ITmz+Na&@zE(NGryReMy;_kAK8dxq*(|J#g+CnHZ z7b5=n8D;|joFjwVZlvDU**D-LV4!Z;6rhBCGY6qLyE*q6Y=p}w2t?-`hFm@8nMbbk zjWbZiVljyv##f5=6CwxX6!@{j+r2BKI&jh3%?o=#U?37jmjiJPs2T`@#iqBR!%{bP z4eI|&b~K1}H3zKp2-w{#&UFsP#cfYqdUdR(PFL{Z&>K1y!bD_^$W-)%$o-U45zB+y2*(@bb#sdA zw@IE1jSSLPY=#QBuc&fYTnLu6kOYhuz~&Y=fEm|D7hy1`8Ksv;J}VQz;?){slF4em ze6ubTOW-Os$(_9b7|ldjtY0S+S-3YT%!~GuFP?*k@y^NcqGX_>>QtKoO^nq}CWjkI zWs2X}eG!V$n&4-cF*3247qtrL)Ph$x=Ii!uSxZA=0+z<8LG~a2!!i%h&jN6{FON4La~^R$aP30#RT< zkLymZGdAJM|80m8`Kk$1*GBYzM^gZ6~iB3uk6F0t9GB}t7wS?Zi|xNBfN>^-%q(GkOOs_4 zKOs#kKK>8R9EMKkQ-=gLSp6zbuNID`&E|hQKn%NHgZYuO9AG9HG>&h4`%5y{k$Dq` zxl^#q_04~4M(zO}>aX3yd(ru@U9fT=TAHqZC_<-57{$UCSuH38s9N$-lIqwH1=B*s z-dFs%u~@Yd?gxbNzwKu_A0|QX!?LWH5IyRcW8-bK;+l|?XBE+94F`5(t)V51`_K#f zVcaN*{SZ32x%&#urq443R9TQEhX@FD!-LB;w4$o zs6|eb>!=RHAPMY375qaaEuf&nBiPCvAcyaOB`KgZhUZn81T?BrG?-a69W1(-X+fe- zM_1H~q@PT9CxXg($x;hkm1Sw44M01A(8aMMqxBwapv#I{i!sLE-^kv=fT;W-FAB*SbosHO$5#qpAq7t*;u=_L~fe}W+~#bj%zUrSIa z*&6GAPp$?&t%t?hmJok5y-g`-4m-Ij3n8bSPhOPN)A1r0Ye*@j!Te61ixi#?$P^~e zZxb_hYrkkZsj4UQ%cbU9%#ByB-hfQ_;|G%7v7SDU66L}9!9a`^DjO=jf@Dbl)9 z-=(Uurs#Ys3os~eC{?4QP>|)NP|X~Ch+*LjA;J35WJgM0K_R-o>*p;`=D0lvTcJ_} zPYpc;4sKbZbHvdxx_B0V9MG%OH6zj#nrcVG&N3JznUgUa!Y)sKH>4 z|3k06_7cgw$2wff-(M)%6RXGMm$7quW?joOdH3{b)TnmtsSL?Iusngrz#*BUTSbM+ zWk@M6p6;th_^O}{t(-%l9gX8a#Z}d!)oF^Mh^mMHB*RV`No8tTFVZ zPy&qP>FR1`mPn_#D3Lpsb#EnDx!sS$K6tWh|55J>FHJkd6oC2lqh`XDXvE!Ab|3?- zU1gI~O^;o+1YNki}_Q4Gv!d;ZW?MQ_)e5E#_HYtt+ z+KPxgfJ@Tj(TbfSd0X!$5sYt8;Di~ou6TO0Xl`}1=|8YHoD7e!(D}dXe%$@ikj86X zLe3eRSzUZUssYh&-%hv|4n=3loxXT0>4R7Wssu*ASikyhVVKRef539@X!i)L;pgeP zN3?;3-iXcMfDqP;1@f4xJY&XXP-2Yxmlvq*(7sY5Ee1QcqtR#5D@~nZU-_)5b)b@* z^3pEE_nS3p;b?TabfG$`N_rU(Kb)4LWs>mOH-_vBe5j7OynrGKlHfWIj0Pb^jVLVw z<_ZU9&7%dMF%eScQg@h zavBi-H9f;L18FFoKYBtpd6Jd)gJg} z=7892xV(cHBxTf?C39(mbg+NcX@>=>|1n&Ys!>cwl%A`#S{s{eKzo$F8*qj>M4B9RtKw}~J$3kG9F-+QbM{|JQy)V!R(iq6{qVR-aikHYO zeP1xy_9g!$9Y>9kT%odkTMvJ;;iB@$eldr)q~xysN$-Kxx@ksW`2(xkbG72z@Z#jx z53DdoIw6UUck7Y-Zn)9|n5&a&JO_XntP^w5KbL^T@CT6CObFQA9kuX`1q9R@ zky80xVIiul{){WZv>^iH?N_jv)ObOuYGc_lt)GvRpZp|2%N#Au|KeT!Gn-dSk&gQs5_}i$;~MUB&q2Tp zppBmnb>6T-pq1EqUHuQM{QvwIQn)if%~?T|ragF~?VYUO146_E!ds5gKOcBc2mm#` zelIqKeU$ZkL|FjCp)mor^;Q$jjej1^UVjBcG7&~3fg=%8Jw;BJ+B=1=N3d-+4HtSFh+BtW5GNSkY9>+A+ zU!JG00aE@plEljhZVUiAir8_`Ww-etMf*dS-gXVQJ4;T6xV!%NLRK}mx04a$DK07s zo*`j>geP{sV(HY+FYI*BL40fH!-)={bym>?Z(Nu3wc)q4QIz|j+9CR5=fB>Y?B3Us6?n6GoGcV0(bF<-4_ zKi3@|BfW5_*164u-@?sMD)?`(u$G-~Je}uz|u6 zvrL{aU}gC8qdXUWY)qibylAEW_J`u(>7s@C-MvQM5<00tC#vaM71bg~F5U>7@?75m zBN^-~&>1pN!Nz=%4KVeO|o7r^uIm7Bx@o z&*90MR#*XgUvb!r0tA0O&c6;b#Of}Ml&AZXfp`zPXma9Z!2%6LdErMv6e%-sC6Mo~ zYfmUmvW8jAzfdrrIcM?@pwi zZ-4Ak0(*fQzIzf>^H~+7?t@0~&3GW9xRB-gx|T4geaN0s2!c^`q|c}mYm($6!6G>5 zcNYyOQ_`@*s>Sx**PReT`s%(m4AnNqUBa1=ys8*d>=)lJa8WP-Kn7?AV5 zsv>G*c`P+&UT|GM;Njyi#KGvF&dOY{uN83+A0U!eVq19`dsBi)aE$)m9KLUKB=}Rn z9ADgJCz-vcBr_EEE%}i_T#rU{`36rNYnIPHHHwsyn=?Pg0f*vq%R-*zhHo)Z3-*rR zP$ILa1X7_9X?OhB*%HXayZ!?~?EXmH`r#b84ZsFua|?Z zpU4gS=sMw=K>&mN(j>1n^OtYj#0Sd%=UgAj4Rug9P;d=0 zZ)kFJ*Pz60!hraPW?=kxxWvhl&-fH6P(gR90R%vR6Q)7vfq)pj!po=zZ!3W;FRCRp zVX`7_38=&8+NI;U6QeadjJLRhhci_BaPLB+?u3;Q$fn8b<@D6RQ^v|bVCQcfwr6n# zlT9mjng~YnZ)oI>;!(iDU6J|5oUJXQE0Vy?1}#>qQ;LR5i_(t%QSh&T+VEgl;tweF zfgdP#a&i`N#0fFADGISc!~OGOjl_$zj0-IgM16DnJ1fC)CM6rcWTo+NqSP)CdB=pEQ(1~FX~+z+-mUW)amx`j)=JGh4un$gVHe+{3A>)T(uu+DNj zf2zIT|JDA&(g|ipc#C>uY(eBL#{kn99y9rg%{|BEABb39q)f|7d(+?d+E3juXDNsk zL;E8Uf=`0!IP&M1VOxyGBr_oM`2y3xyCG!+AQjYT%{N!aZfp9tg1=yj3BFfsc*o54 zd4w(k@irOPsoSo(=9(jCU*j@mW-#5cxgU`G8m;Jw6x#;vmt5s9=*HjKQ#?RE02f!| z`=h#VsC9*%51FZryiKtYNOu&W&(sln%~CQh;9)#_fQ@B&U5SgIX?(JeE*~P|q7$9F z)v=L@@6?oxhFr6*A|7TO0I)OA zDC1xYjh;zSjOg8w0hYxz!8 z;hUExTI-CSElu|Qg%bAB9aajTQg!zCx67t>r=tEeE>H;gfgmuFGE>)(WriuU4Rd+kNl z&9PXA3Ls-z)miyLtiD-0R2WZMK2#t(nzI1)(Fsq8u=lV38=L-)Tz7Hrc3!92%OlDc z^QHc*mKyZi_L&BWr&$*T26U*v1Rk{CL8g2Prk!4fLHq8qy(5-?FO&a_BVn`6{)>Az zp?(0BFL=T$EpAUOE0A#W3v5BrS{d#d$>g6C41UO=j5LnKg7-FOMOt^Ta#`y86!HJa z0O%2hes~hFk`juRBQ_XFl=IlyRRJ?4fSLGnTIR~V;K`tG2%l!oQij$RBXUd(bzi}0 zN*p`7ub@buMG2LhbL}R~M|hwelA#JJ^7VRaE8ca(HBSherT|&`$+Ov+yW;U9uyD{x zYD5OkDkj=C`3~9H#s0d;KM(iMfs-dsBJwF>n798IFCOtv>z~8Z+q)4(Fw7#6vPVw< z<56~ltZWKwjjfIoItqj&FyPEwzl8-K#^xF>hC-CW_ktW8mpwRbDy|^`%aDLUIfMi} z`<4ennC_VrdSM7@_9w9deYb?7SQt?yE~yQS(!wtkJ~@UhyGXH>KHq3cs8+Q%o;Ga= zA6>gg4JUuhH@JsdR%Idf1u{}wv|*59k8-M$#+uV)4Tc^_+5FUT(lf^^K}BK_-5-#n zmQ4Bd>Fet6=nAD<@neQVSQb12iD{@CPFrfaP- zU||+^384H9sWPE29psUgNpA_LhGUdBaNg^)LE&tRBOiAs_PA`S_MceJgSt*~^ZWKa zWHmg99o+yMPbq@?ikK7P10uU4j!`Bz=}_@FZjR0>oUOeu6AXGteYBg`8Awt8K}|Hk zv5wB;65{(0cQG~fr*Od*^|w|niWdwY0FlGU96*G#Ggw!WS(b^Gy_P}>rS%y0OnOG& z0RnE3h(-!^N>nQpKVAkDe8M5q5(hM3PoYZSMEv{`z6!rHkT3Y%FYhGHajZhZ(4Vv9 zw`%nKl08c;PNvM2A-w^Ii5abK_My?G_%`mfOKq>!<;OV{NE_aml# z?tuFKr6Zlg8W;8ie_OOewN!{yRT+=Rp#pTI1=4Q<2JQFRDRHPcdvrmpuG7SR=tN@p zgcyul7^}-;CSP8nnC%r^&5ypMaBDB?N_7<%-`vyhqp}k(-mcfP+*Md`H@4)xxd#wA zeGeDgn=zt#qf2>?1soSWaI50Aws(yF#f~)l<6C6j_$`K(u;v}`RIvAtjlMg`x4G6*vNpl_=P8OlMl#vohR2)Lm*z(x3&P@r4Q6dI>Cq68x!rM zKDC%5LZX@UoDLc~PK-zvOFv+EU`Sx%0_*2cOdp zREuNrgE(PTWyFGB{i?cTk4K4;+3I0t9FKAx z_7(Tf%yph6Sv&Yb0_1RjpocGA#ecudBA*FgoqUT49!A9sQY&-W)}d-cj}plrvbTvw zlk8a1pJWWZ6X3o>*GtOC9j`6-T=SoV5jgKFrMRAKFE&KUw}l3mf*p1Iuxs3R83;M7 zhl(qJR$A+EN)mJBn$0ijuFHPE`d@bTs}j+VqOwD)O#<-vritMsbAnXskBCJ? zFSFj6-$J+IG6KCh);e){wV3pPd!P8Hf6O9+Q8^X?gK;1%vN4>K0SWSjbc;bh zw3B3reWBy8^h8eGMOr>6sUHwz z1xfCY(0Dao$`sp0kx=uCnL93&9cj|>>Sl4`ddB;{FEMMB7H!=pA$*MuX`i3KA zQ*4l&&kNky!o+5PV#N7l`S9m{m0eZo@5XDsyR9q+p7WiWJY2^dd@K+iv}Ye{zsSD! z&O^?fFopO`QwZ1H@rH1LS-1Vip5*iT>d_~86El?MnylaxZj zjuL_bP{4JTlaC!fE`0x)$FQKlaBX&EliY`X+xs7XM0wx%Hg~E$-E))3|Jj~<+{*Ng zk4J$4o*Qs?u=DQuEd5=DiwF));!E*_4fI3H?~e2ciY1@0mu_0!B!fV@*-?Yv<#Q!X zUR=?*1J4yvfpJ-|5NLtX%ndj~K2i$y>j@wS^V%C2jWy_L0#G3?Kcf%qnybG`V?j)j z`-FK@pI!>u0B^Y;CZ}qN`=x0K6VGLs6Gj-oejvkp^-HENvA(i_By)NW9iO;zT8fgG z-VWw+g;U~pgRr^+Vx^_M^5UvxJk&W?!#x)aRKLv5IkS z4gB-lwP9sh(c(D*l;k{uJw@zv|xA< z|3_BF9fQo!v+w3S{*};q~7UlxnGfIwE54>8EX80*?adu zOUtt0`+1(V_TFpnwbsnDhXDp=lxHn>&K{isOaubdd3Fp3LB-V6GR4D}v^SbV%MM^(=(I`40;wHe6hwW((QIUl8gY}{Xn<-?qW%IYA4-kNu5Mx(uEr!gOj@Zh4y5mBmWiiZ z`54#Js**|R*Z2J9ltYlSwd;3%9?d3YJ`7@jC=%da?W)iry;Lw(7OGo+`1_qkTyc-a zP$=+x)i-^LWDeY&s)3)m6ZT+`@G}6G*NYvBzHP}#9)qE`=zxbGxSb#IWFp(takt*T z)K9nWDwgOBE0jE~g~%qXSS!Fv!`k|c;FPqHF&*>KvXoIC^?ojyJO0vPOC@1<`IEsu zSe9N3!7^5?4y7)qq zyYl);J#AnsZo%yV>{tWQH%^Cx^`DY$qZ2y3#mRW!o!(GfG{+WzW?S~cKtUAqsCFFW zDhhg2!kr8xjA9d{++lSFLhw0K3gcFw+Om2dfZ>r}XW=HO8*U)mkT9`YBpuG-1aZFhB^HNid2w3}X_kChU#Wcv`;&z>;k>Uw0PhJO;}}s0kJ>S%H7cZkrW0 zHzGcAqA=|lnc^E10WAB9c|A6CxerDYc3Waaw60qxSdz5Pvy|DKfidF8)n|q$@)B=rbWPc zsT29l{alO*AGugk{=L|8OOagEFF{&Je@TeJxR3~0{r8znru;iA(@=c>np>BP??;z% zVdRE$-Of9~k8cFtWZQ-s>h&(}v?OO9$cr&pdJ$B3f!XoyB?lRFcR}-G>CQ_*nlY-~ zTyLnA2lTIvT@1@&MNr7cE)MD_=(Y|2apDBtUMgqxfwMTb15841Lz0s4+ye>c`}b1At!ND6OdY{3ZX zg%7vH_AIcyCN^GWTOogF-}py)cb85v%81r|lGZ=~Y$_PSUQH3Ly%*r(jOA%~3Zpmx z99Rv8Qyo@b3*U;DiMq3f^2i89vrpoN9Jz&0({ym6hdy?zvOmIlai4tEpvAbinC>l# z@dyX5z@%u9jcp#kVAQBljS|2PB%mAs7UkVh zuuL#|bwLhIBX?tihaZ?$Dyq>Eb~$sfflj7mU)T-Rng})s${E-+GXyn`3~%GqFofoX z!`$EnV{t%fhz={&fkQU3*S#4U50Mem2tjq$!8CXhf^?d}uzhH_PDkSdZUF`&@?->2 zZRpW*pHR+G=x3y-jH$KrZl0>W+E%p=hK&g}R)xjGyJv>D>{L;#|IG3R_>F=HW*JLG z&X}*z3y)~q3u<(KTuD?e2%2RVDVy-?#k(;n_3=>Al{c0EeiUgYpqK7(7Fl37;AdnT zfJrkc*dw_7DLqfXH9MDQt^7;Oi6mV;Yw{hMi42xrENKwE3nB`Eg0E z>LC97Q`~d}s~13crU&l$9zG6gEcV=<=_K$6w}E~LcbS9ZVzpnBG^C<}zk{>%$FZz*+55G9ImSHo_3bc`L2D(2Jwi4+L&3 zrtw0MdeL##Lf}bH7ah<7E_XGyW%pus3oXS17rbfRPLJUmNr$?lBV=tgb^;Niw|n}$ z(M^7J(y#l|uO7bulNuEjw)U-f5&{bpzV7C!IU?S$i+4>~4X$~yX%q;rX!Jn(WurWK zL&3er=SBLph!Xtn$@)Ir!w59fDq7L*el{1SQL@FxoD<9nyLK5k(Di~3>f=b?$dqB| z8=)uAd}uTwfzgmXnwAGbov$P1CB3Gi7wXAbReLAu&NT9YLdidBhtmimbpvS>tup8!^ibJ-oQK;sYD7C!j-IE+O{_64RIH)fiOQ4cXTlw- z9125{JT1LpHK5S{jL(SQY})i9%2-h>;MNKK0^?UJ_J8`Ie}$$UFi~Pb`s27u;#|#mMLK|nSTGhvrFHEdJU7)m%69AT#a z^83cLvXkI-fZ<13W;;ZKE&o=7G9K|@O`hQTj6jSBI6dyv{L%bC&}~9NGU1AKgEo%K zXHU&ZO{%$9KN@60%Gelac3gh)srl@xmqZ7+-b_|!-QO+n59gP)wo7|B@pka!*i(&0 z%z+GCCf_1?vnG`X<58D;$(e;mFJt&a--W(|;e%KlN<=co_t#K~(_a>mV_G~@S7#6# zjOPp=0E$1@g?}(+8cjrpcRUTCzmWwg9_yzc8cwenC6g}U>rsp zae3mvJdYR;UYi)hq;8A)a*c1@X?n*x^-+{M)7cO#+D(mrZjK6fK+`VF8iN~_1`AA_XR1L417^B~Oj8Vbdge!xMCDf^v#b0L z78LsA2RhyVcx};o^sb4^ic26-(l^7s=@Ex|g9gEvE?^s$v^m6PdzSV#tPsgSn_o{E zKFX7zmSiAKMK*sJRAgYcA2eMf74@JOE2o*(jg8=qFD zz7N~+QT%=Y`yRW#zMJco17!r2CKR}h%Z8V!Z5xdo!{_IDsbYC@dKw7wf#duC>>vJ} z|Nq}g2pENh*{%8gd~bep{^9(y`BTn*;*TBwM^NC{rs3wB5EipZmEBUiy+3 zzv%oIzTjt`KlRf;^}J_4>nF}T_uTB9bIv?t$LT*lS8spvlTJJ34kdz%lF}6>-qG`XLORZaVvc$2kZ4TB&_d!`k?1* z7Q=bLMqppjasdzEGEC*udGCgUUXZx;5Jmv_0{b7mJxxz7yWv$oJc@d(=k|w{18WAq z+|1G(Mz8bIABlD_0md7f&p1(?S2rI}AFw%zGUiG#6 zo6s7QtKa($c@-A$z2uvReXjC1!JD zS<^9A`@LA;F@9yxboo!E+j)`Cxca zKa)&}X(l3m{Ne61hA^75-tQgy5#Yw5FM-%R8-{b%``tr7iy|DeV!M?7Dnzof;nh#+ z?^AvF9C;7fA%{sHSKNv|HtI!N0QLWrjD68I2paWzA3M0uNboHG8M8)Ngu+g?PMx0L zI2@1GC_pfXE99lHG&t}OByACa1YlH!1~|oZKsEMHLap znzIBgOJmPzXcE6Z=uY57+68{~nwbXB$X)ks#&R5g01-uo?^ z)shhcp~%moahLC%>ff9v;_W2PV;0PH;ZHUaJ0mV1lT6jQeM&Jkb_Rk;glO&W0eTe{kjopAU9R7iwz18Livus!rx~6?88j^>f4}Z zU!1mu`qG{H*7+7t=k3Yn?bfQdS&j5|YxK77_%;aHIsx)*^XHNw?EZw})z9wV$ZA%b zZw6NQRr+U8l>3HB*NT6>7jA2_#z;a}nt;$u+=;;3{l)&Yv>lLqGoFg%p4CsVo0{B- zrROLoX=|J3eh{bnAa{-+D{$k@#_4zr4~cs}Q}dY2qu(PgglK`oq`|RCSE~6nK$Mnb z<$?sI4p%NmqEe2EW;u3R`TB!|UaMG{{)c4;_cBTTcyCAlL+~r83&!(7+L!dTocBtT zA+P2F_ijF9->E12J{S>JqJi(WJfdhb^vuWXNSjNVT#LgkEU%Q~8QNUUUOCvT{lm9C zeB;&G^9XP}vuZ71D$UMi&m6X?8Zx(tc~H$>%9ZWU*;tW$vT?YDi_g>cMgK5i?CaT? zfIoZY&i$9BmPVHqY@4esSEZ|byLvF|le}Eb#>Kf^v{h}n^r{$z91dlI)mnipX-z&n zf=gNkb`Wxacld?Fh3F|$y$?lV#xijxpEB7L=33`ujhDa4ODWn=0^Gb|*loR=T#_df8D`_n=GAp+P2BpZn}?^H*WAE7S$HF@v7qj!44 z(6tgijrj6#O^|$pQNy*VIrw}Ad5!xqtCiT^mfd{)RDWl*#`--oU3<6JDByf}jEskF z;=(I?@1{#N9x8UmW-ch5RNsq+#1Um6(6^s;j-zm`dLNKtt9;}*Q_;Jo5&h&Sc1)cr zG&B#^kU+vue7_ebj{PT@uFOBm%+l=#&o-U-SkBBA2MEBHZ)=#1K#3c=J!Lg-n-clr zCE)*uR1MIb)bO(i2e9w9{@;3oQkChcdTZLK70whI&tZ1NY=gbL)UCUp+PL$g;Z$8* z#7ukD8g}?tgtTRtG}^@(;?yHKvs~G9-ZV|1%0VGfqUqpxi!{wvBATWd-i;0VW%&n_ z04@Ksr1Eh6#jRWzBq#0%5R)#I#iXBgU=V-O+!4-ouG_c>g98AK?57 zBd~tu2wT}C&=O(<9y#|yMWCr58c3QXJmu<_WYd5nHI$&S%y@UqM=|ukD5e^*zvc}3 z(q)S9kEmFbX-6|xsS1UB_QE0OlOE21+5Js4Tn*?CD9!-*v=TxGT7JKg)`vGHgiAlN zCICQIO>$Oz`h-z(!u#R9n~06JQa}WvT=wV&0CK!YnQrXNJrZF#L_{{3I`3l{4p)XW zCdHBsqzx6l^n^)xWl%1yC{KK(P7w z$6*w2=BzzBUdg7C#%MPW%fpt4Kh|o+aAJ8GQLO4O7)a^8XEfflt|JbSRDxyjCKCEa zxnuw6rO4R{nn%K{nj$4;P>RKB&02i)E@1!NOk!l=Dm>ur2%Oe(BlCLn&yP7sDL*71$Hl zBI{Z9r27Q-;JNU~x97cA$|K|9nBem2EjKWlzz7nt>MU2!m_s}kSCWqK*3q>U&Gp}! z!!HE)`7!&^L{Tx`!V1F&;>&W6Hh%s=FC?3w<93mt^g)j=Q1^CZ$9Cqx*C@=n$tqiyhotTn`?xfm* z$^(|A*>NmS%?2>4@?S~>>rHvQU8wK(sd7Qel1VOxonWF@x-Tc1=4YRmSZX3RdqzNu z1#|YGUDN6}X;qhQSF~rIp~)-nz~60zec1YCo<#86bYi3-Fx592MZqU57bhKsd*D30 z0^c>)((pkf%7Q996p^v`mh1ssBza1KJM?`A5_p`~*S-_T#0eq6hCab)-i$zYwJ!|c zeopldfWhv8FoaWLJTwO&Bw|8mpmsZIA4QHkq=8u+Qk}P;0V`2ASzn5J%o8ZacR428 zU!Tl-FIY)%yaXz-Ux;z>8Aa;K`;a26HYbGcy8<0FRPN1lt*dWVv}hAF<@v#yXnQGl z%C_UmV(M)%@4acXT!G{Nm)!{yx~#(xflbMY!jxO4q|gmV>@oTnqA|%48J%1;nVMz1 z=5LDiE4Tp`de~`Hzkb7LgoozP(zt=A-u}MRu z>VvYLB&kd!qz;iDjd24cRLdYC5<>^K0;+*3(9tkvC|=)LVUq2_uNCW66>3iA-{}rPHVIu-zf-d3J8#R$*Q8KX zllUQcj>2x<8`J8Vk*<@*W^|?sVpSQ`1p&G<7zIr{INYYj*}s z0t7}CnUN2KnSpxob6k6fECr$w+J`xFd{3-oa)~DDhcI3jXr^pYkf^89*(}0KPNn6O zSCM=MVNjzd8*F~)DowdM()!TtoJ4Dbbec^)!1cc)opB^ppdh5vvj|IN1t=G`pBDC7 z7Z$eXJy#Wj6Bkp_e?idy!OJY z5b(Xw%MhFgaaZ6D6k|CWCXz_&`~(ymEB^xgo25S~0qP9sFx}UNv{{j-)qAbjY}u!4 zf|VQ$DuoF2sN^o>FM_J+Tz+sy|MKOz)#EIQ^Wlc?0b2*wfY~Hn86Lyika$2t=YDWCHnV{4R2!UkB%w9RA!6MvsgUD2gqT& z5L;B}i4#Xw3M^{obsZ<5-IhI8#uY$5fiQxJ@9C^Ere(j@h{oihn!jeIMmF`OGbA7i3a3C&dn15VG-t~dil8O>_5fT_Vhjn8H>#PL24 zitOm`$wh3Zm7pp$*0X5~6&Yf;n_97qY2|0g9ANIPU;{DFkPMG<$PdA;00kRiokP3) z9^3YWOYl}E(pEwu7nxk&rEm6Q8F#zlmD1`+frixwa2{C+ta&M8W8{TGX5+RUS7#?IT zUnaD<94m2hA_BLn1=Na~DY2C3=*Z_{yK9W#cD;k%_P`ZL&{T+v!xh#P=_L#+W;7bI z9SDk(V;R$%cLzl8;V)B6Q^5?kSspwubwJ}$f7#c;(S@oV6k*#8{6)$>n})Y(4jSss%{l5Q0`7VJ@~84|O}cUrF?kke1e zNN|XS7j4DS$i5Mus}^Tr;QF^pLMSwIr4RHjbw#-zVc4HrQ^3f111qiDV%Y_|Kzh9yd@b$T#+Qm(*d2s2YLj(vc6 z?^piIhls)X&@U(2fO-H_f-JKAfvz{Q6>5izGheqj#hTq8ytIE|iuae= zjyw*s7w>1Ho$AOqY=Fwzrp`Fg179K?)6|(SrrE}=DsDaaJ#UdW)7%QGVtYlPEyZPQ zWPSRTfcBZ&e1W|`4fZmn4ICyBc%d}(1@K#}XTEg)-h0gjP0cfvcF5$&-tDu{x08gu zEfe3z>pSt{u>rST{{@_dV2^5j*_1?<9cjIo>oZdDZ92_o`& znDmH<0&}oe>`16FMsbyI(1%2lS&khAs4DO;yJGX4 zo$h6(h|+Wbt$b7&5-=D|_!S8EsyGuKc%%s9+A*^ubqiY{JoJKCvxH6uoD~H7sa^u* zfxP@QYE5)C#-?0ai-jX%yD*4q8nFPeafLwv#!7veRFg%#D*IiVcK7#FQ2 z6>wej@evS#o$Gggv*Rd6$38EXiMdaW)<-mL%OzsxS>Owpg+4w7<-ZMG5+N^Uy0SKX z5K_Q6xOk(wkF*mjEZ-ICJ8+)aCS$~z;gv*p6Pyqdbu94%*=F-z)* zCm2y}+rlv*D#?}Tl!0lOHAbZ z@#AB%c}B0=jHCr&=onH-z68kaGhra!ke6SYPPamMgib@TT(7RsAw*cI=syz`A-Aa& zf$)OipmRiVw8XcWc_W-;l+zYYP!*={2FvlfSiBaFTK-;-Eq2T5Ko<$3up6llJb^L* za{&A#j(PZPs82kyECPk|3{sMhh)t|&fKFDao8a=lp;#$9)b+31JJR*hW2(oReB7zF znxEXM9;0u$Q~iia!APO#R2x*+S9Vcb#nD#X*npe)+0}ZMPc_z)jws8ovn)QCL?<{D z`n4yL={vZjx0|6HB5Z-ZrP>-sU=z5YNhgkOIyO+>;>&bhxPvHcMZ%`w@vbvGd5i~7 z=UFX#;!o$zsiEOVH3ts}iqPSxNP4d9qAsF_BrbR!z?y~uTP9_UZd&+%F*e5t>zHff zI}(Xq453+8Ge!WyrpoQCcc3HBHFfx6*gQ(nmqjPuV zU}`GNau@O!zsF^gO$2cSLH#IClsIN5j_BF>{hmgLKkipl|576Nq`M>d?y+Hb0?h*n zhc$5p_3UQyWcr*+*bw(~%6>I^5l^5}YAqkduOO;PhscqxHc2@J1D()I{lS0W{U`$z zo|F_8V2?0vCzH#FQXU;UGHC`a)>9|El(K1!Zqf~_W;($JOCpR9@)i_UM>rHDbC?~S z4Gx(|lP|Okhqjy!;qep>or)ru$UK)_OMB8VD1&I%Y%{cx_QrXr$A~CbBO>|>cGCnz zxz#}Zwq>ZpHjIh-ymAH&`zVoy{B`M2u_4+swz-@TG7kj2_~bUuN^y4tE-xkMgOjf| zk1U|o#F!(6(AZG=Z`U*W=EiNF%P)2J2AqqAHlNpj>gu$bq5JNKT;FNd=5zW_Zr(B0 z`Q+3*P-5z8ABW(1pc)d*@kcum7qy}GWplgSWuc26;lK3299aU``WwspCkJfsPQgg> z#5i#xBa7w`Gh$Fb@1RTq?Op0)PwY0kRoa#tLpZW9)bz<12;&^9P>EbC`y?TXEYkQL z2tW=#zngSRr~aJ&D{S0m$*u7`cV(EySou{&!NaWw@tAFRKh(9IuW}8fu`SQYXCASPM40K}{uw zVs1N<8o_pBp7u$9Js2SnQr7(CxP^BmK~Vt_y$MO4r! zyMOjqlk9Mro491Ivy0?%P8U$tu<=%*6aAxz9m@?cwg}|<>jEHf7q=$dDVhd^N zg#>c_TugVwf-Igo!dNMqUl5^37sR*oWj*n2Ln_6`-3uaS;&Z5&oN67QlKF9*4|<(k zCCcC|@xs{n0^Cz(g?kj5%nAu6AW2%lart&=58OPBq$pJfVG1;0Q~Yo$loyp_!+6+N z#qHa?jjW9=STHbEQk-rC>s4^vkp)I&y4Gg74)&hqTX)A>vevul&bMY00MhV>ZeOBR zK#M*Yv+^xkyTHP#3N4ojnz7UetTC}vf36o06s?}GJ1HhSVjyfF^v* z^Pr|v{-)?JBtqFK0Z1a1=mlrW2wkSi`^)}Li9a~uun%D}!ErDNo1EYu3PTkY?#1|s z-+^o>nQ1FT`90hezH31kjbxSyXyaTJtr-%yNK?eggB?YfyZjF%l%Rq&v*ijhNg=~_ z`F$D-vkFb7e1KhEjp@Njmqb5B3oI5J7}&%|@l8H4f7KCl188rJ{@pE;J&8zW9%G1$)PFH{L);lf~B zUipKP?1n8h7*(%8( z#z8HhRnoWNYokOV=9aRB5S)qi>ELJ&=k8X_MggSOUEYUEKJF(r6K(8kz8)|2e9Rw#dlb(A zK`Ckp7v>dGYMI9Vh<9+Vy$^{sxwgBJOW>AwDs&_htecE@Q)&E%UR+=p&479rmL zw>#SiyWP{`AA}kq1AJkorc~w_U1p9ExK=>ZaFCC;@bh6@_n#K-l*klA)Wb}jXuHi0 zbYLT|w=R7^XsIu+(%0qF@vs~%Ci3}a-2l=={qbVnUekYBdBrpZIMq-%KB^0;Niz;I z1)2rt$VbGi+f_&pmIkG_k!4UjqyAXy9F7qvwvUhpt5lp~n%rbhKX?^v0)bXufiR8@ zA1tm_>;v<0JSSj({jE_3Ab>5y_4O*s!IEl=v>UZxEJO5&$xAyoj#~KiyKF&kus~;| zl6gQDww$?FU?P-U&LxxHEY7kB0!I0~=U@|Fiao!6Ig0#-W=C6$@i=Io#=HOQeJuH^a6-u~stQ zDL$!mCM#O&hDbMH37!l&@)b3!{3Fy%$3T9Fv9sR)bBGa9K~m+g8Bu$?19Bk9%McN+ zGaZE1WFb!m5UwajD~@6SMbD#0)~BBTW|U$w>iVT#e+W33=2GY7pB^;Ym-vdZi-S^d z{p?5rL)O9=ea+R3CP8)c{mC1rl3to{ssW7-iN8plxBMJ!jBW-rs%%A6K7#R}L@=}4 z8iPE{_2mu(J85z@6=!A$e4); z2PbcfES+duN9 zqZM+v0m+>$u&w~}T>>WzDDH#=Qz{gq69MLc(_uI$2;O!yP1HR4TZsN3KDOys%%Fc( z;v~ECYUHojoh;`ZdP)QSIn)4Htxu%l0hrNCyX6C9j(s)R$uzY=8D_7%(kSEw)x;C% zR*MhBL;*hEn|P_xwh>m+4GX9xSvsD836%fd?Y%H{Enz6NwzC|2A`tizm6KrfD|+uV z`bae0fMbtQcgG=3ZwCb<5f`t#`aHJaEQ^iDV#y%@U{nReEX;b_4`~(-CzC^fSucWN zO^%vb;IJkWa=VeiBn38b>0yfn#h@lN&fm}}Lh%t0;VYA~a%FF*Bp^NK;UUSp^;cgz zwgSgwq;T{G6fiFYRwcTsW)ObCK^9b^tOrx~w#qMrGTjcv00!2GG=i`2X!w zCmsg^36oxfOypg!f-ptDm6-UbO?u^C{jp-6)QZ$EVFemTv=zfOiq#n9U8IO|as>?) z+>aT4jL&#Iy_kqPUVFH{i>imuyHUnq2n2wL7AyZ*SoIgJRSVqOtm4wBZ74E`yaaLP z-6IqR?aOuq%%Nu*4kOA7D1ZngYxx%amjnr*hQBbaX|tTzzXZq8p<)dk4r5d0h%X!{ z2EVR6(GJs1`JiJ=MYn30IVyMLeyo9LMSkU)QH2tUaPl(Ma4`Bq`GJyO% z5bs|UL8l&qSG3C4+4K1ZO-9LYX-0JT-wr$t`$cK6!}eAbaS+wG4H1M%POoHj3!Jnyx`hRdR4#+4Bw`I5O zpW-|PO4JE!**0+M*5CTlDnfrTCY&!U5aCX6Enhz^1KZmgY+iaVLG_KHAC8utdcujZ zlB919s)7`oQ**`%ALp-b2r$+>1J)?jMnzn9Vo46c=EGA5FAOy6h-O-rcq#$+DcN5# zxL~oT=Az1qr;>~PknA30gmVWniqJ}nz?|}07x#rl!;I88Figrfc=bh6F8-Km?sAN> z(aAxtC)2Ec{doQB@8hWDki_O&>JVsc=soG72()5+!#+GQZ3gdCg~5&`G@@3R+*`98 z>L$}M@lz!ftC5zvB{rvpYgT*F)n?SqdglvuCnC_SAdL5P5gB~zJtHrbRb@H zyBOVW3(?crw;c*0l6KhJ&tkXsm2adh5oYJU!7R&I(3Psu=wv{?Zxx46AU?n4JoxPR zK71HkE_r5TT^QRso^bW}zGs7iTYStwSAD}@Vdfe9(pT^%36}2l)uq!2ML*xH6qLu? zANoWh6^w_D)3x95r}JetrK5T6e+{}>F zqc=OMHiKl?&)kTB=DvY*e3aKm;rH|Dj_}3;l{zOkE)i{XYtza=c2pDFnDqw^%+vV5 z1m7A1iYz}O!mWDkJlTct$&(>15|3~e6q0SczKu)HXCgkDhE3P7aCw>$d=sCF{_|8E;V)8CQ4Irv@^c@LdS2u-Bdj{f73O>br7H5pCEa(J7oXPQDZx$lr*;Jpw{dZiN2 zqpCR-3qfcwK*fvrmlTU&d5-OZ42b+0*5|srqGYbTz*6NI#=h~^R z20r{=%_dq4sE*TJ z8bVc`iv>b%!;u8}a!SR!GIjYh3mGP1fY+sbJM#><405^;smy&8-SP%IUxOM&tUKb} z=zx0W+867i9jo=q_rrQ8O!;(;ov^f_2aiI}UtX6#U!M#>HlSyxK4RF>TVo%ZP_}_Q z%Halj3smJ{KrOPAFK0yHA^Rhl7ME~ZpwvMIfGt3hySid+>ayuiuS=ZrirW=TCg?ri z?GC5|f>(ZiP$pl?x@xL?r}JAMVG3z$s{B_TZ*T%i88zNd!Hs_0CTWe+3{o2snaaq! zY^NcQpa!Xo#C8Vxv4Vg$NY4bq?8MqUxBoQm&h(!4Fcgp$heiR%Gv;;;&_Xm0oF?1q zrYVC8Z@2c%7;Nd8{o~WO@;UuuddD}z2cI6oDCANMlgyu>29h((HG*?MZ=6~UmIcFj zk8=Uu5k--*_3_03hNNIf_UEmd%ehsn|46G2E_FC;jTED_-JI6Y?%bY)b@(*k1QQU8 z=wcHeSKk3Up8-AL$ymK?J772b5-?>LVY2qm3`M5=R@{A?Vp!y+yV40>f}jZ ze!9)C=wL({Dnw!u?Ji_zHDcXYZV(PxAi3(4pyO&EGo>@C16q$ePtI%6Cro&!H#mBn>~pDyt4| zaGFdGpFveTFqOropXE+Q%5fl46G%4D9vboTV@u8n3^OJ=ix>&dKDre=y8P8?<5HDz zsdMOu{0uyC@&N`Kdq5J-GX7wai=ZbHe;+n=%}UjyT}w4-EG_h?+I>sn13ZP8lM}`V z>*`xCA@6OZFt$ri-?7)5xnXo)J>7pq?@W~9&>)(Ek#g->E^SsWdIb$M2$&|^ z3C9CA8ZkVLN-VUz{=Oc@qhT>D>b00W=!|^(4#EC}QQ_6VbZWeSNSy>b{T4K!mr55i zdT#_)!n-M=X^BLWGCYQVCZ)=NA7+uI=G*m=HToUu_(t_KHP4g7te`&dP1AMWML5MN zG5uPNZYH|IFpzPCvJdK?T^%J4JcNP6I&jj$b1ziII|;bQw5Mhyys0j1LSpH`v2rsI$eh z20_MU`6oT-ojsOY;Ybf=DK!lqCT8UGLA6zDfBAl}WghXMqXw@NpKxCRhP03JV;>unU1FHV8?OC1e{U$$>QNDk;`BHm1fzgrlv;T5D zYB#%7kO7&Q#H3KE*D#^zz(N{^vABiUK6_Y@|M^_ji^1`eb`2P}=aV~Z|LS?cuO^MB zO)kgN*q}Dr5S8@)@p*6?@2x75Di3=RC~?9U%Njs${A-`HF*3_YKx>!=gK23WK=4vD zBM%jWk5u}gV7e|!l0e0d*iRMq16OdnecJt1y2y|wz-U5o&M)0`i1tygapSCmUg9ln zI%vRtb7ytvgG&KSS_EO+1f|#RAY@tfcU+W$VD|cP1qGj|krMzrGy?vjS)r(Lj2F^v z(F9?7#szrjsN9Jl0AtsX2q1}#@P)Zk@y z*6^}eu;2kORrsdS#aHVMBJ3g(vf!py( zp<^7tG=+2{4l<hYD;y0f5R z|L-o)I6MkYwMk}xs2EkrlQfehlUAQ4o={TWwaOe++1klgofQV4JXxixK%r^)D02G8 z8@_lXb9X`wgEk#N5u;#`NB(%jAH?*=Za9P*5vy|Wf(^%2uToNk!j@sH;=jjj3qmUh zLqeNl2i{`x-zo@|DWU=s+`J+{`wSjsw?(w6$|gIV(Aj_&V=(}Oz7FoNhk_?l3Fe1S zY72oL8EthRnHhN^t9oP2oAL7vh1fzL!&yr)rcQ?hfaw;j;S0#UVzTk<096kXPAjJQ zR(BerV&fqx47Rs4yC+}CG@3aFs5V1330xmQDNmppa?q5LgssoTV~9escy1YE>M4TJ%72h5w91b4gTVJwO& z!G;qJ%MYxy9Fj{ET*o8In8B`F*f$Wn8(1`%N`C_J`6bRhUykhcfCXNPC#p*E2uEe0 z5cqAR0B{!8xqWJotYlSs4x?g-Zw$${#btBwd%+J-0>};%WrTh>R3Z3fA8CRU(AhMP zgEYBYFpnCFFRUPoH`Z4|FWu39lGC~sp02vcAJj#4&miz4d~k-DFExj<$YInSW@svE zLV)0CQFrXjuyYc2zzoe?d4v`T;8@c;#+Uj*)~)(z%G2Z->%{WiUWj?f2gjjto41gc zq2})BSMn=YTO~K~!mbB*g-TVPnveG$`B05ju6$BhG5wQzC&n`1rF#fKO|lvvwoMNP zuWaiH4f3H=KCSPFbl`Uuq&*N zlxWhsgH5x$L6;p@qgY@1)e`=#G#G@0P-M9!d&gS7S$B&9SV!8K{U;`qW0Fettj)oQ zs|z;XOoYUOBLB4207A+oCkK-f%S+Wt>6fCUX#S~`u6L1RI9?60jmr<*iq)Y8DR^?k zIdBKuBX9t0ZW$yR1}uhp>gA17=qfQ;ObZ?{5)-}`bu=`4>WPAR4qhb+R8NdRnG%cu zHXT6`#I>&4k-Jpj`+Oq1Sotr5#DN8#4`o*b+=4onXF1A5V5KqNQEOt+_-l6&%s2_r z#(?uzsY_}YO>E1)%okQCB%4}seWVp$U91|zquzjqe$hs}V=I=$x#`{<(@)Ej=f;zV zrzbJHUBK&O0qG{z@5JPl!)y`=OG35iLl7i_r{4R?&%hHRerPUGVa-My62v)Rq<3s! zf!*llC8^#%T+ANdU%CUrBoOLJw`s4ZQx({QMhW3 z;?jUlIDpSm+!GGabc>Ip@vPNK{6|*Jr2i+irWEiG8>mY53CmK0dC|P z%ag3Sj1BANH$J!-NqMF~fG4NmwJqx1KWLqr)jW;{=q7yN04GL5o)R4-FIn9CwE zNZBteJ3L{EZda_zkIhdY@j>V&eH6#}iFf87(eK2_|$FH*km5aD_nm@`FiK z1`UH}?xgi8dTBEjsUjW`RjFfwD%1@5`xM{yiRhto=Ej%-YtITY4Y#YUVulIBw+T@| z2rpIt#jCwEpp>=J-|7=$yXQML)G%sjmf#nzJ=^W!HFo92TUpFaL z;51^nMxqpR^csbUcUCR->Nl@&ll|7*BYYL}fjnJyP%4%*U0WJ*K#=Ae80Cj1!N3T5 z4P-b|^zeE7pkhQ6ervHC4rQU@lf3YbQs6Sal{sLn~LJu>K{cy?I>o>ieU~l@W$(+O-RPdf9BB% z39|R&?qVNRAR9s{4$t-ne6PXfM=vkKIHhL4yzG)Y_mYY-T?hN+fxuz&xES=5=wFq94Miu8@2# zmg=4s>as%So)!)BASI}Uf`Yb$rkG;=xBJ?l5N9NF%8&SxmERzU)&G$y@3N!#kZ1qUC=^TT7f{^`5z`m^5!tQ+gk{XLA(Ix+0L!oSb3mrIw$9L5u;svfyaG30vV z;dDt_mV%fOGvL{U7?>(rKE`NUpFt_VL z+BXV>WHsPhAWxtPSVOCtw(U?s0OSb&ls-RWyFf!89jJG=sK@pt);!ks4pa?GD&HEftR)21tjyZs_d_gHQUqPkCCszG|GTsum3cPg71D#tGw?W$&tHETe=^ z?i}`Z4A#`fb9d=HggZ`3k(1tE&*=kx;wwa*1$|p`P;~+DmE6W}pj9CXfgf0j_DNH{ zuI;c;yMBQ+x7{DQO`KQl@{KE-WY|&bhFQoyBb#_7gAe|h!^qM+e}?Im=8Zp+5kh5C zi8f)%Y`SAfSxL!yY4H&lcgMz$5G^cTfJGD@JwIhmb14u6gYb)Y=x01KsjI0tGl^X@ z)WAhAcaNnWrQ*!0j58%DpkRuT_&a1>wq@@i?h1}Tdw^`1OpHyjV&?~QXp~MCFlsVE zYdzMA9Zl0w+JaTcK{QU@!Q!c5SlL__Sw*z(5ciT<60K#W|_y%^TFQ(EykW z)+A?%KG?c4Rmi4sbLMrWig);s|nAlP8C4iz2PkUPvL?#4Vw%YQEA8QHqHT7Q!^^Qk59p)(+x(@w(QPkkHwn3WcZk>e?qb0ISi(6?p>ulv-#%O#8y3~IX8|lXEGE2ohz@F;tzeFyvODHr)4_iJEO->%M6pO_^G#K#ZL%)X&s&SA!bmo zIomm;;|UlsN*P)WGlg%!Ru=#GpY8rXS(#2&gVKp!xXUzOK=%o0uAr6h)*s zU$=KelZqU%nZ(QR6Tom%MgFSQFfT7P%*%uBhLqViPHb2qV|ANYjypVe@2)P19d4Qh z&qAkztx=V%a6L9RP8`2 z88V7aE@JhM&4=_8|TzNhGOv95P+^ICNmcLg<;$#?$(Thk(oLsW|&@ zyuQoq8dO}HhmfDbceWbnP9=&5po2L(AynFWY@W>Z#Ny-JJ6^!4vy7NL(w-N5?N@<@ z6o47aTCE}<1M{%5Ik2ig^usYH^)a>B@1i0QDUHIHnUTIhoVJ1K3gG?}dB*~`1h{Zl zwh-FdSZWRY4N!kO9stx39>6BGK~RUO>b&GZEa#fq$yHB(iI3CM&7!_? z_xL$|LIOT@)>CnF!*uz2dX%(-`H|O-0681mfva1wBvYg8M~KIUYthUyrkw&1Oeoc* z=HZ!m6gw|cyeKh~;BGSvKb;AqZF!&=tVIlPx)oSVj)C>KEEi+HxN}g-TJuj}$yBzO z%)BQBp_&vzghM;f$CYBN8y{?lB3UK{GR7Y#w;brzOuSf!yRLS?Mdi5U(@yWgIP<3? zRK`Q7zlyq)!D+kueH&5cPE^R)({PYdh!b+g8{LA;@RWCwHbqzC&?Qb1xS!=G{Tl9i zl16~o=BoFE;;f$l%6JgdkLPMY3mS~|SUbI6TyhQi#B?K_@r#|_MB}p9+maS0SnYmN ze4pj4y@r|MqU-uyi_X0@H349nCK`|BH`(G~Hrmu@ZLr zS}CcA!M^;0dfVu5RJZ5Gn9pVDVqmeY->Ey1a67oX5yb%YyNV1~yTv{gn~wujEcr?m zP4p`EHsfIyel@NQcqj&j25_Oq&Y`!VIVzoDuvj{rZ^`Bj z_RhwQ@p{o+j8ve%o~P@? z{dws@9hBfLFJc2C0TqAo9YsjZo~u-SZN6=&j*4$sbhbWI{u)_mQu*XF00!^U*JSxV zFe7JRE&LE)opM!BOruYE#O9k>R5pE=2iWseh^Wto3ln)pm+e*)jSX| zS{hud+&j(CKoGGdEgF<+8CsVaZ8vJTt-vJ^Yh@3CkaqasgM}w3qQufeJ>eeX{#xV) zC0GGmy)H^)!rOc6!cO~EJG@EtI<7z>(@N_CiNTC0Be;C9UcZZ@@@som7U371^hGTl(xsRynq!210kL80ak5-ws--> zUN7bBQnty1sPDU<-5>YZ(ngY_DedCorO$)ijl=FZbQA>QzHjnt825c_ zqz>4~Gq|YU760ve#zlRyB{>guMc@41eCh6=H2w>FzD#aN$t+degI`7i!SvX?%O&O! ze$2_1eQ@LD6uBaOWW|i{XU3vO`9NoIEE!OI90!;#837AOnC6h6)lSelt~&N6+5558 zaqqXSV^7bT|6at1xcdQz*F?5i2SVIw9UeNKPYjOrSz5ZWJ3O{J_QeuZULC7xm#;`= z*18LgCl0KMXCf2EeW@#-i$hi6h;RW0=m+CFDE=AO@eb)2f%xl5@P>yY$ziNgWM0y7wQvEhkw zC!pvPryB(^RF4kTL1&zupiqze(S}K@0d7t{INzcjWhJ{W%JW^1$|wsrEM^!fTn-ZK z5VxmdDSsbgzSC~^&3WE(w%3_zo~C;M2OfFf zM1XDKHsN-3{abGj(OeG4{0VOygUiF6>$^1~s`>h69?%(7{`FLMFy@<=47epH@A`3v*P&~k!*n6Eh_2z5$ld!H` z`DKEqthZ4=QT!NWV@qoY4RhB>R?3Zy4+tptvx>0N4*Bxfi zf^#70&?Nlm8NOau-iT#{nF~>+XLN1H{evHqr+9V}ehNBjgDiM`u?`zVsIu;fvDOS7 zh9CYaY7k_2AebTAY!rLJzuQ@2EiWEO_1zb+RV+;~pK;i(wRxi_*Mg4unE3~)IA&(j ztv~)gn;a&a}O=G;a;>3MW~_KXH62&U0Aiifik34%z&p%+2cmJZ2sj z5@QBRwx8^6cmW)@I{^k!tfG5sQH%v@%ofiDDf1*!13OVK7D5?DlAy6%tgSbk*pF|2 za_&HWi3jF_ZnMSt?(Wt!iNNcy6Xew~vn3_9aAL18%w$d|*c|np=6*qyasmxrW{=JVL%3%TkNxq;5S4&frK)=}m*7E{(JkTO1@Z_HlEx zP}=YiAC%Ddmn6qs%?E8M%krVsgg$IH5!fS_a(>7RKp%UB?WQ;515|Bye+Fc50G!E# zkKpIEUZg)Sd793?x#Ft1^_P^_O`_#+bAp?BdftvzWNfyeS;@$cAn+4;yUx0g4+3b~ z*MK{bj%Yvxi(kE_qnL)WBI{n0r=60p2jq6fv|==5AffpxAKKfp4Uu9EvBV)bsb&vc z(fDt{3n+2CLyW+05U1)b8a4y;8huI8K+mepyFHR|LkM#%|77ylR~2g^qV5;Q*)ISo zRv|Bn?W2E6g=w0CLdxCvcI?%EUgre8bG*eE@J}#lSCe0-JHWsIP`oMg8)iUd-Flh` z^C~ozLa1)?sqMp@Ca8}J#P~Tgo#lhP;u)fJg!jCcKfjZ|#L+@V}KPs<~&X;W@HZ-yC9yHWs!;o>l4tvm{>0&_$J+8#4d zvR;_fS5wDP@UB7qg3&THrraKZJ%LvY&C+9}mpeQ<(Rac}6hK~rw-517c$o79v*13= z1jsYO(cIhh6;7NLSeiK6kex8HO)_zQuBos8cWPH&IaYqRQ?F^-c2o1i&n@*1zDdUkd>*>4e!6)MrdKGK{lD{QS zB!44HlR6?8QQw$TrLeU<>0jy#EQ>D}A@L;5Q~`!zQ`eG~M?;Ae8O2siG8^dj1s=RG zP+7=ZuELEbCKY@`uG$n#4EV~!Tk!(WZbiE^lag+t5#AH3S@-!%@*E|vWlOZ7rjyh~ zR1HUZM&wxPd+2Ky7WjBCOWH`wDijP^EheId z;ZH}lDH4%4ZYpQ#giNL@Q)Hvfu*F!r0Jjb$5|8MJ`+*$k1_SPNe>Q>Mz2VM`OQ=>! zWYwvjAxSW!PEgl`Br34JGBp^$^m!_zx>GU&FiIJnkqy`z1t{yjSOEj7!Ty6Uf-9lL z5E&IR^xK}he_{#s6o|Q3nHi%BDOEoy)RGQ9MQ$mhL3svI2Cn7Zp9-;REz=lpgDw@@ zS+W8Ex;hbqRWHf~9}Qc`^aprLqY5aJqgg0g=NrTFv)Apushv+Kla60f>euTG8NmKq z+i6^T&mKHZ)|bjZRZ_7*egT)Va~nE3c4Kuj1#gaP7)3jWj^%?c z0+Ky~s2UPIS8!t#9Ro%9}M8{6dV zL85<`L!Np+I|z)H-xDeI&>GRpwvf0n#tU588Z=mqX+EoGwaP1S9;)}rmdbwzIjoRF zaXbdpjLZs?p$V^#i!ckaugtn(f}d0}EMa^aH)O*a9EY+@l&k!+-f%2<=$O6(7G>g0 zGnW6$#sP8AbNwFiU1WESw`Io-(K^m>wPtl$pJ79igVwGKg0;ITtqaG23ZM0S&=jDn z|KhKpmxLL#m6qq)e({L3JR>_Ch@6Cvfd^QXJ$V3eu+fj%L1=wj2aoXA53z{k98CvR z@^}zDYC$xZd63%d;4s!amjiIoLC!MBuUV(IqC|)z*ptHstRV;d(&Wzfk@q-{yNHkl ziYI?e7Hbqx!Z^r>_+_%eBU1(v^jm&?MI-0Y$m?&kk=vpX-|CdVGrimmC{w@TE(wXF z@)Gc~`U`1jxUEhmHbe)L!=osO#6FARF^0zZ)*epxbC|MI1}JuO!jLx=zS?awIOEr;LO zAxOb@>OF4{V9H>zLE#kk>b$RJPd|u1Jx~eqU@2GhEJCYF5dtuZsKd25)^mv3*uvll zKG@oOB*WWKO${bm+Zhb$t!IhHOWYhP*~<0xybbg%BZD=4@kPUa)jfxZ(2-9~wYsY# zUFvpqxEc=|tP^AZ>@s6#OdDcNcxlc25T{Rl4_Y69KhJX)4&yS`(J#jaY#wE;{{8hB zuFXOTU(rQR*5<42EP|{t z8Vh%$_)%3Gn7XTuuPS!LirWbhMkrJrQ%_!8Rp#$hN6k}Dhjpw;At(@?Q=$#KR1w2i zDKKFMFv65EF`jkyNcUFWrY}daKpS#xP*`_vUdPlr({sZijBcYRWP{8zlr2m2yM$$fFRCDW-6=_lR+O- zK^qb*s4}6kP53rmJDk^sNAMFL-bF^a<7?W^!dK=OJ6qWKpPn~jJ1owog~R3Ks1DT; zexy;x+9b9?TR`bm!>*eUGziJ^G+~4DN(S9{E@XhBpqoU&ZpCz?B-k9&B)T;Gw4k_w0n{H-=aBqbXbl zR*`A|m#-gl|AD;@~=-U?>a7vYE?7eAhCZ<9b-oV)!pSP&2RKf}8+0$pH^M-3m<|$W$s3 zf5)cm5TV-gHeRBh#vuePO<#2YhXwRAXlsx9d?*oL!0}9+N}M5=t&w5NSGq7?riNmm ziEqo>vM%Z>enI16Ie}~Zrti6Gh2JzbpO~{npLk3lO#sJm^`4Lv>=)BUvXc6yBu-iT zV7b4QBWOG>sMhH)yqI3I{Bf^01fR)$W$^BQVx;Tm%X=`*Py#)S4KP9bON#^W$Ky!9 z1gG$!CP3#*W@warPx@$pTZQ5=+oLw^hBmF5XFg!Z>3w``?`5-Af(xdgn#glG_BKN_rIj6xybsbGh<*QZ62 zOw{T*tvl?8L{9k!;4v9?u+{+yDDx0T71@79edIiceOfOGluU-c>ucDNb2K$8qy+%ZNDpXfZo+X)u`z%MKFKdoW#1F9)lm3jhRlt{Knr zqSYRgXC0qqk{~KMiYoI|tn8DAas`RWWv-+<$S-c;18XbO*-ksc3c}!{!Z7>7zxlWf zp6gt-hi@x8+do%bwMVG6x6o=ick)$LIY(~hm22JR{;!5Oabdh($hO>u5 zWXWu>zE90787YRrDcR6(3`DILrH!d7bQnp@xUy2in)0S85h389uR%uZw*Sa09Fl`l zbytcXazl8P3kyM^f}tzX!W|+PC&7OaWs9M}&R4?1=oJWUrge`xz*zWxTQDc8 z&A6DaSnok)`1B$i=EAhg_j`njPz$`1WoHXg!K(2phthaam8;2SyoxSiE$wktR|hbB z;2|(1o=p1;XM*IAVsf%Q0!M=uN81P-PC^X?VgLO?*TS;}`D+iwqDc5?k+s2m)Dwp+ z2*1Fy%*bn|jx=rf4_kuaIjU|XW;coA!>J^dfAfDGyy_5kVpyrJP#n{%SUZJi zH3@`A04;K3y7e2D=Sp(gDcO+*Ld{xr!nQ#=J2A%{-9d0^BtS_fqC6}T2nYqb62{We zRNa!m0s?ye!pLX~AB^pcVY0&K$Gb;__?n>s7cC@1`OUKXK3Ldlhm34Ls74Pe*b`)g zz|2S~+gVRs%;x_=hA4=w`PtUYB0}*DV(j92HxKDDNl&jq;b-@2R=1kRfi4*gAL$Ly zO3e)F499M+#^gY|%b(gfK!?f|sUlz#0scXYx@CE|^Ahh}{#)g1dWi#1OmsR~uH!}x zObK{-j*|9O79> z^kQBJ2H(5^i(lDZw#@AT&=R*_YJO=9G!OS@pnSN0pAWpIevsO{JU*C(!eKl~hQ!iM zAwl|tKu8TUof>tLou#~=TJ|lQju-ioHy1K5s5a4G^cHO@TlV;K+@HhtC=UvWlKlkP z#H`=qgZzjOF68z285b?}oPqaL;7J!OM6E=A%jZBE-59R}0$ufToHS)5dEWD*|#-s*c zHD>6Cecp+onWaXYy1qgkhd0LS9}8z1YOdX=h2p)B7)&tjvc8fhB{($yBS$zHn9uX- zYr_dQcKIU;;j)JH{S?FntXs3w^{voHQ)qF#wVfY0cFgQ#a-hGgo;-Gy5RHQYbKF1% zG@Le^p>Fgdq1>QwzMyZCjB-XfRsJS>Q)t#PZ_AG4BZWvLc%Wz3n(1kT3?aR;>ILq} z=)Y}I@I1`~O@q(~OzTVk*j_3-#HyyrLsV}0f?`cbZS~VjYSC%Nep(6`hXfk>pvT8| z_!(XscDxBY=AX8v351qPnMx^FSt}|Q<%EjvZ)26 zKcmt4L|Z)&XIS1m#77s0A!-+~ddJf#AG5Epj6RUt`?Y1rF$_3!8B_$-th>aD-nw~0 zB615+#?C$9q_`*8K&C2qqwc{@ClXZ2B`2Jib;m`090Q#`X+W%soHO{d*v0N)W_Dop%n;B3 z$@G*aIJf++x(t9jnKh3ci5sd3x0%FBE$u^$C1{o#rJuYFnnS5xTJG3{h$%Tzn^zc9 zGD`O${OAZG-)oRqifh$}u|N#?Ybyo}60ZdsbW=^ZUrl`Q*3ELBp&G z>kB6Ig?Lf^I%p5z+}|NnVh<+siScrKhk}?HW1Q0Kzpm(2g)7$VbnzY4;B9GIgGH=bBfFrX%m$H5Rer1|?MV6or^MF3(1 z-epg66kJ7Ek!K7ilQ#4AJtJ+V?jo=qC6LopgZS`u;HhFgq`U!|1_S@?g#NsU7cu_1RU z&s4|sX8F6*c#3eradJI`$~}|tNmcm0*wssUGn*L=QJy)(a>>j_XrP=Mr6~I5Q^+E@ z0k~q$k?s_ZRKQ$}9I88qWSFI;zJE2eW!A|rSwDbp9Zf??kuOvfL#0nKOe2LJ*fp#^ z%r}7!QUUU3X+wD_{2rJzo6?qWof9&!_5!0|BMUyT@(*yI;7_8h7$|oRp0sEe{*Lu^ z>q5U$j?_hcO03i;0-Z@{k0EN!_^<+oahAW`V;-P+5Ek{rA*_(Pc?*8Dh%ay|T46XA zT{xX#xK%4&#Yg6gp{NRtipW)Ag7j>9L^d~Z z^)N#->)S9P*OaM13_kLRHAS`;4491GKz`N_7aP!HG-xR0q6u1%Oh|41Msa`BAmSC) zi#K@#aBXSr&dbjdDEHEN^zP;k?sF_h`G;J_qEdn3=IrRKIv@p}Xse?4angs+sR3e8 zay={mh}Ve9_Q4NDSO^~_&MZ&SQhftQfZZCYJVk^oncxqm{|{;J0&VG87HIyLz0clf z|9hYF*QtA*3iw;WlRQJ-v9OZ0RNI$Z$Cv7H-RcrZM(^YZKkX) z&fhM(A?l%X1d0m@1@lpth5Ljb!jGrWCbho~@_#jBQ*;E9XNY>6qMXXT0JZ>C(0AH#@&M`8{Yag6Yv$nxm+vfP%4fd@jIV=(wxe zKY#hQMM0|IN0AS9{qwd9tPb{~uF!4~mOX1`=f@Nr|HEw1mhS0B2_)f|^rkpirw0u1 zn(R;+!n2Pm$o53I7DWq%1Op0Y4i_eY7GN4m3v5C(&Le%7*GyW13Zh+~V@q|P)5I~e z8Dc*K4xv$aj`@-&0;$oK7hN(v`e^oGgL-b-_>9AZuo!$J88_+Y!)*H2p$c0-WrCVO z5BGs%I|S;wa?@p-0#o6fhkw)4cc)^@&YOKX}SQ}QS5XG+w>!rX&qsJ5$nD1j1{ z(~Xiq(_}IH`D#LAm8&n0WN^eJ$N>_tv7r*oMWdhSw1|N199P+&xySWD#l_I?`I^@HA1RC6I6At5PL+5S7RhW$I@3nGs5sxC=Lu>vk3r75`ua z4w}AcBC>m!K=&@oB~_=OV(@`F4ns@BCDG$7+uEBRA%FPMTei;-mp_kry3<^*{rM+6 zpauaBH-jJz2HsOqz{?qH3WGoaWHg%$0;E791zceqC47V0*}OSV*(VEXY6!#07D zfw&MFht4GE6IDu$6CmX0MS%G;Tq4{b^93@D;#lzbP8z{`_)iv34C*g~7ETb^%=O2* zEG9TZ=N$&kElSiuH5-*F?x=yn`gA-Z*l~IK0{WdzFlX-HmKH9!GS)tJ2;RqJ^3T~b z^@4o}ZAc;8K_V%L^zNG!hILgE0d}6N6O>-VBP;8fqA`8eczhL&x4fG_1qm$Y>FFCKOefwW* zae0Irj(f@8a*D^d7%dJih@U6&#wK)Oo`15l#(y@nRG|DA+8|H~cTt+;gu$W7g8>;8 zlDNmvVxeLtLJ)9nXz4txwm~vTK!?wv;PPBCp3Jenw>(|Fwv5d0ZNN@A(Sg!;J79xx zGh#mBG)VR~Fx2J^d%;s#PCFQoa~NCV==2wMR66Rqw*G%F%gwPYF&qbzIEJ&`T&4f| z>C3H>zJCUx#wa~?V~+BEKqD3}_+UysyZkv2l!P)?p%sKobVb74*s>BZOIIkymsY_J z*;xqOV>!S<>OQl+&@Dx?eEb*Eazk0JPW5+1zMw;dJnV%-03K{am*ZpXA6+BZyqu*~ zzZX|cNGFxRxCyD@Gk7Vi!^guRk>BpK5Vqb4^PA&m!SKsgT_Au$EdzUw z!|5mjUVdX>UdDsLiMVqcZajD(=b!%RCZPmV;O6&P&tSt>N8`yyXpecDcyQ2AbR4>| z!4R4bHV7RdyO310{*~>me=Uhkor?`H+^oIc%yV%1FdE;vnPe5-6Lv6UUCW z1~laXh8(ewVCl5M72P#-Ii-R0E}a9>0*WPL5J9T0Pdk!+FMK=EYA;$9`;Dzcd*E#N zc@wr*z_L$U=5)U|F9sViQn`#fX{Wr5RnBRGCj2pJDyMK_11vY~l~36&;oXEM18RsKDy6v@)mwkIVbBN-TzgeUtjFx8PoECu4i5%SQ2 zA9|M4Jgtv84mfPD0~Szis4qY-iE4Y*w?ec;ZgWS+^5qgLv$n(=JLbM1STYHBOPx85r!=w2n`3V z^QHLCXbOvsr)oxmC*%zKIGOaBh)`1recTSJPnX4LV`I+<_%mvor9F88-N2klxFB}uK+c)Fr$s}r+u;$Thx^{q z7VEa^-d_w&cLbqL@@;`-;yZSB5mU;97#<3Jp5jS<2b7IiPgpH*wZ_rDp$2Y~c+9uR zbPyio1=2Y+)fsb$EDrsS-{h64||3E#vio)B-`5T z795u_HpjrZT!98mEzIULy}Gwy8?4PB#g@H?rxaKBGo;a{V<%1X`m5oebRZTv&MU@RCm~8 z5tYwrAi^%J%|E2rRV1fxpK^7R{lm$RWG8_TW8F2ATl-CoCzjO>uv2EoT8YKnTAQI& zp9OyY5TMmRGlwy~`HXzw)Y@IA-k06!MsOu1k4eIU#9fJyn|c~}jQz!am%Ms!k&imn zt1nYjaO|-|d82GS`~s2nhnvmVo2f#K1wA9BA4w35drIQQVz-N+l!Z2f=W3FnC1s*M zC^Z|J(^bx!iCuw-{aITp41YFTYBsK5Mq(2xDtmxH6eS?x50Xz~Mwph}H@tl2p!G~Z zE5k|n#PlE70=emN#u}b*P=Wm%xSb+x2*gFJ9)*6)-Y`T)`i`pNBwiSPn_kOdaZJKI zqvx)AIJu0O+y*S=!29r(`ar~Ks}Gj*j2!pi41ui61muY?cu=JNWH>_yMY?&KRB@thym zcmA9S>>DYc*l-NzpvkqJfj6%&(T8GwbG*w;$|zX zLj4io6dYkoLw9Ux=-oEdRS~(r&|=I}rB}w0hpSaSMgoAKOHbN&uLUTFXZi#Z>f?R5 z!)wUq4lRw>esa^&z&8lL@EI&W6PY4??m&Q!q{pFPioMJtw90he5EhsE zx2WPM*g#8spCsWkr6h#i4t#pU!&;!5kbxMWn!7lbuLovx$R=S|fSP7fwt2;r!aXbW zQ!x;TizaPDUeC0S<4oXWi)L)Z07!O@1MkWSRAj=Bw;fidc>s6TJ3J_if5U?fsmwjt z`WUoZ!tya+!d4S%I06CIsTRLigvIIvV%r?cOibIy{f1s8mqpA0x7czKa+$@r@aX1Z zK(xrKqiuH6l=q+yYs{M~sU6vqgR-#hf-yxzM#>m~ih@y@hjz9w%@T@bvoJF4_2g;b zk|oGCLY6lgKH4VoM37r(9?lRfkT?NpHsvFMJ387&$11tem4VWdJw?$eQ7|@T34TU? zsDAt6jB&jvZP|}&LsbVhSjf?c_$pP}NC_f>d_BFzLob5a@fz6*fB?@if`Ym*{Cmh>fD;McJFK@8={C@s&o;$HeHO}(ScNU|{OVJ=p{IlJ+<|Lfj0Yq32}wT1Ntb2Ae_)c#+=HN^tcK(9wCsCh zeIYQ(oF~Fjz)p)u_xZ~$BNSPp_vgq^5;y=vnsb^jSVyfRca$W(E<;r{&fL;+%HY9j zmQ~Ugox)u~LQ+S7T=n|g7t6?Sg)!@J#(DLX#~+XFX2l~-{>Ys%p;_;lSSS2Bbl11h zbk+XRPj5zNSlD0^XQ@RUwkZrB`otUpi!GHHu^oTvHUpz2cvJ_iO{-Ri!B!$-L6GXO zBTDgK^iDg+euh2%7wvR4&NB|E@c$M_l0K!b6Wtz(3l=5jsUMe@6PL-`QO7 zPM>!bpgHKuY6@yckLFgVF`AIxgJ@_?4O7DpR~zsPJ|$2;IL6oEKHzY=_clFFn=dnc zv=@*o1ynlM*LTL;gg{~)kaa3c9|HOfnfN-N$!m{3 zP8*%4e+VB(ux~K$t@R3Wb{#+Lkk#B#5@Bgwh@BZkq&ZB$xJ4i@J!m)!{hPs&>=Jx& zK`emo-1NJH8E~y2 z!GsCmXT#wuIs01YLM)2VuJ6PBstPh6NNHpaJBpNTPQ#l`~e8HQ-T#{`ag)P#t6%mo+J&S#^(_){Zs_A#X_VY^% ztTm0Kdp{>F*2|J;+MH2+A>SzLqD>n}29se=guYW(0(vOhD)Ls*e#MgDKpPv20BS|p zg;6pxQgd`Fz$&}_G>AXemi!EyI1u0OeQ3^r8HJlblKGDY5U~VUElv`+oN}{wbK2_D zlHD!Ar|f023!Ls{X69OKnVN0|8xfI(4wU2Pe%+jMw(1A+kmhK%3Qwd>u&hwdM7bDv zqHkmqvSok=*vi`9e8e9 z!W?q240;a#7uso~g#epC!tj&_s;Z@8$R_@mp>$xn8k8A?+evW2fKCWFc9063qq8D5 zu7#is4!&}>OHKc=3v(oFRqw((+X&TzxN5~N@ejDu%O{|hB&i2d2jWCDP}=AhiDg3j z9kU*>Xaog#&E9#FL{(J={Aj3}p%pkP6h1KI^2$ApBD_h1;A#DaeW2o4+)xJbQwRj- zpj4?_a%{d>2)L*wj+K$gv>~#uKEA0tB$a>x;2u_-@y>C|JTZjJ^TMQS@I1Z8!&A6V zY`}4E0Ie`FQl-5W+m-cJrq*X}!U$b$!wA7ysqUb24FKFW>!V3NDTm&SOv^v7; zM)${OEEtl0hE)O~vwh}BhKUjdO2VPVtUv|!`eQEY_u`#nEJUYQa3k)5=alq_$a&rr zrpu9*wLV=81YX4R!l*y{-px@nHdqOMH#-e(SVRk)z2b=Zh>M%-h8O|izO(Gf;LT|6 z@3%Jx2Y!BNGGE+wy?{?QJ^8fmJNS;4fpaA}ECKVr_iY=Nu*}(VPt+F!PIZ={e`-sc z)yMVZB=j#k?*)S+ZQE7>&xw=~^s&F1w=0ij%VbmlxqQ@a8ZRExYcGn=ST|M?Zb&*@ z)NRPVdzt~uv}rG7AhHB?rwj57nI`O0nT95en%E)AUu=${hbdVk=cwkV(|dw{0MsqIT?1eFn>ny)OoK?| zSAh)%bX;7AGZpoJWiqrJCQCdM6d^qDR&a!EtULJ@0UZ3zFs^51YMJDliLf2^bJ9&Z z;Kw_S3tWiBWq(g7{q&(;$#_E5kxzVWW?B2m`K`R zU6A|z@1BE`1&T8sXd^wvt}zJW|Jwe578Ywnj{&;S&A}e@1@&kM5PVc&a~CjKXl_YK z+MDST^&gXy!H9g2V)|fp*jldG%@g#Y_Ob1q$$Dk5^p#bA`8{b55^J$aw`(ZjENa41F-$c zuWniCNU#}+OXr&Sc!>i#*7172hAkue#c+|UjR5r8eVQUamp>i# z#7*4_AW*-hzQ));8>HoJ6Y>aQBI}N16otyqTUdmikJ~aS)Q$y0-!M(dM49^)uggM; zI~tbmy5jp{ksM#_%hTZ}7=%pN)JC_e_7Aqh6ntH4`WIy~!NhQSO{c8;(`4D{*lMCv z3A8L0-@z55yOsIu?!ib%0w-k<0SrPp9;yPs{GjxFyoO^ia*VmsOuZKMkE{0g|8CnF zGad$vKDDuQf0m(0#k$!YtIGXDM&n=XcLLL3qjfkfX05&_s#nGJY`ZR3;bfl(O-gPOoV0ET;Lm| z2dF}>urN9}*1c18M0v zm%9eoaiTmi|2+WPE~Acss>pk-qwwk4J|YWoBd>s4$|!?2z)*z}o9A5JQM`n(*v#OsAkUzeoPiLx2r>9rRe@blF zDTmBXOaAH5HrwV(_}}Ybv2G|TB-#g17X73;fP(Y;_@EYl)}Fwf%=ZpI{#7%BO*>Nd zwKVDZKfpXtv7p`Dd>HmnX*>Z^vbqqR;fW(HCK{@ zPsufCVW62ZNz;cjIvsww+H`i(RYUsa+pXA&Ed+(2m^MCjY`VAE<0>4aS{b2|oL?~V z0li;|1{_%$D*hkg5zAq*O^$fb${H(pc~HwdOEr!?Y#U$@;SVbsQ>%*h0%`y{u&z~n zcEoG};3Vu7HzzLBGah%jqHyJ5V4`Q9^|B>7K7ZgAVhb@J`Nc*fU}o3GWg9?b4gtI; zOM!9A0FJ3TL=CC57E;-=@H;3o88ka1jDE;2OpSk%DTAi~zS|qfH<5v2P9#2A*pHF9 zG&VRRt`-=(u}CnnJJwZ+_;OZ~gx))c;%v!|m!7oa=!2Rm^WJ9`jbcW7t-wBx?2Nq# z*7U&fb*d2VJ9~!NS-o!3%+7LSU((Bl&20V@G>_Tg9$^8q-l!-(P(*@0itPhMg(k)Z zLNj}0Bp?lhPo+F`QGTVoGVBw@2rCbDL0IM4*}nG6GR*UEg@toxA?fzE^Xs$S5);MU zLO9GgfatJ4<(weFT5+M6Jw@J0?E=vmc$=l2kN8s3H|Fi5ay+Wvtcq+^q9{l8orKUU?I7(-O>-brEiwc zy9C720Vly{akkU_h*9-9ckPhw1oTX=?^eAAG7FL&En@gFSKlvVIcSFGTroaU5)#@u zf{Y1fVUx6P{K91cfHLw_*Kh4Ce!S@+fJ2g>#WG9&B|hJ84u&UAp9oN&Ms!$gFNJ-t zVgHt~3cgQxoqE`@4(1Lvy=Dg%U+{!UTt&-8crt8lBFH6d$Razv4)3j4_$!nMi@<6| z)d5r>--nvwot&Nmjx>Pv(Xs_o`O|9-sXXr@pl!+*A-M6~qlCN0;Fi2#D}gqhaE%P|RO0%8Ri4DN%%Y0h(tX zmK$KUF=l^o?3YXuT$Q1b(e_;Z3Sjl1Iiz7v!6-XEAVQa*I{(P*8oK(;@H7Sa4@GN0 zp1n2oAM9`{^c%$ux_8rTX!44rn%jmvb>js^Yuzve%nbC9?tpX(02s`UKyqpc;`!p` zafk3djXF^HwIBI(66dcWA&K6{=sD4gh3SU4;9w54FuWMc0l3dnx>Zg0Gb0N744MOO zVWP=#KADCe*BAktrDn0V(PQcnWEy3IwhC59Ap%)8j+#WF3nx?&-bF;**{q_eb>$Uly62%35&3NPtF{N z8)G%dshmW+&ecyq_O{cmlej~-4sqxGG_1745HLT1E3kTb0tCik>F|U|DPN;qd+H=* zDHje#0xThPSDWf44gXjOB`q(uRF8tcLXZ2DV|}@|#uRW3L}3lQE)vM=CDU$NAYs?% zbItbbSbsNqDFxHRSH;%!4kzfiR?zwxi(?$5FWU8Xx&ft~QHYTZe^9dPfwVFPBx2d^ z?o`BoH>0cneC!ivV9nH_;)Dkdq~=mIO~2{VVq7xQt_K}2NA5|g01uKT@uY?d6d;(T zZa(5u4+v~1_uGtCI04yFNZW(ypfesm9(pWFU&2Uk-MpFrb9^luVh^ZMrp3=(<;U__ z$OAK=6qZ-qIAu>Lb6n_R!XfZz4?YP)Z&o4_#OPu&k*PvxWV8O?~v#CEEJN^3#@|Oz(VJpDHoH?428?Y$t(bWT7Y<(D%_19|Ur!{<4vwT9UGpJ?~ z)Pq)Fs4awJC>y}h`-i7b3800IwW7(ul=}(YSDKkyj@g^f9MPBq5(!|kN_Zj&_Ec5D z)xcwK1(!@T5CsEg>>&=*19nW@2OXLt~5V%?MhGyOVG?%AWs6EFANjuJ)bsU z;Q8~}9R=U}8~?^#w+BkR3x0HdG29ccjM@r8g3ULQ^2~i`9+*+u5c&!Id2gs? zh6#?t^S{0!1d{TMSf7wocCGwNY$sPO$@c~J5e!>`)il%w$k8=A`T55!}hRZY} z*ifqyM#7Im)lloF)|T%suA|})6^d4}I!3SxU z)y-j{&`h(Cj66zcVYQhBO>t*TGPM`Brj+IiFVMIWrJOe&PZn&WKX7)oTQ~B7h7Y4< zpc4kfFwmf3o#|m#lPSY0qLmvG;u|Bj+0ai!H2Olq#S~96H&W`88?$UyVuUGWs1jL^srUL6y{aEG2lu*;&IfxTF6JeXraS+2}$ebz{=1 z4OtD2j|ft%h{=c=t>Kk420NwdYNNgeT)F`*H4T)w;%H(5F&bz&NL`9g7Q)8!(XA6@ z0i<2wNo0B!V>DW&|odF*urh; zv5NQ+O@`ZnwNbPekV;pI&H5}i_`T{zSeg$M7CdDwtzXgSN$4QS1lU5Lpvpc3j8jkN z4MUl~HzH)lxW4d-9tU^PQ&{Kb21A^T)z8d-DzV~!A6+&##>UwcYF+y`a@iD{YCZ+T z*uEpk()SnCW8j-~3iEyM6O>N)Uw8sK%cD1&5DEx}vMam;)b)o*1L&K^y$lR*rn0d_ z$@zNrJ(FY>l2XGQ)C+l(B`^_He)%9^T|zQAk)6$+7dBS}GIvgg;6p-wDfkFDgq8r7 zO(`*$;(7!6*X;h-{GqP)0tglYh?`GErL_5|GLSV-r4C_g2ScY@{1NZiE^tuGA2h-@+i-H*`ji@B z0T;abHS8v*|bB7*C`(wx=ut zPAcN;pLq^(irz&@hjEy$22f|X{UJWptZGX`BadOD>Ad>kd?Q4jdqF8_ItY$rf^lkY z>gXw$oF&4cQ<9ua%|G7y264f+r(_YYQv?dcBM@gxH7y}s@G04lmTm-{ZPFn{FEFqy zs&i?ZM98kFKH#SP6u5W$=0Z2B;M$DLARKT*CPxm-F7pHEDH2|bWM?9TGZ2|bW#Wpu zEjCo}Eh&xGPT@b=oE0pr&QIkiK$QFr#SfVv5tS&g!O%AfZ16oIyHH9*N4o+WG^aK( z2uzl(sJ>!667EgQ4W=@F9FH1X{ON|M$K>k{Q7Hh)1%os-1VPHL2&M@LhR}&1Tx;^% z?UgtfQjmI7;z+S*(7da}f!u81h-ilN&{g7?Kf|SnC;%8Z4)dB<;)o?NAyDAC}N=6!5XD*NH?mLREbT`M808V8DleGj9 z;}d3p3@@8?o~bC6lkGBQ*r5zFV4a7MAa&AJ>0zP>8}v7d?*ek)VX5?ev7aba@7G=L zd{b?8GO>mg{dAL|0@vM*Wt}2J3vlw^@aENaf;Z-+RxbA*!yl~zX1yw!QZavGHAc)Z`?L~IU;c$;D+_{(>tvBmj+RNT%2tR z?VduGJTFz8!F@|{3Kqet&#>3-|3`?A*w<@e5VM@Lt_qvbv5}~vq)YMxdE}@R%mhF)Ujg7Zg0N$dyVB? zSa%Qmn&`%9S)mG759X``lcEV{!w07X!&#zDb2Ob-U$;eEYV=t-Lr?Kk&ZK4QZMV2CM~t~>tbzOopmatSAq%CffL2+k|b%{efU z;lB{sEZe3+7n}+dXDz5;z!3a~=bHZce**2uVxPIOuN3o2RW-cLf@4H=JI_G`mZ&^d zG%oe7+2CP{53qG&?an(ni|DmGSdo#%KkGZc-O?&RLF>gHErYiZ$M8|(ld)I>gLCqr zrSu)nMw>EF&_VM*Zo5r17(5mi)o>m7F2ytBXUXy~*{rxxa9~N-)eM{5(xV7BI002N z;6XeOTDcHEM8%P$kpn!+a_}Z%BQ$MolwG#UvSE$?zE95Qkk$Wybyubn7mDglgN?+! z9;?vX&^lSi$k?RK&5X@=e|GNeYO7!wiC{p-@TW64ywXCkVY_Q=$pr4{N*!0yd9b89 zekAfi0E3(vX4I*eYRer4lq@`#j}rzt6wai}CqK_Z6@qY>(}pIAwX>Ndb%@cS?CxWS zk*q9&GtxlXVo!!PhMcG|1;e$KV{Jm=bu?ZR>c=Gi(6ITT5l}`5nTB_;vZ8&#Mf{CX z?U42;CzDk4DD#hT0xiabSduk}5o65d3B26I<>8oEC~d3Ve6r4ew{I2mtj4pXRsmEJ1(Cp zkKMsi=rNeDhszN-BL(VT=YmlU);w*p1_55;HQ32#taMd#w){a^8nZdc{AnrRRlgo+ zxU-c@Y%h$YP;Jhpux@wOT>$TXoYi(Ao*WB&U<&|T@vm>F*u0N}6hcCr{ki~XqVJ6i zOXyxZoZhr-T}nQ|z@+Vz)%~4%4DUy!O4EkTVGl$cPwGLawx8e}%7S+|&aPz-5PkU3 z<~%t#Z(JW}ArL9Un|8v@7TlZ;T8GxqKEP3Ug-78rZI#i5*woLg)VqRTm(~9o3YrZe z3D|NGZ*p6s(Pqk%jIQuBp0vL7#x1urB7z&%A%4QgkP^>ND{2!^p&vO63TwUE!D`kY zTC^K=Vk#8;Z*DuW*O`304Xv(ChZqDLW*$)un~g_eQ7J~y*V__J!c-uWKxIuG-^*$00gAS2ZZ`Pc1gV&wx8Epo4 zQ@+-5J(xRT`oE5^j*Tx0=7uvQHZH3p`_sH`7Yg!eH2K>&)2v{HX4 zCeD}lxt}%ROi|s88tZ#Eg5x^sv-)?iHWcCYufhdTX)!pJrLmB-RDTA|CEZhWqxivR zFyXT5{oAb%SMl!l0yqns2p;q&9LKGuqXLljm)0NWQu}9_UX&hO>JNQlk~5&{t08OUuNg(eS!Cs841$VY$*Q zlVU0#t?LhPENQQP>k#`e87D08@0RH8421cAXZsKp_kH&FcKE?S|B}Y*&r~h^L0G9u z2Ud}&jVIRe>w(9ej^8PvBQF;97lk|uAOlPJt>``cO5MsA1Zmm<1;$Y>pHUX|Uqj?s z{WoL&e2zcu50_Y1bQ>+3sl*rLkprOx#2-$!4~OvQX;$pr!!XyR8Qy-NJX`-MOWXbo z%{cm?I{htt8AhU&P37$gL65$I<_5OJEbI$Z=6h4@=!u>!@satKtv)?jU>d7lB%&8(>ZV#4>L{^RQy2t4`?{$jlY1vU)}$rr>FLOqr*;3S#u;#QoD_w8BA3q$LbgI`Q|L zaIww#7W|fBe{fv=5~oGo-Rnw>L_tV(`ay9rP2+IqIm9)tg$78Ge}jj*>>0cww9SUO zhiAi=&lYb@5Tj(emk))?+zh@yTo%-+{?^rpctLz%q!<;js=#VW-Gidd;zN{Pgg@u0 z>P58Bk7PC=8CFD$02D|=&gBBwFUvO@;L1D2nYfF_>Ng2RaXNJ9_A`#5}$r z#Yq<-=iEw&K6f)lH+Pc3Qo?Ea2~egv^sv5>(q!Fl_(^Fn@H~LSfvH*okN=s|k41_M!uTbMJGD*cmM3)aMd^03Jn1}jG~C!Ll##{;J}`%}gU@CIOR zmHPhZX5TnrN1#Hr?$)TGMkkMwX5Z~MVvd5TOSo+oZVXc$CD-)j1$BDHmQxlHeBmQC z|7V-c0|!P9BL%jf+;*8>-D$-~w*f1%eNm{{*v^&PZ^%OG6ZY4L{F?Y6=p}54ni^8% zBifKC{ zFX%BXh&2Iod(4X~$8h>KHrEFpmh=6i>d(eFrJNMPLy<3@&ApHd;;&7D(YqInp z{FvbjTkBcGC`<_V3y$r`>dNa+ji=xTmmQD;R%*?l%G)DHLZXE#T3TEFOYME6Y1$}r?O_cBj_0rn*hiK z?(<_zp*B-wCit*r5InxbUX7+sb=ipgvpJ0=z30Er$vE~u51Sn(qU^9nFL&?Xe%Av4 zM(d2*v~UiMz`4$E9O)q4a7$2{gb)KtJd+H<*KTiS0;qslGf69mCIl~=<%9-^R!61{ z2isb7-&4X-ljKDSdcjM}6tqLhGhPmtz9BOyfQ5{$gnaIsiYMR&`J$c`5svqccyQPe zpFs(IZYNI10o(vq=$rr#U$ijPw87YL5FmJW;!nWV<-i&xMhaF{A799qkj!8#_+)Ts zM&>BdrKR8qK?0FN*?B;ZyF!6=?8-wJM?fCVnlg*HhLa2KCKm*Rgf!P@%RIw!-21Ni zVkzO`9442f!fuisstLziLygm_ebu(dhk>AJ!*d3xb6$}<)7R$KKzvmdY3jWEgWF!F z=}@YRXn#zX!E0?32&Ywk!{!?>Gx@7wx*7ez5;{hVcuJh_hqpZ(p(YhEj6|80oUADl zdPK=VH|ZdYkXp?G1jG1#bh80qv1nbw609WWL){T6A&)SjDzyUo8!ZbOF3OYef<=JT z;MjyV3h)ouZ31v1pwN)w>h$9L16ihH6W6yUV2A-?YZ#gW)37~lvY`d1O5!MJ`~hW{ zA?O3^09ylcc!ARJI93!pT?PY34$X3cENut|=(LkQ93N0j3rsFr!iZnpgaSl4mc=n* zxNK;1qU;O;Ep7ooY?~@?Ij@AS+IMby4NyC)VjBgI>|Q8B;%SVgLz7hiO{mCcW>aC5 zKx7aT!)9hsGLTK7RgW6om0P-lQPQ1j1n12ZmPrCtA1U*Rv7g7- zZyHq>#EU4R+Z3qb7zT=#u^*wKu*jUWcqDTJU(lFtqbXVH($|jiWYPUcGZRfohU7n* zDtHFpF7e6;kYZd0EM=&O>tICdY@{Z9BpBFsB&Pyz{a8rQ0L?po0smNjA>gYyQ5d>u z#%aSX>;auv-e86j=(y263#>-#2#RKhv;~e5g@w6lQWNH71P++unaD6RkRx- zNf`)M#>q0oP#BvB1@Rmf%7BG37s65+iDSd2oSW8*p9yasFVdGTknn{!k~K$2*?Nor0+ z$%)v*Tti~Q>{NW1OXP&?tR9IZf}kBp4_Sl^r%m`9(i0j!lK2d5&I!wzCsPu{EN9?= zg?Io^yn<=eF?b$hE)gWh0??bNR;O=h(Eiisc`OfH6klwH)P~gU994jn2v|Ev0}dD) z?evEGHnzZnk>q@3;im&)UkfZrwSSiPw+$bAh8WKC!jxe>;d^hqY=am`um=#3C*Jh6 zPe1|HNAy>TB%&rxWTKL00;tL}SO3XC01PBTErBLfZh;85jn(RMxd3K3zx$Iyx?<%A z@JcyNNi*_2CrCGl!Du|$Y%qR-h7j?_y5(eOAxM)u45`LiVk_%k4CED?o$!`IXv-tP z$UBm*)}wq3qvj2=U%C`8ybc^rqI#nHCd=SQG=Wm{|5k}o-yitx8u~c=BvrzMY&>)(R63hlgKyc`zK+)D)CpzQ3d&4A!5N%ZT?O%r?Sl_!8Dh&+*>0bTo zD|lFh9`Bn&z{inn$~k)QwGek+Ix5v*R2+pv{S!nV#N>Jm+dJ#~m*QZjb2HXd-Ui2c zNt3m^&Yf*n)^VY#J68WQbF&A|B2D9y_3@on=Cfhm%%^ZZGNqUgvq4ZW8{Q12pIQG< z=}TKM5&l=)PK-(LT^*2Dr_I_6=@dX7V)x^Kxg_++@UYAue`{&M9G+N%yE^H_h=*Y) zm-T0s$%TOd+z2KPPe;CXSAtnoUub_2F9^=4KU|uX_L4BDr_im<2Rm!FSva5-24 z^fZuwSwZ3E8Jp6tomCj0N0k^dYX!(`DKY(KqKk2%{R9gLoZb43G`c!xzYq3SG@axP zW!m z9Kal<Gb<+#)b3eTMVsjV z?vUaO?Z;qpK7R;}CqYVy307mqi^ORLfQx2FVpLXtWt_f~v{*k5ujhQPUtFsGM*|u9 zaXB-_Zp!mUD#BFg*iLPanQlDU`zR$8|SLaRBzsVz7ScdA83;80*FL8hl zF4_q(kxa;_uXUKgb{h$mTwyt|J5gR!nc){iqG=y%r)87f4E?K5H=&B!(U z(TOs%dzEee$gdGRnRgeJUxf(AOX3x)IK;7^UhZ$cT&8}xIbn4YNF#L>XhI_0wEZ@0 zuR}>1cL<4QcP@CjcU;X3 zeCTf&2gC{f`A?tpCY&?K2OUM48MeR6{=ThnX5*G|UfX#Bs71b;7}4aWa9_oU;aIH1 z$lgk@DcXGI6Bb|uP#Ga`71)w?)ObfZLycl97@x0Ay5vg#Ta`8Z=yW3v<>Sa|okR7osAUY0q+bpMTG8=h(lX2L- zW&MZkzjqp>3i@V{1=`%We$6tszT{qmth@ZKJRb(v-KFx>DHJ21joS~cKa3$22B>qk z>Ep}Al{W#cO z$O&${_WT3qf~Da;L~Okf1#Y8%BwKr=>%Suf`lQ@lHR5 z4p-lY_?@6Sfi4#&5wWG0XF?kYn~9i7V#?#5TZp`1yDi6-8K_=M56Ms_fw1jPsfAxv zOj`^^c{$t+P3b$7J`SdjU0^Ew$g>e53Ni!(K;1vA4ge@Xxfb9dcHly$|LV6aPep2w zK14x>^Jr~8Tfd#9p3ua5hEaE8iCv)|=90(MP=J#<&Zt5(tZ4d$ZlVNj=&b~EXaS|N zN%K5^+Mi78M<>7~#GVQs%c}W8vY1K;0F-YD3|Yg6Cn?7cyAl4AP2n>tD!)Dn1bi26 zjChI10jg$QuR4QX2u9}heUs1}sa=gLH9R-!_D$glxC{h{{uK$sG(<;oz@q*vd#H@` z=CPa{f{-*wQwJ^`~0|&xr3R~1AXS~K z_21H1tm-OA&D1|M!OjqPv4X>Cm^;^UPj-VSyF>1!fzmB`sIFn-xMg7ZJk664f4X{k z5oU-EH8B?2Qzs1tA+1YFyj4MfvuZBDC#gfK8bXyLPzJ#& z+RUA9Q{2*<5T7vU*n*A1&Y>)U(uG){22aj^r8lu3qJX#MbiT|Yb`3rYf>Tp~1G;S}eJv7z^; zYX;sEE2aAD2+Bbv>p{i~m4 z0nBN8-d0ah$!Iu$>iduTeO0VQa} z{NNqVJuQEoQqF=4bzTdx1Z#|(A&{39apN-wtw1B0a#xDgktSi1E`}fpCu?uE0G|<& zRTseyBP`ltG$JQPuMU9Pq#MINn;xRLv53OU65E%0J8$90!0gP(6|| zI8J)D1QS>~2Ex^mR@%W~u2*&O2aNxF&|4qrU!<<)`pVuL= zK)NU9wZbyUXrcaL@Q6jXbsQbiM$*<)Jux-pmAX@ciWnQ|3VsdEuNEE1CCX-6ef(nr zR$kJX#PWt%QN~E8{mAtwSXvq!s!Y#V!}=A=j$OKkzK}C~gIE0Dk*>BF?l9I`#7gsw zM2P@d=-(n9M#ScQCDo$dvqjZClu>mfp2x6ol`uEVXGIrMI_rOKHxaFD-C+W`eMdhELyYoj`qs$Y0LAp z20Q=|jEuEgcHx#VO>mZu+I^Uik7gXN_@|?pS<7s~{Wq@%8|v3AMp7+9!^h9AjahMh zi5|{>)KWx}@X~BWx*Fj29)*TS^%n3AuhCA)jIPM>2<6<-j`bwLDzAkTVi091Yt4E{ zxMWY|?X%^9tar#{qQdR7U$FYfeNAz1>KM5v9!PGmOz{6j^n^}yD<707xrGl38JrNr(+rUcu;_1P{V7{B+4;}|e2j7AL>1xmWZ z{{gEGDoBC+-7IW&9?Xdf-@7g)g#u7<$PJue;SSJlp>58r;>mjvxhZY~jSZrUbR~LX zt59qW?Ay40p>zl`{d}-t4wMv)YUeVPcIr z%|(uu0_3BX;Cl>W0~d4iupB|2&BvtN<y@)T%>?#$&ukyLd7?I((je*%3Hi0r6u92?8hv(6#@o%FduID@ zRo&__IQ|8eF5C#@(>TMdCI6q3Ba4^UF+e-(wJjV-|5*2iv$Fc7W#nV* zmzrA|u_yV4oC?!CK4Wy9h=kQ|5s{=EC?e5k#cxX7=waRsV=jXBZ~zB{Oe5=D4B_rg z7YEcVR&qj)+E0VFydirbANO2%So4vatogVmf11$%)V~{YdDZ|y z>V8GPt?&&KAGiSqo&^Qn=*GAh;8=`96toY9Udo)Dn_&-R>y5niLrKIUna0T)TYKs)+)>r?(z{jCkbx74lcegFNk}Dlqct!U(s> zrOoh)i&qjYp-oH|_{-DlpIR1jlw64<)qBm7%}_mmd}Rt-?5ifEG6rqFnN7fKdMVqF zr9Kq7#_rD5e>R4#7xkN%0(xEVNOoMfn|7PqGLw2%M%fS6K>de)o#i2Jv%E~U*%f2< zB6D^>f#=b)*eju%(BAdwbR$=ZMrLDWpna1{HdaC7V5$iF8X&ycf_!potR_+ z(`7Xp0t}8twHEH)ZC=~rnN-8YJ;^wBU7C(#t`On%AOf0FGz%7lzswkV?GdlS8HTsC z>H>i!Xcj-Q!uITJ!dpE_x0p+C(QwU}TaDz6ZLh4r;aHJ1Ruhe$?xWKJbB{L%u`{H- zNLfPeK!i9X5Sgey_sn*?XJiA7BtTRNQ&T*(6-NFgm@u?5-K_|f3#>ssyH6SMi~q@G z#kyi&5I_zV&t^I>F9p6Z8ly5j})_k!-qj-qubRr6;c9^NH5-W2jTH+Klru z3+(dZ277gG@^pX@ zHUVNY77afufL!9Xh`8~%kbtK7fS4yL@Gwo_Z_7x+jsW>~b$(RNxAp=qlRmO0UG~}^ z`4LmAZ}eK=1FZB`aA1NWM8~3*bzywDT|O7@zf_-PFz?h0@E0Sov|_VlQ0qpyNaVDPCQ3eW3Z(Vm3l#KtQJx3`9)SXP985#C~lw6I- zIdFjG)zHDhrNk4f4JrUm9sd`Y(y^aV2+RsQJmLn_PD|=yh$ao)#6!Es8VFy^?_6x| ztOD-O%5Z1l-|vCvKMBLtRHl$sD3S^)4D1n}4#YdOz2D`6R0(oIQ1Ze&mCS|(vx zBom%kL)=xm!W|wg?{RxIqlq!HYoQokU;Y9vaK9oK-rRkyuP@&bY?Js_H$;VA$=2fQ zinrrzm^(s|n2XIGG9dk8uffy6u{auw&t1*RP2erz=j)1((Yk{7Bi-lw{XG1v?0xo1 zDEM9R323h?E_&-?ezpo&LdwT^1QL5#?*(^?Ehn_ZY#1YF~JKLf$16} zH8YQqieaHMOq3zSbwiXiJChBHh#i4VV5}mV$M^Ux4bZ3kQLHOX6OT#badZk#?yKh{ zj}u+Oq!v|px{#DPe`FSTDOOqDmPeS!dtDi}hY9ZOumU3|0wC9W%u zg}1AIE!FkZXV&~+j@9dS7FTZUS*p{SA`QMYCn8YB zqF68P`Xr}(CFD*Zvhpxz+yk^Ecyw*ym>svX zpu*V1Qaw*yMsIVdCmWkw;6Up7O=0aTlmNCq1b9wm(;4u?W$E)Y9BK~b0_S`LBCvx9 zK(nngYoX*G z*a&B0)&ivUMBSsv&*E!G<5+Z4SdttYk1C+1tL);^;1cQ^hA4*Nwzg^IE-YDWrc3l2 zPDg&dz{)--85i!t0SWs|3LMNxhZ&V!H2q6TEc8Ld+{WMot)}klngVL( zl{k32(vD+kS<6+!)#DwuUVMmy=aL2s1yj$T`+P)dAWH|Pu z8OYNQ_;;e9_;6JCMR)Qr?js|xaPtxrDcV#uFO?3~^%Ts#(S=b++jHnI!=70BovzX{ zjifQ|Y-t&RqS1Y{*>iQy-c{Z^`hqwDZ`mb45R_td0m*R@J<)y`>MB)L7usWIr&odw4yonpLh2+ zPmxp)rWFHU)m)|eOMe&!Y~BOuR#{Y}0hxSf zF9Eyp)$E2?qytiy;ev{5+?=exKgiaI>^n@M& zx z$)#rZC2E5=Za9Y7l4CmVYFF-61)svoW0P$lR8o8nh+F9^I$5J)O_g{* zU*Nh}`vOc7Ap=XpL@$~pD2L1V#0fVw6D1`T9yytrk5NLHW-ri#R}u`20dyNL(qL3$ zfPJ$f^ITv?2!B3Srn~1P*Ib!cL9>#iMVVwhdKPe{Wwhmtmljd*ifNQ1Ni*I8W!xe0 z81L-6G@IN!TUtefT*T+dmwhhboTF@5v12s1w48WfxI1k2%N_U2?V)M~K+}^|LH_-u zxkD80q(N5Rp{m<{nVk9p{vab`O(pum*lL6W!B0s)O0*ZSj8JxG>=k4YLtw&4kcj|E z_>>WC1UJY6AhJbu!H-E*QL@@4^>*<%%i6Bt|2Ln#{&b-z(gZw zJ5z@`;{GYZge!!CUf;J)OZHCme^7^E{3mvf;n}4 zr3p~NozXJ}1{scKU}&_ex{<-;H({0Y9DQ|~U;EIX>Vzh_Tw9pUqX@NjQ zSn)#ei<|-;iO_v}-Ay3Mk(*Sw;?4qhBDMvr<4pqMhQc&$xL{;fB=y<$v#uAMM;B9} zi-{HFdN_{^iICDW8^V`W^HjV+1ahQL?W^xwOex+fY_L9t2uYY5E3RS&>CY!zrkx!N zjU%GRw-W3}bls%AiEBB+1Y=vh6++@g<2|%xsiR_&gGjG3Qu?9nZ2iv30aT5m{pRN} z6wE55@Vr3cB)3cEX z&i4{M$H(Rg$w#selid(B1*mSBvFNy(ki3FX9SEBta+KTOV7^{@jF~EBQ-;}q%=)rU0QVW|OTD5jb5xWe%h(09UGRSWc~YTS8uvKJU1@?0H6<6|f0>x$AoLH?_LN=6Q+Z7M zBb8tNr2wQ&UKR}rnV?9{n0tf55fA%N=>w%6$4nt8bk|yH` z4&Vg{;Wh=|kf#z3%Tr%i(R9pwL+_yuhcVS^v0Re>aLJ8*wH$|n#pW>ZcDO14AUa47 z7Xn^N;ve-R_0*&$r%2paeFX%?>(&o#7Tf#V=@Mb$_TFV2^|QUx%nA9p8)YXnKHWqA zpf=KiLl!Xm1O|>!#=L7vaf4d_!aj(ZkNpi5Q+SkcUo<4d8r9eNoC7Me*jgLY<=ep- zVs2d%`2eS>%vSCDpi8EM;$Zz-6Qm4hYlyo8dXAtYupvOp{=nS?)EPp0e1zd^op4(4 zHKK>{nwqb`cY!*H>VK#oV~-OJlG9m#DqOp0dy#@hbWQq0#;+AG<{dX#ycj{rhcVxg zy^ZKZn8&vv%RaI%qT>OY$TRf$ahOHE04Udp>%4izZ%z7!izS=6So}lpVuGweLMg=q zkc?cKgjbybAI8e0J-ih+x`kvCE-&4qIwRD1l$B5k%A9*6Fz*L5K%A46(pWi={zpIuJcG^Z16gf z@1m}5j46K28>RG;GLvQ3grLI=%-LnY9}SDgE<_I@LF zivc3~2M@XvM8kokp^)?njHh{m$1Zn_pfXeN?v+dIAmgwRb{^0~f-_CT9xbwKl?H77 zOlnS;djyWdN38=p^5F2jaKV``w7-t_0p+nfj9)Ooa5s<20nImZg z4iiGGzGewQV=7i$YGD3xiKckFV_3n**%QEv_1n~sOOSg$9B?6>u3DaC&jo|#DRIMg z;=wR6n5*s{yjgj8JgV2__<>4UOwP_NqpjVs%+YyT zjaT9ot9&VmB$K2w@;z%l%@qQ0)KpbHG)x9N53}1DJ}cWYGMqGaQdPfEuyG}cr2*!D z;rdX81kLrF>1H3IsITGzhPl90X^<@F#3W6>n&ANrnN=Y1LPnr7T@4FClVp{iOc zUxO!M-LP9w2LPaqTs4nMBd*z%Z0|0Q+gIwZn$eBa&RRA&i6lZkUAP3>@M#>T=t^u` z5Yy;4jxO)oi1Ng*Seb%(KFWQa#N*kdzBdJ;n~7DAqJz!$c+B=t@sgbb{xeu*{RUJz zeIE9gl)zrM4eS;jC!_E&==F8vH9M#sOq zTt8k-rPdA4nONZC5R@K_q*YKOkZmIYjzQbBhbVIic>%cYQV?zYf+@fV+TAVq5YNB| zOD1TpG|)H)6OyfO*|f#amoU)Sw8c@3!;-;}v;}?-WDwLp(B+jnPwvpp(doK2qvvh# zwNbnoo6!J>O2#PAgS2cO)My@pJh95+LbGV9I=Gtn*^s?&4K5fLcHq7o9!T>OJRATt z#(5QXj0>13HdzC|!DH#^35<{j(8F>Mm(2Ak6`dOP|tb@S@5(ht>13 z9sm*prZHv(KG#35uMyzjeB0I)4oH97I34yxp8`b>m6;_cR;$UsA%Q%=IZZ=pzPc|K@-k)R)nnr<1;U*$= z-ZVi?HCWyJRmFexf23)$NLgnh0b(1dUm1j0C%sV?o)W{Xt$F@J__-{~Y z``$d6+zOzLQ;?;y-0#=yO14dh9Zv22lrXc#HM@D)4JSu|1w`cn2vvCnF24j&t1a}U z2YEGoTN#>C-SzptWoZ z-yl?}2c8A_%gOyE4#RZ(kmLpctE0Eaqk*t0Srp{Gh6Wq=7;KV(4-Y5<$g+Hnl=OaB zLQLQ`647XJwKL>cZ0F83=iV_X=N=3y&0TGRk<5q^Xc~aP#^Mos3M;JWsgnlUx0raX zig@%qC3-W4gaEZI7Ia^tCpZl!{c^wkvA46vt18|w>FbK`;_}Zw(r-WYBSuPk=ea}BnTy21t}7n3agG>i z|Igp_{l_quU$MwY8P+HbOF4pDm21qF+0FBv3epvDinQ*hsoGcn9GHPRtEO|HeM|R? zv>AT={(F1vAHP>C;^GB-G~sw99#FJzcyqs+b^`z+Z~x(ic2Ix8I0jhDjMYQEO)1k? z;D}X5vAhlleFJcXl`f#yfF=zJG3WB-FMu&Lgq+Gs<^qZVUKmCV5{?`(+)pef3K;8!sS*Dy4^Xo}Vu5oJbTH*fuoe*GHs z=I*&^AYNFVTOK{gqj~#_@#uWqA7k7(!;y~zOx&q_ET2z$eZPJ6>j}x;n#MRQ+rPi$ zis|5Be8`3NpYe1b&SbHZql&KX6>zKl(EEJ&p&W@B4&xOZ03Sw?Jxaiul+UfiE@kQfgc`F zT10jlKpUzQC+Pq6+raC%6RJ-gDp31HRCN41>6!W;L2wjyur=4qbp{^V{)gY_Cog+( zQkt`$ZqrO|!qNItQIH5FxKOKk0D;dlmvlF#D9@(1Y}-uMjo(k+L;F*|k=}dK z)~o8Sjjd{VlCScj^naF!^5^G}MO>i7mIR?7GuF%|oaBEE-lgsBRap4H^ZI3~mS8Lv`nj zporG;P-A6(MlSl~>0V0Z4Zqz5(-?h+2O)WxrVJwRC0$KnYhSOb*BmbPY=owlgQF{E zRERV4J=qhGSTw5Db;aihezl)N#ffsNb4x|T+SAI%^?zKM9w_cg>HnklTB!;JynYKc zLbd640oGo+-`Qy-2h|UU>&S0EwNlOYE7|vxY9LKfq|Ntg8jc&qNuXj+PHT8g)^A@I zo0@Gq)GHpZ-xBTFgs16}k0A#meF>wjz0hcNDR_B>BIEk0X)1bY>c87DCk5%u)TzCQUoq`jyM5 z0(`6eW^f69MOgMKrh4F>TRoH(7;^4t!bU7Aa+1RJ6gw;u1&{zpF@#^SO)BeLRj`|t zU>cX+L9rZmlTa0R)E*J!^SN5@JLVdSWkw(nHJ=uoFzHnC`D5*dih_~b_@6QS~q@!O)3W8vY9_a;|iBz$84{&sf8U(`6B%trpY9o{*zDH`BGefCjLhK6+7YuWfb1L&IhJbqV_NIuVY zKKhCdV>5ZhiXtro_G3L-u1Fw)y@wCF{l$&;EMJ;+ziEzTc{7{$qpLL9H`gi(EIDrz zs~Ns5kd2>L72{LQ4|q69rvzSadV{Gm*RQw5%i8OKa)v^^fg&7#CxVt7X?T!8V#7Dz z6m6&+7nqZ_Lp(zzvLg#s5ukC;#7phFz7%1GVfz-O4U!;`JY^NE1vRKdl8vF%R}-Ou5pyjk*vT^%tua`(y6j6qqcNwD($Sz5Ug4I_o&PqlGMGBv15my zzJnue@$~Kd-AzyoG0ZC_z40-GcxN>)&-S!G!LSL?9?526Dd|`q$Tyz_l~bi9G}*Gf zE2SpKgjl0@yrVKQ073DH*6QP9zB|46&aQRHX;}X%=2Zi02h>4qJvI&V{pdNlIuvK= zJG2Kjqk(WwLTp+PqAd=A8JezzkjAs>j5tHE+Y0su^=0Ke^~07^RrK71oSpyX6$ptoi0Xhy)tX~OmR)*en1 z={XT@I%VhMp2X<^!z(a$sK13%z}(r|;RSZYbK3-!g>WAzK*0(GhEs7ziKg*ItvZu_ zdKh3lfp}{6BaHM~V3JZ4lUSq7hbl^fuh(P$M!Kzx zTdkCJ_neZv3le2%k)Nr}aRMZ4jguE$zysHlu34OH4w$itRus!GLWgx4E7)gtSXA|| ztr&WOtl<9<-syP*orvsR?!jzx8p16EQ|Hg^bFS~|mhCr(c6&LUnGMvV>fde*R%tYAjGuO#!Nd(rCjd2GqA(T8BvaB*uaT1x(Fz6r{APbQo+0;C| z|JDgn5%vb$9iE0h0j2zTk?Rvw5P8wy41fcPZke!qPL-4hm1y!#=8v%t5Ia2KMkFZ0 z9Rg)oQ??gf4r}Ik6tf;r$E}qU#Lw9-Hf zzq6NAVN+$`VsrpW@Ab$_63fIs_(@s_tOf4E>_aA(tdzfb01?eRR(*X-H)(IHZmrnh zhH4F!2073(>F|PgcQfc*GHf1$od>}shQeG}I|j(q_-;_f)*(K~P5J<~(LK8c3}I6L zm!GWsz(^l_C5_F_=fG3F50@1&jca*7;SR6M%Wa=Sbm8al3t3A-h>oq7vh{Lial~NuhioyTBuf?LLKTy5B z@@BVUJ}P$Uq5!yZY80B%!s~ECqzdvhDgzVdRr8!&1iJ`f#0{dI$XWjGi#lsegYnY< zaM#a;=sK%c6QqZN@oO8v(yP{@#8)T7;Oi!?UwjR7nA9~Ekx4KEN@ z6GALJIrY$d0)h-c>_X!v-$)J5iO?rfhwt<#dnjYRqSEG$1SvZjqO%o8mbT@uKo8`H z2oOI*<>*iLpfa4R(Xr^zBg6 zJ6?!6;A*zi?xg;-YCmm;&O<|sQMC>FBPY{&gP7YeBalmUyaQm*(E}YK(l;&m76^%= zC5)ABm4)=oz(_R@6rNobIvRe7t)J6f+U{CsDfRQRn1mPzA$lH$nwIoU(g7MovRWAJ zUIZWo3FrZWRlk6(bQWOP7%Wl`7U0T}L6H!ZRnMg>DeXgV7`1o5to=lOJBe8#JbW(h z{NTtdo%oNN7%s7a&gRaTc(LAslK9OS~#;*s{9td-}U z+7t=b{D!7scCJt;6G+FdciKt4jbVb=Y~$0M?~A;x}RE_IhRt zI75qWzX<8^{i1Vh@5wtOm2|1Z{~Zo|%&{u8Y$=q)E|#FmsIC|qPE3epcoIDuSwM_8 zO#iLsZHHxnWU6Df)a*$TA~2STNqRc}pZFgnBWEnzYuA_ht>K!~u8b zGUMga32tu=JNEOya+xK)QNot0Cm5x#h%vw`!&^rT$c1$kDI3HCgOIIpb%tQwu$P`p zuy(VFQ6n;gxaZ^0*X!C?tVxC%!zVg}pBxs`B5Hc9kWRyy#N2Uhz_4~Cab{+WNyxMH zFLXWz9v)6X>uIKE#b2ARf=BXp026GT5*94XdqUCxAf7#=$;xV>bTSlDa!AUddf8yd zFh~9WxA~bgGEAxHKqqSZ%;!zYR*98X>T1f3w}W)e>plKu2=~Dq?In#`EKyJb+~5Oc zJk83vGlDd$(^K#>id~fPb{t9r7N+Ub()>ZSK$<=~1*2eGbHYfbD@NEw-Da#CDWs!z zK_gQ+Poq2z1AG^Q4)rsw!Js{c+d3=JHQ&P>n;W!Oqmeo}ezouRle>KIZ)0w7TSgIK zRNRfTf&NN7gs{8+xYF#u$K!yi_L^4noOj?Va1xj!NDJZ-UMN#+Aw(Wm(0o+XfKH=;f2 zcb=dXD!7DRupU=Cf_GFJ3Wz6MmKk+0H#?vCv)c%Dq)t`lQp^!=2 zT`(kdc1*z($Eh2hRIa(&%<(W3-AO=For?kQq~V6#iDlgB{3$T@s+Stwr3DR$dEg^a zCK>z+Wb}c=gQBC9*9aYgZZz<7Y#mTZai}^=ek(kpeu~05pQkNKWCjx7Z?3+H{`p$B zv2uFqU=gd5IakLmF}e7Gd#`#X14{#8!$RkHW}J;n9<_gngMef(oH6F+jkm{;s@r_a zA{)T%n*1n=@PT$fpG7-3+kW49JRqjv-ER)Au`HkAN}Phl8zygbr!V*&x#tEpZ8rcL zN8cgY)BZL5Nyl2k6E_OK+^RIOYlhc&R8`g}qbgS+9ykEfBSI{zm1}(E^?wSbz;=JO zDwglg?*y4nvSSxyf+jM&JJ6#qGjCPqs1U%NUK?3(d?S(6n?3d(p|aMb&GIWVy3k__ zJRjyEVVwDoW2$y<6kuYvcN=;B{|{^L0%loN9%%2q&pCBY)vl`E-PLp#RD=5*S}mZ# zjv>$@iM9LZ7RBp0GyHi>h6K&ybm5F3PsaI&Kp6;8K}Erc22HC>P(#O#F^ZQ!lb}IE zA~Bd4H3kh)qhdP7Bw$ef_x;vBRn=TF_>8dI4;OcswjYs{28<2;tq({87fq1*BQ~L5|Gjr^;CVP8*s>{LpgB#zC9p`f;bq zCOVLgFzq^?bL#lng-H}!>rQ@t>y~$1`_0eY^`W~7RKPi4p*jqv$QWU!PLoh*VBI5Sz=s72<5b)!SBFo!5~R?5_gtV8rln(29Ak7GpH~f zfK$8-CvPQU`)-QPQQ082n@uGFE`HeY7Kj4N^GthBgXVB5@JT$rywgQkK?;TT8(X|c9=tI$`SEaE@ImJyz|v{zpIk_`3;?w~)nk$dx(X}9$`kU}7%BaB zS98e)pp%LyeT{nSjSI%CuU{WO%zu&|$PGlQ5`-i^0Pwm(1Qc77cn6@cd;?q{3MzCZ z3MiS>8}o=DqC&$GRbF9Dcf3mkgq>&Huw#~;g0oz6B%V6I*l}n(B7okDNokOcYI0jQ z4e0-A8e-3fT>v8jhr+(IS;4=BB%af{o7}6y)IO&5x{VTmhCM-bx~f&pz!3N#ss4}j zVI(`b2q#3E6}ItM2c`p9d8ENPuCqAWdRi9$JNS&30KRnhl0W{d<|P1e>w|sv>ZPu2 z{k`2MzWT;y>ip*0pJ9Jq75CeoM9j)jgB`KC zIdvw@GKIrkyf)fEoSAfl^`Lwju4i-7GNVmo>kCF+b6!(0(UuLPHr4Fo%M)qNT;t`| z40c>T$8zpBY=+AM=C<+yg$Pg*vPIIvmbJlir%k{hz6XO#x$C6uu#Gl%T}ULwWhr8o zS4h%nEv90Cp2f}N5Sg$eYcnU>f^zjR>nx>4vy={zJ$E8J{P3y64zn8XE#W2#*pUK~ ziQD+TYo;`a4W)4VtGv?1lQl#~cWD4k@Pxc*HnX=-#!oy{sN+=ORc5Rl;zRlR9_N9l zUTlUR1Jb`GfI>)2yE_E%MWj7XQ>aA}t$+chI~i&YP2bG-EHMJo+Q!)n)1JL8KkUaa z*^`8IS9-wJF`+pDNBA}@zuKpZnE7Id4EDs_A9w~~dF;exMo!4L@R+w-MjcvAZvs?^ zKl_YQe6)|xdbE!*h}7M)7E16ESqW@J=yT1d0f#sqQp=7QyFtuRX>Ez z8q?=&@p0_hkNMc)v`GEp$7SSptn3oAejqh>C4BZ*T0tQw1S z#0Vhz^Wpq32iEMIrQA5j5@X;bzN8&7M(9ypXR!K^`{j)I0YH(gdVjzv?y&m5e-nLS zueVmv&>&?_wj?FKn@U+mj?dUg5ni7(N#u-4B29B~5)~L5qVpa{7y$uT^UR&W z(yVmnDI(Y|eML>tq@n^5wMZR(=l?Zu@cSN7pwr+^4L76&Tq$o+Nh^9~PZduA=5+In z@MxwQSir1x+c2JiJ$-@|bg>F-@m&c&OqLWLtmfGG$xHT~({)qTq{7^@(&ZjZkwh+< zfgIZ_29#7g>|L-^G_}1by$w5B!aGJso6b)zaMxLYjtpXpD`#Obvniq`JTjO~6B7fi zF#ZPN3=B(|t&PZ~7`SwT%&j~ zi{iKlu}v7nFrng(OwU?4gAg@1!ztzf%SiN=Z5d%v&}b35gT1B=ED16e|22F~VLES}Xvz_&9k7{gHG*>*Kvl5}5_zH-Ga*~pX z1R6PAeRt#djxh~JN4pE8ZWj(mYN=RI3E#15#3~UrF>z)E_^vW>xzO?1Nsw}gZzNG$ zuBRai9+$$p0GpAKG&F8iX2CyY7MN~*gqdEuHEad|nrCUguGr%gFxzQ}%N&olL2;q! z_HcM~0`Nw)B$Fg*Vx}UNEtZqmeI04@cFE@Ax{_^Jxa(D3+F(JB_h*(O3 zYG5VXgIzou=`HGkrF7$L9ua1N>}<{HDuk-YXwjEU0MjIr)Wc+ja^mZGBLy(h9=T_^ z?%y_=0txm&SRoBzO@-!gvM7hB9-qBHa>tqtyyHE~Hb_T*I3)zdMuPF(B)=%G&nm3} z_4EciVV>SKuc~#C*&&6oc||88bHe|IuM5KHR&wXvcpYY5B`*3NK6b4Bm^gToj$RQ_ zM7vk5bWIXr3f#%mj)0k}Il+l+%L~10dy#s`7*~6U*(>_)XTEj04M7VOk?_p0 zXs0!tW4?GwequfCsje6{F(KeXYRoK69!gp~3i1C5!ws>7j0B?TKoRIZN-eUubZm(A zD)_({Z@k@%vlOskQ5YM4(QU~Cpp=Hyfe2;+HgLj1a1&Pu$Ttd$Pr}vbb8AYi;2Hbl z8Mmw?>t|i+wgoP-o)EXDN2FO9nP>*Y!&K1jBc~slGXNNWtrAxboi?;BPE*ySU}tL1 zBl0;%p`e#aS7KwQI+?rV18X5XNu$T@$NYdK6q;?eC&07fT&#m;vF z&C8HzCV>aoKYAD#8sN5|+2TxApKe>6z_;luZ>G-n2zBCLQsNX=5tN^SNu=h%61K17 zRHq_9zqXLl+){K4pK=DVh#a4i_QTblXcKIJjWw=&d?w5ME^{k$m%azQX@5@{&&vA9 z#8oE-Atn0aj7gu987vAetypbKo%?Y%VI@>OvLl(K#1IU0Yq>=S)ge$U$>qF}Z$OM4yh2o+UsjU^nHy#XQ5xCJfK6ZNL_vms zz8NLkY;GtB1i-O8^e=Rj4npj+O|n^aY0U*Tgqm6h=tjM%O;;{m@$+(DbEMgwqgT zThF`zu)^AB?Gg)8Mn$up5IL^F>Aj8ZMT0=k?%<5y|gc(UUaJH^wY*Y-VKg zRqO(qTN0aH&EpFPoUa#Lj>pvtrXS}-(I zX&E3=8WmtM76Enf&4?_Vblt1NUoq+^rXubsivxVVx`2%9oB4T#hJ_HAx6Wb6Rsb^nUXN90v=5RP~ZA>o4QD%e?wO;9N|b^+kk@`PAcErFOp z#)a6!35tUmJ=vV#^Fc{~DdB_inkNQ;=k04k4-$;F|6iFz;*pG6p9m4}+`>YR2 z8J4(u7#J`lc;HE|5Qggtj2yL#V@l*4KKu>Sa`-ETPhxHk*YLf5g=nz~9V#%1Q+CPo z7K8{~twTLzBA(!IQE17t7{n{#5X*EeCK0nR*ClFZJWDO=I9STeC95m!;Q6o& zCTenb@gHU;VONuEvw~sOeQbHKe_T&D5+4G?h_Z%zaEmLXrIJ#ux%#N*lBlPi1EF=V zJ>kMYDwe7<60EDbd{hO)5kJ6<4oyA=hXLL&T9LvikkQ6CcS2I&nKm)Umr$0yW2B+Mr#IOnEZt$fug3|0{+<9iD2X-bZe}`6vhLitWQJ| z_GwXE^;nc-Liz|h6u~-ol&;^y@9ulCPqdrO_j0fG={kQQ zr9x8nOmte!lMVwqskq*cuTyPkzWIPP#>QWTG^g)u6Tln;fPpJCYcR?=w0H!VQVL2C zMSMt`boD!@J=h|q`;mb=^5qGERpEk}_xw&iszC_0b%O`It_zZ5r%tTf*o%5D5=Gx) zV|RxdX$y>OaWB34z(-MFV?%NpM&7LM-_6;HJk*X)#x|zqe4%zv&NS!E{}*T1c7U$- z*Z1altq5<)d4n042$o{00|bBpVgh*G>_ejHW?3iiu(*I-(fn2pHDYaOm(604QM4E; zri}li)Z3Iqif#@`DG1p{t_v(9mXxO4_&3w#$Xx6)&4ynXNAs-v?VL_Af_*%?kj40l zviyJSDeHo6wZVOWlhXwGGh)8FAZ9E(P0ialvD+B+tj;uBc#@;;0f#%V68u%XiiJDI zeK0H>iwbVxYsf7pq9$#BbA%!^TJI$iVgdtGenZS27J?1vguyCoT#@D^5ex?~cCm|R zG8)YJQ4iRJjIWMPp5j&jl8~2&Gg-!&he0Ka0!@MLT9%u5gIQpAI59E&{^q9g+M>uW z$>Y>ZHj?tqDwtvy3m*U_HlqYES5_{^p(()^{#(l9Sb5Zu;Lud86~a$8gsMYpfk zN&g7-mmDrKn_$8OQ;wM*Y)C9)4x{E%STx{FK!B;9R#W5unTKr>tqckB$-T7%B@p>dT83$6H75W_9EY3tRYLxY6sI7OLxy z2m}6T?4h5vz>&oJ##B^NDYU3W%}}y@JSxmZ&eaY?n_}5AdS*MzLZJ}VWhwecQ1Ss$ zmsR3G)#||TW#E%ESR-gL?>d;oetijG&&EzpKTh;m7sK7C2dG5wncQbgazcp-Hr6UM zcC^9bj1W_PnYq8TbBVO9UFekL8ZJUYPW}YBAi~NUZN%waWSovV+wp77dR6xV{`rAU zPoMx5yg$N4)*OG#f4?`S(!pJzDjrJzZ1~=eQ$W8F8_M>pmlj%5W zNacX6WRx_$Ex|3nQlvyMWW=0MUPuG2nE=+*4G|N{P(A1PIQ2-nE^);BsrNS@-?Kq6 z-CI^B+-puev^X5*gM6T}&;vx04Ce10k@%)dD7NP}FjsBlbbVp-cc|*|XmbjX4k`=# zHx?Olt=ocP6EUGzNCTd@3%!VIYH3WDr5;I@(|B}6Q?Ci6;A^a{vqi2_z+Ku$%=o@9 z+oI#Y8HB7~?IlYj&|-Zce%NhLhg0vc!bPlXxR9wks*ROMyJ_(>v8jzCfRz$^<&PtV z1X(uV6O^YfouvsLrP-57Jm7p~ZS8zSRw|Z6`uxza$xrTc)5t9I%a0T<%Ia zKawP%H=uy<&|lVxkfS1@j?aOfg|`++^X#_rEd_!IBeeZ?vGJJ;2vj^X4rHB3KwJBp z)(u&WvgO&CBKLmu6&ww>@lFsc{A*W_T(o$!AHh2+y`Ky1aw zVy5L&1fRE0sYY84Z%~l^DsnX9BKRQ2I=(lTPobS7U5hyLS18Z8HDY}F4Pv$kXV?HZO%DOnIk1LG<3c!pTwKylxJxSp-V6Bm%HkZFUuaepRuVjVT#~YQZ!vrZleY7q$JRLj4JelVv`io`J(E@y9jNIQt3qK z^nEg-r!N~E$Y3xcT(V=T&v>pF;}DVYOSL!~3@6$HkAoI!r+_M~d&Z2#Nv@n;?cRnw zLRXtybzT>cv`n;S-(b*#q}8X91XoljV1Fa<*GuVo2FQm<3c@hbtb@GB$+W^efbyWDNq?xFOQr$#1)k|Qbq0F)@E|R03w92 z4xh|&8yNUadJT7DWT0F`i__RW+!Kpqg0FkT(rr3GCaOPSL9&{PJFy}7dwcu)@%HzF z+7^h{xtedxl6V!rmb=4yW`Ry!XAz)DuJRw_3JA5KA5ge0r({p%87lLBR2lZ1T|PVN z3kOFi8IQ_|dBo^A(R}a@6?rt>TTArG`|wgwj{PT;^Js_I1!^@D?at3;Km4kwzF11# zMR#JQmq%n!KPLtVRbr<`^$~HEVxNRQ=N{x}0M6$@cRVT373dp3+lP34nlhcbzB*lO z&w@g*x7`dP1P7nwwAE`J$IH@uBnNdLdI+;5=r_^)^WF3$4S+n1>vdP^$eI<^*Sye> z?i?gJA914klAWY>45if%$aLT}dm-Eci8O<0x%zFB>|9PO{ueoKAF~o{r`3aPCG*t} zQx&PsZPoAL4u3>dNSu$}pVWtH-y^wmCaklSZF?e_dKaq!evA8oX`_-o-ntZjn z1eNM5cy<(HU6|&~IL+q=izLrI6S_c;5)Y!M;x_PS663ndHIo?E!fO27NgqG2oRkmF zmM25NB>#*ree05st3`vV;tKSh20*srpRUQsegx4W(LBho2bw6eMu!mD94bXUk5E3% zE#&O=UF9)pAj`pFrHyz+5TAL&8zU)tPi`0y)FG3y!HJ>-L}W~mY?0-wt*_`T`DIEF zN2d@7D948^wILD*#bxxWHENkM0>q~jVZAD+Xq@jA?2bJ|L8r%|#6`mq51ffHVe|qb za6cpl;L*e4K!cnd!ho<%Wh2PXe}-Sm`56w}9guJ+Fbp;8APi6>Qp+}O9;qt~ z6mR{}shjb66855IMns)}G-l8UaB6aKzc*q6+({Z6jZg{VQw);7&%QaVmAFNyQX$YD z4GWlvAY{7pm`j93ZH`$pu1i$G*re2-O!&)FIn0~AjP=f*#B(h>kBtlZCe z&%Ev&Qis=|&7!5!9zEU#bYNqlo|PZ*;EX0$H%L7}63{H%Iuao2TL5mFt9Hgj9>l5R zmS-!3`-xt!Q%p>B<9|u|1d7h-6fY2Yvw zaIpFuz6c(58mvIAa^;}W<&o{9oDrl+=v%!_Z#F9cAS}|rxEksuw{05Yw1(JqCcnit zcf~hLJzbQJH}|QeJP2SmubJ889G}NSp0*-}4Ms zOf+HrH6{@m1Ln_&MgzrP@CcI&B@A@g!E}vFB-3q35AK#~Kf;P(jmpb}dn9t3_7p&u zcZBB*V#g#Ym;xV({h_$i39sR$UJyt3Rt~jlhPCpR zUNT3JW3hE#P!)h=A~g-Rt_KZF04|wQyaE>WI%FLWMQkjx!sUl8Ga1gq(#x07BzmI8 z2oDZhFE#*dv`niYEXYgGGDTIc_E%aGTwc;l%bT{4tI*W|l+@eh9h!|-5M+iq9~uty zC#~gf^Y(}ESr{A(O*RY&3ZY-)chVAv62zG=U9oIU{0ZSF7zFLoQtXKUrHr8rZWp~s zMvDTY1tTh&>!Zpb{%UY^Mmu3DPX_^gbr-?mj3LIK*?$AKSI5umQJ4s<9l(D#I|CTL zC&wrt+C~+eZ$lg!mp#){l%$%7^^sb8=zj%Ka^95bv*nozI7;3(JN(R6vmdJsqg%*%*NtVeihC1drk-F7L zPth;0KE1(eg~#m)w-M`-drJa9OO9qSYLOjtGoWO;noi$1Xk*q<2#RtGpp4e2Jp*ER zc&i&`%fCS%4~A{ld6(!a_eD2uGq>^~7y|SmRRVy1a}p1rUOw>l zQY7h1kjO)*a-W2jS4A+oVG@^3%Mi85_}y~O)=;GBxJ?#M!2ru#Io93Kpg>AsN(_kJXp90bO0HP37;Oh?2HQ( zRm6aD=+mjD~&lKMi-;##27mCl|8dK*Mw@D;5ln^%fft(2RAgp@_2e99M`5&E5b|#d|EF4yMpTsz8r4R>N6=EG~IL%k(E~$1Ikc*yHl^m>V zCe?*v7R(yOIQCnnQHUdt0)bfj&{`l+mjB%6(2@lESOO%CWGY(riKKcy+m3q-m1Q6y z8USjLJqF1FWF1uyHP=^ACQ~ed=!zrI1Lb)>Lr7p@g9`(Z(}abR!B^sW#(IT)Y9_OeDpp%bfPv&w9OVqElquOi@>Y$LjPalmhle(EeB3_HCU7F42n`I$DSZ&K02; zEdA6=OQoYw22fi%4IOSf{3>rHlUCvIH9JclV$kexO*EQq$3Z41B$JR@eiX#p0VC3M zQ;qWBw#$ch5+LJgRzjKxe}DAR?;hSq3&qy8=tOgKVCSL306B0Izh5xFEu{YFj%1`= zOffZ4d5rv)4IvbWDA5PfZ;IvW0ln&@8{AIdEQ5aap95RP0|mq#vkCGj zSgU);W6pi>v5;~G&F9vkU)#^12_c>hnxA0!sQFe1j)3r23clN+vSV^Y{BL<5J_mJd zs0yJr>Wl^ZTlkS1;0H8d)@fqRH0^1Lyb0WpVhIePKoK4z28Xzf7@1VVFqOK?MjubR z856lj4IO=}3weP4m_qNi@UpP@(#%_#yxVNvkmu-({fRbE9LTNmwn<--PBv(z=Bk03 zX8O`>5*Z$1g>t@X2FsE;Y{%cvlQV&ex-9mjjkxNQqA2lCmh#O_eWB2>BZ+0L4VHK+ zAl8rGkoA|2VT>Ioksj;Bp!Im*VdIDX7&_Z?w5G3#p1MKSHr?-_eE8Q7A!*Yxsz8*E z_7zrIDVU}NgWGY!zRgr(h2o{g83YAEi{C+C044B~Cl#@yUnb5SNU>y{?M$t(FT05Qoo$>WP)|5168G zMz2{*x=6tm2Ee^4r0z}zPVqhb!L}&&g9T&jQu*$FK7DEPuDp3SB*@ekbnq2yg;K|6 z7pKr5ON*`6xVor&BIrB|RTX*=DSEIspKE@ecz71nPZu;vdE|*D`{6VLi^%=xTA$@C zTd*5J$I)_{y*GPMyJjl-s~$o1&$G)MoIsalb7?3@F7b};aj;-4y>>XDI+gOB_Ca`K z9OgDDM*&k(wN|REnAk!O7MrnD$)Pkml*wUbo}~_I0cFks;<(M-pd;`Z<3_k@@_GO0 z>JkS6EKXNQulbAU^%6bP)*X=UNwvaMMUUDiUrohWK8zC_Y9C)2A!DaHfF+)ei_5C% zKahk~#|B?o+G?p&fh)5VGLt=@r!jI;#6ppOY;1sG%LefT!xN=lL#+|yd5MN zz*iw&(4nAsF~UsIxE-EvVW3s>I*xatGhxFK83b6{0l30qAx?-$Q}78^&gwJCCj&*~ zw@0WO>9Z02s!I+7%9e*EIOAcyAzrTnA^`44Wi%3j;J>x5v9O3;Mybw2zE!R^aOpL+drjRK(OOj_P>3kjn_UQz6D~w5@t?jM7X@4Jm7|KFUDWXmiS#;?G zOMb9`#QIn5S^u9bM+;;`b?4n2(MIcuQ-u_D1R>Dcy=d=j!@_a^&5wlFIvo?y;eaH} zqcpne=w~v+b^}FS&Bz*N?qHYLg7}DkDXz7k+5+H@a8rWsjGMW}o<<*Ng(;#I2Fe6; z!k3*{#Mmi2w*$!%G|{{d`-~?zI%e&(amBW*9ZU>dqPlC-_)C;#51C;xoIu1BHs7P9 zLg<(dm+-XgUMna+?Ez|TAao)4y}|S6b{H> z2W@guVGCct%ZMZJslfQ&jufn4G#-SGZD;Dlg~eheAJVwg+X-mtcMDJ?Y9@{DanYLK zg|LnVy8z9n?N|kMM|%aHXt{E)`NQ9Z_K*vxreYUss#jBxjgCOU7Y^~A*2ppRrP0VH z=&L^^DG6YtKS8VTnCRxEO`4m>LmyrfFhf$$mX&0pVJ6dHe6CEPH-}>jPyoAQqB@=Y zwcRyY$ev))RXx@37>3;_4PABtLtnyBoo;~vunG;shy-beqEunlGY^LcA@Qi0R_c`O zFh^V%ctVJ0GU|bG&F1trazqq5t12NEA12!{CcML9tkE;6AU&gbM1&owNObB>&ASi?m0bU{% zmw8!3Wjpk>gWMIjprs&URq!tMXz=TrFT*lu=&ke)nMfo_axqpfCWdngz(P*0Wu`;O zWBxs#kXhDo-3zN%GOA~RMSZczbwF)wRTPyt9}QHXMb>NxfnJdhSAc~VP=kZrN}31P z>kt9FA#7^gSJSwO{DDZoBX9?SVby$>Hc`gOvY3026aa%F&Is~47A>Kd9NHj2&=5#Q z71D5PZfjSG>yvq799qYnQPrvN`H3QshwX3*%(ei%2&$8iI~=pYEsL9lvIz>j%Mi^!hPXpn>PQW1QI&uB(ybb>mNz=4Ok`m{wl@G%K| zRGJQV0W%6UCLPXTMyn>Q*?QZs=g>OIZiqq&3oKuNR7zK=G>k_KeWy3E8WFGs>-JIJ z4u>1Mg?P&ZRiDJ$@l|?)hA4?yMo3oCJM861B2C zE-L^SVJ1A|X~@prDsxcO5O2WfYHH#r7 z^N>Uf#9Q$hiZ|8Al!9>W$G`j<4f$*^U^Tl$F{FD=#E$XyUL_nb5{S5WA~krG-=ysl zjnP9a8KBOzG4UbDq+$>KPLF<3BE5_dd@bwEfy^z#snC@lv=9V~X3>q}A=RvIW$3V# z;fP|X0M@+Rxu^OzNhB1k=d16ba(LB8`%xTsWzk=d_U&-Rk`V6v^~W=1RdtElFXO4YLlj z(HLnUYT4AypjvS8lZ#DWyo#rhEnMCr{#{W^|K`v z9D|;IsEtvQCJm4P`NiU5;#+vN%cFok@#Z+|5W!Ky~z~Yq6TdTUS#a zODPs89>h%{|FL2jeC}>~VBLvWTxN*;gYM{a_UTu*`pc@hH;c*h?)`&dPOE8#!2~KO z2Ykf4)7c!9Yun-`77-Jr9z=QGLf*czsrq=NTjhxqFxC!3XD|w~zsAnZEO-E~^Xlu> z2rHJyg0CEiy(|o?dFa-HWD+=UpPp;6tiFLXEmBe=-3EWD;mm|Y#N>lRZA58xupe<~ zAk$5OP?x<3I@_M4A9W6voW^C>!3+T@ z8>Q&Ub#I+MBYC>gwldd{&_A0J1V7$AnKC|7Oh%0n2&li3{czWQ>r0(-Naqqa3}gms zi3Z0VEL)tW2tR(}%M;3ZwwXk~@yElvtNBVxNP{G~*SV8Per z&4UD~k}~P(T$g1Za8?fv0$ZxrP2QLuyl%2Qj2D_y*k?nhkI&|`T#-#Na499l1`nEB zpTPbP_g_aDPNI!VV8*;+uGLW-0dc`F%@DCno; zSLg90Fs{0j#RyWBbCiiEdb!8X^8DiVDdR}xHs|L0NZ-CLmAkQ=d*s9D5yMPxzgnO}EVV`>nHn^huUE%ZB%3p9CWeIIJ(+OM z78H~v< zes?ydr}cvf^N$IcP~nZVwW}4ybJS!a%6}{-rg{e|f)k=&dGUPKc@Q{64N$kvLMdSk zpx3XHc&<~vyAAEW3*mVHFM*1lWQ_Dg%i(v|1YZwPtPlOKvqVW4r#2fRtHB%4Ocnpm z!ef2MwPe-roAgPRkyrO{Vx?zpP92{eH$qBAH*2oF3tOX3IMn5Bqsb-$#+q+_wtYI; z+#~;JKJw}ILBA>d>|Z|B-eZu)ep-F39FY)DWzSw!-`U{7NK~rX9=w22)w9Of6UWHZt1f74X;+F3?V4)E#<49A#J=}o_$RRkqFmx>0a zRv@Y&!q&LXAi#uCw1s=r-zP>ZEJKzY#wR3|gR^M^h}vq(`~d3_$EwwMfVNpHMU?(P z)TigKCah%lt00Hf$#6{F)r|Ejg7QHfoEU0i?)lDxBDQ%MMT-{r_ zdR%^pq9tatdox$>)!$@`A4o(M=elyR@|+~WLK{`j#wHv_gS%0i!ZgHo1<#VY?_1sG zy8uvdC+v!UBa%5Z069RWgES~Mx2XcIIZ>W-`+ zXX6gP)97Zu0Qcpvt9?^W1s`a?A5n7JI`Orhl35M&hqOs5P)S3Ff6isJnw09XrB=CH zmC8;;Ux#!70#27XJtF$W+kmGA#->nEkN2mccFI59jnbJ#`SW>5rW@R?IYebx-b@~!#z4Ia*}v{=oWS@NKvK7qzf8ES+S#;mLKUPlWQoK1=6)Eh@Ld*< zBr`OGb3=v^U3B3@uD2G*w8_J0Q>zZ3>?$;f&iq+oE-s&@E%A)lOkV4@AJ0q9{H1w$ zMKXUS{U$ySxKjnI)X%!fhRK}U)u=#=G2|;E`Z0$IwJ_0$sEIOkOyUb=V3h>{;f*Ar zNP{|V6+sy6*JnsVa=JjF1a*e0gHpS(3xKk6B5OYXx+!#q=AL(dG%vr^1&un0-XSdf zeJqZ}f^AqlxT~oC9%8gu+#MI)J$tiG8CfFro)gR#qN-2shw=Lxf?7HF++9rsxIWiJsec z$!Ib%=>&@MOyx)t!jLAbzfq&7l6KK|n~%T6$jFVo{3dtL7tKnX)~r=-rL$+>rK9pj zkdw&M+v}-IesT9oe(8;v&cb-RQb;nRWd3iJ1V!Z($YlxZ2??VTB&W=sMzB#$I8b=z z7fN`KzY|Z-Kh*wy5G^=zk}2sT==+Zm7ai6o&Br(Hj2j#35KE8&YmDn-qjD7a64lwv z-38q+mo_rrAr%2kY38BzXrf<%H3~fY3mWck`+2Xw`RS)*H)A)iL-`h{Fzg_geH&v% zB^{S9qaeRe&)KgB$+D(UwbKy35!IapHX%|sy`;I4nLHF$&lrttog#t3fpR3 z#LDF`0yRr#3{W*U!uP1K`qPctdVXBfI2i|oRmk^N6)bDv1_)d zK1T@UBri1b&6O`&wfLw=n~*lqCzvWOM`KUI0Il^!F4)ojXABK=v8IUzvl~|Bu6@fiJc;444E0g0u*r&> zH>bxb*{n}VHttrShaUPL7xbdo;6i>rf1PA-fz-Xt*I#wc1tVPe0BiTv-Rg#PzE?U? zv}l-xY3U>)F5cdDJu{m@8H(n<(bKb^4KjYSk=+=yFf=}75R_8-1?0s#i$`ocZuah- zYEF#9D(DOt`cpjT=RaEMNyQDK$o{t?^b?mEPhMOX5|it}NPll=e-FYkyuV-8ar1T4 zSP?Yob9YRq-hI!j-n8=SSyD3Ay!AvRZxEqAQ+O&v({&#jP2pw!$Dl1uZR z_^IA7=T7c`3u{>&>iQ(#H;2h3l)C#d(xgnhpW zxutk#*@S)nHR}LJ|7&*@58I?6gv{m35TT|vkPwiD5UV<+!-qJ+@$2?bU3`eiUZVXj zDzO;7vT*h8P36bCX>T1tSE#ShqdPbTu(i39d5kH6^3MbA@!r#GAWQ6yXhI~;1vjCp z&9yr|@+;4H3%^VPS18Y4#f>Zz-PPRpTW|WwS9mH<+@IQ9^QFK2K+c0x z(}ShveLwi6d;2_K{C;s-^U$5I|NQ^t!D%4eU)<4a?z+Q{UB_XRbDm4o9QX{np>qIC z#E=$YljomG^kntAtk14|Y*lYH;*UWhH+kXMKrB$w3%bxIRFS6z%?`9>wNl<6)Alsoc zl{|baof`iKUvEG9#v7@qdDYb`7J1vF7P-Eq}4Q^|-s`a_9>&11yf*X;`+!P`@(+*6M)LMqKt9kGbduv|T&$Y-2iv-?y%~3Pk>rYP( znLxfy&k^R*HP4G;DeX{#%4#TKRsd#lrvm|{%yVR zzJK?lzJCw(@Bb6>RwS~ND!uv%tCGO|uWcO5fo{sQ+F?VLeB^%5Rt0xs2slhf zu*w$|;7+jAiRI>hne-Y+Vay*9So70y-MI+LZPEV{bA?A!*)0I!Bz_tZy6L9q&8Wy^ zEx?(r!Oh^J3)x9`p|!Hz(xkSmcG6=b7F6FPTPmn{Na1=yOKWtOiPGXazo+{6oO=S8 z#n-=uXrk%?6)9%9GjCK$w|NsL$__JZtWzJ{!eiTFI0HSl$UgS8jwtZf@2aLzhQYy( zK`ca(2ot!d7yYU%6~MkuE2(W`HER3E^A}=0%#2f2#Mp(`~HE2gRTei#=HPt*K=mzRqh|W z{})|YeZm4Fd3i&=YFSe`b@~W=_(f0i$eMpSm}*m?L-t%-cdS8}U2^!gj~%m*6|Gp$ z{67rVj0Dp9dvj_3A^0GD&71Fki|-d!*SO*deH0WH2*e9&;amU){oT!{exuu5`}0rUP=_H$!Y09k}}1X#>k(>$^8gDne#U!WGSj!8yozqTJZq6k#Fmw%l<{jG3kk zybU%{lzThuKjK@)4#+@#7DA)ZirutB^g*nBmqs26P#z~3y`UVuRIrI)#U?DrokN1Z z1Op}Iauk~~9iokPbYEhh(m&9SP94dthw!~jT#YcN=nlQmWJhc2r73QUMT=OT!n(jE zCOA%4=kJi}yX(F{Y$NK-DwFQjmNwrPTpeLywQe+$wxn~=mh-$k%0(0!xIT`VYsW>y z_$SQ$K(=AK0l#Lbj*fWhR{CFioX6uCe&Ra=+bK8gash#Lh_qr7E_XdEkfeUYPvZ`FG249S!K>8=;vBj*`V4&L`FS912f#Kfv7etY zn>7-G^WnLgot=*eKTA<8iH0!s)!$UDW&Y|ZM|Ay^nh@BZ6hwM+{Z#n_CR|yeUUg-u zH$AL73P`M3^F)YNSl^j%SR%9LI%Z`0gr-?KA1_@-T)AX`p4zFZn3v*q@;FgA4V|d1 zFq^SqgBm+SK65E`L@;HoPV1G5cEX88>*T_PK+;6=SP{T-TGsyW=fD@TbzwcGI}&=W zs9uYkfB*W$vue-1HF&V5%`yp+*s_rFZt=Gn@rI3i`Qo3B_Y-cIvh&ryG8g-I0e<~4~h-tiobA!Dlvw%O98`vUcfd~`v zae8(5TpE4bWpFv_)1oZ9a8w(HwxXhcI2uj>ffDdnOo&v~ezqdj63gZ|&EO0ZR zVeA@=qxmjS{2~kIIzjfeH=`l#<;q&wa0Xm_D^-ea2I6`0 zu?X+nF(uhEFPz~o%{ZaLl2S!LlTt? z!Gr^v!c}aqhYn-IBPV1%<{^KbR5%ZIxjUM~&)+alc$7!|1<3aC8QXr@_$6wl|6p|F zBJSp$PELq3n9BpWaW^lZ&zu7|EAGjN`eiS2QAjKUzNmOKYU06mFqQo80( z5mk~z1&u~o7JrliBY#3*M0%tAfJ4*7lXjBa7rY@XE*oj9pf2s&qeqKFhz32MKOez-pBxqlZ!73vvN+NV_?TWtbgZN}Z8_;Tz2)I% z)Bx6_2}O=x=AbJM6x}o2!d>s8W~H;tp-XHUB7l$kp;nEFi_jWg)@)rK9^P51Q|-?o z2yP?Cla#{G9Af#ZkUYwt#h5oe$EcUU(o*6AfHnX_zy$alH9OnJqsBV#=pD7>B>-uf zH6twupf+!{dO!fa*ul4OQP&8uSeVS3Fbx`&cIuw$wK(h(ziehC-UY4kX6K&jw_wCD zQUtdMkg;pqL(fsR++K1H6C30BXf(6X4p$0Uv-!RH@6Ib`8U8zrGiqiQI9Ri5HvtG*fS8)AE1g6X0$cn76VDd%*h~u?`66wFVqwb)b)Rbw1?5eQ|6xXG4CP z&S@u8Typ@AjU=?qTA(jsP@s#k8(bQghXJ{b)!P}^38?aBPv#rG zv6H}ud`qlVo1IykrJ41!1VUA}(26+EsG$r<$YDwbBS^p$*Ekb`#<4Wr+1aUXo`c}Q zCYsy7)g3dWG|5FQ`65^1X5x!j@L8T`Xi%5^BZZ;4&EH+qQyT!zEwvZVN4d1##SAO9oBvx0`tgt59ij{u;RLpS|T1q6rkr+H7yEIdqRN> z<$af&+&SRUL_G1#0Pj{m@F;Sieg~39>&sn4N3nj;3$(mBl#c0EFK8A6aH3kb{Ew}n zk8D6dcsgtwe3yLtbhmJ^`Qahkb**|^U@k54mR>o7+7bEL9B~IE3@&dhvc&iv?QdPo zmRVkOy1j}1U~(#4D%5)&rxIiJaCgPDW)elCDSE;y@OAZ-QWdX&EU!*PSgSrY$w^w? z{TTD$9U&?4HRQ^#ahdvcX2tZFXD70*EJ&y)cgkqqMW&5dYu+J$M9G`t9g+5YV^g%P+KFZf}kWOI-d zvC9lYTUmauNV=qZp1PF@)tksLX!_5zVpfrPs z`ZvU5F^dGHT}{*(*h=tXgJ85QvUdr9patDTL_gvqYA!7)6jebLtT-!BQmGNH{s0Oc z`fwZI#llLp%A$&wvHO{lMU$T`&NOep*qK%L8cIT&Y0D6#BFq`pmxxb11DVrT??;4g zNbV8u=mDL=%ngO48apPriV=bPyYuRAVFb|~avxnycTOZu`)jPf=AVC^4xd;oaTitW zbfDI7;`m}$Tp<4tAR+6ucIt}VPVN2<2qW(yj(K(KTzTJyqMs<$-VSHTV5~DZ;Tzd< z^Y%+M*CFf>##1!CXD%!j2d*I=K%7;tE36 z#pyFFn|_j!2HYX3ARjd=2msT;+D%e^L?k=ImRV0jW6aDviBXtjd}4#yyh^;7d?1bx zaLhT{+$!5oYIwGLllEl?FIlK(o{}MkF%Cp*sdkyD_TY2^>UoL<4NYw9N{UAWT`wN; z)LIP1UL?p01o^A0$fue;Jp*x?zr3o)DmW?2m}5)`(FS(NYGt#cu9!tyYa)x}!9>~Mul4$!tuBe;=@iSLSNZ6d{{nooY*kg)yHruAW>5~M7|`1~+}oY%g*z5Vh4 zUn&8?Ihq6UDYPaDGpae!Gxi8HZ~TK~>Y&$C+Bzb~wOeAv2ob3sfZ|P#G5yNxQ@|>; ze!Pn|qo6E{U|o8>w+*GB$-Ok0Ho*pGp!NYvC-s^c$bVr(!f+4P8YG^8V`PSChL9VfzQQ)!lHiR86 z4|4e`kK^Kt=+D;XN6o{B2t}Oj!9v@N>ig7L5Sq%-p)yA>^2*xqR>s2%Fx8XCbn25k z2ceG5FWn`A9AxL2!WD>&uepc@?O|KfJ!*Wh0GoTFBsfn^j!nrTYi{l6gp7e~2lFic zWp-wOdE?@ol4t%)*)E_BnQV}-*HdpN!?rAF12Tcy?k->j-tI59nFBcxyyeS6=Kz)d z63aD?@)cX_fr>+5{ffn2bG?IFiDJt1WAup%KiF2d-;xjfo+UXQ&h-(mZ)jid_4V(> z>%HUG%##+5wrAJ2C9k&RSH5FKSErJB!c2yl@hbDb>OBK7Bn>|%X%dnL6}bwv6|kcM zpH#d$UEkyN<>{I%@@$Jw*Pr+L!9bXXczd?Vr)x;I^@o8FSwaY~Zuh;}a26@nl5rh_Y7L70=K2n7 zl#Q_oLqW;wJ0Q^5SjPu-!Tb9+jb`gv?*22$uCNrqGsfuP+L)zNq7y|!B*>fov+9AN z$*P%+i3gj{{uTRim~4(|rGn-jNEoPBVi1mb+s+SYQWvVB{nly{`5Ig5kRj7w+Qi_B zJ#|QkbnHq_I>A|@FsOuq`~X~_k_*f|e zOf*wXD|pN}d4S0|#&mPC`U6g1RVgPE#)ABLZ)fcCs*wX*5r9yyx$@p_F_lCBcBxDv zjj@`WV-)Zc&jCF_2edh4!w~MVfKxXNP>xzCTs8+X_DZ1~spBagDp5}7ME(z_z#=iJ zV+Ozu_|QE0nY3O$Ct}^(Du|ehL<45eYH51taD;7zZWHGT;EaTntvaZ{5L=8M8Hr6* z2#Aq0DZ*_LWYr`@c{+ult0bMEK%UXJdZnlfRJ7p(tl0i%Y&EFF-PuKSYU~V0aci)r@)so zHdfvC+Sk18_B+ghN-o0D^s7(hY4mD}a4dRz`O4LGogUM~GN47u<_~fzf>Z2)(_-pe zI5P<1v5;cenny#Hc1GIdB6^lbomOFG_>zmM$-Yi zdio=$W%0lHrFT7zuyGU;TAhrIHZ0yErC|Umv+}Y$G)wiV9YP?aHAM$FAf^!FVX9Ar zwt034vlM&Y_~M1*i*v>oFB)H*JHB}F_~NDGi}S}9OH%yULS9~zCl58=tQ?f5kvhl? z)+w&Hr#&5N!fG6VJ%tysaVZ))!gV4^gI_yZ113TxT&3*c{E87*r7I?<@*JkBC7)L7 zu+~+dnZx-JLTF$y)zvYbntvD{Fiv2he1s)w@pmBjHp@5mi^)Xd)_X76DVa*)*Wl&9xB-_afCps1*Q^x}!p3Pwt#! zUk>jHvO6W)6cV-HNa^^V^X(gH5+~#QF^!6gXQy38*qukYY8Mm=i-5jZhz?>aEC$6Y z46IB5)?~tw)Wp~>lbH`4SZz~$hSn;`%!~^(gW+@s3xZ!d*$D#wG>K$6Ep5a43n9=# z&B$n~87w=zJv?c4V&%=~Zr*${jMoCM`M|-5=29(d@?lt2rhJZw73v}Z!}?@=;Ck>~ z^k~8oXfS!ASL|XVaRPITY*9}fQ=F>>B(}D_)M$9DX)g@T1upl8v7&f#j@*?c(smTP zuUDjxPHr+h7>8+=K!^|+uD$I6ZfP@WBDK(IQ*|EM)As0RY}57x2-UF~4Q= z3_qcWYl=%(Udx>|^}`R5u)3&`EXV0&ot_j^*HBSMje|2R3ZxvXcj<^}nI-@tR|4o6 zsLY9Otk<-nhynsFMSJjaTddta^v*DipmmewPh$aM`UBu#%L^OG5$Ojo8gPTi8o$GK z<13w2Y-OG6eaD`}nCU6aLk2`v9ru=dGtJLR{qtdG z;G#~E0@!x&)8NUEelVFYj2Q{W*CGD_*?RLAOBL&hpF;@gc#v!HY=I@k^LbG{G?%D9 z;i7<{V>8g2E(tj`7ImA6m(_@Nw}Y6xS!$<-%jnRO5ORe9j`;YP&Mw`sR0FFO&~dfc z4jc7%m(9Xx`G&;~m1LMkU$uQHpDTDF=D@1;!%4s~1r9UTC_tVz|}c&jqcQ7obYwJ`6NJ)4W1hs95{4B+Q{3q%!={`mGAe1+@LM z*!;!2;l)fd>D7!jPNkGg#WZ>gN@A=+TAgrZr6&BLJ7F%c_aX2wHEi0U35|f}ARLcI z6b|~6VwTeoagkF082x*$4!u^fNQ@n+*^wR56Sjp{4kSA@nq}yAqIrE$y=IQ##D3WB z=}M(9;FO10%fJVu9TA6!1k7}W2N(9Xjqy0tX7O2!2fT|3;^R0SB27(Hj&R5l*!r^7 zdXQdQv>@A|fS3TiyE8?66H}%P21G#5w#QKDQ^69HGht#GrS5ifi{o(v1EuNep!rJn z@>o-}{_-&DJFPDlf{>w@VwxoBlN)BQQ&XXjTVaZ|&CD?o)IT-~k8i*#v z#XZA?nypq_%!>;?lGiNZC2Kr!ieNvi@!ZdePl$PCyjA3)fQUjgV372tjk>_A&2dC? z%0m$K8_6+CYpPo>MmU4J24II36cqC@7srray8*C@o2zaNp zo(<3GGMGA=Y#Ap#OQM+Gl?{Mr5~k+eSE&IH+ z>bE@3>d&J3!yae#OKa6{ew@|sqWZbVS^dtn>Nh>k>ai`?8y{!&^;-4So<4beGoH%+yJkFn^7v*vmHl_kc>3h=&3G#N@0#)S z$>W)EOO{XTREPx93Ec}pU&*P*0WqQF=8ADg(LSk*>`MC>W^)t7XIxIV)#Gbg0lVQ|I|o)^Aa>Xr849iS)^90Q5W44Zj)wV zS%x^$u`Z_>9woOzdyzO8R2Nn|xfMJ5-vX-=~Gs2AWeRxl}a z$Eybqj{*~IqnGc~X(`<&fH3rYRR17g($`y%epg zhLn|Je1O8Zi5{Jy$z4h0bGA|ESo|-PWW?M@MhANDxDi}2ulT$+Hm-`VA^@`eN>KuV z&n)Zgp&`L#Sll9Fg+aTorc%1b0IX^Su5+mZCzz4}URhr;wSY_^nY&l0!?uD^ZoIAy zr{#%OfW;J9*i9Si2sX)HV5W|%>Ic~8eA5}C_(dygPb1S%Tkv78Vn%RXR6k@+z_<+K zNgK}6EpO@MgS8!Z>-Ez|)lOsbLIr8%Vc-(7>Ts%HXv(W#kfg@#(-|gl5)8+pgl>nK z=+I!y=VF_2#{ppMrnnGret_y{5Q_92!v+9}%qnhQ`r+GT6B zOAUzPTT%nPV~gpJRi97h0{c2mq)K1}*lCeWA$ZdHz}|{!vxD-dGRL1K+#%tJz!ky> zVFGD1$lwl-HHSZqDSpH-v>^Bj3j(q#Ib1bHsUkChj4hJCMvG`Jon;n&O3*5@#FWOj zZim3G#J|7IaLYlJ+AWwJa)p`&g?HdPCWTlpYV{Gr6&3(M%ZQAlU|Atq2fhKe7&-Rv z5=7>E%@)GTwPp()x9;Wi3V7xhgu(&sEQ#Xjw)is6!P?q09u;5oy@32E#9XjN>h8Ie zNh;JROmilgH!K(v2KguHVZsqW!%eH#JQtKtV`wpim676x01~lG1?(Ie(<^_q-^HqW zaq)}eZ3%sLOkH910N_+SIBlMM0GEctIXos)1|A!&9%llWW_45sDx$P$-qZeeqY)z-9kG33 zlw&@S86uNBdE@G7va3r+2i522lEvXsTY(I;Z9F&_&3p)TjYAA&J9U;Fe1;bX$Zq|N z^o+YKJ8$7#AHq;OQ_p_vyoJ$*U34KjwEDeWj4I;G)w&+NiWe`8X075;M=@J(`03LR z8Ab;WfcQlKWNh+#-{;!@F1?IxegK@st!Ywfy0#(kG0Ix9uK0>MOshB6SKr1ZElK&XLt2u$m`AAY+E;dvuQ*-CuROto$*r0S+rK2W6yO0P7Jw z?I(YtpYDtv79tIw(3!M>gSIWot=%0>xFuSLi6RP*h=g$^wJ>EIXJc7QZ(U8Gf9m=~ z+E4?AfRhSNyr2qP3#V~K2Qi%@Sf0w-8>iS3D$@*m$b_aqM48XYI z_$;f9{3EoC$hGR*b4>rl>8dI?e$a02n`5s|b>4-v!*2}?F+C$V*BrsQFmxv^`s;Xv=Y;;DdKkSo2xgyb2L|P zJnzEO>BpuQpH6?~e(u89vKe~Y=6Vjp=%!({+MjlgF^n=|vcSZ2fJ>os%!mgw&d#nj zv+p~q?zT&dOG<=}qe=aV&A7ikV?dp>w3v43E$)ff{vWuLMO8U4V~vLDm2A}*=b5~* z(cG0^R0jbqTB06=M-hxho9GXlK2AJ8%>N$AhWE3$Xrw{bgVlB-w!xjlTWSq3!!jm! zqTZAySMd_Kb)8qU{eXK$U4#z-#It*(lh&)$_5kKUmEE>VdTdhNO}11+26b>U`gJ-Z zQ}PdUNKK5#3nLxlqcJfEH$?1>3v!1bbP*j{M-M_w4UIEZv>s?R@*II!r`J;ml0kmb z2@Y5?tZlWUjw1%qO6_YeSXmMkTySdJ#TGi9+GP$(oc#MpdFnBh3!P#R4n23EE9?qiF{d5?GaewJT zU|lcp!(u0+F@5jV)5=t|u#vxrpeQ~ZW*TjcHoC-FkIWTao9M85>KSK~sf$-~=tHLw zw8u`sM9o&;m4a_@gO{Re1O=7DEmX9Df3uw>W^cCM*s2Na3R$j?U=%OISn`W6^JKpx z=^95DGv=cN%-V{5(%R({*De=<0jqKWeL0F5aYJ zKh6!0Wq+A>FkbGk47w<=F0@}Be8%u-LC-%ZB@pY@TYr)Xh;B?I=L$pQYIEUh(^9e* zZA-lw8cKSNdM^F`%mQV&|Jd-!yqflMbUZK9)d@(?d42rHh9d>I@#hRKKIfOwP6(q9 z^SK{+IT}Sxak=BgRD5;= z==zKp_DSbDG%Ht`RML`0Pii8aWn1X%>Gc!qW6ry9)LURf>;Q)X7aM%$#W7GXfR~J; zd@q*`s3P>oWJa6A;aI30)^+uOI(Wy%z5Bj*>};m}aeqW6J8}fO7qod2Gar^#($+Ex ztv&OEEO>6jT0kKWrg%{OmC5*4Hxjlg5)7+GzUC*i7!;%w`cvSv4P6FiJHN*uDPYtP z#ii)L71UErM=@BVODHmENZIENQx-o}DP&)8QM%<*~ zWaUwGnTWh$zJ4f-z8#{IW&)Y!<<AV_*m!B+M!JBrsD^ z9GE5#IQk(Rin-C}j_p;YxZAwpY7CtA8=097HVDWeo~I3hSenl#z%Vt~*{oi{@SJT(uf)LAI4cG3g8E$0B<^7I2 z4;y=QShS1kwPE1_OkFB(>33!bDhx?<1ud%^k|+foFF%{1H!vcqKBnlN+&R};nqU5? z3f(=ZL+mlb5*`W57i|mrO!MfF0ItUvtN~53Sv8_?YFzbCo0uoxaQ(eas4Tsk6rWmi zs?hao{zcqP2*Cu+j5n*&0W1BwnJ5hS;Lg4>Y$rTx{_G*5ciIhYnv*GpI1%wnMpJq= zDkaSh@%k?~40uoMexsath5QDof#Rs=VEs6Ty?%TGIBP!n%X|C;(?i-Y2mO;+@u!I5 zp_$pbum~qSlosy2tW%)5p;+xw?2X_i^a^rg~19L-`A^p2H8`(--nHbyD#>Ahz%A zOou8hAkz(0FeNK6_Z@-=Sxmb$AF~{Fww;eS#fwmElR)?Kw$~^r z#}*g;G|e)qV~dyn%vyKPO5Hszb(e~oAG$kHiH_a^U*sBXz5KwYlh$4Vj%>^ga%3DML84McbH z<^Dj_odLpjYYFO34oM*^-uQqsxBvt!K_eC~3zp6YrOzLyJuMX3i5 zEKr`2)!C0^P~acQ^4&YH@HgAN>x#?0D0VNVi_Y#kU37QPr;Fb1DqT$M9$azx#pPv1 zq~wHX--GT9#bR-x@nC;p>4cMZJ;{0=^9S2wWiJ-{ufZ`B$e0LM232hPDCH`oTrLNI z(o1A(911O+Xg+XtulXksL}JUhogQ#b&@iy(GrtUN7!w9a8#|N+bw@agP!{%z<6NS= zit{5X#7(%EDy*sSx;0`W_3lCQSZ2~)pnPv(aOfpLlE(}nE_S$Q&_g-6f#4>ddv$DW(S%WfE zXb6J-a}NlWd_NaY?P&VJ!3&K#BAxkE9J1)#abOy)Ws%i-sAIrMLCArMLAq@i{#7oQFO1oaIB$Sv>@##>}aoBqcJ`8`P&=Z8vz; zq(wRs1IcYfj-ZiB%4#~(UzB$=_dsa`>xN)*DN&h_U-s9l-XM&~%8siPn#agnv`=E| z5J9-J8m30P`26RP3dwU|A%EnV-s7WANGA?-+xntSY6_GSv27B}%F4yCBjeBhCJAYq z(o3yfs){w9?-$6K^*wvLElWAeIb*~vbzsyswF!aES2oA}bLI$CHuWXL&zk+Z7E1TK=D@^xSHtSqDIQ^&gebT;9HplMysi7w-U3j6p=ckr^pVUiB ziIe?T==)?d+kb_=Pd4jZAzlJMXzqdD(6tRQnhZ6E2>fVZ5G61vy|Mx!N-__Ubm{kz zTYZ_WVB63+kFl(|@eSohrjb{eO%|slfCfjf2jyt$#6DtYErfuXhklgK_=T2(g7eRh z8vmm+uW)a5<6o2;U#c@FT?)U@581g|P4lWf<-moyEc|&fPvl|Yg_Vh`USoMr|D|c= zm+=M08urx6p7_H+QT{yE!@e|vG^c$8`o|DgIfNefJdR7&gAnXSH&He?xiaZv$g_^# zGh9eoO2a^`6ALMVo(?^M=n*wcIpN@IObjU+K7&oXfZNw!o_oU;JkZVb-?q&`0tAe| zA+{)g5n?x(r-l@)2lt(fJ0cR`ZF}wPVoOlVT^X>J)4+0Dv{Lr+V^?*>aYn=VhxHyh zm(W1YW4lX-J;}k)77acblY8sQcPKBqE1ZZ&#lO4FUUDh$0O}JjnhJ+iQ!RFucd7P8 zfTE)c7*Pu{d8J;(_W`Klz0|Ban9r-aX+95kn|?}iI{1IW2lB}S`Q!mVr*n?^6RewP z%8V&q0KS`Wi5<}eb!B%Ij(vXMX#!*hs2m=gu6XgVtd=A4S_Z+jG6+-}(gS%P zxzbJE@Ot$*ee28+`z=vWrx%O<$6%q(9UCSrHE;Z_DljUga2X1!3CfLlXb|>Nd4 zOVc6CisLMa@J7f)p22MpG!A2kLB}msJ+*5MU)LR!hO9IGJX)>B1(%X5Pk#|fQt|Wj zdmwn)>2Wvn(dCes#bAbjr3-P^A_XeeCKt(U3dR|_uXd^ z>qbIETb3-fL1mqCx~?(O4gQ7xizJZqdMoV&Wrb7nYk49ynAowX@I-e5p**zp{v&8s z%rQ=Uv|?TQ>()T+AsD;S)rUU1+`@o{0#viVc~lm;kxd?olK?KNu1?hsLNcWGv)ugy z*%D|Ax#c^~N=Zpq+TTW%j3zanXKy;Xn<~6f!0Ej)D(JcG6YZrb;!$c{#U~FYU>&S2 zUOGtnA#lXMg-l)otJWCr7B9KBF5hFp1g1X@&QwFMXc{F8t0Nsj$m-3{8Mx6s|*suVh z=bK$|m{W=5az&l*n(6fYbva%_TZAbEX_-4+4?I&MaC%VZ2u>u6^9^-LDHwD=;|BMl z%HByZxk<2xC}+ry>m^=+IfevEFxt2H&P?*`l0Cu5!@jcSV=@A73!w}lbg9GFoRYYO zCn~s^?hg&wonOZ;3eGKII!x|`FZj{^YxA=Nuzj}QxJ#~bC5m;3~&5q3p%a|3cL1b3s3jYuNSm( zh26h@wmlp|Kx_5v%~SUs*HYs@>l;stk;Jbl(w;W>?y06}i$ZSlq3lNN-}fL|2xH^4 zP3t$4UQ!Yv!l;;p;S9v)D2phkqb{4`o~IzTW!AsLSqEl}!!adr+=EneNKT)~YC z>ou_KJn49m+GvXf%fpBi?jG8#r*GPWbONN40;%=QcdrGu1f6h?kkP}cDb!nrQcm9h zT>ZaCz6Si|Tjz-%d5&Ym90VN!9R0c=nCNSIpgO92vixMWv?(E&AvfP4R+D%Vt~FGA zA5Q_S8Ws#Ku#?PItJ25_92iuP*BAW>PxkMh7{uIhKM%d0DoAmaB5BRv!d z58~1_hpwupf7~|fgs$#JQi86?blo2cWV*O5UDvNl4{Ug15)VTayP%j^j*v~-JaAlh-i4Vi+GC`1~ zKF7uM66*8BZyT3TmGuq-0VmNMp0hdZ~e0om_QE6!`J&C zf7R+IHLd#JeffB}3!wz?aev>t@phJB&^cscMG7CLAg; zkb|&M96?ilQnPni{Y-LYxO(+wd_q_ylK}@Hkd_7Usz)dEgDN{yAjnu_SAIocRqE z|2u!*m-S@1g-5R*{LVZmCRoT=d1qE_j%agu?||<$hy^*C1|`JfChfdcP9-RevOmAh zTe`ToMOQ8rzOntLps0ClHS&_3bXl8BH}v$ucIm&xXPx9u$sLb~9}dGc%WNYtIQ(ZQ zUS0LS_OmcW*h}bh7#UBRkI9qv8{z`fF{O(v%`XyzdGq+0{x>N_)yVi?hIB4HAgGy| zRlcYt9IZVCS33^(Pkvs>8YPg7k2sPn;6hWzmNU34I6%h~%5sA=&^giWL5W$g+4s+K z5=`iR_VSXlNm&{&mF@dB*+taJg5z5R2S`Z=Wy(3U#IK|r0+7Wx{xdg#q#n>1sF}g%4;XTlrv|xbmm}H zEuA@34CQb-@}dk1zT)uo>(N#Ax-AuFw1Dc}oG)K`6-)JnMI{M1E5!}t!CliYx4rK| z2{V!JyA#>Wh$DZv|8f--axYnNgl3@of*`-nW}gtOXAwYj+sEJK~N->!3f&rdpR5enoxtLVc!G}T^k|Z zZj3>AzMd0%@5_ld`ND})N`A05h=Nfc?;H{>$Koj3INgYPrfjE!#O1T}?L9QweEk)U z7;#ba>*8*GE@gxiB;upzz%_AjL;)GAOI~YCyd~EuvLow?N0=*enW5@<>_I=A;FT1w zks77q=O}n6sU;U5}GG+k%Y;+yxw`ak;_8? z9HgDTeLx;g(i-b{%y`>mpS9Gx%TMY#8Ux2)Ui=#5+(H!Ep&sJBKe+8nG^TQ2Gmk!oC&?3!zg+6Xu-oZbAT?2z9?86?oU{5 zEQq|WAd=NN`$^X^Y1_C7tJzQOko!vE0@7;c8~kg<3!p(2)ZvIPA{+R+7>FmOa0K8K zjf==J^fmiws*^TM10Xm(DDbt|!RqAE+4t>m6({5V*X_t+%2Pyy4|7*JnXcdg z=yL}RL-quWDSAPcC_m=niH@%64(NDhTqhS-^|)vrZGuw2g&}>ldMS&`1aC5ppW9z| z7DVyniTR^=8H`b$s82~=9z|87gah4TVhA_LCWmzP9^HhXl4_Bz4!%@=ot8yOuj*7# zwPjC3I#)8h#Wss11_d%1V;d6guI#`2H;TSEd+!dO02AVQ3{?ue&4U4wpQ#i8c0QMs zkR4NAyGM18Mk3;E^vQ0!NJ5E&Pij%bK`2Hy=;dGBEz^y zFz2=YL#du5Au0ypN?lT3qrs5iEF&W2Ua0HYo1h9le#YW?$$*Tbt$4AvxyHm&uMPwh z+lKEXu`=PP9|m%8r6Y={rAE)_EWFN+r-M;GWYCyh<9uDb32eG1w&&BxMn0#NaJLel4+jD zFcdDay1isVo(kL0g3E;osI%F}*Z95J%}mRmcC(^MGeD3Ku?6`tWa0FRpt`^|h)lc( zft9+1V>7lh;tUq@kH?ZE?(Iqn<=wKfniuAWsvVA{Zh=;gfBLDMe$YZqudiRYdq2_x zuM2XbmcCv>ie6vs-apMnJqXn1bOxX;*Twx)*2V5@N~-mwJ8E>^j(mBdM|*4b^E*j^ zHTR51N;HplsY2h`=Y{YRV*YO2V_M5AX$0oMRz~@Hi>filQ$Ifnb^~(vG%k;Z*F`JB z=_JWWkS(kGC{x{}j@g74Y937?FeDa-YxoE}qpa^$PCnYmFJwWTn_B{bVbW@36!@+n z-k;tnvNIMMDyoxmGKH!pv-j^5bU+p}Nwb~(pQ;jM09=8N6Hch66!Y*JA85J*${JZN zfP_J-cb`Z}$hd5Pl3~y8j4Ym^e#^UY@cbfck~VF`*L=IEO8Fp_!(Y$jSr$1g(qFLq zzOx2ees%l;bd*;IIN_m9MhIoyP(EIp{p*%bGU7%{6d$|4MjIYHZW)$kpM53jet)5v zPj}C^^P~L_Cyum6PF4Er4+2F0R&T!^3hCd{u*vHrP4aQVmb;^A_=aEtP=M)p5*7)9 z!U96Y-6oiV=P15Zxa*G1H2sg5@s5A^S3T^FHD6G-ykGCT&vOd65fd_n8iKdeIvzFP zLafdN4bvkSV^qupvc#_^4y`#``~=aIz~cQpMR*tTzZLPE=gDPYdWR72K7kzpce1%{ zcc&Qobd-VJrW$3PIzG$|(co2oW72sfjo!MfYgchRH?-%C*oi+=^ZAo%B0$R=;44xX zLe44N%qR28Bi6Hi{SIeLe1I3c2GI+p=+zME9dD6Rn=sMtX+(*|fu%z~h2$~O4CruaC3ZuV=ur*>85 z8QqTA8FqX!L)4|@ezuc+_fA$2v!Vtc?UfJtt{uo`9NX!~)gxFu3IL<{W?7^>ACJS z&FHL~a-tSd7F77;86}5G&Q539bE;!&ZT4#P0xEY@V)wRU>YWaXXWz>T@OiBaARTRI z2wQx}En(LK@?p7jN?zfj1>Hyx2!#FRIues}8u<(h_hwRFnPT$~1sUFh8V}tN-^BA( z-4++AX8*NNXKJDGOqp&4T8$;J703!<5rD7iOC7>W@KP)qem;KK04JYL^d$+@v4DiR+P z&>&dHv5Yr@XUs4zv=`?GcEf)W9I*cjOwDl|gHSzzr`>wK)up^zSv)Cz@tU1WTvf@O z0s(5lIkF$Zu2h8g+9*yzHkfl?)n()Wx-12%cY6I0Yj@CCW8e|u@(?iEhjR258lhbE(#3ON=flnYAjF^p z&tK3{iR&%cX!tBKY17jSY5NOamnw-bLw?}hCm%M*5z7W+)re|VXVV)45{cOun$}59 z++&{>)@F&mwkE+ELuu1Suh(gbjc2RO}D5x7z zjlzbm1vD<-Jfsr2r9rpmFRXpN98-LEIKS= zOR^&B_nPHN?{q^aTAw@I??JF+?L~nxUVs^ ze7rEP?c1pmtc9&4eAf*4%P9J~L=vyW_-uz$~fAkv1 z06tXR(Wwrhfvp~HB8{DfjzUwzL!AgMGcz^C98+c2SGm1tb}xx zszJ>9^glMstdh7fYms(c)?hyzbH!Y7O~&jxy@7EJ&a4Ldg~Y+XXB#)+YCxC@#Ulw- z2b>xNuc7srcwbzUSQa4JldkmD%d2t|UM5qah4}Ws{J>+5!DFF!{#!OrZ?wx>GgguX zhCIeQ51zw#akGhGgzdP4}j-~r{kZ|+zSpkvjn$@LkGdKMq}{jrUo zr5pyM)$EO#p5wz#_#$NyyuJp`brs}A*P$;uQZ5`RF*M%)k=6UEMM<|_H}@gm7}mPy zF9P38ecDjGZXdomz8Rop@m0!9&oVB7yG(6NgbZM)7ffT z>BI!rQ(1Z2Z9SuSuK9*UE(90(sjVw{F#}i`TYS`T@*EO>K?j#mas`LnTQCm&uTWDV zA13cD)D1qxbrA8*H_Gm$uFD@19o*Js&MJFsbv+=_864ntpykr+tvgIEB2v8bZZURB z%7D4wj850_GhojQsqkEVq|A;O<&7NVBcx-z=(fde1-Ky^zJn=ZV@vOOxy`$<-s$u& z+Xz}@)xc{bp_)s(57jSThE@v$hPvgnDd(r|Sa|Us3vKVbWK1=3?ReGrpxhV63X#cK zQ5lvmb?uV#=A}~A=6KZ(FSYw#TJ^*Lcqz{pMV`nj!%qjON8<<4cmlk3u@7?Vin}8q zfh&0#3}8%NBQl&vfQuD3w5rxz-e!>%*dh!rZ=}@7K3+|RMwA+Nf{Wo*YUiGR=={BR zup%Q@RxM0dV5|-3SsPjyrv)J{C|b1z_DWl;v5Z!YB=5qVAtYzN=pPvj%DM?V2k*L* zQLpvw-Tgr^KLQEr2MizIVr^1cHT_pj`@)i-x{fW~+(LHZq<;|lKY9@?HkXq8I9jy$ z4fzc>c)B)JsHoL}7@8{Sj_~@g(|KVCC?3VWhRscEsNS%Wy$YWq-6EgDfW14%*HCQUHbryy$K%_e>E339q zV~xq5GJ~$PkzAe!+=a$Ryu7V87EKjXD<6RFFP?#uaGL80gL_~8lCJ)$X0-d?zU16+ zxwY4L41E{^yH8M(Dpa*Dd~#jgFTrV^~#MdG^(OSp|(O9r-XupRhiJ&Y!=J7=Kl zWLn@3;A?%KOb`J8PH^+Ybow35N~_BbBP6qv#RF7H3PCu}#j#MJdVKY4+JPQ!_oKZ6A1eY&=u-AE*vHoWUV)wQ`zGo{hS;fG*febScZG$QNB3Wm^PNbn%nm0V=m_4T&|U?0B&Abhf!yL!5c%}Ym= zX-Q)H<)#^DU)ys2ie?hG#i%*e4kA1v5n(M<#~do_6Iox}jKFx&8`W~kARx~{5S>9O zK%q$GT@^qcJx)_%(9@N{oAX=Kil@&WQSNfDx?DSuOD*+-yxKJ$?v&YZ$`Cj|s`u8p zkyJF$kKn3lO0MUs6YLb)d2#6ptPDvfrOR&Zf<#kv4DHIa0xNQ1E*p0Qp}j(If`E5( zI{oTZ+Gc$%^NhJ^MhGA}dj6!CVAcQDw_~@?-lm5)Gw?m^EPUZwi5kbzr5~n9by&hP zCGf|zrxv4cq+eROE}LOx5>H#iZFOA^h^mG>CQ_O1jnSoP@DQFyHVQ^B=WTM0`sM{2|p1!G`0TYjcx)xXfqBqVC>v~2hJ~g3LSW7p0 zMuEePm+%Z2HraIsxllvS(!{@cLJgukZA7;LeWFf0-yR=VqmFjCWlB^U>8H(8P5(s} zK|FsUos$xH6M&VJ01N?uXNW1vQZsueS)a~4v;cVr>ZXS{2c-_uIEnfZ4Q)F8gl4?~ zEFPzgJ#&)BgW*uGKlICG z?6}dHOJQTI33v!75JFp;Z5lYalsXt-;h z2mev8>zdW8l7fp+Ef%lRu?ohvw{;xuv)uN6=^GgjP3nYsgeF5R6ZTCPm63&`0A_$3 z8Wyp_g-4KR*d6r#|BFN`5_~#6wOXz%%N4W^OvpkMwPeB3&Gn*k)ftmWl(=gpzXh^%eE8(0D}~&%#X0o(W0hH zNW`ylpau^Y3qa|_!osiM@^NaE3=?P}J|d+;Ob8&Nm27A`J7^GGvRY8lKcZ~=kUEFE zj%95zeOEdUL{WSp_9B5KbB9>G;Kg62qa+~YCZz^ZY*sh7xM>41ZnOo%=Fr5(po`Ti zyvaO^Z~Y{hf$DuobY~$)x>%P1nZP<``4UHlKn;KkD$E+gfo+c77Kh9yd9Xt8wx4rWhLnr@CvfvtjNSUILRUvfw? zcpCJgta!`|)tMHmWg>tIHOTgT2A6IW7JflKSd63Dk7@F&-N?XURB77P;1i+jxylU? zQ4^z3^)rR~6uK$81)gW5F|YO0cNQ~ zcmYJi+6zuG%?+cmv52M;KWbnV}+35fl~=4EgCr{%KNy z1_8tcUvi}Qj3Uz?LoaIsJ*k!n!}C~-Du9?X_tMkAHd!2_Q*$=yrXDEAcmr0%W&5ab>rCq>$ooM_5RC zn2$UK3*ZCvpndr5`ZJm`>!m4Fb^6nxy|?-zth5m>|v0A+9AXNlFF3z(jh$cwr8h%iq%zswCVc|0%Zzz zb6ZMx_3Zcc1Ga!c!#t!;Qyv1r+P*E|6kiM1O@DAz4Yaq{L>1WVsyq>E!&ZxZC*a*| zIQguR8Vdoil4nE^RGObP%(K>ds6fv%so4AK8iV^YK>Wr-2)9NK`z#iR7bS#&%CDaN z-$|H?&mCH_F*5wF?vCEWix821h5?b=?LgWq92FQ&Ku`um0E2bCL|R%rzQ7z?=>pP+ zev}ghfn=PBWlX98%bYN>?^zT=7_85V)*@G(K*mC8s~C)zb6AyT|5zhLs5fhMJH^+t8-4pP%#Kv z5;P{CnX}uLt!AxUzc_N{?BcLa63Pm6k2G=RU1we%XwqA4B6kzq7*xO;RrW*M*U z)BVB|IdGM!J_FwIj*@$}vP^x#=CmM!^SLwN3B{m0a>ka++|E9T2LZ@n$NJnHd$35q zU6KtQwj2%Ccht!IJ#uuF zPpE^g#xhO@to{$f`6JXQldc?V5eR(Sn*Fho0`W6TVopv%^^u3kDfI;wQ`0|?>RX>) zYpyIRD%+m1+L}|o^PJ2@HT%#`3g#7@a=lVA?QUY8%{R)EBHQHgB;rB?0AN@cw+%E(|dGQ*F1>5HoNU+bF=N=KcDNsyU zY$Wp^-Q7neJEqMflclAHV!p|gQC`k$hAhGft49X1nX}Ysn&eTF2J)uGRn2=}b2KuO zG8zOgc?xjrB0^=gzS0JEa`hku^b(|;n}$)YuVbsV8J<=%rkn;ViE*ff@d$b}`6GQ& z;>xK`hx!xq?V$|AA)r;)sIj>A!9|bcq&s0J&JLgj-nu`2cCmB$m1D4was`k=PQ#+$QNBR|t$*e=bJI1to`9@gT53S+EBE4(_>i4(_>iXch1C3z%j;5R+^~ zu?aZl?88iLHomp4KGX%)vxHdpZ$?wsq_lAgz^JLBnH@6rdK z&tYERC>4XGF_D-2n`Zj#x+Ncmh|n&sT&TBQGzNsi$jrK84IzG;-A`06-2Fh6)Yy1X z`nTcHIRTckf}v41pv5HdQj8@nCo+pcC(!J{Lag%Mo@N1t|FiDy?d5Hi#|qVa;Q%;K zOjoTR(n}b{lh3Yb*w?yeYbw$42dtGXYK1G6l*=GT+#%q7E{`9C$wJaUaKo*9^es~vXeu* zYEULql{HvqfEP{*^g8H18K`C-g(3&X!xP;($0H*Mf)BxsGmP(S@buF95H&bCbiGcc zGTDxk(-1&K!u&rFjp&<^ylcVlY z96Zdlsups}y9Mj5Z9i!NQfPd0(p{;7tDN#vkLN^%q6ApGY!QqsnL+s!?=;K#Q8#UG zcv2tVQpI{`LbZ=3hQs$9#B%=|S(`#lj6zfu5gr6lks9}q!Wz66f{ajg;#ogLK&S2q zGw!iO)^EE86Za4Mq*T_$SZN%ueqrB-@xa5$%B+?-#p4*a+Gue=HpmR34LTTk z;vx{TUjwlWHm_Nrh&-hmUF_B`JtGHYJR+Pqh9H^>at^;ebO!49vjZMF>lZ^XNoOb$0eV=KJZAIQku0UV3~fPU}#Cw14UQ3Vowhi z=$O4|NC%!BRvvHOFnGp7QO_%-F2bNu4lf2t%lPR?tCJ3o^sfXR5eBfb>H1gH%H+r-IGo0T6&7dcg$E2zCJM->DO zw&0;g(;@1$o*d$+8Jx#3+JiR!avOpJq&-%5k>&;{fP@8j=VEeHf{|yki;gBQV4m@f zow0-A7CnI>7rKHMqUJgUOxD(NLKG0zC#3@##e%;RlnlZxuD=X{hsY#mt*ZUdWwhcU zbBuso#%_qc5L*#xgFIW(ksTr4|8xh6iOd=s*eCFD@|?-;HrpvskZ4p&O;)c#$=PbP|VjQ%4faiV{>ml;XT7#Q&zcCNPJ{ykov zPQUDOk0$Ki?KIWwlRLy>rdDhvtW$Cx@i{|Ks&zdQ&g>Y(bIE~iRP6U6w31oj4an}| zxJFnSH!=M#8iH$`R5zA^lAixAi_v~zx|{1v4a}T2`==We32tfSIl^xngH$?V014LxiPuEIgH9>%4+YncJG~iDw-i$ z%1zvFPGhL-!UEW@mmxzlCqx#@7vv^cboZJUV&)Z(Q=-~YhoE~4VYz5vAuaz95y_6i z_X5h?YKf@8Y+&@=mJtTpJ_&;(BvPV&;2(2bu5GW}@d(ww(#^nlVF%s;0g5FNq20+1 zGRfVxp~~U#v3H~3T?-*i`pxH(bXALoW7ZWE9}WXWJ86nC{|#mZ0G*N1#YKQ!Kldyz zvRq(Q({s%Bi5D6%D)yCa5G{&{#n6?H*xT49=C(0rx>Gn_3*5|g??%!|dum^DhSh%5 zbXy{1bLdfjL(zXSZrlpGgCARMqkWASw?1Itcp5G9q3H)HFua;ss&LVT* z`|9R9qCp^^^n&P~$5sdOH4YPBWX~?O_A5z6NwqGM+d?-XN^qPHIufcNYyFHwUdI><^01sql3@v zBwSfHr+|5DaMd_LDm1uivLttI2Gp7uR$g%zIuz>48FOuMz(NUEp2@)zq{hX3hI6iz z$4U=k+gWr9zQ)-M%eqsnP-b9!w!h}=>~C~nGf%}LN`e5sa+GoP18`=|S^RcSkFxf3 zP>{gw~2#5~e zOq=3;%_;{b1glOm6F9qzBfEgtxBD0A1@!r zU=oO`u6($(3pW}>cBGce!#R^OiHM-;fGik^wtdYTN;Veb>>tqZ_y#c>fb=JPaBCh} zK>f`2RF(qQjxRP?!%7RC6=Z#%&RSQ{A*nW*7QRweQblyg31=Ein;5Z4Lq!LONOv)G zguHqf4CC@5s7`LuW7N%kMm+&yWDx7X#0A!ww2Cm7OXqon5u=4sW~+_eAP0wTgxO{K z)TI)zZ{SeKWg{0xsH*C)LWU%~*v1-;&J2W|YWix0c&Ie*Lh^h$W91P1wN)t?fMKtHl z>ul3W#ng9DjN9~%}v(Qt$j*2#^LgCEcS&OPWXx^iYE#Gw)=h(Mo8K1StZ84rDEZV78nEkdzm)mbv6AWd*6P(9UuHXmYU}TUY0+cdiD-!D~DF zdtS~^U2@yW{lP6}_dvlJ+Nj>?xHP*>n^4{(H@XK!IN=w0ncfIT4luFmv1>^BXL8ri zmc(cWobu>742=y$I#x0Xcxe^S14(BJq%T7^K7gz&lng^~dYA=GTKx=pWC=ES7|KsM ztJ(#GRK%Th2{)$z82DjAar}X?nPetO>6VSs&57Lb=l?R$CWpF#6O`+-p-NP-f%&&( zg|uabWO-506}5@++MP&7%JT8B*~ul8)4eGgRChK)oaqU}Vi*-(Qpf{&q(&!whQgE2p z_mZk3_g_;!z-78AUqsln+bJLLDRg&dvy;EjvdWsuZM04@UQ~GPqoPDf=gFvWA^NAA zbPPl1WmuqWia>!JXT6{f&x~sDL5lTrOz)u8=(lD5t(7*PDS$S7ks?E0DCbE|cPqTl z23?g{pmq)s5{nwbQlnFKehvx*nAIKBKBahquJ+p?~0->yrf;F)$04_ zKdX>!_1E>-JjBXqxFwpyz`>%$Z^C53oI-148IYV-Ggdxiq+! zD;|>6S1>%t8|FH~C&4z}@G^0?VsaBkw|n3}jdsWBRcms`3WO=q!OWqJp%LHI^EPxe zXQo9yIwZXhNqu^CVUj4j`b)xj`5xD#WFf?~YUSEl**xfgXn?E48rVRnAO7-@EIleb zGHf73UuE<_JHm~E30mW&jU$CzA8v4zaY`P55;eE2R8}eT@Z0KXSC9y&=ySd79*L{K zx+eZCfgX;e-!&m&Hm?{iQ}YaNw-=}{cH+5^61B(fBBlZwvEa5@9^;^8<04u{qZh#l zp#MhqAXI#vMELi2qjUGDhOj;2=mpjnI~ZcV68+op%iM~+IF4DN)qIP7Iq2-Ub2Cn+ zxxL<;;^MS>)RWO)wz{1sFRr|UnZqH>0uQ1+1nE|1Z0AfYe1QPc1v!Eh@Fx->krQ_X}%JX*S^8{|oQ64;R z?FmNZBIM`~KX9vdSGeX1!gIeZfzvU)Y=^DL4VZ3g%#&6Q^cO(F;HCz3brDCJ*VF0M zaReHub^Td33Wy=h=S-(>YKqZ)-m@~Ut!Q+Q;~gRIK|WUX1R6@%xUc%p)rREgnvM9q zL)SY(hlqD{7~CBmcW`gl&Mj|OxAZ4gmBm+u!%6 zuH_QtjH@T!Sf)eky0U-F+Q|Bhm6xq3$VxBk`;%^8pc7_)wtGHBUsG$;(x4MYbKcn% ztu)xJkzduEVg_3NjkpR*tltS#W?0$wKT;B%%gkv}3)s7&xT8)8S<=yEcq*#6q=)ku zb=$NgT?-v-Rc;EdnP&ay7_ab(^yF*nLBRU56CSCSeYs}f6kI9G&6yjl;CPn2Mef%0 zjLEwU*p;HfCIskb%0rIV4D#Ky1ck-fQJD?HGX#Ur_Q%fBHY1lt?3=HAVHY`o2Np;@ zAZ6|`apO-%%w;(sH4E2q0wI?ynX+Wzf*1qM8FPn6{WxokW%HtSGb;aU|?Zjk`MBGq{0bN#HgSU~r!Xce*vpCAa^oYJTM z_{hIJ&r)!rImN_hj)6He9R>oGzWlJrwn_vre@E&G3v-Cu{wcCX%dJis78@(ph2Yx5 zxaku4w%@etfo(B^K5;FJ$VD#p&x>SZ)DL1%>y@_Ll@)yspBlJCWDfAEv`k$}KTV zq~>5U%Ya$2u39F(tf(v<9~^~XfdCc<5vjtlO(x6~$jk0fmYZ54)J@{YVx0j4iPrO4 za@a^%b&a|@04gyGmq|wI(wKIn9|nWM8nT zU?MGCE|==D03XHyhR?uN`s);?P%2l73x8C<NhPeN5aJ$kLkB&*V51P!$oGl} zNYVu08C>xW5?o;~yJ3AWJ|2>@%`3Z99` zzC7qr%@y}fthtBJlNtt1rWI+(_Ee6=U~H^i;|ycuI}!Xz4{^g5yQ`v8^830o{jYr( zm%_fRCb=0tLHtz=q0r8O-9dF-c0RUfrc4Vg%e)L~zTohMYHH+9qMcpM ze`{tT-DnW9d^214VQoYSp}Ui+ciI25_g=Oxbxkbt{a1M>KLO@M@`G5g^&`Q+yqsX) z(ie0afG&G`3C8U_Te{swx#(+FkM_H@IRH{C3UmhP5?I~7zox=!a=iStVM82`` zyXlZCvbf64vW8E6R>wdD*8xy7N73r|QZKgf8<^Lz0c!!RnZ_b$O${S2Sj=az#PS%G z52|;v^}W-h3y?lok+d?AZI5OzQ5LJwjSM5Y)e{SUTE@c)qLU3a?UtS}7%4JX`D29t zb!?_TchAn1)(E+7YVZ&jiclO&W;Bi--4Z0YY2;tg@fJI^33efLDF)r(9BI0pqC1s= z*l@SSW_xFuSO_N?WoQT4T42kAA5a*0b?Yo*4(kr&;Atty3b5)=V_Iep(ue>XO&owF z!Or&LXPC`ya~bd3=AxT-c4rUMIlsKZxbVUEG3L@`k3~*L=3c&C{Ty(1hay0=Z$bkbn86q z_ihU;w9G2-t5mu+3se+YGxS%QJCQcel}33Q09Y6xfg!RI*6D|jWBSw*IXuociG_a# zz!^3VX5f&LJ5q*$vx1~w^mzTrrV|y%8V`}UhEHg)D1}lT`f@07gtO}!E`ow&lc<(J zy|IgC#?#2hvJA=~TrWHgr^4o<=&E+Xa0eRZJu^GG(Jee^>d4cN&XEci$@1db5jLV6 z>8`$Kam_h)u)M~h5; z<8J9TPd2yGC4;PVmNSn)@J(RjnhVLX!}o#LT*dtA3;R6Z#!aTw>|R*EJkMl6!|_F- zAf`%iBqpaTQ;wM_cVO1-$@4Al0bz$N(lCE?W@^N+f$5mV&}3eZPDPvqjgd$Qrh$|2 z9%UFHC490_g(N^y8*|T?SRL=@#lr8F1>x5hQ18bI8%IJI-Ap z82q3cMLdMjL5xh#S2ZW#UaVdwm*A7Tl)BjI<4#`p5{R@LQR9PC3cbIwRx8?{07;{HOgtH4m=i#WXMK*_6 zvc83-qBt=jqFFB^imHgXxjQt#+w*peQj}JFehov+v!R9Q#X<+?B&P_CM>*B3Q8XHw z*Uc#+SZq1A^P#qxmU75LdRUTGV2!HPC|nG(v2qhQ806`>m`y@hIe8*yBaS0i3sKvaK50;OucZ_c zkL6n;LU2TShbB-f2v}>JYQ2=Qov2-BA@<;4i)&w>}@_652jlPW^jw;!3%! zujWO~6RNK*-7W2!^nHE$zJB`hQM0v*su`HUH)BFW0%r687hqHkqk!af!JhqAkscOwEL0jh2894WFbZ-P;Wn( zvcEdZ`RxGU&SISr0g3K0*q=7=HY6uCBRtvC0uixq6T}ScK;+UQ0X;L_I?0D07_to; zI?;}0TOg)|S!#syIGO0`PGM7ocE16x%LfIf23+z5gi5SNu2r|PMx;a2<^aKzp6=lE z(+8SaAqT4#n2G^4I=v&w?FB#^cZqYlSuzd0&3^X68gF26lE4_YGWgB&A{%ECOl+Kf zQ4xh0X3e@!VLv_#$Yo;j@ikqeI%Yyk)Nc28Qdu76g(R7`%5 zvNYeJG%x}YeJ`aWc#vPfxi_qtop1!2O^Nv3QGjsLeqE7eOL1s$&XkTAwapD>s>}C2U6;i4(u)i#A`V6wi>rV-qc4!w9dZkaNK{3 zHQK@jg*{(tr;&3}a;Gky;~8{z=2fup=<#h8)oS{m{%Lr~Dyo%W0gw534{aI2%8$6b z2=#G28}QJgS$k(0(;f$I!a@iWdIVbqIMcH%qeaqa$_j&Iva*(b*?97f@SK~mH|1m% z{L$r^YooG(#f4rLAkPi%KbEvFIfWzNyPwdQob0@r7!7n@x2J%IU(g|2Wrk(Or6wLL z78CA36@tdYqxdCuJY}KL)xp)z_7M(vzOvG!LWQZ$Bp*1UyW?ypU$cX6IaepvUHNa65P&jdFBz@4Qykm&2Ru!8MD zd-TdS5ObLWvYe6OvC{QfyMcw5w}+9@C8%M&-B+Vpvf%g=RcY1h?g+{UKVZ5d?A2J% z(XV4Iug3B%{=$NgS0*|=j^OS1;6B5MD>!2`ytW(+5#*B=%-JUPe(&=!gLg++v>++Z zM!#Q7E`wSaWHZi^d>pFn4)50YflvtEgqLAF$7nf5hx?|TwU4>JA{Ju3N*zFEOlZ?> zY>@4i7D>uqaj_Vv(b#SB>ChC9jE2$)zhy`OCo+~wYAy{+8*N36y^-$7Fabt(dxHc; z8?)Q=SN8u+r!=|kDDw(k_3Mgz(T5`ecECRlGsOGwg0gp<3#-zAM3R|=yv>W0ftG&Q zIrXCR>t9_ZFp1_xXyYV@b)pcX{9{(+_mRtfllf47t1W{N6a#!Ja%|3WRC5DRxDPrG zGLwDKgI$969n}^}UGy6H{hlJRej(?OF$iu50XE(yPUkm!vw{!CfdeWgTp{o6U;a@~1h)_9h3wm&=p2-LUhzU$hv&9BVtp-vm7%u2w+i2Iu&8YJ;&A!HF z!4|BU;}x`u+H?Xgk(uv{Yz2)JmX@A4Gt1l)CmQ30KeN&d-?FPG&6wWJF#Qey?r07u zgia+6fRU%?1OCEVvg#ktVkp5eN@j&Q>e3M<_oGpZj>o@mTXfu7|a2qU4~_0 zj_DgG%X2T7uW>Qrwiq9#XHr4LbD62zZ324@vMtzIti|CcsZGiwCsPBwubF30?gD!{ zR?+atm-c)rH@@QriU1id&f>NvN2Sn}29T4KcVr~IEq;d)L?s#~&n5dvm^QhU-n5rR zH=0Wd&N+U^1>!hJ8w)u8-+~R346HG3nq9~h#ZRYO%X$HRk@%*p6Xu6wFBojh=Nx;l=o~<1yTYme9iw#$e{`nx-SJCyI||3R0*)_9m2-x& z+SYbIM4scp7w@j$m%^y(d!*`vZuhye6&TMa93>6N`dC~N38k=esDga9E4IsOCr$Cm zFfsZj);Vt=!4!BKnUpj~8Er)4s2I`JV=K}w8WZOVYQW;VgMX+#} z{Dvmuple-rGB+w7sGX_cz~4Nt!3hE0%5);wvB{CvFUavZVq!7ojkOCpZEp2lu*#4I zp*|dc(8$>we)CO0(V+Y-oSo3OLx-09Cl#40RBSd?@Y` z?UMEB2A%uN*7UM~sAc9Qwh@4mit^0+1hWeyWb58Ur}YaX<^3rm)puH0N79G*0zQ%jay9Bq)WlmqN%w=f0$jJ^#DJMnle ze*0y}9USRSYT$F%`Y0gisjiP#^&$t}ELe*=4^wl^{P~fPNU@l$;w20U$^b0+B18q> z9rjTh)vy2npkWJ}443{o!LwiH&(!lWf6F;WCa>mrZyUEV1U=wmb@YHjIToQRl(XrD z+o_Fw&J-9BH~R}~F7RbZd9J$FNvTDMxN}rfc|i} z5W)0J?Ee6&_LIH{JH8JytC6j6^ec7pq(wmdrW3&9CA1xlH1p9vmQ!XGWZLzvzOWPH zFK=IOGMH{r6tltVMlV;pgS+XOXykn@h%rlABe5$d%YXwL3Vz{%8A!NwD$v`}4R94d zPo+b=zNJc52_@vw%sEim(sX(P&I3S|i@^6*&eHgx7*%$k@OeAuo}o-mFV&HLz5||M z)D8#j-#lcDU-#wF%q*5}zU@9(brW_dbd%fgD7aqLD}#a5C=A zD3>t(5ynL5A_I^){?8dzlNjM}xS>9={l6`60RoEk(N*~l-4|bf>rY5GIV!nV9)PRw zcHgGc!&l*8LPAbEG|cmwDiHlWyPVZSIyOt+048t3Me@9UlIih%?LmL2wJGDzsklN! zRf&w9p7Fs>pTO+OK@f~RX(kd=ze+=9b#bq~>-imLh|rghmAwviS3Pg~Ds6LTko~{3 z-B&d1oN+dBjH8)qMgT9|5SDoa!3MD7GbUe$nBC67jYkJK791qR0$kmP*`>6F@A4w{ z`SbNK0{o-=vK~IpPChLG}>} zvxu<3MQApvl#;|Qw54tdZgX6UF`o_pFPsQNF)X(WljHGT{R)`l4{Eu$a-Ba zG)am=LZLFg+a6uBw%0+vH!&QTOeG%cdMl+C%0rk&=nKs*WX~>lujxE0HGCS}4(|(f zYoOZ<)rqF3GrM0xN0Dj}am5Resj^lgOU))|nz>;Y<#c*;8e-(5Q|+myOe*ZjNiY5f z*IaD;oFs_XVeWJ&$E+qqYq2h;Ph=U@Sw`KW-L*3FBR7XsBq!-hxAmU7xQ6g-wo4Bc zu3kqG3=+83{6sdUqMMVH3OURZivr_HjX;(SyX22mOH_my_X43Cb3k*|$1w;gr-y-- zx%w7H*4N68o|Dh|`tE63LbugJ!g!qkh$3xu<0l=JENCTS^C@7mC~^SJ6U<_@s0DR& z+T?GSIfOWX$TYh}atWZf#{InrYu8TN`>uNB*ip|Kw4>tsNE{8z5Aw3yT6m+~p#>a~ zvFKr8N$6*wIb6&n20(<6U7{o;cHZ#G z*!5$OI~R|*S|Cz`ZcR_o$R#hdVB(kS{fCY8CG)z2*ZppvL83T2T2CY$an8Rn@;dB7Aduwee2;Ud(hmd|B2;04||Zt zRS5IUPnNjhu=VS4Qe60iNoFfbP#_HnV6|825w?L8`p-27Us%93;$fv8ZrrmNG{nip zg+~TY-L9E%5{BXh^gy!wc}I5$Jm0da916IJPbax!kOEj__{eq4L^mz#okgV@v-v<9 z4%k6eDUG(wjnf2OR;@!VFwhm=jKxSnMZ8$u_T3-2 z{cDfz3#u8asOTG>WJxu0C7Lfu*)HF~QK86-@(;mEehN2U5P>b8I_`g_+yJdThQqovW5E$ zG_x=`YaA1Si{`pvkP$5meD?sbK{HAfSpC-dK#q4cP?V ztTd5W{Y&&H_F?#gD^Uz_7(9!x;Ajkva$;dO*61$Y4OUJCT`Zwa439rR?fCblX)__g z9b?IYTc#Do?ijeh>-Pi|ESiBayAY~8OAIn1sH3zyOd~ulCS0apO^39~!oZ!?(J-q{ z=RSO9+ul~mHrGAM3z&;O1XW1f1+nyGS(oAx894UEJ+5n-Ry^-C0W_uNO)N7fpVrI< zVQt0^R282N5D|exO(E%S2uS3X6`#$8}}lM+w-=5Ma9$l+(YD&94-5T$T7$#d6JO1@@4` z?DJ#_*h=q?NC2?FZ!RQtAmhKTgs946Dc`%2dv6)9WIJ(;&}`km_yJ=-@MPd@W`Bbn zu8W{C@PpIoMdwFn`yV`i?up5IS-C>N2nkVTCH{v`KSBf04h$uYG~nssZ?ib=FNy8_ zcYXW#OZ5M9RjSdqdp-9$Phc1~p|zYV5PaL0Y~s+_{&|lC{(WX%)6ze@^h*JN2ZN{f z_djC;c0l-zgHNG!L*Lv2B(swW*q zdHUX=wfFq;bcq{hhk)I8BPi5)5?jCj@S^q{^kRLzEE`Fnjt9yYNr&?1FRMFj2w2Kj zb0co-6&Q8@JAd~GbGJ8dmybPj3LBsv-B)wej&0Tb%RlyC+rn%2L1WAkBot9j)$@x( zR29{^MBIw0E7xZ*F=u)_Od=S>B>AP9{ky}9YW7lirD|5N#4=OrvNgyZFKfgxZcG3J zCv!I(2O^RdEzC}b)i`Y^J=`+^Ou7NNb;j$|$E^Ct=xZAnzR1Pdw;#?c_yP(fdFJea zQ9T-&NZ`AJp%H*0YuY71fm>~b%S$DNKEXqPLMFpf&>lh`gR+>6J0Qc<8?e<*|GNql z4E8NOo-N?f%OZ~JE@oLZy)J66`YxkdS`cw7KDK)Bm#LZ&G6Z0$uTUg3dGpcfi`wWB zYPie)w9_#Q`mZ>9V6APM2KPf{WVr5sy^_}T{olmjzpHN2gJ1Wb#I4sd&7cIy{=lE9 z9gJpgTR%z?G`h3x(HhTi9q9;=@nA06{^!_tIk|-;_~CrHlW!-^DmwekTih(9dr^=< zp$%YQ*c^7~1~7XZFoltCsivRtvpr z3Roi6SC==-i~A5(EY5V8aSU@KdVK9Gf}rxy9wZq(ORAxHhCuJ0pirb>GyT`h9q$i< zB)0Vp29^x$|Os59uFlK13{5aLO z-!N~c@j{))Qc4f#eJ7h6tY@0R;OuCz__Dd6MAA(FK___l1_SL&gd;f$?xGtN-k>R5 zzOAJf^5d;*W{hQs5-n-l^YTO|ljG+pL@NE?h8V_ab8r8rub#vVx;0HN zHaJ=@o>=ofF)Ev9Q9nejq-&qXvO>c@U#~Yr?TptjYKlVVlehI>0`o)VxA4d#$XT{OlqAreIFY9I(pWr3m z{C<7kQ7GklX9t}xEN}WvLllZ9`Tt|uV@n5N?1b4X4=);EWqD|PI4TNa&VCYawWoj5 zCdoEsve~0-3--vV!^dzszqf{mVm}$1Q-jT2lXsS(IUC(`)hXl6G1C1rGjXetCuChV zEwc<-)qaO(7$fVlByLFoctOFW4){Xtdb2~`MwxjvkXO49piu9azr`h@u4QaMuQ#5? zbJ9sW)h85?eHkcJnd{jn)>zBgsFtHg6=cE{ukUh~CVt7{NR3Zde+TS7F8ief`5^K_ z5AQ;8@fA1S$=?qlG>8niZM2%b`ta4$=?gJF`~L*m;600)M?P4Y{omJ4H&oB;%9e_9 zU0=pp(c0~xK^G{Np?0m@i#3L%na-Ke7n8Tf^j`j_mhgg$~4r4C6h& zJ`TemK8)7ToZ5Y5%Fo2{ycQim9@W1x^@ZSCgYSx260>(Es- zcVZL${!yk-yg-;r^j!}~us?lBd^K7&_-U9{qL6r!n9=^ztXO7C1CxZIO!7Jx5kNGx z-hb2uokqtwI)q0Sx=i+7UGLkyUt4af@qh!yi0}Q(aPQ-^2lGK9#l7#c6~$#V0699e znX|>`Yv>0V1j#^xM9qbDy23+^e#0o50Mq~wbt3?%f9Gp7Esd%Q*+yULt2x@)YdLHK z^Rxfxuhiz`6EiS)F`lw3ukoFmx0$td&YJH(`b#h{u5le?&3=GGE{vSg1;$bTYk#Hr zoK9G6LkD}$qM8ms7wij1U{}8ZS7=q*5Uk|3Z*OsfkzIcHV|2;){#}1>4Ab*hoBkbt zKjI$17BhmUx$4vZ`S$~0)Bo^G)soB8Z+_``cp7A{_aCorP7kVsdFkVy;U&KJ-~XAt zF23(GLl=Uhl15j-X6+<`XTSxGYj=UT;@iGcRgg=YPs+mBxT}BF#vMmlN$f(pnbtqS z3r&BZ+$HVsMK3}pbRWHBwrpRt{vh+=x@>2!%eOV=auowAX94p#W0CCvQ^Wg9n(Y5R zg2fcEAIQs2H}@GeO-NaTt>PT){RfOZ)`UKthn;Gp-e@-34gNRU*ckIMS=%U`c{EP{ zYk#h*lsd7l1%#*@Qoy18N={Vs+c#nD-h}azzKg9*K-GxfB5qMFiB>?00_#O7vJXF9(~jL z)Q5fQX5LuK;A&3OQtgX6c?inpy8xG{ela zrZJ=)5e1NY?R?^oe!%WsETtXGMgH=qmbACt|6iXPXfN%+DQJKN(f`9=*cNLTH?C{P zC8i(*j$sk6?IZlPevj~%M|<0nTG<{%n?cC$zjKMUB}AM3hkrlpU_g0BGr{;k)V2Me z2NK+sn?kbDkDW7Vc5?t^qyN)4EC&QPdqXZU0xx9tVO; zk5J4o&+fzf+Y8Gc+x{~b9@gV%;5`gI1pJ0S83;JI+(*C#l~Tms)VBMkR@5=O&{Bgn z2>)%2hRG=ct?(lfklz*~5k>!P4dUcV=q+PwwJ`OS`D6c+Z32>CP&dcAJi(c0SDSe}k7QlyPh1}g7db;8IR za$)=5Ez=^ah@vpCiibZ56#|9)4%z^9XafvrP$*mmfhId`TUTJyzv{E)IcS~dviwv~ z^4#Ft%ZhmQX9lR1cJR;DLM@mfd1gr#_K9$P#L3v37G$sBp?(nwSN)0(b2SCBwwvwk z?RI;6v^}2iZEbyHlaJ~4pxbcRR&*P+6+)mu`ufA$(=AfRokgBnP9so;KP#JxSlfFv zcOu(v&KtS2xrM_l`V;5U*vCgdLLYGH(#M8^H@8VO^){1qGtzpvy}o6Q2thDVIMBiX z!u#KO83=i_P?&d+S5Xia&Y7KC;@Io;A)z z0zm`tDJW8abCrpozxb7(dK09Yg;t)mWyMZc`iwr2Mka9XbQrDpyFO}%&{x*$N-N$# zV{$rKY4PvjkaKtg9K!(7ixC3B;$Qw5SX7-z7h^?#5HRll8YcWO5>|i6bHjpy)JHP$ zz2`0y?W;%o-(&q0iJA428*PgCmF0rW+iRof-(|UgyJ5M|SJT6+AO2ifE<86Z7oMw? zc@KY1sb;@%(H2g{NcUMuF@u&5MK!^-Sn!N!_^Jct=9-4`_f`51so4%T3qw}6S)02Y5jhl5fm_`!+ zr@m;^&Nl+E0q5y)>`pT@hE~l;?Q4D#*rXYpx^%_ZuGWdh#8VvBRA_o&K|b0zgZ6{&&6Xd*dIq zry3R-+eYYogtSN20Dz%rrj#K&HUMBgbk`u)s(BM;;%|U%bp{$y*VUR>wx~-iI}P|e zXzSYPDWJsoVGtTgz;CUN{YFw_0pc6EIDLv?kY1a%!gSH|Qr&#V^slDmwg3%t?t}Wz zcHgt-#i-_^el6Qk>C+t1s((+s8Kd0?&!0a(`xRQ>5uLK3*@Zr9G>AA5WP>^Ji*pNfwmDlez{ex*bS6^4F1Pu@HX$T5zlVF__ZcEI%g=w55@ z7+T}%J8h~w@XjTEe>96S{@n460J?$ampBd4LR^Q13mdj$Gd2p}8 zC2MmPpov1qa+?W4L2$SWk5MRg<6$q{l65tw^re%#*uUv#2EGP}&zN>tXNo8XXQoLC4C1?OnA zg%4W1hiJ;CIGfxDj|-x0sg z%LwmSXq`Rm6NEpisp*qL-T@#LcRX;7Wj`C-Mvk=tTDEm`>Iq#VB}#jV zoP1f;C2@6?<#7x#YpkYt>|eIVbgFGw1BV$rU*jF&^S*j9J;Rpm`F2O(Lkd>9yGNg5 zQH$nDgL;t!z754^iD3X#Vm(c;$FEbmd|7P6d`)P8vVVHsx4{+y-s83#E@r=>Sq>*Xd+_wpYLkIsjAKI(Te<@p$BfnzLHt|qe! z(IodZixM~AY))m)I43Y?p#TAV!YU!Nw-DGv@Y6G706c5K`mp#5NDt-E1#U1Ny1n!9 z6`Y7)le-x#zjWZU>V^S4^iVsrvw9rA507u%-G69!yu7@xwVw5q_xJZgzU}1?uG-o6 zCQn>(xBBX*mY_fYifCcCVjO$VD1y`f+@Kp@eaq1TF|=m$^_eL|00O8&WTx+w;*&2? zi!(2*_wa%iPEOufta`{ae6Qm@8LK$#!v$XU~ zuvFm(hQYe^vlPvdb~z+x6SX-{Ot?8Q40P1syf%morJ|u%u5vhDR->MW*MfUI9-{XF zm?#}rEGY<)jJY00?pNXncP~aV6vazR8QSf}Q#D{|XFFKdH!NBUX4MQ$}X_ zHRPaQ=-$hue^Yd=AWF8^^t?hU$Kz@ksD1HT)4zgip!JLugP7EY*cdmS&qf8a8)C9N zu73T?VPI#3?GDX?oZ(op)u&&(lN9XF(;mob_$N0T;3et+=cAT1@YZ+r4}Qni8G61& zTV)TrM+8406AmshyaylHTBZtz3Yer-Ylx$@JD`B}?lX$=ruSh+1LwFS!jU|FQGEV> z5+{${!fZfC)35c?yO0G>Rn5^XR!V<|LPjy2=C1^SHO_R-Id=BdJuDU1f9b?aH*A$P@KM4NAs|>r~5F10I$E z(=i#Z8TT5`TAq@77IwU7){H>{KQOPv;SmU7f&wHaiIh0u-9RG3BVfRoK)Um4;xy@m zB&I_GbRN*jeBZy%y>&}A>0~gfbM86&?8m?V_x|_48FMzuicN_XC~d*?AP=LXq-n5fjXB>Yiwev# zspMfAp~rPGvT{|bOiD*4`%9*C+H3`9>Bj@Y>VDo7g?x+Ys??jWe!9#T4TsnyOYlb4 z&}e%;8x7SrOa9UQ`D;m#W)NO8PtlO_B5gChQqur9u%+PKEOSLC4bv93vIv@uhH@YEOOkfN9p#WHR7-S+ z6U?lH^$p03Q=e#?5>WILaCT!JJARay6~_QlXJS*$@+A(6UPMfzA!0vIM1 z_VPlPKsF+p6M-& zD2})i3@^yJfNJfLr|Ljp@&kdW+YTkYnt7$5A9hiIsMPT&7g;C>uWBuRHAw|a7m3>%+}@a!9QQT`MWyae zRHeD8?xU)N4!BEI>dHv2Kwb;>exmU|AcC=iDQkt019AaX4Rs;qIy)z0{z8Trc*vkUHz}e1r zIn)%mULnrIb73U}9d_ebh;)fEj`18^uHAH>OaPfhbjB%UsHe+fj`9<~)#<>6Cs3S) z7mxozYI9e1f(tu3wzK%(RGR(TVw0Kv|E)4d3QP|xS%NPTI?sAhvt+J9jV}g##8g#t z)kkx9BE+iZbd}5}RH)lPjI7=Be5}&>+s4Ir=C6LNQb9s(;)Vm^!Uc`C7{PO}Nn(f4 z@zAYc204?9Pvf!be=>uvJa9+9Vn#y6VwlPX6u>0s`1p=7X_@hag#P92V}x}9iT?Aq z)tC$19vIt~;mdFFu)ytdoaeWNGrz3BblfD5Pp`i}d#R-Vc2rxu0r3&u!o!4UCuLRQ5%6XqqCcr*9e@Z~b;&*iuEaur?w?nL9!0G-u7Y zmzeV!yc~ifC8TD+WOOq~CXXS_J#T;1? zP5Wd=4jXD$n%{!Q{dU9TMc7%qxnbuGj&3^TUL^idt$8*2W_SCyLAp2Yt!L{X4=<$Q;s2G@Tku?QL5iNK!s;Ol;aRtS8%wi{d152?DiF` z07)w)FP5(!yYzt0!M(xYdK)$y2MoD6&1 z*+{ILVbweuj-W~MNDoL)J{KwQQ*Vx7QbMf5ILU-^N5`xBJy6FOs}>_3)N9wq7paj& z%ws*;7Xr0xF47-EA>1R8vvPD0nd7${Y;q7=f`PfP&H|)Fg0R1X<;B$q5#Oh&v3tCa z8|_i_ND#OiGs{^HICXl^{z1l02FKNv8qcq>^gJ0f&kv6GGV*A7o}o{N-EsI}ATvr? z5W$IaN{wxVn`TJ->@`PJN<--T|%B)kF&65-Sa6K+n!j*G}oEedHin$TztNob1 z`~&iMyn_K5=+B4P>DPK`hDNDLmO#xQ{eFj5o^n@1^Y1)h^&9}lS3m*jCpP-*1!!Q| z2{5I4#_h4GrdPiUGM7trJp&U0fL#|JM<6l0z?#6j8?E&w|4ivb?(+UAp`^a+u(_b4 zd~!qdRV?NQOy&@ik)I69WoAS?#uJwL`bMB_bJ z$8ipXcBm7V{l;Js3wrSLQ_l#a8z*S+1MaGCnhK8@ zBl>z_BP=58!Z5OexS*1q9q()UXYVBE_o?trBkhu`2*sP?RQimj3WFG^LFNm_&o9W| z7Sz%yE(0A`jNkOTgpH@W9{G6tCpeaC>FoClKVA;UoI=u&m5tkcaS~%6@+Om|K{pYx@`|k zo5Aot<^M5r1@RelEgaqhqnG)CPUau(0{i&k{K)i>5x!Szi!Dl05jqUXF~X1S(}QDD z6H00**ImNbg4x6DTHpI#&WMJJCzoE^9*c4Z<=(pHf(!r4NVQAyy{F}ezs zg6ds2gQwG}*!vP2srkr~hh+nChc&LBZt~3_$pL7RZ^Be25rp9Lt0R2nGmE?K7HuMU zdF74-oFK}O?!~T}x`9la1fn}_vQN6J0z8W<;Myv9BqZE!cO)8a8eztl3c)O4ymGe0Z4fa?NMf; z>;|{#-dd{?u<=i5tyBkk>=NaMG9X=}^-jg~!NDZQDL?a&J&e9X?dETjM7Fo&;FyZ# zYo}5!s=kg%XRN8SrZX&~^G^2Zdlt08fa z>|f?j1T?ur4&wx&aR(?k?w^Zrmn8F)*G->6;jSeKgJz?F^cHizFvw;0rUt=5bO)r* zwlP{ydjHT?`v4K`f`^G!*BS#p#J|B-u=mBeXYYIKcb9rp`#O~f5oi{|)0?`oxuObisdoAn&-%+ZX&=0iV!PuJX+u6j$H z{KMqPjNrJiEKSk=6pW#oh%J<03)MF5F{(wc=*^&t3GoOV{UpZxO+_?8(Xl9%AFznH#~i7@KO|4=4(dCb0k16Hh* zo<(~xJ(6A$KSA0A(N9=P23$bju1u)j^e1>=DBlh^)QSBFrbZ+{6-VIo>kk~L-h3Gc zI|YTGCrmhYuSw!(a89>pitVipPhDLuHt8n`O3oDHc@fGYXN#@3#oEdOn1dD@3$kj7 zu;n*JwA+Hbm;FbPF^mgJkSE~`+OvgtgVW^JIMsTQThNPnUnD`qL5eI&@4DrH4T<9F z*G0mk_^D)k-2tc}^x5NF5VptCH>wTv@@(`H5@0E{O_?Er3aIsd7v=xpGlt#XUu>g$ z!DU(;^8BWnvyfrw5`yF1`Hz0!LP@^*9{!I+3LsLz;q3(#M9Yo~Y8b#xSORL_MgyN+ zHS58m5Q5O1!^JDIto>dzW*%_f6+OgjMW^3Zp#65b3Z@ZXkv9=$Msd8ARA=S(^X*aj zgHbs(JF;%bDA7^6FzBp%$R;4;;wD89Cft6Ypz}j(Jyg8iJX|r52k{|>$wh{wMa*2S zuWD^PuP!@mU-nwK0LNAQ+`26|y$O*nP4Z?Rn{dUJS|FqC^s&?^KUZlklmK|a$qI-eMuyIV0lkFQ` zkXm}fmk|41OMsD6AHyEQK_CXvbTl4n>N*`351te`yxYHgRA$2|kg1!7`ZbVI5EUKf zu(Gg~HiA90U%I=W7S(xm3^aM?ulqY}OrD^*?i2@v?HlizeSL%{Uw(5BS99>n4a@Gm z8lyZ$j3~wNL0#?Vze*W9d@Nky3;J^R!%{In?(W8ZPo)eycKod=t6>o!YNG zXp3JVpUAJTc0(Csv^!wPnt!4bZP%Ebi@0NEh_zZqo6NwNkS82Fq_gVD#UnPD2-61s zA{uxk8qkcPF3$Yf4*H32>Erp`pM2xT?bG@6iDhQ0`}n&=j$dCDL&MW1SwlrsyFog2 z(VO>S@OgB1+h2~hAN_B%z0+ttT1vinrciR!gT&1Tq)W|j z9vtL?l7+}LQ_7h<&@oJ1x^p9b6$)~9gG$F2{@CS6h0#KN&#HBF)5)|Ejno3sak=%C z-~m*P{>P+E_y7{L^*9f$_R#8LVqg^y=1QXW^KTWd4>C5+@6bXCq+ z>=jfCOq39fF}I)T=GLP(fGDd`&UEk+iTt<5AEhmJ|sQ@zaOLcvh{XPshL$6 zelL&qS)$`~i;2_SXI=l{N9RI-t5My}13CNY96W$KDcs>slCV&ERj|U-L4OP-Jn)m| ziLS@VM)Olm30T#StQug?*ijx9JIt>-O-tCe$bgPt5H_JbH`l&BlrE0^NJ22?3;j&p zX0g5N)&2U*5ki;aCImJpfuPoQ3KCa#C^H(W`P8S>-I}f%#vyix=&i>hxsCBD2W(BH za~k|v1q_q=fyf+i4_k+lHUq&=<`=c=u?Tk=p4*yxQ6$0=Tj7CR=GynXyEg#@RiQ3E z7<=8R#->KqR)z>gRVp?i-bFs?v@zIO=yQ9nK=b>j`WN<)4NFe7ha_Cp*k!xLX3xnn zDS5%mJx!fL28V(!YpVK35AaxEgpz`42v!rNNxMwuu-R`K%l*p2`d7+uwYs ziX}Zb0}4JM1h?Wug8BA$(<{QQD2KtUnJ2{|zmKOiu@2VecJBd-~Bf~ zardvJorF-=bD_2CXyuRZBN9Ww+i{VQql#0C1hJ4%<>eGfuRHD#ybse(T>n6%>j};! z_Iq~aL{nO>&yyXi-|Um<(DMpc4CX70X4rQVs>;M@e$PT#pUS6>v88ZW!s3Tx4r0F* z=n*)2x1vX02djuhBu|g5S;r?OR<>2%yOrOqO3*CQyOG-zXIz1$oD2j|xl<=dyg~K56Hgw8I zM%JLnoJ7z9`^Q4=82SuE;a!QlyA8lT#o0>s%f3DX|Gk1mpxxAdh|M%bmOF*shb_mJ z*Vr92Xn&Rc2k?~T7{M~TtYB};4MPTnZ#PSZ9sAxi9M0%v7QSU?VkAT`k^X@b?tzY# z;yuJe2Ql?VNWySc^|&HUVv7sz%cP<0%tjnoEdhZ*z$iw?ZDMWM!umJsA9NTjiSgHf za(me&!koXQE!;hnWjU4!3tr5_Afo(fH0XjPygcivM{;SuuZm?gM}~7q;huo()Ft?X z8!o0nzeCs-$zT`tf=%|m3|ynCMyV4il@o&jaov}|-Oz(uOg<|L^Vw6G`s``3a?+l% z4AFHS0L1Gn*%p7Eu#@u{UP$#C#J2PqlMpzyA8KH_(yR7N-Zt?$(J2+jG63)Y*9c(^T;1H%1S8P{x$nLpW|;`^_D0Ht}(45NyWP? z{`bgLZ~5k2g-U>Ko!r+L)ki>@*!dOk_*B;Zn!M&iq?C_EfYh%>o&WT4nYz%xj5B<-lL7Qm)~=vOT_`lK6v;n-(P2@tSS5y!v z5!c<|JbbGb$N%k>Vf%mH^tl4}c>Z=`xW$e!}_#X6hzh*hpsvu&-3*(bFj zM^b`7f=AM0mVRNtY6(>x8|QS_j`@gLbozLH5Mh|cakT!NCutulZjOB%1y5Zt_0J%} zx42=3BFCFLLrh+LaAQ=-mp8-+uE?%z>)TSLC*sMWqFYmww*HJAEv#CBOphA+SABAS zaVg34!9sS3Ml2yUv4|lFp_aTUA6=rJawZ?GNhN6O@jd6Y+kKk>{LQ{;eJDOl(ES(( z`5CANl;t05C+mDhjz?lKkjA4+i_JiY)n?uCE0kD6?KP_b(!EYfX_49gz_58m*LVRY zMe^FB&;N=%oVE+?l}O~COIr)53Xv-cy|Nr#6nm@bR9S!gz!;?>4dRw`xj9{KN*BVI zV08VKLz4MR$P|AcjlV>A)t_KQw^D+IjGI$5BzLzMDCs`!?e>EobRAYz5c?i!hT=sO znu;K|H$J(zCEeZhT&U!|O zv1A||Lim?p{2ca4z;E$9z+Sv{8y7OvD!6zq#1{#{^34*RYgWZm|Hg99c6bZ()_ZQ?EyPHzegi`-*@vCfvmSkANmrAG&5N z9_PC@2kM7X03e~GQpG1z46C?{&f$n93%$I>%~c~5Oz0xIp!21KF2;Ki|2Tm(D9jP( z6agUf#Cia2N}q35CeX%F$E5IF0G7L;-Rz;oJs3FSz30aNWUH7c+RIlj4cc#Se-Gsn ze1581!7gjBYQN1*2~VQEnoOAvYi)EfzlOAohu`3h;n~p`NV^4e-29X0pzZAS=9z2* zYRbd3Ki6E0LeyNwzU;Z?Nly(=dugA`W~^yG4z*zXfK$9ku=$XTSGLqjr_@g~TZAK)YH_}lAMov7F+UlY~sPhAuv{HP$7$I@PahM6Km9;0@SUnS>A3A z3Z$V$qgVP3#wPk)?9<9}R|_bCu1+kq&CfMtg54f@@~S~RlzGc}vl)sw*d$`~#WY60 z1%PW=yolOqs{I1mjGJ=Gvt!^CKwBGcG~*YJ#?8h9?gwth*MXe?$44dgq@GC*{{V>Le$`^AS<=; zMEpc9G3I6Qvs|LJ$78Ta+SA)rrc+VNQRC<3XoS_P7iCpQ>32prAdXIxVsWmWXCI=b zT-G)%!PRT%Y)!*r+7@SXrvGL2rw+t{U+tl`=}L?&aeDHIA1fVt&!ZBj+r>U2qhv8TBV$AOo}ui5~z_HdiBFbq885Rejdd_iV^!4 z(Ey>vhGE8b_E>b;%A1S6iv#Yre)c!I|4!X?-BNi;B(?yeux0+j&TI1RGc$P~_yBY^ zaf;cXkIN7+S@&?dZRuJWl737mk6-P*pXf6ki5tc6zBbej1uBY$gF+V|E*=D7&e^>S3bXN1-#@vtt8o3}VPbzeALr0z)kReR z#uQ4#Orw(wM`&EeqyOT2`|W?hZ#{=+6(ghFU|F_XHcbzj?Wy;5K9y0ABhbh38-o-z zM`&@Kw9x`>)Y{pLLJ`7J*h6;rZ@|cB)Y7@IK_gfI;z)bU41ua&QOEW})+fZ0e^=Xy zUi)4?hsmSK#p%83Heq*nF;}3ftLAQQPgVyymhi1V!_FlTgy~p!|B{B9)lRgiG*f&C z*5C}x6qg4dk5HPeD0J423y`+$)KdFXBw+2=j~r+rQy1O#wR0_S2p}zV%gg z)cLIc2pS&}=CdMy*rM4eFE+c$Vrx$Qcl$1pAoZZDA5RjEAAuq9`;fn>r~dVX=X3`Q zI@10rZsb0dBiBZJ$PfjHt{Md#`Z~P}VfMKL=n+6ZHYq3&Wg4+6`l2$)&GroMc+Qf{#3QJIpIlp z^ozZO=QAT2yfQ=5jTc~z02A6lg=kmy03-R1fkW;+a=iuQG4fYPt5wyw!o@0yi<9>J zfg#FA+(8E)UCH+u#quax=sbf7ekkHF^H7<>8rkdR1nAAf>!=A3UB7jHLBCZvtvxi4 z*e8^hcP>ybPI_+X2jy@xq_7X#Cm51C*p)IOcr~$lR8<}2%GLPm-LC)2_|*&CX=uRgT)Tu474N@+vRW?8E&5cto-)FQuxndu73nwUi-O0XBj!D5k(9S3hIr+6 z`*K#Uviet$3ZrH7<;^LMm)ixrjG=qZoyWrkddywnNfO-PC3Q(XK7RD2CO-r#`JUCR zsH02llB;QyDL@E0`S?>PY(fw=4C~~WjjQ1@V^DdET3{-@&%BMJV5qgAXNv~9(5)8z zqkYrQ_9GkH8Z^rJyh02Jlt8EVb;ZQeO>L<1bGh{Zthz=7Q+{PjioO zO8@cc=Q5R_%0x)~B~x-_r^;=HQ+DB2-1P9=bzl5ru2K8R@&4uYEGKtmALLqCtS-HD zxU{^d-n(!A%7N934qkkzzHe9}LS|*SF5hWR`lKD`fvwN4LO#PapRb&pJoS->8$f

dMk@K zJ2il|zZ$*AE}p)}|21i0`W~4weGhp(?*0A%Q4A>FwTEeoyZXbckOR^{7Bk$JYKftA zkM{ZqID|cl6m%;0fH5vIG3$f+7go{ie?i(XgG4jHk!p3V3G*=Bki`7l(5B@nda4e| zSf=$WphZQHJS>v^im8fvQUJOhG_^L@u6p7S84E{Ujn0ACFQt(|ZL!uE%YS^vvFAD{ zFm>>SG<(t_-mXS#x?#tKJA0x6qE@{(ni+;SX91$jIy{L9>3O~})$D#wGieeSKnGZ% z>T>ZEi|UUAdSMg`R-q<)0IF;)g916mml1e}(*Hf#fdp{6BNooZ$C)fFcLH(w;PUqS z_K=tkQ#Mf_K5?fM?(*=yq@cgY6x7uo*m-g5VNN z1Uug3mlr*D`5TNTZ>PDw zqde6)Y$6+%+5o*k9M?*tl1idNYeU+*F2Q#gm5QIJDpV5`Qs~V^p%t=15G{$>0Xk?7 zuqQ)J8f^d_!Y{5vfBdk~Wb}hVy{&^PT~Q=gtDbkG(D5iKPtnO8Kf-!Y+{Qt z6JTh1FyvF7UrQe+U&oCuOCM~GGWv|_8+;QrlR7LLZd?l-cS|vq;He`#dUE_2h+e7^ zTQ?`HKoVu0E?Cz7qE?xQBUalc6w0v!j}fEu;w~w@2>b}vkNO+zPd-PbvK}1%gIFq{ zjAspU0Fvp$;y|Y^!ArQu$QddjP($#ux-D< z!L*(5lLGlkIvE~xt5@5`JjFH)24`(RpHGCKo@T+71{xYLiat(ifKybL3R|Pa0B1yl z$K9(IMt)HpB(9tn?^~(glg>J_Z~EV#>zmey|osDYfd$9kQ1ttIoIm6?iAnf5kn5?cQz*rv**MGGy&S3XPUVnWbF;ox3 zg45f){@Olv9fL}2W^SVkgzhoWZlz3xh^-upM%jnEld!qu&+I|np1`cDk$D=jU}@B_ z1_%oX*v9f=d`Tq43R6tP#wOOvC={=-9Qb!BF@u!i(v@Vt!NN%M(4F*FRR6;?NI&pW zA{$!T^eqBWfDJLPB^c;fTpm*$FKE2}qG`^+9)8=X(}2t=v@U4~#AS&zFPl96#<{~AQ8wr3A&5sJZ>#9W)&hO}-s~(q^2gBLT z1OAWT4*S{uuY+gIi6tqAn;>9A20lJ{mbe>ZzOwz1Q?~(wgnf1~Xk44}Gl7taOBHU? z)-tCtYji;o8=O(gN3$(pMC*I2JQ1nZ6%#ZRJYqe8VfAmYfQcWF;xI(L5RVcU?tJyY%#WL@XB{^Zy|(2Rd;psOTC5PmVw-phkKK?#06^N79aH zEq938QrRy;RZe~Ip3Wr`4jGRu9R*4-7BEkU1ZN3)O&FZ^YJh9&bkn-US+rNK!C_MR z!ZUd=i)3PFN=(QJnkejypK#pPgR3q{QY*3m$bgx$^#sHsfT%3uK`YWvpx&!QKx(+U z>4huX0<+U%widcQJ0<1grleQ>)98ytMD5Q88eE=(ChLV}eMlOwQ`KVh_ez%Ht9K0Y zc^MW-gCtspk+~+u+An#ges4QKNT843W;n``=KPY8cNgn@#Qk>l^DLxDRC1i9H8ff0W4&e zny^v@FN*&l@qWBOA!ae3O`!Z~wxcbIm*mB$fAc_4v8U!1AR73WRhdgOPh9wHI@&A> zP83z(^PPF!zIbpO|2UV)uCj?B?ogfvVDFin5y-%Qi(@$xZ#oj~)xW=Q)K8-`7C3!c z$i8&ZecZH=^hLgxE{AyzsQ8%Q48?xdzwHsCF~bls1z9u3-Qb3QgrsV5EWH7=`U5Do z0vDLsDfL!ryDo9rt{onnaN~kKW(_qa1zrR5=ZTHBfAnkW2v`z?-v$q|*~s4t^I-7O zlAcjJhXlhjilA^YiuZ zq;vCF_ERJd+E;#BC-5YSOl%c?*M;j<$XsWgTyJXM`>?mUkYfle!^Lv;-UVnEsT!Z% zB)f^dZ*va8p@^i=c{6uH*E!k+#K2Dxls{}6{#{mK4|6*~%JOJ&P)tPBIXD3d<_f)3 znbKLLU~1wf;g@*DMVmk0ct>G5Jd1p^)ApOH24I` z;5RgaqbAfPx{&w{ofLnNEd1e+@lU9AFq%PH0Wxte!-FNj&t9EqEbbqQdurTd#9|`< z)YK=p!gzz32g)iobKCXrxCvzEr`oOL1J&Ka1?0Gy9;g%|hmVN|hqgyqxtaQ(Md&}3 zhhY@8M6y0w2n{aTqFME_Ja)CGk1j+`=2wZ_11S;R^lO5iB4ryHPq{l3HVi(KuwEcY zC$j#TRk2nfy)pJnc}38higX0_1V^Rw6U;`Jh<>tV_nO{|0x3xRRDlJc{XY*?Vj48V zre~_BUhvzrFx(NfhB=U)sshsK>cRb!Y$Glq7*9pPp=O+oZZK+Q?gg;dK93ZehWfDm z0S>s)%)vD+PCK2LqED|rAn)G-gQT)KmsG|}yVn{s-&&RS-8P?Ea{j_K$*oK}N6H&j zkf)xw6R!Oog3WU`CWI<<$icNg`ZBN+l$Hb0H6>Fo^?@Txqvl7>eCQ?pyRKtt3i+I~ zQ~|B4-WBpuVuCL;s4+VJg|2uA1G)o|L>ovT4Q|$J01gIQJiIo^ARnIzejUkIIQhpetz$ zm=D=ZddN#!<1E-n+YP;F-HAxRFFJL-y@3vBf2sZhT8DwT^ummRF>&CsGi5h$o1kV1 z!V_HZk@O7x@eK>AI%9e-+G2L1#IbZikXimnx*T?D;7szRGR=Zt!-Iq-N*X*!S*-Aj zWp6q|4e{$C*5_iibjo3%uhD|@?S?SMeLVVTK+3p;xtL8;g-K%l`aaimkI1>Gjm{7- z={g{P5UM>_dp?nd4$K11#}X`$Ca535-lB!W21C$IITUU}ITUWJMuoITcw_s)qW(kY z$@~kv;|;*1$)lvPQeXq;gV9sW&=M~-5}JU4cz>tD03$&2o0hKZEJ(JFkwzi5y~ozM z+27Pp0&GiP=b!8AKT1Ogd$au^HrGtBTS8%qF-PL`-}kLBoSXEz)`qN@*5ku34={tnj)DUKMT;UF(y z-3}P&XoB~x3m-8lD1d(bZZ{&K!izowqF{`O4ml$5wHwi^zPvLxGb5sx^&d;94EoRh zb5{hO{6j0_ejGFb2^xXYhYV$C09ATCv|Yb69tMK~tcTKjlXVo(`kz=Oy%r1LHc8{n zqK0v<0CQ8E8|^4y{b3?V`WpZ$4n<(RvTnzHNcCF)a*qS!#`ZI>jSaiRiJFfR42F~e zxcw6eWR)ry=~DsC3uR^2e(|$rUfN;$ba5_CZ&*37cC<~RV$pu>i4v)Y$Ii`e*0M>h z1+1S*Ny#du9tOlK7*#lRSS*o118rHk3I#`L_u7Z2;s(7<7!0?HGZcXYTwClVPd%{; z=l2#Do$4EUjDy8wJ|IM9oTXn7i~)A$HA;YbDX*v-4ux3ecM_QXjgR!os6EA(4d1aa zrhSgtVezn08a&HZ;Gy&rO@U8%s0P3?x@rQ3&ZXCBF52F>l9%rF(3uL^$l~r^DkXU( zi+$@OmI1@t!u4889AsUPRDb(o$>?If4*L_qdTV>^WH?yqKS4B%-|}?)(uQLAtnoJk zJtp!8=*|$GS~+;>4}|dd6GU57A>ty7EYu9~SB4}!#Ohd6$zAjfZV;0Qt7&janT9Z; z`}Po4YV3x#iYyxkc#m&bhB^0=@nsgz;OV{Xb6ekb@7?v!uZ)*;6@t-G69D_StF}oj z#cavuvb&A^Ls60waSHvC3k_hk&EuNK?2PviDeJOpXliZ4nrUNSf$a-g)_*np^3=zF zxi&uVuOm-lbM4y5U==nyrAcn)yO2togOCqp88dnC1?}Ix_?zVOCsDE2cJPARQNgKV z^=Yu>hN@|iy7tHjP`zR7w~ui81QbYquU^+g5~@}Dl_yhfPjlc_Du28w=Jm*{y5JzEXz>wU2{iXfY zc+L2=#b&K}(n}uD&I*XY!Pme|G=N|#9icFFQy}ZI2^fRWE{Bu-qJBSN7ZYT#HP?D) zYR#!tjK8G0=-g->xUO|Gv(~I9z!A=D^An1>Go~oEqa_ImDRhpuHDj&UXv8oi6l8E; z?7szVh~5CLp3B%YqH(xLrTky018Jfk=-Ymcmvw7d4?WiLOh&=yI-U@25?(j733+#= z6FWQ%q7|{;8~QD+tf$3JvC5!IQPs`H4Z2!e5Hp^fmBBgj#G$aUh>~}lvp2D^z)koi zV_>}ltwSL~xFDmHmI4u>UoZoQqCqh16T@~FMf`>4k8R?%P+3cGZ8~Cq5bRz$AyC85 zq62-3VGLj_*-@1W47>@#GeGLxqzGQsWHt$&Fx~rMG2eaqe%03Et_19U{b?*H!r1$; ziut4wng}|1aRiC?$;vtybw}Kukk}JO4ZOOg@n#sOmTFIZn zaT*GbxueCQ7lOKbnl(+SezUf9)k`11sdih_XC=T`fX_n@i5E4?^1C=nFxUzxCdd&I ztYg&UwXN*?mx|z<@qlW(*L|2eW&-q<(Lnyfr?|q-fpD>&CQ=!~>p_Bg^VE017gzzw zoZ`HxyRo2Ry!~r%=RvI|kZB=`~v#|6S(kuSE!@|!hmOp~I7cG0@4@^N)!Q?z$w8rNv5gQDT5xoOqKRJ(DBMF?d@UOW3v>+k4Ca7=kIYZP1%^Ab z1@Sc>vmD42v6vbXFbXm%9F?K)ZJzZ0sMAl6{OHPb!82rR+EFgJA&+3F6n7jd9(_nH z`AAnEg^v2cGG_0`bg*6jLDkaD#)x4>?$3uL6 zJev!a4V5~*FeX%0OZvOBx#ZZ<>*v`%DXIuU)?)6pXUWd{F0%dfj7N7D9Lg@4;P5gi zBEt5HDV&~c2ve}(t^?Megu)aTY+9v~h#}HEre<&9f}{AR)ZngBKbo3_DXNUpDjlR< zUA*^cx&Z3a+A8n=cm3EPF(7{aZXek5pNgJ$S!bp_Pe=#I>48)*3uG`kV4Q|=D4~Ys zinm$mr~arHt@I;GzkS*Jg^c-%;v?vdvCn)|-tR|0|AEvsKYByn{?Tvyk;#+%h>&My zae}kNlfmgGi!xLOr)RYh(M2!Ir;i7ryRztrRSng$O5la^I5^nIC>_CW=TauNK&r`Z zjkL64CTLaHmf54m%Jce!C!48=dLyKm7@DY7td=!iLRZO}MdZrg ze1=2CE<_e8NvQ~fPk2euS*Qz`V6X~x>9RddKg^5`HXXOIa_e*x>8m2@$p#Wvkk*Z! z4~*@>`%X8(lK_m;MLJN19RzZ*=Ff`#GgI#xC9>TNKF9UG92(&XPBs3@ZW=Cy8D-Nl zmW2<2T(I)^@G$)iHuO8LKeZseCA77;@PFb`qkVh|5h9LCg_WT;6GAPz2uD-OF{@cb z#)PMtFQwe-d+|1dUk_ra{uXMhRNIbCpoYFU496?HrtWl%63M$LOh7cgNlem7|0 z)Lh?97>29N2&7cC>2k96pKYIKMtf8PKgB=-nXoe$g&oywqNmlZF3o6zx~fye#9kW{ zY!)^aQ34d#$NnEWjFZ2~6gBE@iEtQ&clNqC%A(KCOdEV1m8gV+^yx<^xAz}z&OB)4&zuZb9q$AQ&F%;WlqMp7UskN{A!z_Pun z$@u;%urAf;zQW=#FMoj@^?KB0n_+TGiSl4&V zm>r<siwQJX>VDw zV53$bmCwcv{8uuO;!uG8JroEl_?? zN+L!Gp3Mc(id}GgP0k$oO{xwC*tuS#t>>jCZ%UWVbh#0Ol(~$>tq7o*8G6eC7w2NI z5XS-+fCL__2UOrRw6e1P;gQSL#4V#K-E> zt5nX!9;0OPPIaz)f!r;RQR7U(;Mcn!Y4&{)@?G+Xq8xn7aITI@=beB3a+6mvAGma# z@#rTE(FwP6V29UdxksGc0eaM7G)agB$RY_!5X$pH=tTmzfPDSpRkurO2mkM5>!CqrmUo^sie3 zY8uLjE@rT}an_r?vY(6b>&kLGVyfni>w``08|!wb$@4~;Ps5UP9s9aOVMJ2UJ#}M2AIL2v}mwP?`!?$l8}Yq(S!Jpnm^~`9fgcx#(~met?W)yGEq{c{p~ecQcvjxVdv zChktXB{Mg^$Z^F2es8>BOof>od)Ii5JAW1E4w`i=MF*J#>UWsX(y#rHJPY+7t&G7% zj%8^Ua98;UJN%B@Q%>pyf~9#~xMpu`^*-DtK}uCd$nPp*6?a74)wZ&qABXm0)h<8Jv+KA`25*XY}bZ%N?pv)5rr7n z96u&VsDnAE1Qj53i#Vrvy}2|(aYd_AMB-1zqFl`S%*XGb6K-Yb+@UejE*!sH$^e_R zHqKqc7+ng6EU9EQIr{q_5`f+yyk?1tF4W9!$ywR>R_Qel4w@d~vhbKkzb}<+=yRmN zu=2j$<4<-w%}t;wVD#ZyPXKR@3B2si@ss=%;UCkh=X<=wj#s=7gF6scgXfb{h2xRY z=Xp1Xv~10)!&b>^04l)@Vu|eU3|!)}V4CBRfy%4=8a<29y@1h^AWbGUb>jiY&4gt2 z&!(wFd0oMP488=r>?Fmav7S?^K=;?mOKCd{tS}`CY<+Jx9vCK;l>g~8NjUK_kOPT_ zx(7o;S0X#dHKRqzQ7|*}9j@TY7SJ7EFJzrJ%C%2?XHGCgb}IlN-7(OA1N+>a1=0<% zoOpTp_8bgLS?Za?KU+aCF|GCi4C~h~A#czDoE&?+ndXQn`E$+Aa zCsvZuiaa_Dpup|$mYOF-2;j8X0G+Fs6w}bhsqd@DY>?JjRR!?49@=L{Knc$*%0U)p zwf^h*ODLwE&i*BYQUZHpTI+zK4MIBFGW|#Mg0cr@N#5Sh%2iXoO%2lnF7PPeQF+Nj|ck{nZlFNiZ=Z`mhw{ntUwXl%a3L)DeJh zwld}uSZh_<3;ut3d5G$Tse_o*37Yh7->DW9h)U;CBLwMq`ds3-D>Y%B#odEqzh_Mvq#n1&VEK_+QLYNz(TSY7W zuyzys#9X`Sng9TN>VQs3K4e{(Y1$Xm2%4m~((-&jd_E`$l(3H-wBs$k?$bqaxeHfe zbQ<)*XnJi#H}|Xe^->1JnJp=p)l8obszBMD-HzZw9`O@egt62e)659LQ{yNmPBG_* zII90LMfnsby$c;OVjB0_mYn;&TTT7;3jD80fy9 zQi}I>&n&`_5j&r_Z9xV)!evvb82w(C?lycDsi(*NSbKvS11Er6)9Ln& zz?IZ{`9mnT)Xer+=Mn&j=Ekn2wLgXs_eOBzag^OMAqjbdQ>Ac6UNFL&VT4rNF)@lW zNnP9{Bk1fuS&Iu>EXhUOBlsJe3^rI)HSV#q{CuA*KW9BuR~%6xg28!=B~*;3=zL-H zi8gmVW=7*xQtDR-f0VB!AMa$bYMC$UUt*_#vvatb_vc4Q_uWNX4|$Px=K!oMONysl08tYriK?(-w}_!4@%RwX7%;Grt5q-YB)S;?Q&X!pnw~=$F7nL4Y7+1 z|CJDv$PI9YfU6JZMQ^&LQ87k?P!jHM*-HeIbuI;!Czf&QS4~`3fOcnDFZL9`YuN{o zQwx01DJACmhoNor7@dlvQe&VIZy&t{FlI%YB@+^=LhEw zbU$GBz0D|~Wo+rNf5YZZ6@|H-UH~W@Mj>^o|COZ5lM~Yfr%Y7~53s`J!7V@wsYu-2 zMChM~V)(@}NHeGHDk%fHj|jS7rSs^Ei6vEQfds~7Mrcxi3a2<0T~U2I?}})t5rW&g zF(ZVt@Ftwp#}K&*jbpPOSOjlN>m74#6|1F=5*oHIXaVg#=lOwcZk%1li1E&{V6L=P z?fi+w@Fe;T=XLsx^5Suy!j%(l{&5#t6OQ&%|1FJ% zM-2&E&1u-F9^j71XP8mz5#I1D@PKeOK-`68ARqy#SQP*8GRw}gv^m-v-3H}mBzlE% z^r)6#Hl;8Jn14*-28CXHnuICwX8xLp+f!G_aKmr{E)O~E93<#ArDW9+rh10hy6!XU z!|kopb!=N>!kwQ)YSkCX#@2md=wI2MKF#VXPCapwa!Fo@Hs5l&1Su_-i%$(Jz?gso zM4qZ#r31L|fPKksmn?E7F`j1V2klh5%8c}8R*Bl2^Bb*O@xQXl1z6bSEyZ* zo!(AYHd1e3GsB>nT|G&~COxq^jWMMgwy}D{Q?Un{EY7eh+S5HXROc3 z0dk!p{rjMjObDJhL~KkqK!RLgsXP5aF0;IeWGP#&Bqohi|2x&4Wi4^xMR$%F4K5PX z*LzdtjP8EA?B2rLb9grJ}Xot%A|jg=R8*^!J2t%yRf_GyRLk1Y9Y5Ige=vkKV`*fe=~0X6?;MQhQilN0 zm`So;b&1KIec;fT# zQ(oEe`Ww=knNSO>^Tt@$R7qgAtNNunoER(~=NO?tcsFVGN&=PJJP@2GxF}hPj&v3S z3e#QGlnAGQf`diy#2C)&gHEcnh^22@dz#a;A;KQU|4E_Zah*j=9tB=V0$6{3zE=f0%%u4&1v^P zCOwTA#I;W92Q9-`6(|V`3;v%~KTeDc-xYwa`_dlsC4RZ4aMg@yk2hvkfd!lhxFB%= z0RKQL{su1saa~}g^km7QAGyUXhmi7Xt;*nR?ZqLqSf!_x?i2NCc(lEQDFY+U%5Xx( zT^d6a)~{Th?k>a|7tBY6;TDQPC^yp(^hbrvCEJ%=b{vKtUmc((LwKfoE5GVU>cPkz zRUmpQ!kb8Bpbcoeh|E^8&WskJH1i3bu`9{;EXLLMuaZOtoX>6IdC;JEW7!w?ip8la ztbzbuhguL7qCncCj)PmhjSQ@u_4IM1()1H}1Adsa9!n$)EA5O5?(GH##TTmukrIV* zPa*~cS{&(V3_$%81Pn+>d>1iSlF*;)6^2rn1vR@QIcQqtgx*Uy1u*9y6Kpg-=_D6@ zLu|w;B=zE`@?A&h_}I?ue;&kY#4G4^o{Qx)z%XCYvC z2V*Me)i_MUTgTyQ4vuq2+cdp3OfT1L?^+B7fuM?jMDR2cX~;S6a5j)ttnZ+spG@1| zA^DS7mC>KEtKs8n;v7^u6zzajiy7acTgb%D!86#Z$k1B?IHO zqC`hTMtrWE>}tm}Q@Ywsts{{#=%NQ*&>0v(*gWnSN6V0$!q9XfJNEIUGDkoaiW2`w zuxRLGU=_q3{qA@}$Rlys;p4wBU>UK~g}9VraMGA$Nel(k4;Df_ZVa|2WL#kO9DJEq zXy0MSE9R^=X$I5U13vLgRW6``cSuUa`5$R2f;#bb28;1e8kt@DP^=&u)ZT&YiF#?J zSGNC8U`sb|(LapjZdzhh*CFhH0%0`iA|UUQnms}79>WuZ&K>l{0wW}$y__&K(cI0@c6gqW6Xh5@YbzrrcJQF8pGf$$s;<)ZGXfVt(*vH1G zal$Yz2C3SFw z5;C74%9I3poP`qCyBZ}k%A+E6dBOrsG#Vh~ps97uA48L9glavmEsihbJ*3i-ya`WD zXPN{DUyNdKNgp!@e1no6u^&Ai5u)VQ4Kwy#>ix( z3{|AYJqT*FEFWK9paX$r!Z%nNGBNgM3WF5W!X3La&CPcJP5oEly{*o)_QT#SRDlLS ziP)CZ3|yTs+yn`69bkyvoXLTFej{l@MXjSLRIdUQ%%RWBR^G$JVh)-)S#%EP61H*) zGoWA%aArr5qu*nQwRF1uPuVqVsuv`H4*#CRpQ}H5fQYi>7U)&)E~nPCwsAB*49bo4 zHk!&%b?FrYCv@&&)Bh|3D+LdN7?Vqo$?lGdZodtG<0Qd2CT^5t61oKA00i?~>{1EJ z+YL&G^}wXVZ7huwsJs1ZG~PxFr7gMR$@VP!={tkBMwH%4R`>> z#ImoRqb@G8QUwAjW(MmCNQKoAsaAXS*;NwzfSkRwI2wD}5XhvX7OOCJ3HBjaE#JzY z0+|=pj~6&Z*-Qm=XfCF?Ys1(RLK|CsCN5H7e1ZVbeEqYlqlJ?*k?N$!py@S;p+OGR#Yd5`rXi0S_*FIM57YE6QS~^h@*hOfMIe^TC4cW^=76qXys;R4;&?}34|hy!DMg< zp-i1&;9ni)WAwM6r&`M6WV$jN{+jkyUv<)2eU4PJC(>50s(xVV?G6cV@8D(#cua?6 zgF6sVK3$NCwi5{ybZ*Ac1&o@nK&AT4!-(_AKd@u>p}DZ5_9#cczjgr3icu|S?B64az4p?{>svBl_U ze<`E8i{OGT9In`?#(q<9l8py>NPsY?K^P7Y6-h9w5=U+^l1XpN#1aR>elfz`0=cqK zX-$*L>ogvEP&d`lqJC~C-#Zx-7mltJW=ER(0eBm1bFNcbLx2cMaFIUxmPWLM)X90UNzGD-vHV(Q%y2Mt;Z{c373(@?``$MX~e z*19$sJs<&|{CnD(ggKxp7kJG^33n&rjq`eqb)Yo+wb@z;b#;x@AS)tfa&LXq2DHe`<)gv-Ws0f>@w;`Ir@{9rG(lco=iRz-UxL6UgJ{XT{ z0&d^sZ88fm2~o26L}q452FU+)B8{@{Bl2w#UFTA~42n2&;C^xpwKm1m*x3U2rXc%# zgDZ4h&Ux!+tUy@854Aj0))=gtg9-;fWnFJBrXzyuFrsNcQtU3h74$)pNPLJCrnLiS ztOd?w4?8Dz2+?Bqx=Vf+fr-=*Kn7P}XTR&Vyu`gQ0a0FukQ9*DR}e#CymoXKN2`A_ zgr#GaD`Nl26(R7B4FWSzXdLdu4B&)6(cs8(Iv21V0GPH?bAz*2ILSn}`2|pRW`2fs zemI}9Wjv-A0LTdx1%R$75OMZ^UmC z+GlDF%(5=PIh||xKCtWt)Xj48a8~~c0R_*8moq!T@e3}tE92)+?a{2jl3V0<$34;u zo{Kqpzrq=EF!0Kf4R_32xIiB0vuQ9+z)KQ_mV(Xic<1{16`s)DLRELrsLmGcIuL=& zfJolKQD8d6c))kT8LyA;xwcBzb8erY=Uu;H3ZqQvK({CeRa~hKAV!275CXRCCKI3A zd;q66N_jk(;Na@;Ww;sZSP_{3;{kKVngE%>9r8Vt`=4BK4@*26FKpeHt8<|5_o8{~ zJ`70J^I*b9%ugWq!e-k>V&usXh+P-qgGNz=!L}bI97k>GQ#|zBUj1oggD4m4b=C;~ z#Bdjf=j)GRE!r8iFyx-nVrS_pdYtVve4vw4FbNegnZf*+f)Y80Bn7(K); zBq!c34IpqVnEK7qQn;3G;f^kcsGKo|-rmE_SH9>1V->|zkO4~_)m&(rwA6Oc4N-ur z)nnXe2KnuVCtB0X2H;&WN;Y%T0}0TKn0ufd3&ff0vxA6|8C2zFs+?>c$!(|5=Milq z=2bX!Yo)+gkl13FIJJJV&-YRO(1)*_!6r-K**!tIu(L&Tjo|BG-U^7q; zXytUW;;=L8$j0a#fr4xa<1476!$SlGhda4TjHtpF!Vx;vNF2q!3G@qBr0VS9?T+GE zC4gJRzA=`>C00U~(4v;=uYk&lT@VtdS^Q=LjiK}iJMN0`+rTNSL?|9^fQ1?h9bIAi zqRj={cLg54QT?WP4bP!4Sl8rivJ?!H{{mx{7c-V*vy%U1f^lMGW6L}ZOC@99*$5Rz zpKY6iN+k^>_y5K-08&^og=(P__S37Vg^3dRj8A|VHBz!@wKr94qL?as=~vWXkGp6* z${vdMAc-F`MC$~o;mfi=_1ifWruvstvk(DHO^lF;(hd<@jw=i*8+hEd0+GS@V8M_> z$iq-g1qg8di&$@oHNQT7hH!V$GR%h*D-gj#I_5FpAZknpBwpDY$}kS9+C7d2CSkV* zRdv2Z4u+-WNw5PtJkXSYc`JRX6B^JHx6VWCVN4O3*M{%7LYP@u3v zgl0u3_yY!>3_0e?l6|ot5H}4;qdPe5g|Hg9O@P_zSN{u+`{G5rX9~_^2tIjT1wz_* zh%eVsyur&K21kw6H`u8Ty76#UU2lWr9n6!1Z zufvI;4_7+@+1-Z*w9kHs)6WR^=)mqzAKioP-;1+}u^z&;pnzP|83qGTip2Tf_`ska zI1;8Xt}nnR{cJGg3gyf5TShU?z+y#Du=o;Q0H)~9QEU~lPCFhd0myz8aZ<}3_r$2R zZJK#YdU%XFgv^Yeo^EcF&GIwh9V~Bx(4gRvhWLe`!ZMurvGz}wCWLbddm92{#>>r~ zXDh#2H*saxa5U(i=eV~6H4JkUKbuKb_>znofff2@Y?XFM5P_SI-KDmQtTt%6^BS7% zh!iz8=pyQv-r?K7+S z(lBabz+GNGq!KxKJz2h@a`2f&W**2JT^r!f#x}{8(|k9Y*FucB-{4UMl+{mlUgqJU z!EmW%BUDtF$3R8Cf$q7-A}7`x0fk1Okq)P!C$d+)e2wQ9ahF&g=$F>{choP&g&tE! z-n4QHgy6ya+1NYD;k8Yz4h^c^Io4&r{6G+l5hS>nDOsmzlD*_6L$G}nahG#Am4}Wk zzYwg&YXkEP&r%(`z!CS$I$)L=n)qac38Qqh?FoiDe-|4h)`Z3qP+|}I9+j{H#Rr{$ z@g-^mUET67k`Vid(N62#=%PZWRkQ|J+##FeGs~w5R7PKsIag{3A*db)8=60YAXk*7 z5&#_BY1xHG)gjL~8lljg9-$dxj4ejy?4e9}z(zbLb!cDkZFO!u#>x*qWueF_J%nJ; z_BE{oLr4k{F&@b=0g}oF?Tk4G2*nDAq$EVN2lBP&SecCwvPI~cpcoD*(^ZW!-k^Kd zLF*+@FM@YkICv*W(e#MqK{A)L3qLd*Vh_d?6Tir_#7%K9HVf>gyz_;i!k*`_bB=nu zqJbs7`dzEe{~Ytr*J2HmA3RN07Gqa+q^r?~m?VCf!!WVe=mVaDlfjpOh&~GD#@HTR ztjoc%;C|w{2P;27YPU2B>xb&6njJEis@*(;v}C%}w!w0ct)WF;*<#tmsgNLZ=4hV{ zlGyDYedZk>8--d}uVfS5SC*^40Hriof5V&TptHj~LT%}NXfIsQ(?8e)f0EOKXNizOxbFl@$46%`;a4e_9 zkV63q;Z-DE!qJoe6jYLx&|AH#l8a(OAu$zKF;~5-b>Za=#jnOGN&yb6jU8*!Gn&7$ zJb02+z{kdcAe!xR1XqLrBxptPhGFJ&XqQePPO5)_F05us>*(@1a|8)(;AZ4esJ-zU2xnfnhPmu@M|!Go7{W1HC^tbg5DW+Mjvzu@ zPh8jPB{jSVpac=dO1vVi7hn1|_z6$Qp9He0P$1{&JE9V=Ad@X#QjWC+v=7&;N~Z;d znVthkk%vEc>fuM|4J`=jtKr)1_6)0~el4p-OOi7>KQ%FRgl~FF@~vcqb?$F?Bpsz2 z-~;-QvRXjHVst1gdj+Jy1-T%Gn?zT6#vYcYxOPWX6zkzH0*~ZDtBe7u4wmC&ryoeh zNWM$mRumvczD=#Yn68$SaEd&G^C2y-<)m}Ce>egP?_ww*hU@E8;*wUxALaEgvS!3O zvUT*P^4aB-$$!(@7s{fS_xN81K4oaF=RCdO307fKWD^B`DGG4&4s1Hha{Eq3M~kS% zqF-97{Z<(Gi%)=o)t}@=@uGRKPMG4O(whPFLi-6`nK{s4VGJr=uFMVXex zzy-Mo0H=aD^$2RDhs>rl#Jq8Gm5U>EI#uXl3QmiNmX3QUo(bFM(IBQc(J&2?<4IZK zykXUe9rNG=%u9w{h$BLsZUFPtfEb8y6F%?uXwAr%gqCr4ZWJ-ccIvu_IfBl?=Avcr-^KcOLU{lP)3I@}aQcGBbULcGD{&|oY_BUD-el^EPC5sL zmW@@Xf{*WF>ex(YQ#WW?@b#*v$KtF~2r5CBa=Q5=r^Q}$Kh-#|U#}gKPs60nD?WBjAI1xyi_#a&;*H3L^^MEXDw(BwzdpCN+WZDX@>C z0y||PnmI_#DFcP8b2*+vY&@#Qt8Mlh{Av*>Bo1E_mKnSk*BFXsp$;;bq=hw!faWg3 zUJ=a}QS2R9vx59txA6kbD7>*ZNxrj`F@*Rc$&;oa##GEZ>_wIvmTBUa`WCwdRZFOA zvVU%+nw3BuRD{Wl1R{Ru2_;a(BqRy+GcYBiui^SK`DPJJIGqtu(g<K&i0rCwY;4jzRiqrk@KPvJA`q-FzbhgWMW2X$1c&nxeUrT!ADfS8aV5U_qGDd4793W} ze06JXhR_5Pm`CmjoNd3nIwp(E9Vq_A=FWdi9a;)*dRVBNDpuU4~FWTkMU5a~4 zFhrS$@1=M$R!geRJ8fXx5DPHo5L4UV@=tC%B!1Stw?> z=-wCpV>%deb_t{br25&IwRJs8<{jy+{g1gd2QzYbp_mr(n%FBa9c4fqrgpIw_WXa( zM}?@Px|6;ZG{BJczy$l$*Wz5)(`c2VMQU#rq6FO}7@`b)Pj*o3?#kf-lPxhLfD=gt z+ulBFg*HmeqA7&96)G_|Ax7CR9h1X{gKh0Jl}mzjj>}+Ri3IbsOTkx42!S!6PV0W) zo%_g&AEb8F5q56|PW!aUecoix3oezSJ!0h{d=R!48GS>M@~~MsR~T)~p2{?u6}AVw z&#4QbFn0PKl2(ckDOzB$o`68OaIBVTb&7m$m+uxSGv1YlRQ z_9s6L^W%tdDoCbD)2W&n`XkYvGk}BYB8+Om1G9x+d=R5sh)(T5GwHVD#ZguYJNj#v z0@KmLpUQGL%P8T;QuV|X6JVWV#0+jR839#$$;`OXmzGXq0IJBd6zU6o$wHb{zixS& zDYQI*?lwgvDK>fJx4IaOZqf?)`S6zMtBUohUiF{)X+nsnSlGytScyqMa=27QuiQ^@drnTCH*sRVD*E00E0xa&uDNiLYfkR2fl1X=+t zN3`MK93_lH-rlc&4XfYI3;{$u(EuB?&8R9-B}1W{kMMNE^C?+>e3fQ%qQFJN^eHk^ zEM=v~TCwJ-E{R;ug$`iyn6c6=mV7vO8cj|d(qIEAU0%&8*gJW7fzN>yCEyUV4<`CA zuRpRHM~%aG5#}dRe^WFcvJKyyN82y~R26PSlB^gXMlMjkYzvN;2aR0yq6w7|fyQ;i z=>$zAvb4n%C5gxIL98e_nOFcUg0=ymx*&p~;Sz%h$aRxoKGhH6oVaOHaOkM zx$j##7G8aNk5Xd`qz3o`@gfsS1P17{^(R)jlyPy|Ukt*Os3NCzqVmZbexFZ3-Skx< z5%L{J9K&=x=(yyMAcbIX8POJgQrJwtdNz#uLLLjEc7sWak{s>ydkmkt6ZSACkP)#@ zpQd85M)g-(>KN~9-FTcTkyUeN{Or~d<4jWoF=%oGO(W<xm_UQy@*uo}a)gR0e`%0P`&U_?&5^ zA=xQ4^(~)@PT}H$43__k5Naac(F>rI(5S55P$aFH;6+yf zyC4$C8|$RQPEicB^Wu6A-*y5Z7ROj76cGzJKj>Gs@Q*bL7}ihWkENWQX(nQ6JxVhk zzzZcv_DFH6r_TYvATMj|*u%~X0p5W}4uP5yU#0fxIABiP#jOlh!z6e#s#CzYaRtH_ zR3ah%fEs^>0S+GfO;iW4lb}AHIg{x>D&Ko@d5~pUj&q6Qp+nq)y~*a*{+QgoZ~E+w z1kt|`=(fLBjBxT#YGR-F8h3rX(3ZJ#$UUCI1Qas}GIKB=C`;9E zsNj0vc6P!8>J7)U!OU?ZS;lZT2m#8Q z;soh55)~ImI4l$NB5md9A9!}CsnhW@;xJBLzJWs zk-j0HtJma{jUUtkp5q=t=MLY?ymi`Jm#-%%21V)1(i4dOY@b3N>deV@d zi3^0Y+ZmHixJVVSl8g$N7C9p$h{p`1PbXc4r|toakzzah4fCC(5eZ8n-ejvLnI{is zi^r*;QoGlsb~;9OjyOEy%E`^<_StABMH^fn>-Nrj5H#6$ z0gQfx*i=L58|(+yNbV%H@*6+`>SWT;Q7 zePyr~KE8i)bEJE7w7Z^&D}So$fjlK<;0&DkbNC?1d-nMSbfO}rx0_}3X-C=F6jCrA zGM+#g-R4oz^#3F7UEt%o?t0&~XGYJl9ofyRNt$k{RwX%-EI;BnP90}#%eGqi5lME^ zHbog}Mz&(TG9x8U8mL=JDWwz&95_JXwueW#eNcMfz=7M|nhW%DfpaNbpuA~$KYF3( z(w0YCa=yR+T6@nPNlwzj=N>1Tz4zMdz5eh2f2}>_T#qaDoFHT0i5kDZ?{xnuccNxu zc+~Ya&TUXlns`G;5vb_0H&ke<*z|c5ZF=z*xP@nW(JNp48dk`QLwc_B~!_Wk# zaP-a4A@1*e}!26or0;>p8B!%5|LUf8%;eimAQ>V$rqwK!`fQ*Ff|sV(s$u zC~h)_kedmSsa!Xla6&Gbv{;|g+c{V7_Y_=~ykmOpe8RU!-ZzA-ZpE!%UB54l$9I%P zl1LD@XeHH|chMPL-^^9Bv*I7GntLJlK&BBm3^p}pO`%Jz#sE~Wr`s|=rjna zp@Q0W8KYIFj?dLp>0oS4l@7H=3%2h7E2L6jBOqlyB!;j{j7jKWQ<#e}7U+W1qYFX! zy1-@y`$V@H=*zfroF_*7K)nrV592~^i4o^ zc?0YWn`FI6lO0%R$rgj&bG@-5KWVI<3$gymkl75VkA>rt(ihYAKSrREKGoE%eQp-K zozD!K!Eg13YS~sWUl(<`!4H~^7=F+s4l+#k%=L3&&^x&>1##&^Hr&`ysS}e%>BH~T zEC3b~(WJM78Nd*-onXk@0d2qU-~A?l%q(HNjynbXAp{q!kfrJm<(rTE4Fp3Yc21xz8kA6U-Fhq$$+R&e_Kb9_(ndA`zoy1$6VR81rtP=Hiu-z`oW?~KmPYnH+swXg-nUFTW`I3NIRwf z8;}&u{xz-Ckm5bJ7cNX@HN^UeOu(?hqWA+ zi!T=Cwz<+XQVs{Bs#C7`(&jh3M&@{|@%{%)>OtE_K|}7PUk|`AlaI_Zj}jA;bIm0n z6uRDzWfT2OlVC!Pg`iV00Up3GH?gv9cc`hRw?$olsx1ztS~CExjTB|m{+tGARcT-v zpK6tyVA^X`{9G<-3xWb{T--(fiSK*kG?Ey-=J8h6%nP?Wxe%PDO$K)mhQI|+tKMM_ zBX&Z7+i8_Z=ApZ?5DFk@6Wj;lOHzQeOTjDyIxPU!V7fgaMq68L)9-b10}jecaIMB6 z+TnU~)W@G}#u6RuutFvQqcNszE`;>u4I_>8Nq?4*w6j?H9N&IWcZqEHi7;BgTs}U{ zo|aOS5(wf`7SOCe!(c;I2uK*-Ioggd(TKQ{S-)b#cc{9Zqu{=|O@e;POy5z$A|x_l zHragfGm+nmfKApm4@rQv9q~ea`#PoJx9tOi28q$qWxfz4ii9Uis|S*o{z%_W1Euxt zG?#mz^c{Z)k_f#efr9wf1NtP~_BgJOV0vb6q9yOZarGU0ide!O(xlny;63Tf z=Wot!_4_J<;A$brX`rt?;eFLO?|?K2W~HI&r+_G?B&JM1=eD;*=FmW$1nPQ-D#la9 zZ2jfUK#y+zN;i53&4v7-Awk8o3!M5%9yEOB22R6qGZ@bG0&$8jA-v`2Rc>zY*?u(l zmYyA5e}1#Yp6z(A6;Y(E-_q`D(WAlt4e|E(!cj>bv0W7SV3CYqdqwHH45Pb)3GXlo z3m6jpqhKgZaHt^<6V^tqR`c@uo?_Vup|++i!(c!qqUH6)$$H#Y2WYp!0p-aqMW?~~ z_O8Faxo7)(vl-gr?K+LiC#eKNfeZnVux+J$D2u&3CZ<)1G9UIo*7Y|x$CyIR*w^2~ z7ntC1unk_Nu?YC@eu>gbOT~bE9)Q7o>Y@ppncUQTYf=(t5V*1KWJllsvy- z3@BInf4ENZ{F_|QDVOWs!Q*ckUoxFW-;V{afxdXV*NZr7Q)Vu0Ocu`q%nr&F)AjkH z%t8<@LHkisNc-MS+^H&mP+9>I?4YL5{#RY;mmpfD9Vc$@LDoVz(vtEgT#(>wNG(Pc z>{S#DD{R9NxRVs)LDj96!M=p7lXKd)B4W$h)S=_Tk38N(Ji=$++1VpOQ?O8|GX-+y zq_B|tx}#Ct{=v5#{lvR}?oBOU?~l&!;NX1G&W^4xwSTk-lnXL|m!uNFGO113wqyYW zQYxoQ?hU?P3coq?L16$0{iN&LL-oJP@j|v2wVy;jOU1XJ$w3!s8W2)|nU<0YU8`RAjL)BQEWdwgkTtuByC_}yii&x?zD|4GjerD!D!-!GE(Koy|j^1md zvCKO4LHlJDMyxHJg&wmfds-L(%Ovwo`q^m+sf=uD3X9lkxJeS!ve1N{tUZ)Bpo6AO zSjy<_%kUJ~Y(a^?_Uqa1*R#Z8n#zPf({NoRi3x42C&|LwxCn{^^}N$pf!kTocstu} z^c8I}I`&5V4c~Hy+1tERo7&kaKat?4yB&b-DA@){W%29P-sRT|x+RICz@jiF!t@yz zu`N`M^dO?a;MxJ5c>Bj@w9lq_X*@b>)>w4VsGn^nH|qE@`vejbk^?`aBWYgcvRq|2 zpbH;OVQw|Eh8VhwAuS@CvS~m*b}|{-L#J0M#zcOCm1QS3MY4k^x2cOZ@gd#JS%yF; z0U_wygAxQ-Gh=wx_1|xn2_V|if!quk7|8bn-yP4e9YdYHU5o(TgFt14NAsm|^$IVi zp5#03M3xepG&a^RbhmQ!7$4~&r;w7Hu(Nmlf0$#B^683xdo2wnI}H-=8`I!Nu7qSZ zP@Q0WG6%Oq{yLbQ#(aocrDNab=R~wub7JF^0xY;TFyIKjWZe2*spxnH!Ds*i=-pm{ z=}y6PP2RHcFvp(Tfuc9>g}h7suhny4?^ zn$T&Y31gu0Aqm+Hmao?Rqg-oDzI_>P#%)MSS2t1V{|5Ogh9(%1io1ae z2o!{GlSYia{4zbE!Pqg?p0Gi=uyaGCOv{qN97B{k3E=UzRnd+frSHttK)1;G zb6x*&D)YVPKDjjsSFR)bYWtKk3xMK8im=mV!{TeZ$Ak&Sup!N7i)HSbU!( zkAF-9F#%Z`D&P@BC58><-X8dj!u!;FJ23ahaJ@~9x^u5szu*#i>Jp*57CHWYGDeP z5bYxqF%mWiqG@Ix$QKTI0|LO`u8(i&Y5QQ;TRQ|)QO<@zo}Hkq)_Fwy!!^=*( zkXcPG3#_g1Rt#$e^9PF8d6%M1Aewk$Qe&horF#itK#r1!ywnED=g_7ODqI+?U4X!p zK(B#P2RQhH`SYxG4;Vr0uHtXW%An|Qu^r;|fv$IKFCLIbG?FtA69r{+m_u`=k9@bh z&PWX(kWsWdvMMnr>FYvJ?3~Ul|2oSbnfY{GeyWo33GV&1mfu{l@5p<^!gho;MZK2> zX&2m|enIUbFyKErpdYiaNIk`%%wFlo09xDCoMVQ$Q;3eaqY=V*MFDfz2zc+vA;^g8 ziJNn>t}HW5yXYc!cJft0%&ZU+Euo7d{uSDLtj2ByKm$ zcw3*yL{iNcKFSl-F!v@u=QLGj84ErL*o_Pm;yNe~OrC5n!TBdc1D*JE7y2q0STpDn z);kuvQ=&Y{t={~{3e&}{>f~V9Vl=|>h7gkwcVY1rM%;HtIp~B&(}VpfqSy^sVC@^5 z9ejMEgaR*Mpp8~={|`780r9V=eOrrjh=8nohiUls_VYW~3U8KI4U6S>sFzSHx_u+z zr2&sExC5!0V?#t)q38oSq!DJ*D6wP}OEWb|XnrGSDhJ|=W0X0;fh4Zn=7TNBqTb`( zL7gJ7n2#;Td7{94w?pUK^?9pY9io*1|nd? zPOH)6sU@(JmNeZ38NwJ96i@G1TxO*wFJ;QIVhSLu5gSR5MH3z49D;!F-vho~?} zd8@j&SgO&`PN8Y5OvaJu%^gOOX%OlT`gV(-d#i1(_=u6o0W^h3TZ!0Umuw~CR$GL$ zl}NXZtwd2~D^Zl$N))AAiKt3jiHbcI%rZ#jIlp~@xs_~VTjHSUZ`)oxNUuuv2Xzxb zN1{PMEnUxviaE3JxO%s!s_@T9l`B4+hDt^VK~h{`sWdc7wicp3_z)+~bbWEee?Za9 zW;s68LNE{Y&<1Q58z{&G#fTnL@SkzSW$!>B2Gh0_1U@cW56==cXGYsm8oeB8J`r{J zqMchTMeBWU`1*5DaLVvu?SnF=f-YL{ytPJn?G5(Ue!Adi+2<{Kej)uS z{5$sNXLI38C%Lc~!5)b^i3v;c04qfQ6}owpSrE=L8mXDi;eCmUN7>99JUvXTnJEkF z%#`}Rr|>E=GbQ*67@OUZj2vqH=4QJi(;Mtqk=d~%yP5F|>E2|GZY>RV#+G$#ew*18{ z2pu3^RD2CBfGz*th@?GvW9!0>fLkZgKr9Rz!Z?$#BTVf8m}XZUWREh$%G^dF!U#-? zO3yk3UUilMCHSbVJVKM=55g;O#Z5-^O@9-l!>5eDcx($PagC#VDS<*}EQ zVfmuB^hA8aSr*QgqgS!}<~f7#=(Asb_SrUOt^keUPRA1R5uubG7>4;n(dk+CO&rPK z1jq?*)E{=o7TG?lh*$}#u;84~R6^44I%H4_A`dcX-uN(Kbdo~>Q_U7?p@KOizB7MG z)DyHnE>o-};{@epzq0my> ziuw&bh(NKOX-*>Ep`cMU7=x5QnuT#%JOStpng(f%BKs|x%7ElnF;}}A33n`4;4^?_^<0n+m#9+5S>i6I!Z2NoQ@%rWOFtamEFFZ8 zzDWN4R}MB17)h{Hln&P$yI&OBC*kNc4o6A7;e?+n=*)7-7T4|Kd)?4#h2avKuesHU_KRQbGDa$6=uK7UhSnjZBJ` zU$$#~+{EmCdihT~;+rSXixI>1_yrz6R^Hks&fbcSgl!v}j#gi#>Fpg76}j!XNlSc3 z&sG_r8?8ik|M?x9^}ZFSmE1^eG43Gc_#CNby*B?H=;DhZqxSG^QS9k>%v-Ht4iL@h zrl^Jul7OSF3d55^suW?>;A7z8mTgH!^;VUjT}HGWzJeiNJw>`aSm*tFs|V0aD)B*I zQlccvq`6I>;1o9c>2p3S;mmqL@2vx0)Uh4FefHTyMFQGRj(>X0(dMS60eKi>4R1Z= zNyu4j758)?rK`rCqJV<<0)eC58_Kt^sAi!OZjr(scg+HWgih!W<)I^wTS3&)deY`$ zB*s(>s^^?D{g-!!c`E3Kf(C%5M^6XANT!19B5Z!jaiLwdZqb-F#G#ew8*hL=3!I>U znIvPQayCyfmJm!J_EBnkH)!vuTh@C>OxK^Xzt8CM7MfuqAcoKaPa2pA>qrL4n8{A# zYXzK@^pt&{%)*O^LBN7@qo$V-5!~LU|H;JLsu92`H9xtSOcEoMV3LTaWl;?9hSJYx zm=gzKD8dubq_ub#MEz)l)cEgkMa9bI^#$Y(MY|Sz zP`P69uwxl>$lQP%ocV(YL7%s{AGJZ1K5IYpC=Pv`v8DgOkhls-jp_W$+?#g-DLjg*{wz~a^vvSuJ4U>9I1 z0+#u`!GdGi3B%;u=2wgM=!~}svT;tYQm8`_;R>BfD;3y~rs-FgX{l#x*H0_Z%%TA_ zy+A%M6&*`}t7s>rg$8>FnBA#+T=p`yS%HRdxD&(Cq?xkw9~0T96pm#`qOu*X3pEha>f{fMS&#E)E9m?%oJx{S*6c_fMWQUHVr@GUy4+*2b*s`!koA~fDFOqTsIx=&pp~L;f0%LOla!B zfaJq+wO7?g65F&eCK(UZYxzgGShTWjKq!>{=ntq`foY-=7^{o`LK(2(L|y+y;!1p90 zAClxDR=$7QA>`BD7j}HKsHgxaYzf;>AX`M?Q2JHae(}h#+bb0NTw3h+yNb+~bP+IX z2g9;#k8`x3^&~I3O+*`cJye-DT?#V8ylp1eMpl>PTKt08Tx2Z<>$b>a=tX_=7&hK? zC2!bz752kPU`1Utd0ZHa&l|dIn`(H(!#WPS> zQbtr8WxY)RQ!cVn z4r5qYBvQ0iv@7Z-A)g%RkZg2e57o1cISzZ1N*!I_aEmq_T0UAJyEkF+?I`g!X6&18 z!KedO;I!1z8?Wjk7Nuh8jX$}q;eL|VL{sWrPabjdF)HoOnVe>))l#VhPj$t#x)#f- zv8g$W?Uf|LGopypwH$&hx>z15!DcelnQV4Yv9S30$Mj|KV(B~9_ea>J+C&S)uHgu| z;lB;IzTI zO#EjiFbq}BIuW;ChEmy*SvdyJ3lUEgGT-TlakYA5t5xc!v8VkqXB8D0!N;3N;5)FT zoBn}P-NZpNrDi~BL3@!Pfg*r<6t{YyRG_quEXkiJeK6|!0;Ju(x5OM-hv+i zEg7pRWwdn<$glBC3HXdujqA>Z5Q_kBvL>{2u`ZytiE7Nf+7dnCmP2}ge*YZyJr*r=DfKNPVe%_q2 zFPRuPKkUi~Yi|o~j6KG2zMejHHBlH2O+(D22KKtiE8->AM`SF5)SEF`suQ_5 zQ_LfEZcKak=!UF{;GPi0x~x_5vw#^Ay>2Vtjt}$0d`NPOG%?nzu!L;DLX@%TBi-f# z=8*GdjqS4MCWi zYqI*1$F&>+NkJ;8XRjF((YHWAYd6H$aVEMb|rS(bUM5#Q_lz z7{46w4HSE;3*u_an5m0e{MiuLQEgyAR;Hq`S6%?DWocoKw)k%4ACT38rYI*HYg5Z! zH?(O}u#%rPluqgC0w;JO4iSBt*8I9g&P*scCHyt8KLxjfZh!K>W>}yRN8k#GrfF^4 z=fO608zkGV@`BWdod9a_8FMUo8BhfI0WMkZp(v;SRIOBTV-w!}v-j`w+a zY{cjM4sfvs2FeB`Fo6O%_3W7q;owUU_6qbprP-e=`VGq1-`nVo2xICj=V6Ly7ZFmH zND*)`MkUbDAEbIKU|wYPT1%f~w<|4S^DcCKn`8t?t0c5M8S}$YmommhZCA<1SY<${ zQ9;MaTGy9$kQ*9GZbnqZQitP2)nE*;uMP1)H!f|>mn5jO%?4HKowt}}fIf+fz6jb? zqCvf6(DqU$Kw6{ZK~TLoin3ZPbw5^$7khMuV6vk^6f*~W$aW>c1|wD1=MZf~%6XP} zw{^5UjK#J(`SmPT2?9lMCnWTdZNdn8u9`F zqj7$<#v)J+DBHXlQN#kI(MoG#wyABas7U99MzAe0eJUwhB_cCyj_dT}h;{fC1EFa& z^Eb#GXI;%BJVD~LdVC`i!<($S3IQdDLNZ{qipJ0^NMA1E5LN`5KUo3$lulHL+daS1 zxW{D^z#@Rzzl*cYHEXitnDi#o8KaU(9hlQxiEc7 zw4z71StD4B(8t?K?|&!a70KSzP{KB-sY_M>36mA@<0tq<41wO$$w%oiGlqE105mNe zzv$a&_iZpu(s+1_lw~b_??<6M{v~^WyZ&pt*Z4Ij>}%0ZTa&7S8Oj!`^c&wPk_a+} z4l+&MNE1z++}K4eEFUt6d`CkU0XXUAHV^6sgJRY2q8p=bEBzHsDP54sKFCJ*LkSzh zFMMR(7>c%k8kz9=gJ25$7EyT>kktd&L2Q(uUG~7gSc10dil*TMu_H0eqJY(g3bevR z@1%h3civj~xqUnX>kc3$!nt{V!5g2=VfNS?V6!JAgDh~&U7|}r3Jw{5DSU`}go0XQ ztsxs3B+N?Uu}Ck*K&Sa20hA_uR7KX3&aQ921%<RvtxQCY^IcC^I@I{7|O zoq>?po}sSo39W1=r>RH@HH069MqwI84!zE9(jsN`7Cf2sGFL3}rVbb$6vnG}w4asr<0I^xbCeZ*HQT+{F|7a#82(Y{* ztT_oqfibl)0&UzzbwQo^NIN~vX{e{5tbQG!it08E5OpKx3F$Z>xndPmpLLd}%qjMO zWP90&I&vFb(YLRBN%!GyGZ{q}+|(>) z+;b?QPe581scZ{Q&VuAmrtFZ@jQE;5aFZbsoao^kHLJ4CS4n?$BblK)ul@mAR;wLI zX?=yJF&!qZgcJJ%%{Hnc|GIr|_?vIB9Zl);+i$@VZ#)SyB4UdWnFT0j-!szy;rkt} z5Id$lgUKGVRJ51g&D0P&VDY#t3zPrsj^bZOX$ZOe|MVQes_Sp%l73$#DF<@H*+D2y zw;Scb6>JkHsmJVUIqkQhpjMfy`9$D-X8n;FtCeql>a;bhTjzopuVSB4f?JGNgJZN{ z4=!F50Cgfi+Qas<9SS$0)AmjflNTuiARs}C0F%_ps0p2gjtEX55!|Uq5~!pkd+$Dy zR;w$$>6coHquvxi7%7q`+%0#=VU5*+!WhDk3;21dEP}uOwnEnrW8A{2!JZ>gEi*dj z5j`-fj;FO~+Awp;6bBTV6rme@$kwJ7P?4 zx_b5M6|JLnOxn>p*nrb8e^w=UqlIBMRizM~A6B)Hfenig@|8(9v&v4T{ktxpiHoiW zHH{DwE72nW8K-DC39b6V3=%IZa$oc|jRJt-Rq;)MK>|;j3C`zuo5J0YW)C^cJQC2c z94NOAt;!=mL+QfQaJmEzc{ILuwvj=EO<_c)lAIzjg;C!Zy;b^(dSv|2&`Aom^)bxc zy+oUt-6@T^ROkqR#|9N_&U{J~gO>H!j49fX84PjRlD3eV5xNPLqa+(nP}LhHTU659 zii^h_M4yJ3nL(sj)F=N`UT4Cq8W}C=8jXJj z8H*HAw!(sXWFAdk!qL!A+wGGV9*a}>6W}o`0z57p7YWE6qIGFlZ^Eeuq1WmxnT&&-F_ z0W$kfuRxh^Ii#e4=eH;-> zslz7ew!jE!P0l4LLvF}f6d+ueoiPH!Pk)&A6f!`CRhm#LP#5GGuyy?+$}TXB+s=yF ziyValbcaHiD8?8ZjqpyYdURHtoToR`AxsN0*Cx@tfI=iir--4VW78=9 z?0;_mWWklLUM&>g_dU74|JX;}1CFSC{tEn+_&dtqG5*|&YjZkX)7;@PDKSm(W0n5dN9hhZE2s0avjCCvE1#{7e)7Mi)gW>813wc z<_b|ORpnmH`7vFbOPe%1GtQB(6G#{2>VxsSl^6fX-?E011)|PKM7BNWz zhEsO2bD?qEk zNNX-X%p!c8x})g-YGbmZiJR#lH9g%*5~~3ms=0{m*SY;IE%{p*bL$P9Yn^v3_qDm` zNZ~=CiWwNr-QrsEt?li(wmS>o31unlZI8A^H@8GLxHfc9)!I^t-c6aq1y}gj+&`## z=H9g}wz)^Ie%6hQ3=iEkEIi0N_w`VvceXt0UiYGRyB)X0FWnM9`qIB2eDtneBcHl! z`A6P8nETOq=*}<3!|q=nJ@5n1zWczJqwag6&hkAww{{|^EgXO};H{`)^tzTkeX{807R-}8|Bz3zwIr{44GAN!N;*M8>DKK;li z{;K@C*!|ttd^UQ+^tEfQWu-R1JblgGuu`9{lvm5s3(K?Rg=tbck`(T*<&;v0564HY zF&!jCyY;`z?Hs=5Zku0vqP#F)iPu)=h7QK{>f>wGrP*q{JQrWv5zjoeTCK-(wdKWl zeyLKu5?^z>=9gxd7grXltJP3OnHmL~GQX&LWu-d1TCK#@rM1Pu_~P;^Rb1hO(Nu9wb+)p&BA`C3|D8UnlyveXQA zX_ZNEch1yV$7-#%ToW)PYp^^wSFc)zn~qoK%4-X&aeZy3K3kh#F-W+!HNI4?`*L;F zldesD6=+Zj%kS5dAuQ4wtyb*Ct=z3GR1A1#s?2eHdEtp_MP=e+>6`hb`PF$a;^}I9 zZu!Y-ZStw5S!KB~Sw=%Ln@?-MF^)kEa`;i-7`Qg!z7$})>ENYS0^U$2yF8pgV8w{4DCb1cHO z$o_Y)|5~?lvHrB{eY&^a`?MQdiYKOO)oR>ZAMULm>LlqgR&II)5M6!Bjm?cNu5g`S zn$yHR#o&p&F>cA059oW*l7HCu>*QwHlu`?&**@g029Xhr|I|9J+#f!D-MU{cV zQ8g=cY3^(n{l zROc@)*}YBv#oF@P%5=F_D?eqcF*ChBrRinJxV8X!t5D>L^6c8$;&kuGo&yUDS6uz^ z1qjb{t-4fRtWH0^w!B)Mo4?{%VO_?@UNss6A1BAp9*tMZtD;u%>hdzkBrb4;@m%8f z>`Ha1w6+R7!X{jqyG3aC>~!OK$los4Ry0QcaJ;s>GG--KXBI?bt^h#GR5ixkV)=?Y zU0poQ1yCH`cjd0$;R9FP$Q5^NVze|h8hdJW#nqm4wdE(>3V#rCU~#p2v096l;TCfX zB+RYW?>oQTTvtkq6jm@1|TUgNDQdQMLhGs9tPt5Z$ zTV4|QE-qK*=bi#D;+g8K!P@+4JYNUgEY~rgt6Zs&5_p^-x2cZv=ZtKJ#B;Uk6PSF@ zO^lr`O}sjO)#$4S<0o9)yRfk4dS`pr+zHPIsisyfSK^h`+G2S{U~!n2y~DenKE(B` zkx*eqz@D1mnCIbP-X^qXrA!(;d4?$=xq(_2tE)_v6>xi(Dzr3L1vC%|Xf}CqqE@~r z%H`QJxR>G~>|wbkwhWb47nag8b5N0m`8tfhTAmXrhBOO~X67$uvjvnvxTs6?0P;+} zP+fCrT}(PNtwtdb^-D&)A?XZcZOI4EAb(89F z?M#*13-+6laORg*)>fV9*!1fBVwD+nM*lA@*H;rjr~t&t3JaSLL2u$^+Nys09;!L_eM*6@`~05Sx^;2 zy^yoGxggPG+>)BG9r13}0p*j`)hXRh2Kmn(N^4iZGvit;~4y{S(r@(*`OYo6$CK*;(*(4ddkWCFRlol?>$4-sH zYcloq*3X`akDWbpVtfn{F0i-CGDu?7sJse&Dla{iB_E>O=g*8icplEw=-9EfT79{8 zN_{jG;M&W0LGo4i6;3GFB=~$H5C^NmKyfI6<2Vs=+;L>pu`_f%<-`G!E-{l}NO^V@ zu|~WvEdq-Xm2stWVL#nz%crOoDwvYta>EXVVle_5NqcxlyL;&;LBCNIs;txotvy|L%<>fCBzFb?^- zO7cc|$xW=6hCK2syQsj-wC+Uu7X_~gRe?TJrAhp}?ybdVCytL!#7AEpL!B4nO&~pR z)s(QOh3iZ6D=SoD^9@7erZzvj>dr9TOLGgl&B#&9GmjyahOT)*Dw8ywxu3~COv5m0 z0BtCkJWa06_~gorns*XxkZT6hhxo|eK?7HKRefzGu**3#TyZ<3Cg=MA2qEG+daTR% zsyk_*TCGiI_($E97oIFXRd-+k(i<~{0D>GTn3a(HB&fmsNFu2V+o@`rJ9TD_3;M>T z+Vave_r`-H&6F1;Ts-A@R$U@EEHJkIt$Ri0UeLKYKQnz%($~f}(A-h+LE5^wBz3@4 zm<$!HyY)n`?oh*l3c+<9BwoHWKZC4lW6Fq0(7nn=LRa^T?ANb<0O)#r4F!Y|qIf=0 zg++Mr2fmR|oKU29Qz+F677y8Uxp6G=vr zcU(^x8}=4#5}6(el2`-us74VOKoyDPD=a0#j_X|-`SYirJ1elf zr2fsVE!dSwwQG5HR>^hoi!wTB7Gbs0S_8Wi|#U&g5kqFRJZzCEE zJ;quA(ma2kk6oHahBCpwQeDBCgVuvaz+jdyqZYuK>TdU?<;Cjm+S<}mwYGb>28&!< z*$shT;5EYD?j^C2g{Ox0j_kdAXwTlEk-LYk96T_6VE<6C)eK<_T)Q%KacOP$!u$+1 z)mL?`*JkZri8bWV-O@QBl(nImwRtpas@|BQLQv65yN8Fv|CC;|x-9u3@Bla1JqI=p z)A8spi8c;K!j(P>qwR7_)W!}04f<0Pg$>S zrg}-;ofsRRg3AR+SS(8dLJUH>hDro+Cl#1B7MEqCBC@}TSW!pe&Q$FwdWL{HZ~ilu zFYsP~n;R4u@0a8QMXxLy^Dv=Ind*R3hR6jL9hkOzV!pNt{3WtPP>rQ}X<^p9nkD;g z-PycD+@yH<6c4YoDU)7r1p_Qfir4vgN^;f873>ybbPMr?()jt&Nw0uhuZNn9x@%Zd zsH!o6{v{Tv)u%9cNMM+j!By*6cjL?DN+}3pZf@ZsL}a1N?-O&>=PfxJm)S564-XH6 z3bJQGVKVOxEfCADn;}#%jmcuZvOZU1+$hj>_++)xm>KuVNmPI^$Cs8t%cC;N_-RX2 zcyB9{C?ykS@0#R0e$A3EDZ#9Q9yA_0H9CQ2V1_a%<0bSpxUtq|+K7l~=t6T!16hn$ zl#$IAkzk}FCd#Q76{NV!=qE#QBaf|k%5G*6G_c1d*&OS7DJSjoN8RRfm1MUY=(I5tk)i#o4+`P!_kpug; ztcSYI)UckAsD~*RGZUv3kk*QvWtL3ZT196_o+?!sIh8ghPSi((7W(#>acy{44>V_? zGK>Bp_9e-%4!2yywuUvgj9ng@18&aLWiTGzRd>VoJ7~YbPRH0Yk34yAhNUI;sI}UB zjMWer#EN{#=T8jzb$m#&Wt$Oo-8)Z1md!g5Ua8(*`ePSv2C*J0Ve(k%3=3@cdTIBT zZP4EaLnxXW=85vmbXuk&y?b)1#9Fhcs++=-x<1Iy$d%qJhxAafU)&OAE5}^kkSnAI zO7WAVBh;`QZV}l_z$er(A64U3SZcGIliMS?7Mc4LR@HvYiJjE1itp#9@75>E zMC*6TZ^q~ zq-|hS^UPhg=agfsqLdu?xvPsNZkB8d=ven=ortXKRj4p+9&`*^n&pMiCsTQRt&kC! z&OPUm;1@i=RK9Tx4c5wQ`h(gRKsU8C#9jk43v9|H$Q|<0oie7Y?3;4@O2}&>6hY5} z>W08o1p;AfX=!6aspZCyh?m~tpP-jk$1H*7%nbZDh2UVn+ZX6!;_vAnz-7>I!r+ar zBR38Tz+Kg44z=_vQ~%y45Ad1%2k@0Djho&_dXbx?D_m8UYF6+ z=i;M7Qxl`3@rkn&$~H6962fLE5I zH=*#zTz=3j7Pgx;ISqlU1w{d*Ty8;pLIlwJW=y7OO7T3>k^-y=u50r$41-y7L7Z;N zAPrd7lBSAMmAEO@_f59m4K?`An>BEQ*xOVGqNP2j;$=EI#u*$hK8JUe8-~(*{bvO| zr2Cg9CyAeArKnhR&gX3iZy?M(2#4&2g#Q9ayfxL_gSb2go1`mE<4L67nx(ESiI!Yk zLK02n`8Z|Orhr2b(`qpWTuzB$Q>}?Hxhb*1UEJwc27+uER+1tFKn}~Kj=>QKJbMiyKCHV~8d!1TwyWQ3nm!!jYQF zX5Pom(nZ2}f&`Ie3aV{=?qm@3hbi&P0>Clj;E;bzvM9~FM3XowKYjDH*|i10E=B1L zv+CDC03fHWsfLy(G-|>{Fgp3NsXHeHZA=F8A`$juIW{4n-pOlkQwKd}UP5xb-vISj z)FYEE0pkEbKqlVL%b|tzWgSEAVp%*a^LCT=E5r~7Xn;u@Hf zO~YVpbRm`V9&GppkXd?qpTy;g45wmG)*L3G)fy}fTS!eJpe|Z{VmRif@t3=l+N6|< zl}vezS^av3d!SNPvl8O{XWe>wr|{}xEnh8P@$`y@0#z`U6%!LB<5zf@C@$6*3`$m( z@Jedl!tzB7HU4E~zJ56#+kG}X60fjaQzS#RhND@(K`D_AJ-vvBwUHE_%z;pI6LQCR z=cqez+1!<6O+06?I<(hb%sat_tWht?c!9KHZcH*?IDt`(>nNJixlte^F}CQiLf(ks^%S1i2CVLg}c z*J31?ags|HFHWvjR}%2`0gbG~bnlAEc89o%2)UC)yMax}E*4O-02C-XiGWc_y@>@B zDt864xV&nHXqNx!YHiv3@n566L~E|1OG4dY!R*O@dnQsiQoA8&l?>Gu>y zp#n?G1KzmUe`L?#k-Y;MU%SsWaQtjy>z^Ez%|~SpSFg-2tW~O&VgKB$Wp3KqW;*6z zC~acE^c8~Xmc7s)?tOYD*>2Cmj?dMrYn5d;zAHYniy%gKw0d!VY0^|*08c@r<^+QV zdFx3`%;TDno++0;9|9wkkf96~ETJ?7M#S}%GSNSooaQ?<@-^*Il$;643xbG6wSoWA z*05qKMk!+gahN}@93|dl%sL@!LkQGyy)T=>_m+rji5VRGFwuf^IioJSzTSFYYV1n| zM|UErD$pdO02I$Un?wx*bxJ)@2Fc!(DXWV@ph6Aw!YhX%*m%>@A#ZR|nNYE5zRB`j zb@eH1?@v@0vg1rzfP+avY3i?=YB6 zVN&lWN@q@vdae-2+_{O-W247W5_g{<(h=Jw>SF3LRai#qk8d!BhP}eZV@g#Z_=!xX z-XTp-%MYoB13yb1&rOt0o-Uzb4h#MoxlE*JJo=!e5Mm<>by5#x2cjB`)XwW9A(+Op zWkfTTGJav614W?YxkR>PXyOIy6}2o+J3e}%bbfqF)|hg zT-MV7Kt5n_a_a1a9E4_#cNdQN>%tPDXy*8GWf-wz42uM!$wOcQq?uUCvCP_-YL?s} z)>P<$yI^F&i4VEaE66Hb6`O6V|9BL`#D>)TSfkiSv0Am<^{kC z*q*NA$6Qe4-7!MeoL69mknr7XbzaY_I4=?ymq4yKlEm8v)?vuc&aLt9(G|pS#8$oW z!YK;`%8>v!J|S-=<08UjA@Kvj{dt(s&&p-DeA$r+$EH>kj*5bY5zro%mNiYzM~WNzFv;5ky%n6Qr5rNgmm-v`Qzcs`yBNMMqCl3%g1t41pCe%ioN8FPY ze&kS|URkSO!o#Rv`H%Spr}+h`Le+#@&-&XL0`zn*Kr|y~Vvh83LQthr@2y!X@yg5) z|1ppCpc1%$c|lI4)k}+nj+ATEho*FeFh=eY=f|q7vB{IS&H8axmG0xW?6&;GMYn`R z?j{r*dRpBp@f7Mo$FWhGREL1SjMlSsF`=pOsjMeMCRObP01t!GraN3$=GV6VqsS?3 z(qCkC@+xy078ckYGl3?itU@Ha67n^#&X_NfmH^xt>k*O1A$b}zHh5s6EECm>@**7I zw|iuG&)vEX@484F_C<$5cxiRcX$6+&T(v?Bi`O2ZsH@_BV3hb#Al#OMtmcNs_z@+U zpCj;Q&cSk>;)xYY;YRlC-M9bl1NR)fw>&e8pU5dd8iK2s?~(nLq27_b{wLPmq40BH zcpo|RbN9%c+s~i6IVTz;cRR$|;a0z7H&uf2)bWX*g@-3{+Kj45$9p>eY5pxG;)dF|=q zbze8e{v26=D)1gd&$Oqy*UbujD=)0`<7fI};}AZst03_45aKk1Y|$io!a01XSAq*Q zn1bRDFnwG%D|DQBHD#l&J51tLH&&n01r){x;VN^)GMEP4EQCyqp1{k0<``iUetNvn zCyB=#f+0ZFdjQ2KE+`5^wSqBzH;7+u+ZbYu<{?m(RrY}pnVEF#3-WU+4S!7=e#|fz zo`RXq5zBqtr-M#UfZY!4^LulUbSx%7HTnI0?!tN7=a;IO%(#tTp^Q}syMAZ18A2Rg zkuVCy^5#_;KcJI{pA9)xB*ny}-g+afYm?Whu7AMM%7fY97GBSko=8y~}Qu%{k3xGn% zC8CZ7$D)nNP0Q^ug)k8cf^zRxF4aIp7Xs8w84bkQTaIGsv81qLFQf;p}QEf-syf2jjIyAiN zL4GU$zjrHXdT7sA)3mlu5qAXVFC1cGQB ztC%?K_GIJs*g`EyH3NmcheoOd^7ry7gbu_}bA836Y9&cWwzEK zhg99@Ql;@=tUwG~^1__mFnjEfB_(V73JRzf0#xgeEsZ8vVu8Cb;jiUIWREo@&x9u1 z+s=u`w&Wd&v)7t7hfu~qfh=P|)=i|N5eRCvHS~3R3~b3HFZBlsZ*FD4(b5uVu<$K6 zzbM-R^1Lg}5Ef>43m8jhpPIJtm}#FBa5X65J}vd-5XNXxF92`WW8($4P1wibJs?C9 zqRqsljCzw2uqT<0s+H#}bQ+JJkfPrL0EX^211$!o5P4&W09g92C#>6e5M0|&GD?5_ zDGcw6Ay-o@8H*g-)<9pw6oPKkt_F#=82?Rm2<4S$uKHwHdKAfxSsq#!S{|@!v#gEC z^du9gAG9oQc5B`uB%-lt8NEmyRKcL&`aB1hX4+?rYJ<8B8OtseW06P6cC?4|j6==! z)vB^!RD5M3IEjhR92CuUHQaVdKVC?&M6BBrrrRTsP&EeRlb}FXrg3f2!vq{RQdv!l z93>Stn`XYCqXC2s$K}PaJF}1AXZD9Kz;oqg@*<;SvpZXB*9*dI)VXZL>)7)0<@qZ5 zMi~-gw`LP?MAM;lqQYMUJF%JhJ9E`pnqLc_8e}G(P9Gz-W+8aI1b-C6x$P36~^MDS*W-X~ACelt~frs~G4?qt>N9hp6} zcSP42yGk?EvpMRti)MPZwpMYc%a^MY__k^l9EtVSr3yE;*?a!d@^S^EL4`=MwWW&8 zMknkBmgb#dK!fbt@V4q@SdSbwB`yT{y4QP@coVXMS> z_w6pZY)$HXrdik;JN@;RffZf=0lhug?>uN+Z1ACx_EnM2#S2)^-Ro^%_~l zw%-z&&TeV#QIaB4k#kbUi}SiS$+ME!JUuaQnjaHH$Y_0Qp1S1KX49IyBS;GKCWDtB zj9~j^VvL11#)yJx9&*=g#7|VCIEV@?vJ@49F5G5qCGV0hB4p2oV33A%1e8evql#1p z^ET#1`JgD0nnz54ZbW{_E=(zc@J7c@p3!*$(CM^?;cZ$cGeli*DB?F7Ep3wI2hw-} z15#MFcRL}{EwR@q0S$crCQ2bAg%@EOe6V6~g&x_G+D?5Q%|cO+iEKc3A$DLvl}Xd^ zo;P1w0&D82Ut3BA+fC;fs%qPxM3roBr}$jPeVc4>z>jGTFxrFWSwrB3~-BaeTB3mH4thgTu>ES*he^`%uL2_FYZU*D2;RU<; z->KnQd$Qj$x1pS55JMC~38zWc;V?(IlI5_jy-yG7lI&kd1k5BlW5&CU;v;)(yOFXs zv=7DViKt5481`W>08OQOm|U0p`3VLLT!f)F8S_OVzmPc?DPG2%0#4I6Bs^yo(i0EM zmpo`%7PgGRIAho#{wRbU9Hu6A?moUCbH_urcLs{e9-bH0fD^0p{^4(bW9g@v5~pTR`>bUb1V%N_#lpO z`jV}Ya`h?*ROT+3)|$^88Oof^k-&fl0m>oz4EfEdX~h|GkriYrgmCn;-)bO{@)Bc% zTBPmfyuI~tP8x_O&mZO71CCLlsfp3^lcVv53qghj~fWMfom*W7KYs>e-w!CeS>A@omOEvNkd@G6N{EK~rQ6wqfVP ziP9X`vkw!q+kjHu)?>1M5Z~fmK18;GcAoYTW#s7Br zxGtPwSh69{Mjbh5Eqc^Nk6vqiR2Cw8Dk^Zv0?Yh!hh!6NC%3m9Gwdm?qE6e>4XF|@ zG1aGSZv?)wB{oT=4O$9AT_?0p^;8>! zH|%$bpCNg?B#ew|!;zZxN_4`jFQdydV#0W20bo0Z9u` zmQ1hhQ*6zhn4EIzvKxK)*yy<_cjoLAyUDb5jJau!^}5oZrlYZpPnja83pVx;J;G61 zr^lxJ_6RvaPWnt-wEXGDVW`z4yD5%MLG_rYNe=95P<2r})A`T_R-{43UfF`5`a|z* zOQTpCU^xX$+MgTh&D4=ZQ5t7R0;)BP%eD%G_kWy*yk5X0WS z$+WNuE0ml}yll2N0))oY*D&izEVoRF5**%bQjj3RsnODL!EB}i8>i(4P}yNq8XtSj zXx!U93HlD<2I9aQ|9C!ZFY~)^Y~Re-nW3}iMkh){`hY8nw(=WBCr3*Y?72aL4YO6> zko`2VNMl2uFHt&dvt%+Zp<*sys{#*x(kuaXNt;2)RT?GgwsO}7T-5GiS6WaBl z$$7mGx($2ng80*0M-(CmU=d-BEkY10FCDEf5GZeAmkFgqITJ#uCaiFS?U1&K?<%cC z`vOc+IIR6iULH%IH!GPD8|e^UFYnFo@9XUy?ybPX`vyrLh@W{z74j=~yjn9a*q*z5 z_(p$o8DC-`>kgvctc$GCqhL3)aFIli0dyWT@1^bRCdA{CrN}m6to%@%;I+r7fY>a| z%QRtA=g)b<3C%cmeqwU$!e|Vbo^z9D%4ghz`bl-iSWQ5|;?gnsA{`nozp-&1?bUKe zM^ExQ2z7{0jU7Ke%6-sw!lU-jy7xDwX>itRj($0mJ+gZ^T}j7zlO2{$JP zj{7~5gvVeONsfC+PIq)5^08BNM@Z=L@~iEe2T9`#1J*4DJvDKDl+C7gCRf0cghF{( zcfD&6j)#(y4vvkNV6)8Eu@aY~qvF3jof#Da86WjrX4Dy_aC;7rBF5s=xlrm@>G&vb zj*cQi9Cyu|)7{yznUOJD34?~&US(4tHg?l zJ~XOqu7rL*AF;abdjw&kCv+5oJ9_jehnFil$ZS=q{$ZP{;Op#PfO;c=6hd{9;z%0HL+qaaWZNIRwPOy5I$qaVyE#OQ%V7y?Tux%lVAHhMSgfXH39YZ z_Qhd`P~XFSTFThg-P=+80As5ylx-B9?%Tjz)zV;Z<*q#k2jbCjdS*?JLMp<-UpK_W z1DkKv+g2b2na-`k1is&ahCt#A*sX6=?WVv;B$Ua%a@F>0?@z%l`8u4ugTt z0I}>3iP4S>RCh+Xd*iaR?R5vL)b_MlwW2hD_ZDCU%|14ASGhDHv=dk!2L*>h~#6?D~+|Fb~{)_rAN`%;Mg=-HGwi$;nJ$ zAATrv+M1_N;r~HfZ#FINxjV7+$X>JGt?VbVXZ67TZ0bI@!vA|M)_N~)=!<)uIjX1U z_w7xXzp-mf!D!rrasLWO7T$mV9>UM;_T_|tW#M51o5#qJN;nXVv{qeRxvnr9W(;7p_H;UQ1(?pq@ zw7sD4nsl>nESoOG9%Qqma9@)!R3Gkp<{6W(`^DV{o_VG(h<}Y3f#w_@wd#q(C+MMm z<+B`LDn$|%0>}2-LkIT^+&_|F52)okZ;+iJnh$E(ygqgD7568yOJZ*D0k79-D=^JW zC^TVd*joKT;+VV%9rAwVV7Mig{_u#qckkYP`|jC0vhTpbyZ7I7&)o+{M2hx|m;v8g z@kbOpy?(DW_OJ}~vzRG`x5#p7+q2@4;SqDR?HL|P!uvN}>vWcII`u)BkSpUpe)Px% zx;0|rU8+QfWz!o3)+_7sBgS4QzXV)SHwt9xrcg&eO7U6xdX2oa;32}ApZ4J<8C@~5!%cL_rv#gm zf4G5-^mq|uyW#vUzQ?kiMU@jByQPQ9VTUN=&J4RDH;DPMetCXHTTpgE6vC0KyIlS0 z!>=6~y7zUvoDY?m9*lP-0NPvUbgB%93Qh-6U;Reqo56@M2HtsPbV!c%kO}*vhTsk%=Hy2umCYxwp>y@sJc(ie<5~dJ> z1&v1yLiY!ty;y7#5l}pRFzZ|E&%7Q8vNOXeXtMS^QG%r|e%NV*zKw9nh!!wQZca2Bt~df>N= z(=ciUABN!ROvHb4CMZ0827(k#K$MWq5Y7gyQYUZIxCOrh%!6yS9{2mW2H$CsE4HB7 zX*E_`KuS)NAyP_N=NCCT!na{#wxYwERHR>ibrxq+=VpH*Qw_MS zO&E7=0P~i)4YLpuCwA`eX)L)FuUmC;aPU_I7$NfBuchCwTQAp z8-+9DfY*m>d!4jSFW2oU(gdu-{VV+(OEG8>X@fRvoD05)b{6b{gF;i~kY8p{nrCo~ z42pGy`vJv1QFZc4_~88|fRa2L15O5`{)W}<$lk*b_w{)TnfXf0>~&;sjM0nQW{(Us zi78DAL?AiF5Jis~w%5TUgA{C{(rHPvn3(z1Oy$`^>xu+M)QlqiR&~HjuNACAVBsi6 zhJlUJR6<*&*#>gW3ZhEdY9T_3-Z$nZ=p3BCa(KiqH(<9s*RO;=A?9`F&GPmi*)R4j z#;sb9>}#G#6X=@yldxIV=ilB14AYGU>Bisk_!1K69C8;;yyF{mUx@@m+i;G45xh<3RHqCgO=V z4u+N5Gnhnri8Btk+n&P%UUbi2lHwmc&@Z>33iHUucQ&jTQ3y zfNqJne!=%8C#$23Xu8o=XY`%4rmX3fUx~3VxKT6GvDCAdDZ-vtcBMBTBW4+l|G(fm z&A8Lp$U`~p+e_lklIcPft?SI*?4)fh*_aC0KsvdmufM>B@Oe3tm1BYw6K|S>^nI6z z9w>4CEKB^E3Yu^1=T7HT5i^ts-xhem`IA_Kq~k1&)zcVN+CuWGv5_&g=W2==J))sS zwa|~CQ7gtUAsN>bbiWFDrfD-Sq$Tc8i3-*RBQt3i0!7M1JH~R9kqokim)XsX{Sul? zH1K3adBpAgpRB=&JVN;f4@nX?3g5=4;`L2&5|*`OwP%}y?DqNXvA$QxOd|tcbU@@V z!69i&;zPn=C9By)cp{5!$HzSN4J76uqx?n{{SjjX!932}b;@9vk##=Iq_W_}92x9O zMB(HNU{%#Q;MyP7R+NU_hu0klkqgEZVE_aH+x&zom_VlhdSR8v`dq3KTrp0wJ)t?UzM*He|m8|XyDV@Nc#-*>xXk6W>p#j=l`o3v+O*w29lL9sa>3leEvQXJ%sF(R5 zreBw4NtLXg7lrz{#v44@?CV<;vVJ5D#HCe1Wr&}iJ7?9win zc$v6^`bJZmvTGV_`9ipO zraz+HD3@V1GcVSS8S#GOOR0eLebaaANBC6$M)En|gue_lsF7JGo>TSL+5_U5j3JCR zY=Os8kaX!QVD4#``cw#(Mic+;-SU|E$aJsU>0TB9GxavDf7X$pJr?Ym2;_E3=w?ZW zpErkePl3|9jDePCAx<#UFn?GdN@q&ruO@^zE!-4Sj8YV``7C2LVA>hS!+YvIj;SENy@wrbVH&Z!=HspSnHQ%LLN#c+?~P|nMI**!MZ zm1qQv`oYh~TKhQ+`sd3eiOU5T&JGAK6Su1WxSwC@6kp4AqFx1inpJM%>oWkU;zMtNN|Pw?u^^8<&?B%DZ6p}U(f z*Z-N_MJsLYt=IHp+bd;zyR=Ov(YJ^>x4p8-vycBHn)^Do-~H=tza(78_PgK2GVc&) z$IS$1l(P-zc${dtob&;X;pX;KVa_hOH>8{(ufCPO@umMZG8?`*xQ4gaa{TPx zB}%Ysx7{U%lZ4zWc?VMPRKLZmq2p>#W4~;2PM{aN2V%gNe{PTnPj|~CJ54zK#lB)@ zoB{7vc@eEOcH6t$76Wuy@nurHG0Z2Hufri=_`XipMV@iCilrgM*K(u|cDh}=c8TDp z-dOc|*RD>-7SVs(zbn!I+g9j`w&!-Vyy%V_qnmQKj+U3SzcktzJyy7!TgrVX|KZ$6a=(`Q z_1tf6`K^xM&V4fXd(o#_KAroE=r0R@mHWH+vxR@k{Y%Riqb+^+-GAonxBkEn{4cNn z=I{8f|MZ`K_AMW7ZRh|1AX6`)I-g)&$zNTli*x9=EMTc5DTW`E$uyD)OpLs=Pa?7cX&J&}z zpK71nI@Z>Cq5a{WZJn(rI$x4MId)He8x7w}-GhbBt6z9=+kN?$oR4nUyY=h8Z*Hx4 z^%veYKD+hJBV9Lle(U$Oyy@ik{>-7beEd!Kw(TlB()#kw6P^7nFM8vTAG-SS*S72* zd{yDzw(SqJ6x%x9_1gnPCcWp&Z`^l7^pe(Xh4weT{hJDxTejvq+Pc2=(W_t1zwswq zZ@FRn4JSKRUvc#do%QyWn@&7^llgBrdc#d`KKIhr)~mmDSIhm~ z(aKQ%w!;5l?@hp|`o72EYaUA~Y3^1Eg(9g)TvN!DndoLt#>_NOREi?WP%6zbG%6Ae zlvJWg^BfHl4H`}F+WVYyC+gGp^ZR|D=l%cR_dbrZ_da{xYwx}GT5GSxze;zEw5AZ> zsvDEy?oJlui#x9Yo%KBKyP}z(xFCPLw1Js+T;*(GK0!O75$gP_`YQ=~O4^IZ9iBO= zpOT=M2!DU!xNY$d1f&J}3B)}VmK5ZZk`$Z?jaLv=66EhMCcny2bd-REfR^BB7^b+6 z1R)xqfRK=|Fu#bfsEC-f_(+LilEeB9=r1WHI6z?Fz(Ha|_+$l#@(mLhE;fR1B>$Kp zbb-nI$`UGkQw3G|)%Xta=L;5y{^IWt>K5n`D~gIv+PG(`gJV*1>c}VkrRJG+bgE3z zadBn7OxU<-bK0R}WoIr`U%C3Uv8jh9h^NF1O)YH`Q`dw|@Ugh;O!d{e`o<<2Yr<%u z8MDySD`E5Y9ark=`$;QmY8je1ELaGNz&36^1Su|EdDhtUv7fY|iKkaw!twI6=Nj&P z{1m??Y2Sgf=Pq5YfB48atKxcfUA>8grGwK#=K4)3$4-@;yHI`kp7ape1uoydclX2v zEPeW{|Cqp_k)xO^Rvtd`^qKjY@*%RL#~8sK0yJq>#-6-%r{VF(PhUeqQ^LY0s!Ta} zq~zS?`g_l|(R4GjrlyRk>#?wOULYbWH9&q!Yg=H@3~e1fLmQv)nkzSM-hc3>yN5<+ zj$8dqaJ7Nx2ti@#RfYZI3WVq)t40b86Xg?}BB(AXBETmiEFvv#B{fjQPDDU(q_~)X zsDOw7KP+;RfU!eJ7l1)K$)#Ny(IjuamvW+oW7LAYR_ zq^zLoZoyR#CyGc439UXspCk+)HVVd_6&N8P)sIGdW;k41pnmx43mvuHH27Vu-ga5G zJGkI+#~tjqd|ZCE^H^lhw~oiyFL~FG(YD$CtaN81_RsE&O)VO8W1~`M3--rvvr|=Y znSa};vmN_SUO7;dQn+-7TW2TsUrTBoA2}|kd0D4m4=OLfCQ5g+N$uf`PD$){s5|8v zJ#hBwW1WMse};XBxkmbc%4?k?vEMSwcxF&@M(XQMIqbizxq8C2HDUK?T}s&Bo_Q|w zdi~nm5nbxo-&8nBHcMQ+ZF<*i?B8^J`lJq@na8cV46t8Ir~1~jJ2eS@U1r!{cK!Bn zd4ngH)^yoof5w#wXXIL;hJ>^$)x|6ZrZerQy z`y6eD&1A_>_AHQKI~6l5tdlbJoMvGcOmxIZ_i8F z5^Qk#Vs|n2`^iY1`|(k9!?W&k>_4Td>SZ_K==Go77qP!EcG->Umpa*ldup*?SZng6 zivwI5ReJ7V|LwF;-)uR#e3PEX*x&yuE&lV#bzL4kjoANwxOAGQXjA#ho)+w{O|%LU zt1VB??rFzyp6Kc9p)u~}3rh&;=G657($EO*28jXt@4S86BZ)u(e(r^r z-bWP)&<0~)f>`>Gs98FzM$<-O{|On9E6eIK&(8!Z6#50bG%u@DTbW`5vZCm>IPP7+ zzL~cg0%+>kZ#Zt7>_mCXU5T{W*e~C4@s8>qj}Loj2H2lhRFgc@>*>)7ni=*#@+ug< zN%V63eVQ%yZyU8#@8>Aji=SyO*zfpe!~rE=^R#|^p4i{KF?hIVE8jzTK7Z^tjcnVN zy*FTw9$y&tukl+^yevxWvkTvH?2jD!BzkwFZE+-D0`^y4+P)xd?Si#we973a?V3?@ z$!2;@AzwQ7pUBPpt((i?0?0~&92yG^z;SaKJ1^6JnNBq<@WuZe1+KWvMXzE z$hPWlL->oazwYciTbpx_OH}#GvHvAw+-KP@orz}r7qNeTl*{eyb@W3R-BWFw3X+5cIWIB>JP>&oM#Z1Bp`?V%1;UfbQ{N>+b^Jm{V6>L z3#a7xY&tKXj{S_%_`8^ z;qip$tpJTSK%Pd+UCT$))sv#dWn|H4FTM)Vu5Q!15cGNNc!;yAt#Hi(Mu2jwAdSX1 zX306v4>JQlWFCCj{!{0jfaOpc-<+)~V%g6Gx@O8qO=%1p-}1How&-KQiw}=pFCP{= z?SR0KGd#Ow(Y0aXN7XR@0TCPJ^m2bUf1@R`Q1i{BCZhRGm7nw^|B&$$IMxu?c}abyNQRIQ1w>qWh=vJW*cc=t>u|&(qc<1hA8)QSA>~M-{tQ4sb zUx6b&jx-N!Ch_Ea9^%nC;{i#<29yeM=BmNU6<`A5Ph`i}$7*AEGllO%w{g|i1j`EZ zl&uSKMzZt5alAY^vt+l&6XMFT<61#A!GJfasTmTTXRvfr@syDY>87&N>4k%`x-dO# zb{EG~rP>B@Y9Q`dc3dkUJpq-<23T>J5^00)1N70>c1WiT>CkTgJTMhtb!A*U1q(fz zPjFHG4&^q2MW1rdr7_w$+zBOQdq zw<(RWk8;nWF^Va-8PuP0gI0z+l-mO8Pq{52f68q|VpK{wk{@{Km zhC0O-9uk=KSsEgV&Q+(z1mK=lp2IaL1YB}bHw;5UfVj#t>;R~cPNlCh4B*OhALY<@ z@X;pp6`uq(|7fH%99sqL1vplaR+XKW05aSvM03Z{r*pO`nY%0lpu?CUi* z(;&!$#i{}I;r7UQ11ac$)Ev>7AOPWph(DxSWMFy&=b5Ax5!@#_7~f>wF;_-62H3W+ z;L>sg2A#@kT!0m8W(f8I zFaxZQ9Eqe^_@^V%0&6~E*x>?a0N(~4$1_6qVS-6?BQYc z0~4U#7mTHlB5Z8&bhb$!rd@rIS@L> z+b!HbER-+`f{F#oUnpp=WsRJtuN#Q^hQcL|P;B0Tz2JixF37+)6y$Z0El&eOJv-3h zVq{5x6Iu;n#kEHF8Xz@U9>rV=af4AsS2VybR7f;-dVn%cFyiRzNu=kTwqz#c3#>xn zYI4S)OlvbiLk_Yr6iv)4G6fc5G7IuS6DJhudxZ!{!x~}SgJ_W(Xo^LW6yc$U@B(8@ zp^Mo~4Ix@#IQ24AYZU5#6c*hz#jSl;F|y#1Q?zaIi8#rsE+{T0>f;8e+E)YERjy?{x~S> ziw2G?*39(#I50C|MiE^Dy$`dWJ<~ydFPIC4g5bhRP%!W4%Lf8`8sr?10VqW+Bi|4Z zRqLY?L;>X51mF}Fh3Wo*&`-#%4SagSHJMHc8(|)RD#)6HIT;J2LX3<;oCw$e$0dvo zS-WGzDwMeD1UNG&_(s(xET@B$f?Nf8Iega<$sH=ud*4@`p$OoJe~$3q2_9#{i`@&r z?E(*aqsJ8<6lWogQF>KaBA+kREy61lW+$OH-vd-ThJfsF04ya?hfqSZG!qbagxFDa zCPRr9ppNAd>+5SsU_|iE9L`HPT75-ihxrCFalKgM3cQAE!V=nO<3TN01P}Tl_wQQi z2H)IKB=~vYhfz~2Eb&v~Hx)*clBs}Z4_RA6CIagkq^^{3tEgeu7f)zbGs&Asgybr3 z)8Wpezb9cR3N$^zg(T2jN}>pVD3@In+?d3(7WWUaT(dvI{0akmRshK$kwCXW9AC8h z61xTF!iS=Lhj5W|+1(371E>R%)j>X*7<85syd~fbIl*=q7Uqv{>0o8p1UH(aOlsF+ z2VsCl6@=x(EsRt$%N#cdf{clb+JdMb zYul7(*MN{>V6qyE_XZ}(A0)9aEDVFupT0QJ=#3P4YTNmB*Z4SZApUYl4|R7AW~ z;`iGRP;E4>>g=cl_JG;|A9$V1kM~b`s_(cn)@I%7n8ZMKZ7mQFa<>zJ-n7-N)P3smMe;$Lxodx8&E<= zqe;Sp&J_%JJmJx&F>3F#i3X1;H`rKe0r!a!2=DZQ2mMe&^mxNVu0nX2h((CdSd5oZ zu*~5d1kEK*JkT?o4dB_uU;=_jr#QTbgB+o5j5xwUIp{|nhJ3(H_6T8Z*N9-X3XM?$2}6Q5RKF+o@mYtx`GAdI^s)DsyHQUHx>>=l4S zqPQTIS|>Q)Xi>nKW0NGXCBa6mO(9+Xn3`xBZ+f_ii-UrK|Fio1`?~y7JuqZ(HoGr3 z2H}62y2K2s(q!YeMhFj5F@uUrrc zMJdR^2P^`-J+U@8L^C0@x{>MJk*OO}KOEr(qzf-W92J5@yfC6~ZE0geNBUOj5RsS# zC>mOw+{sn~aFEFe1#m=1B3Wz&ca@-EI9@{2s2DteLF}!)?l-b4i(DL4L=HckZLyEiamP})0A_8`1$>1NqKa@eDg>K!Cyzb7;hgF%q7y=TMIZn!gw@fg zTUef6v55_ahz;$d;(`V}92E&-;N=GUaM7Gir@P~DN(nhfL2%ay7YZ~kAd{)8&d+1&xwg(gpy5PYgIw%DJB{f(SlCW$-j|L^haQ?t?6;;F`3-~#J8iXJm4PI`6 z(RAwSKeEI}oFnN76$K$ypxS__@M1tt;KUgc;`}reL0Tu0tKBK^=gCk4%6gbQwt+!=k@a6>1`827Cqg>6h#=@w zR0r+?2rd{wR-lIm1N`GiaFRiUSa$6wSpR?P%0_Gqod3}#5khSve+622DE@ABgu$8% z=IeqnR4J$tTMc_nVXe&^OC}fH32UH0u)&2}=_qodb)1X<9j?^uQxU$thIFuU)7dbQ z6;7O7`LR<`=mB|_!Z-my2OO11L=h85>3Hd5onzr{G3PlIf8;u>(t+m!`erbr2@yjG z^B};>rY@dh8Y4!GsmnTG(#w-nz(;TbnB+}YHsT%vTo>Z%EP1f_Edx~#PjrKz$UsCS z1%gYM7bryo6LyH* zLiHdi#9p2hsW{MwIuHOS2F0LSK{8XQET}31%b;CYVwEU5x-Evs4NS!bdXTp%h?66m z>0pWFyF^%OjKh`fm}kmJa#sjSE#iz5lr9Q9&dawB;bWD9tE zqmP6L3VxHC<_B<)2!Mr-DT5;wPiWX~p;0OvPnVi0!qytfUATE$VErZe8qHhKk6U;5`4Xq8)f5i$unh?}k z1>T5^!GDX7CID|W@C(D+7butn;}c()3&ob>0uh#rwA4*SRtJ3Pv+3%KA^uQ^&u%N* zg`ttdO92q93}`xW^be?lB1Q(YmPm$SC{Ru)<|~-k0>M%!O9bAp;f?BwmMt@Y1#&Uu zSW;k#1Z_|d1^>|84*+3_PLj3*^qh;7I}n61C^v*RQtluS#-Q8~p3p_64F+Kh$_?TB zDECsxpK_!8DR&6uPq|V4lsgpir`#z2ZZiKc$e(hf{3&-h zflnOc0gDJ45OTBBXDvr$fDK+GZ8#EODU@E(k*rCtKz$N)dVL`GWVWOU8G`^9TnU&| zWS{}eBm_&O*AkrwG5Z-wrO}BgM1HfuwdIfn!FwvuzVW)}0kL2&3TKny0FOm?`jEEo z-l3pUG92yKRNqp5I9bMk-CEdM0+A+Dgf|dZS2@%qpegET z<%X#Ih7e4nLAUsNA|-k3V>cm6hq@H?Jb|Z$x`DlG%>Oj;vIiSd*zY}ScLh!YDcY!aR{##;!$VtF8D_k+Won8*&#;&A72xJxvpbiR;Py<7kX5%DoEEJ#b$VKs8(q4;xPKctS%tb{!yT6ULrkc9NZr3~qt_ zuy8NJI3a0N+86M2WMCfce^{^uzJ|a|Ar+F6>w+yBNYV|b4o)utIpjAJw1slIDIrZV ztZzX{@~le=fB-Z@?B19qO9vI;j-XJSlY{_CNF+lANMS@bAbC2VyK(vniULPuz%TF~ zPR7M#0#=APMIIL*x14Cbd06{kE1nGVyg@Jg6u}@gymL(vL ztw6J3q5WJmAS9Ftrl{e+BFAxb64ev`uqY8Q$pdO!rg2aY^|hQ4DUt3*Qj7tP931Cl zEa;{{Weo6(djoNwiS|&eD57h*<|;LHIVLnumy^T7Q5+{7?9VkOPVQ_d>$7nPzKMFG zg#)A|*qx68rePTla$ApG25hQi2ohbUN;r=}E;6WvLz3Y=nTzMecu2x>5qfJhEH zVL`3@Fhcz^X**wnA90CLI$sIH?IFA(|i` z>9OxXRMBa1zHr^ggj9A$bR4_l7}Z9Ks=!Jh-H-uBM3E&y642O6xfBStKgFjZ^$B7w zKV5-j^TR*`wFwE`!p!ibbZ}wHE2!foGEl_Qo#?a%-?723UU`HrrmoSD(V%u(lw1Z_ zkeoXg)+&H?L}*|#DF?BYFnd>=kW@g$QZ?wKSbeuZbSNP3PC%##as@85m7OhmA>33~z4Cw#u%NT5`~2hrq_vTF~wFa-cc;icOKZk&)EM4Ep{$jrA4pk9cL{lI;NnKwgpf8?rA-@VmJq zP?ceAu&{wsVJei~Kv;QLJwV_SRP#``!dI56LGG`(fh@@)lm&SQfs`-$hf1mSQ7N^5 ztQ5rfL#5ORDJ$+u5eKZltJJhUDmCpND+MwBP^oEsRf?E6WThN1zY-4ul@am~x+-xn z3@f4L4s;g+hlsVj@?0b#1iM(=BAgTr6CoLnFgciBScsh1Dacts@Q(WcFb7;Qs!sTj zOH_(`?D4`fiIhJ_rEm$~f?+tco0DeyVEGB~^&x2O!07?jY%jCuz;Obu20I!dK}+EQ zw-;u5f<8o+dq zMlv&qaYhBvds|Z^5ekk10NZ434TQ)Zl^o?56bvK6l_k+laH2*MF;JV|Rt7b=&y?_i zQrgH{4=Oi8SrJr(C@oY$9K#nL8!NDY%&>N%&ogvV66ZMq^y)53^CX<=h`4}+^L8m` z9QKj!Pq8>p)_=^72ebcup5h$i%4$3*j*7Q`3@~iCgbkKAgbh|)wlG+1TM(jSfexz( zEPnNySx0Bs*@DCr46wPO1<0$j3P2fg?ZDhKxYul@_E`lGDnqk}4K=KKgrT|1>Pn6w z=4di0c!4fukR1s^PUPJTG5~VssTj@}js@W%pbiXO=wPMctrZyFLJi}z6bS%`JBVRm zfULRLnZss@?io?#|J#0mQvt@8F`bGV=Ye${VLLo{J8SV`9|*vgypkJ9(mH`w4OXXpGrQv`ur8v#_pF9KNuAz-=hVaMn- zwlm#(bcxiUofzG}R3!PoV8NWan*en|#~CC*`Y%Bi&TAYU+h?b8T&RQw^*vP60Hgbh zA@PeqqLyfL=ILs39i($WA}0m!_X~>_FF^N^6YK5|>o(P&@)KttfT_IDnU}LVV7h`r zButFZj90L+GgpAl!CS2g+VzNidLn&llsEZf?bMuyR8|_ID8M}-ge(?gQF!6xne(h6 zfHkG}VkAydweV#Gj@D$BKMlyHEy zAGUTn`<~rD=*aSm(YyekiH(+T(` zl9Q(o!bUtgj8fL(l@K5jn5q&6JyxUG*@JXHN)M6%gvdPQp+=C?&yI@oB~l^yrXnHm zhS8^p%mnm8+lT_-3w7s!Y&V2}Y&Jfkks$;vK%1-)5r|_EM{S}=?sF2Rx=<~EEs>y6 zun11X7j{pvTFQneAXNTGm*J`1oZUyr1ycUMZ#if6VPyv5;WQ|}Z`^@v!G!}U8z-2E z%{Q?alW>u-FU*2yF$=Bush^z+NCIt@yov3#<8(O zLEKH_{HBa0{Ym002iDZPZGQqcxaEK;G(@UDw(noGkbAn~GRU3(UtwU=(FVbbfdMhg z04e;&)(X)TiU=Tl0c{`TJxD?&+?e!6_c!n?=?mQ=JCQ{bV`4j=C?5s%56?J+6ZJvg zLf_$wLvQ*UVbX}8ryER@a@??C`~M0v1YQI{3h#bBoyZn{;Xx0X z({U(D!-XV51>peAjU@i5^ZMGT|G05*C-|#L`|k$`A!@%J6?u75+y!WNKraSG7VNvI zlpA$Eh-qp?lqTtx9ID5#iAke)cQACd;U}@?h)W@3`&K; zqAy(MuL#93^pF8TSmYVDzoV6DD2a39Y5%$^8c-Ef5s>5S>FJ4$E8wA{=H#R%zh{QvV2 z3Iv&k-vHCU9XHl#1hpDZV1l_s5tmuZ!tV;^=^b|GQL6xlTqH~Wllh01{BBzOG0oph zjhW=s;OTdA6XWC+aS2KGT1nZo^j-x2-{!?3VcBcukk%fNU;$5O{(0@AKyB843~E$0 zaaWVfNUk>jZm@sTb-nXpxA%8h{nbu9iyX3l(=T)dRlqC$c_03NEBS+@_3-kf{|3@k zr;`1KFk|%VwL0`AXN=GX4!#q$0`RWvq!=%own0TiO9?5;`&$?{Vwl+&qKF(~zW*GV zl-j3M-jNCL(uEXtGYO_X_rk@RrSeGrP##6bGSY;&6!LNiRCDIYrC^Dg{3t%jR^pZ! z=e$q=s2U50`gc=V6;OCii2Vz+p^uULA5A$`)xTaQsaxl;ZD3=8za7YbyRxZrOMd*# zF51`9##7J#S@)=s-9xJT@KPZJEW~-T;r1aV@~0mZND2Sp`-yBiFuL(i$|>XPcf_?YEV>J1Jhan8mEmq6NRneu zA36Y3`lj+@RE>jOjqY#|m@{5hIBs#UYlEx6vum)i4GxWGzrAO`jUWPUj$|#cjyZix zb8{10YfJDl^yjh5WKqI-%Eg2YEm&gM%E8&>k-kGY+sD47!V?#XPXRj0oh>QX);pX- z>0GamaLyt+6p@)wf3k&qdd2Ol!?31w#JU6ZozOJwn#4f??TZ^g6o?p%zflRQqbkYS zw)5l3Is$bOZ#*nzi9`TqIMmQ+vXsU%I0$p5hfjQ39Jd>h1|POS5qlI#MhGbFMexFz z1GEeeC9Lig7b85--`CH}KbkFnLo6u(E;_0NK@9OTsFW(Gf8mpwIQy;@Nf1MH8y>_3 z7p6dD$Rf)@4t>$(2)-%i(vg@Rp`n}5SjzhUKUUZcTaJ#JVy}6My99L$^<&*c1DY{v zFxnD(|9E(l3S55Bt2#{))N`}hdVOcY$x8s?C{8a{&H5Kv5a=u$4TUQA3WB^aPpx+= zcy3uOViuVBubhxuqa* zgD|jC*M%U2TCR!qsuAra3RnxYDoQWtI&de#vmTxe9N|gePKIYAJi72ucjh;NI|ZIp zcr51`T3cIMBc#F_?2TI)TAMTUEUj(z>};IOEcNFZ8dzAG8=6}iG7Ky&%$#sMYdb4j z6H7>k%&dYNP*WpII|~CNSm^Z`KxD8ow6Lv$WJRnX7LJ z_ITlPkcF+ei46qm8^hbq#?Zpj%*Y0WdyV135(HZ_EapO(EyF+${+s9AG`z9F;$ z*AF^Lf1b6WjiI%@AxAds4E4<{ZOGzL-8j59#^$KgiG09gW8zFk1P?m{@R&2K$XJGk zWSxn6GA%4Ypqlu`jy2cN7FpVc%NWLFB`q!N**<#BktEv85%nfSQ&}n1F^rl7_1coPhxR038+_LC~EDwc`jO`S08* z$*X-F(TF1+M*z`ntO{T`Gmg(zCKeVj`0N@YII(pCfX6EX)t5{d7@iQS3&_QTNn9Q< zFmy!F%7j(N+HxM#%#vZi#0#GV%m6bd2qK3SHIHRx3TgJB)G)AZM8KV0!10l!H=)ss za0B5DUq=F*4-`gIsRG=9c@rK$Cc#P`sEDM_hcHlgZCA!K~^o=l3LVkm#o~0FGGq}^>*#Zxo+Cc>c zSWf|sK5;3cgno^58Y7i*Z>2GIg8Mc7qYgxepPVj!(VU34ZVK^EtRmh^e&Bapf(nBM zzZ(NR@H^pNCVqQezmDE?W-h-Tyx&F`&p_`z>B9ryt=p6{bq~B#M_(+z3vcy_U;QNY zX>reIL{~BNX}R~;IH*VH(`qby0xlHj)3i4)YIJ;|ubX@_YSa-K1FN=|cN>S#Ge~PN z7q0&pXYkGR#btiwlLo~*9z^R$H5+W^S5TUiL^qr+pjOgeWNXNu(A~K7!Ft101IJ8x zBY)m-?7i2&o~yPSHYexKa+;%PBzIIav27#MNH$RDd>!;lWp~a!wtL8=+jY0*jxx<@U$G&-mpUV(gat{9@GC1IE!cL%-cU{@B=TMZHsXo3zPJ7yXYVenuwM(`;Mz3okcu z$t^x1pK#11v}Sj%YEzR*isn_(l?z9hZu4AussD)irsun!MBH*)V>)%k*1>bj%1q}3 zKJoW2{%BfvWpLuU7jp9$ox_&A8|FOkkhaxJRjbr_gUlP9=XhS6=dftUC-W6Q=LM7s z2JU`5#mwMKWn<$z4>OsgUq&r#%rYC16TClsRK3||H}7@6Ap+)Ku|>d{<{>{)UR`t# zG@sistN)p3>^CSwQcCq(hZ}x zjI?`ebYjStNAvCW3|;iN&3dif)zMM63*MZu(-}0`^IGsHyZ-m5Yj@X9upejRh)Cfq0Mz#XtBAVqu+E|>R7i~j+gXk?Y^gj9M34m&3t)#uj3^3-0Ful_Z_D* zjH9GG`a4}vP=4R+qVM!6QPQbDG^dr7>Au|hvoZLynX4UlRNlg%N|*0%bHQXq^GdnpGe*diIPX+mAmskA#d&q7qy7rfaSQfpO-aw$V80+)TIsuwrkd1k%~f-AS^kJ|#yEDX%L40x&BLEv zb%_q0cPscB&DFu`sjzvsx@$n=R^#W|OI$A=c2zF)&UFnResZQm+#T0}ZZCxca>W)F zEH{@k5Y%0G_t7F{JE5?JwF6H_Kfk zb?DyYl}z~)nr%(8Czzj@Vt&t_ykUMQxn}6lFlte|^{yoQ{nm?)$*)r>v{(nTLqXGF zT*V^(Os9m1>7N%>bp}niQ#sMCz2ln7`cN0Qs1-UD(`z=n-E~P(($}nZlT<(P{dP@< z+dA5fm;_f<_d~RV?51Kb_dB$vp3&pByC=}wm^NQ;xbJ`4JR?s{$fJHwlc9C2rpJXf zNBp*=1be9YY&VKJvCpGhSI^P@`2&w#ixt05R~q11UHK(Ue};i))QPFqgAJoS?Vp!> zJ7pa4)R5G8{=MRrr+$k5{qUlpUV}qzEpFep@M^i-J|JOkyw}^Gsxn@~O1<2oRyv;@ z)9R%yk@Dm2?Q!0cb;nQDCOUYhtYS9yEZOM&^XQq@XYQ5WGj}X$&g}T+z1BdeX+@cm z&mGh8M>e&)`K%w+;(UB*hR=JsgP%tCuk{%eDe7=Lk#F(6^s7pu@24;Br`a{*s+Zql zV}ow@onv+{c5bNXSmV>M_?T%?jL!ve-y;@eB+v#eDB{bG5dI6 z?S%<3qP=+K*KWcyFn9rQncv!0T6OXNK73 z1O(Ssw4AF&i=q>ZogKH6?_z^ zw4%H<{pEn5h#bF3nj;K@(&L3sin_-HX%CcJYPjNPQ0lrzXL9s41aphIRg6n6k zxM4TkDp)@;$KX9ZA=oy*N@k+s>EL(i>f3`t+k&N^&@N2K8^6?kFhg}pn&VPGr_Ehu zr#3BpbuZ?rx?0uJ#~X(YsqFr~)aKK!?IGRDA$Pv%?-n`d9x1+AkCUOCFT!S@3ypnZDHASn#Om3dy{6$l z0cjFm2jardi$+>>*B6I>x;pq}mq2s)-EOm@m3s7urdZUpHu1_e_59CBivpN>W6FSm!5+V}gp{KuzM z??S67E3`6)pZ81iSRt-G_NZ`V)(W#^MVFfNx)n#iXV2;yDX=mpoR&DdamGr4Ewuc3 zPXboHT%c=enZIY{!;l*zU)kPUnVKTlIAT)2*s=$!1Lte%#VV$bzuHt25j!ocaQ2vG zg|WFc@7H{(eG!W^ry>Q3bRfd`hX(}G4K0mG+A;8MfJ);7G-NQP7uH8Or4*%)Pb3D} z*B6ZV0*42*eZ#fzMVgD)Zly(tkiE#YchCxU5LR)P-0v+r7kp ztl%DSUQiX|!-@c?vPY1o7xV)B3l0KxET9ns*`fl)Dxks@>KZ~VOgn@Iz((oW4n|gyP+c119iOS6z z5&ymD3O=Bu#KO%2_1NSo%3#Gs(H|l6fZQWSisuV=yx;l$r|Ex}@BbhEztIE81~VOO zKI_t~Xp3mUv^bzpa_5^%J4!oGyFq(S`^fW!JKpbn|I_rp%lH2q;jk3oA0PZjHc>g2 zmk>~WkC#GZ!UP;Zu{N~Qrb zp#K}%@Lz?X{~J2!|8K)zFZKUbSo*JK{(s3J(-`L~MI~zFEFYJj?K~FQ6C-tIP|n-8 z6OM^#eAlke-&Od|HrZ|SvB*)$0^$AQw_cW&They=>yCyAK?b*Oyo&ibs>r}kAyjBy z)?A^sdMo9mhX(5aU1T~e~9Ma^F>PfLR7f5zTaH~ zd-Y_7Vb-DiD8KcC#N#FJ`Z3xz+n=>*u&?&ZsvVzlty1hL)1GXU{3k(i!=l zf6s~ZLwCQZ4Zin%nTX}9{qY-~pSGzzs`H@s>%%MKTvFtU9N#YP$w`sYJ#q8w*6arw z{xdf39#eT*Omx6_<&K^dgSl%?ZaAo>ASr&Q;OEbt6EfMaU;5A2QA?4$GDNF0HnnKX zjg7C;d}1FQ)IMz5v8z2x-r!hHB;#4V^!4mfzb>oBs&0J}-ynTY-(FYOx^rY~+nf~} zLMHfmH&0hE_SR}#Sg>hiQGKfr%}ZbHp|0r;4g2FB_g60w(VcVWZ0cf*aWBIy=d9bj zNAT?dgGguom-)^%ulgO*xF)pchK$V8_-%Ho3NG_+3(Q$})nI{hzs+$Cqq83jOsHI& z-Yp*O`1aKE4cROjFuts^Tws^;;*pZ2`i+{ysOYA6D z_kO)RWY_p5yZC4Aj3zIMe($(L_hg0*e%Fy#=1$wy z^J(76D+h{F3YYFURII(IN+&XC-0}h0CuHj`-X3c{vslXR+T`uFvAH|=z8NoBWVG96 z(#nqK>6Qun7qSENICL|qcR7}~ZqfdDRlaOxf!*}z zlO840!ygjog#HWoodS*xvU&n|1V>_=UwT_P*m(%?C>V@b-G7F8E zvf&XMw(d^5GJ06c`s=?WG+YM`{_xABWSRFEpO(0vbqVc5obsYaHVxN(6DR+oWc4k_ z!!yKcRZ|8erXDSAIet^5EqjfGPWN${{Nl&24~|)TWeLrvuCC(QwmUDjJl}lKr$TQL zjj{V~(sQ{07o{^b&jW0tbT^yS9v&K)82bI)FGu6W8L3|v#6H})eu-81zF9|<`4+}R zXQw0zWawF65l#q*TJ`n*z1S1^@mjYJEE8?2x%)z`{yK9}gQs|6>NeBS9TTs|?tZT2 zvL;s8Zd-2aXP2P!&#v4zJ#k>Par1fO{3-28M;$shjaqbL;zPMl57V_iInat*ZO(;#et@Y8*P8>JQ6V6l<(5Ai`xe53O@R-w0eZt?i|LcZPU-+sZJg#=kvpT z-W7pc9(+|l3_9;^j?mpcs@_%4Ci~rRxIhlGn^{qd=4Hy0irWF(cIeOBYe*2od-64rdn&XOH;S|>2UX_?(ig{FyN=T|;)cvHW=Y0AUq z;nOU$jAsTlXQYmeXmhQ(^-;&;th2Dh{_^5OHh$qTA|IqW0zO?fuY3O@U7>oQlgZoL z6^-9=J}#QJ(dEIJf_utKJzI9DUsReEcl1PkeujDU#tNTHjIWV%b06o%+jZvN``EH* zgvrl*rN;FJAt!fSTsx$FmizXSh8eDf4Rzx#g)59yy{x%3vRSW)h| z^!v`+d22IO1^9D%To1pmFh99g)F!X9E1o|-_Ui+0<=yUiSAA|R>yoSTcouSFQTaaY zLr>%414e&$+IelRUex=38*PTT?Gt?+_HoVG2iEr9^Ml_mh`ydQCe2@7CDrHKGc}{7 z5B%az**w~Ne`Fc$OXj)E>-B4M_bJ?}?Z5AB+{YD1y3WVv)m%R=95?X#I5&US5!%Q0 z-TJkPDN1*U^NaQvu3FuCpepC6mQa|+)K}L!7ga>&r?yw+Y!H?<=zMfm>vW+^L-D6} z17%|wyN4#N{V)#v8VJJj>($UXbr#G8X3AodradN)19N3 zWmoBQzD`PXU%O>fPV4HBWyLFPHy)opsl#XHasB-9mfPHRzEd4j)&6r;_0v1)EiY-m zggjS%`8jj8^z0utP12ZB9euNwLaYVz(H*L6S=!Z zT32NE)!)hcvF>hDocO){W=-XzHVTN}{&vW0%-HK@^}7~M_6km@T^Dv|jE^skUNtvD zOQ-tQvpY2jq0bq#_6IGBV)c=iWyL%8r#f2GmJV3lUR!B*dPi+^eQ~R|?#98Zm!8XR z*z$5r;i{>2y2kg-K2=YlU7PtxUUtv&C05S;bBju~&UXpl`jm5Z@;ERmV2Ei-s>+-QnU);Wo!#ry1T8i>Z7aKed$_#8lS^HSI%{5hYSUG?koaQi zR@?8(+c&*-YMp%Zbkx`6IZx8J)j1BE`XlVj>Nj`QrfwRnd4cc9nAsZEOJ40x)+s-h zo%rd(Hu-B+uMHkIl^m3jTJ$yJ^!XnPH3t+ZKHV?`qhp_ZA+9fLG457lFgu5Y&Rv>IP=@iV23m2wa6?VPU_d-IauexrpG!s*2F0CQ`hb7QeB_i8g@-P(Dosz`BZn!YMbSOWmhksPw{)Fvfs0N znaAQ0;=UC>kA_z7jj8L>R-Q9<{sFfkIToTW2PAi&6)xTA+a&dCM3uYvmwHQ+K;wy@ zs-AB7@$$>B+P12_kJdYvL}>cCJPODf`8?d&NHODw*I===)2!E@9q%~kQ|5^?-(sDT ze9j-3+^VGF_+drvlCU<}65W}`kJ2BHbSSpiSDE+zNu!m~@PTE68vWhe&A+61%ZzTi zmz(icc2v#JGWyZTMc38Z*2*p1tTJ%aplJ(nBZ zc=Gg->y2+GmKQem|8};Y(w+@|hcxwKK3@0Rk#<5wy2P}o+UClwEdL8@4I&rCG>V3F zN{fvHM)jCpTl8t(wE-FlWwYixP6~Ne*IoKyba%S`euYG@)?$my$~p3f4rs@u*Qqsk zE4q)<4qaEOnCM+Qauw5B$9ctcVbQ#a>XYJnJa&JdSby|!rN~w5sTb52^^{*Z`H}z0 zmJ{nceU^V>%+yipd|9Y}CyHKp=KKc@#rT8Ucl0yMPnefJuR--t!WWOt{FkJk-SJu& zy;FR}ZLg2z)AheK3^DCFDQje|vQKA7248cwp~a?iH|yqE&DvPMyXYIV$>6(7x%=PZ zAMaR~>eM77G-|*SalP78&4(&&e>tQmj~{5d+UU?OPcPdq?|vws_*szgcwE+&xfLrF zgXJQBKKpK8zGd>tC?}1!Vbu}M9ZAlo>}Ef*c<&(Kmm4{_>DS>;pOpO;^5-FcYEo0;XzN3+`l(0%99?|v zbk8l#T;0i*x@snc;vgiZ1{^gXOFDCGJ=2mxQEsTm5bf7 z>xve)z392hE%_cf60>hs9dQ^WTlaa(~us&r8`7Y;f9l&Vr5o z8%2LqR;GoPCU1Qs*#G#{fukf&oj$E){3Ws?wbD3f;pETN#}x8RhktBnDgH8CLgUoo znWNRLpV0j!6kZ;Ev9ZeIVf6&2&4%>xB2F9o7tM5DX`JIQ<85Jdi}8`8^Y2%G+~FLr zY%p$Umi&lEkG(ei@HbR#{NX1fb?(PU(G5M0BRsF|+%)3S!&|Ky%1MPfQ^skpGzxhc zxp`fF!jGbRQfHRbj=JA}L33Gpv-O$UweN~6cMY8TE^Gh0S^MRiWAo(PPt3iSHD0H` zXX2(~jk|6YY32^=7u&LF{T!K}t*v|G3*`i-F0Qv3HhGhCNq%$kzI)0-v-PU-n5R@# zz3e6&y>1>Yd~INR+6?J)bC`Cro8oRrG@ebmf9B=g!2TtZb{xyS*nh|2!*xp*B#Gbh z3>SP_6K-wb?7Z6Wv&u%{vuSH&ItKbQeEj(GZmagti>H=_e#r4#v?k-OImoe&4&Uc| zL*e4u(uuRpUS#G!^GY*cDmd+Hp7+#wyB=sa6vQsOQT4Vb0V!*|=VTOZ%aV`q$lID{x-WVV?adRX3oDP=EFXBr z+O?nK(DRGzjx2q;WATW4?+z3X2ps*fG{2unf&18{6Vu)XUlkmbapLOD8oM*T{f?d* z*RShR=CVcFLRyn2T^!)jxWsCC$%<{I$*%+#Y>U}&dXwJqqO?tmzC@0!{-itXgU|8X zqn&0PpZU>l?+lgA%(r~c61}HfdTQkFSN*cQOiR@*NhViYYhK~_@sT6^4{ca6>+o>V ze&P!=TMF#PUAFn~`0|6Xrm4x(E8b*;hr8GgZXTIvtMV|Cao1|)t+Y_zY&p67cc*VO z+>~f~vM%yQ)j8#&JDzV1=2crP+*)kv{xEZPOTrv!i8ifI&J}J?e%+{;Cx2j5!}aD3 zMaNpo(q|bKjXqZY*>u(Z4?~~&w0G*yQ#w(%>Rw&&y7czuj&g zE}iBn+EkwKwfw-yGwq4{-L{@uVLG~{NzMPpt9PoCMO;UvZE8*1Qol>zT}7=&)1-Kx zhFX4PY2~^*zfO0b6HO@@m%Ap> z^ZtnOy;(B*eN(=lZTL`8*Zum@2=hee{sy5ZGxry3FqpyXr%v7aBT;;@YG<8Wf^=kU zf%ft;jmiBgyUO)qni|s2-8CEGD;SZgrhU@a|AxOq(>X9cVeW{zg_jQ6 zj0&0dIIdo5ve&Sej|igi2uRb zwLCw94+acMM&=vfR^`$d2hqER#wbDWUo=KFy!mx=XpFnaVO2$AyhaYWb{gX=I0WJ{ zvS^GoZ^1<`KTBh5MJ@%uRvKeJa{c57vD;#B3F?;Um(ds(k#oTQJ8!`%9&%24ldze_ zcm+Z#;JGU!HqjVcko%HK`JQ7m#vX7B>jJ7J@s!3m zjy!MI3y#0MfySsr9{0ja@1u%njGN#QiHngfnRxF$u;Gv|wJ=aEYVA52;}h}~JBaHH zS$v4bSk){lQ9oh#huXRkmAA=0q2(qF)^(fjltB4w_76)vy+3F_<=2#=x67wotE2o0 zV(CAkX6dXV{klyOr#0@0r0gdBR!=Q_1g~HDe1-Jy%NV!HPJiVY?4KokBm8QYU8MarGb9FW!+zb^*(YQ~t}Ls|JdgbY zcFm(dGCniBvf?B5-&s4lJ+a?OtL%sjTi_ZY(oOpw5F@Oor{D7gygFWO2DhTQ`_{@u*>?@M@urAz<)#NdQL!) zOY^ciwUy{X&YtnK)s0DC8i%hw=XwH4+n^M;=Skqyox9zqOn(kS(?v7r5)Fn=`Sbl&Q$r1Mx zX*Ekae8m&@+t6CuPHpL5K#MVq5$xBzX>r`Uf_*b@HN@S&n`M}~FNE=N>3eUFy=$!I zu8p1i@W9;p{uT#xXGW-7<1T+)dAj75rnz2H!}Em6Melzce|pes z-TLRZBG*Vx(=yw#eaid9UF~+Y9t{WTdrZbeRd!79yS&uz-0TghRk0)GhkS`?Gn{2% z)Rttg`LKTJ{STRiC$GKwm=vDbQgcIplf}CV*}Z2+erUDUmhZTDM|F?KhbQw|J@WDo zn*A8rA;&aan7*VfY|f#pZ&NfHXSJTX8-Chg^1w|tT`x}LlZAMV3cFf#2YQ>*m`WJzw5b{;+a(Mn-kZ zhTPyqB`Y`XnQdF&G-`BbUQtc*Os}U$ugz1`XYQY%HSoc&BWt^5o}AtJWl%}~Hj{$O zoyvpHDQ}peALXy+J3TPt=dxK3R8AKP+GdEvR#lpGe1EtuCFadN;djffEDW3Wxa;BD z;_SkYYxgewpnXOm*SB1H{J8V?3iBlE7WZpb6TLY3Zo#CZ{qEmN<*Q2Frz8ILkypX+ zO`@0Me?5@cyuKx|>x>+5TS7M`5QZ}uU|O`_U) z;nBEap#`drt1NBr&e|0DDg1Hn*vrSX54(Bzn$;|A2-q^{%Db7PuMg8K7hr1EnVLj7 zc8tI5rJHM9Ah@jYe4X+<*&Pu%Itrfs-n(xbwN&rtDA$YW@s?^8H}-4My!vg4^a}eL z?fGhDi$Tg&I`i$BCFd4rUmI;vKPV}C<=cptOAOm8&#gQ#w(9-|ot=9IZ@)C8;6&T# z0nJ}5-ql}fYKhZoHNCNQplOKX%}=|}j8SmAddSt_Oy=WFhYbTaOqv+-bN1kHndXWu zYQyh|DSvf*Gva`fuX&n%4l{V-#Mw?NFZR?&Ry{xEA0H`sbV^cCOmZ+sc>P%dParZ+&5B}Xs=dg9(OpU)UvtdgX0UEZ|Ao!)PDBi)eN6g zK8jQP+p`_VE?Aos*1DQ@>v?p=x~1n2zx(8KSUP`ioUqgVmUkP2hkLg2J=~gJOE(Vc z|Ms}!+SG1!M&gslv4b}XiK#wqJ*Yls_56I9JG-1d8qKT7Xuf&j`pc#}!>@=07Y^O& zzf)~|_W2^IHAkNaR{Pbz6fo@ZJ2y4k=DocpSDDS>-IDVw(vAfo^1iux< z%c8_SZ`(J%!Zq3FyqZUymHB(a%;!cL8D%B9KYBX&_S3y(E3)oaX&q{^I@J@SSC^;b z+H$t8c*CGEO?#guzl*yvw`|J{zGE8x(}JpHKaPKK=0RR*0>jl{E={6zREz(ENds=b zZrU6s<8;2?fwV!5mxnj&r%rjaxM)UJ&AP~;Poj4>+7@?6S7jNVEmKTiXnX(4s3k9A z?tbaM8=%-e>WsSlPV1M+lHQKY1C7~38{hFAGki1rXiBWg_j=d!3tc_+M#=6T`#5yW zuXg?L;E1x7CLxCfx315#85wPABD}=+>7)J+4SyA+E%uPz@o1f(z}>Vot9w80p3(lxt8Yh*k!Uc3g46X3Z1g7wn$97v{^pj-9-F$SJW!=Bj>K%N){oJYzI6omN}+y(Wk3Whr|}sH9S&B1PO;vlA_-s6?Bn zNHmOW6|yD~l9DCd)0ryc;`~|VITk8NiAvS9KoU|?A0$uXYKv!G!b`hOnZB7hQs+W&!W76 z_T?*EJjAD$X)*tz27E$w5DT3x$5 zOLA^pUjOuQO6Rr%NedlZFK0XIk^{nRbCYbh1n)Y-_j?V?>U(dRMeXctzIz`kf?M5L zi(`i`G;Ccrl*V0E&|_{!wdBZr4>dd1KcDZs^g0PKW_{=6onv=jA6gtGaNTm*8TXVZ zGFbcYIX0IG3++Dp#)}b8AKtrE+;aYn$`K1uwTN-ocA7Cl>T@_uJAXfSN!Y_%{9Y9Bfl@q68@S0 zrB7IUcisJgRvF?!@dl6d72To@LqQft8aDI3Kb2YkVQq(7sO2g5eU6;30u77R``7F=>~Qkn-ss3ib4*$?1g;+ z_L*B3pD?-|&1O5%$UZ1~$8%48VwFhcsAkr9#H>EKs>=$mx3Y_~+)Qt}yKy-AXsV0k zAHI1eDX2c6r)(ea*BTw#Si)`n>JZnr+n!#9raBIODoT9P6MmteDlHRS(<;0lZ@u}7 zOpJNPtKngvdg0GU+t`qa2SrLL^J-#$Rv-Ces`T^wxaEkJkzmR5AO4(eW~;qMb}zoV z%{A<-!r2qW@9*~P${bo=u*Xn(SK$(qQ+x{!xULwu)qnESnd5_P`{s9rsh8Eh3yixf z`B2BQqBZohbd}2NZE3j|m4zB5)h`{3&wE)KykdU03P)?@vxv#p$qVB@zI)6Qb88Kc z#>K1GR=(clE4Xd1QTFx+d^OJsX06{OD}1FTJGo=!d;Wj}kF=%N`J8OK=av#t*BMgx zr>}QUx~+ia!pOOg_3SnVtKVIJYKeQCbNtYb)}^P%#f$}IYGvLKUs#t-LyzB2XmWMV z_w)M2+d>gt4|y%v1dPg#?%zE*ub#=f`h)R(HuLszJ;HNO&lmT!g~4t-=N0=VA39!s z`elzqC$CVvt0PdqBbi%pzJA*(qpvX{A(FlR@*KJkc>1@++S@)m z`BUyttqR*~44J)*_5!z`fP2P#W9^XF4#?-jF_)ci|Y%Xcd z>&8I?_LszjBaPExke$JM2k36F0Sov__|HuPA|%Mz%S*Wx_R+q&Zv1BX~V%r72-eF zMwi>HeDy?7U?;OjME%r{jf(kCzIuLIY`)%k-94k2W!E&GF(V&@MVgNnJqxK>CQ@8; z`@xCJj~pIrAa;AZzoNw_6H(Y)%^t!Syn&NpsLehFO;alCNxg5z25R2iR$wu$S% zzWdzI>HTwcdGw1`IZMyL$<$R1j(603*PN2DXj5{I-6N(DXeSyRb?L&U2FqEx&rc5f zrm74b8p)Fg3EdcAl`6m#t_hUV$~^2YDV6x-htrX<1%4-gi_hC9zWLf#yHU2%;j6cz zys~2QMhyHHoOavlA%iMTCahMz=elBDK?eoHcN=XITHeK%Iu&U@eWvsGvFh^gO$&#|+zx4!9&eo-#O zli9Uzar1MneX*N`-H&X!&UfwRuS9{acbhzUoCHhxI)d4B&N&>3E?pOPkRka?w7Ey~ za?3}&@p2sgP-*`zQUGI#L_r1dR1@G?d@(r@pudI9!*>B()K47p=R`~9z z-7Ly-_|TqknIn6a{@zeGUygOWPul-vj%`s>l&!V@10PnY*_7Pt^<(liBd4TOEcW{5 z-1Sz^JN^Wf$O>+H`(4Sdvu=5nSYUP9*}dDf8BfpKRPV24oH%AT&P*F3L0U(uk-*x34T zmd4lBRIz3LxBWbN?Vby-a_q3}9XR#yX#4L<>m?O@pV&>$nqGSSU5jrOLxq2;`m+DG zC24ti-^`=d+jG`W`FpPu`Lv_wu!zKn^z$-3pSrh;PleURXyO|nD-~J{SL&T#u~S;_ znQ70@^si_^d*{`g`#UE-d`Vq-n}?>%FNyzH@%YIPh-`lAa&tH$Ewvk-_onhgxi%=-Gz!{n-;k z2_H+(oZ?b__4vcKCUTAEqw}UJOY}MYR~&y;6`Z|@cWzAVny7@b+h4YwDZ01jqtOm` z)$dOD`NFTd8H{NKSb>oEYz2#rBE`D5kqgL1dNYSl1M{z&+ zXl~vHcHBEiPF7x4K~_;#Nmf}_MOIZ7e_>iqPEKAhXo-}z`LESIDDlVn zI^L7azj^|7ILfT?l{`z#*|Sh#L+W`Vzhdz0g*mQ&bqw*3-QOuyw_3s1yNO-n@3as! zt7Ex&yB9HLR^Zn7J1wLmjW=w1q$BP>aSODKUEsI)BB-?zg;vEe*!Qszhy%`g3jr}7dI(LEK2}|eE?Z?-jJ1c1yn2=pi%t?+!j~@MjpT6z;y|5ZF~Uxy??^l zOe+vt)CGkGmazGK6Ql*G!n(B;@ILf3>^5HuAe;+BJj-Ee)h9@jW5K~yMe|i?o6COkY^lGYtCMv_QbA9`3h(fJb&) z;fO;O@Q9^AeS8+oP926lr+wjekv}*P7a;VcCuD^0hU1O-AeFiwMyU#jIM0L)FP*@m zTnEaVltJun9TYi4fZ&_cz_DQ=2k;ght?cJMIc}d<%ehxhrs?-x=)el;OqHJkU?b z0;iOt;C5mwMA*th(xT@;c5*|6+dYH0LbghgN>m(7`wd#9dj4p<$ehgV~1hk!3GeWa~*6sc0ktVkI?H;3c(XXVBmZX zR*L4q<-_7oR{0Rp7n(zK#}T;D6apJh>;R6G0ysS<4n|~M0e*`@uiITXSY!YP+PA=! zR)4sSu7FnD6wD0@2RY3)xTtm=T3y3ID5nd!K5d3YDw42=kqo;Tu^=%2BJg*AfTN5I z_{iE0s?q$gYl#Q!yR!%!Dqg^=kX#VZ$cMUN4lr;Rh40UPz{fQ&fM?--2-zS52mC#N zd$R(}i(3XeH{FMow~HX7{}JrjAPC2bj6mw_H7K;a0<(}ma1Z_fNsVwgCKnHCS#y9- zWD$HQwt=yWB~X^W9gNHGK$^;1_;ybj9$bur)~$zN>HaqG2xr2fFjM&bQ3W2|d;p&o z$pM}LzF*C7z~~_KbLN1!qbyiiQlQ7$1V<)v!M$Jv#9}|e1BNUd4F3Tf6^meU&1`7; zx)QcLO#!RrQK0;m8;rO1z=DDlP*|G{gZGz#iQ`?UdwBZn+-40W%{g%YU>S3@oPN_adZ1>x~8p*a0C1nD=!xNS0&F-GCx zpbWJ3B>>t(LQUH-h<+su5+9$z?e&`gG7=!&EEb+wae%l~7HntuK=Yh5@VGw@ymsA$ z#ex#hk|PC^ol_9<@ehO)Ou<)=qcHp4Z%}8rLZHhcmMuSA6Aw>s)q{*J4`fYjh55}DaIT;R5|5cAZKv7N3Oo<*%W8L=(IZ zv_ay5571N~2Ps#I;k5NTIQ*p)2AdZ^-X;Qcm*|3pyB*Z!kAcg@GI%I$3-h%5VTJlu za3~1|@u?o@dOredyvyNuj1q9QmjbIX0$h%CK;~2##B#j^Z`)Jgoc;vfx?BhK6JAg@ z{|Jm%-vjZJpCJ299^}AI=y)<0PUWqG&U5x~>O&~ZU9=I-FX029gK@wnz8I3udcf3F z0a)rVp$T8R`Dq)Iu8+;!91(BPNK}XO==y?|azq9TEbD$QamEM4inggh= z)`sOxui^Y}9{9EG2*|AX0A^VMU>qL~=4&cJ!lDHV%iaU;(#x>S^*J2AbO1K&wg+T7 z0KW72VKjR_{H_=SMN$fn1JG?-39WnW!6`BkE-EnLjnH#&uug=}RwV57FM|*20?_KF z4^k^yA!Er+SiMmfc8xSb)7h(V!@Lkof+oRo_HXDs^9VxLD1&PH8gPnj0gLYY;I}~) z%DWO_=iC&CS9lEf8t1`bPJM8EG6=pG(%^#O6_}`1hSe9Jf|uVmxLUm$G?H4NMb830 zpV|aY-?ze_>z`onJ3H`lN`btGXTjpfEs(sh7BurLV3!LQtd_ldI4kO zGSH#~;lhS!@FYUuZpl~Z{OAQGn~Gq=XfKRA;bY(;1&+5RVN2{|$oJ-jl$|Xg+PWVO ze{O|@xTSDc{1P0h5(Do3EYMC80pqLX;Oe&@e7x5Jf37^FjYYyj%Pv?w#0A;7Omp1F z2<|N13E2}5!0*$0xZLUhibQ8zxT^b;z*GC3m7bev?Z13b4No;h z)}N)z=ZrOeUzG6_R*oE2`r{|*sJF;TgVwnCt_3jpXREXawQ2JWGGUxAKYxnu=KKi} z<`#G;MKO{>)fN_akWfvE)BO48|GpmV^;V`DZTAhWEAeCHQ&lK?l&_J?`5~&@p%xgsfs5)ho0w=aa$+r`DvWIV(JJm-!$c0Rzd0C> z+Y_%7-IwURVxstZ1)Hz8qY3|!vU2qTCOYQjZ=0HMh2RgFIBK2EM8BC+7nfy6661O& zBriV1{;tWmZYfYFZq?Sy_uXb9UYF<)#aTzl<@J9u4kj|uynGo6Q_D-_P8-1*wpb>z z=rs;!GIo%aaz_jELz!stXhZJWH!)@Z@Yh~iXj&EuiO`|y1iHtRCc zIgYQHoM#!t<(6|X28fB`?%w?}6?vIZ-RJIdRuyyoQ^QO41`{atiAj|l6AgC>UzOBj z5Ut)FjyjS|w3o{bc|E^Op5H(A`O`Wkl6$~??DM!E@wrWS?D{Ia|Ec545?5~_qnny* ze3vj$77yPEKb0_och=X`KtU$LPkV%1TFD1jr@s4IdVLIW;2oD zsfQhGTZ0I>4~D;8*_deC&w5+6)+mCt_`nmc-vfxlCUP_5b`Uv~bkVN##{fFCV|!)) zwu@xNy-g}Kj0H%5m!5wQl5iQm5j_t^u@YdDYXUOXJDT zy9vjj2Xi z+5EZEg{UE3X5T6sK(d1TD3TRRwAD8bN)!*E%#5=ys-iszQ3>USW@-SPZa4p)zcQAX z+d38hgv8r(+YJ=sqR6z3*FB!(4j_Zuu`QKt!Q|G1)wi0T4In#J{`|5wPtwoix}{{+ z0Q#}Vt4~)jh~&R$Tb}Y5+c!=v;Z%wxnnkmf^)dz!XJV*K&Q&jB>eZEZg6RW@+k8dG z=l#)yxVe7ooCgD_w`}Q-z*mYyj_$6|4fh6+QsDu~Z}M@3{_6Ba$I}K-l1IG5wy)tN zcZ)~v=Q{)Ft`T!{DQ^1>wAhsClQMuF^+<+zngx&|#g&S>w+E25wWVikRV=xFjfJ1; ztpRjksSo46fE97_p5Z%-n*&H}&0dQ6G?sk3+4ta$ydEEuE9=Lbx zIn!xBDYMLxc#<@Lc3x;V-_pV$1LAg0oWr_WeYej6VNdduU0=65)|uzR3bzdek*Chz z(NDlS^qa*h`K3YR8>g+7-?6TL$Gx}WS~P)d+e{AQ@K@e3%59PNB7@n!pInIJ<6P)u zKGhdT$iI|j>&M#S==%qszdI89jH{JLaQwGcKb0xHd4UvMrfx2Zw^vbR^U$-yvE+g( z!}t)qKUT4pZVK4TAk$y1)m?`7my6Yj4?Et)k^L976PxZ1APAjjd3qhZhQ*^-^5Z!=YL2GFkDXzz)&M@fFu*A?1%1L*$5=o&4pi{uXD zwj{s&0kl2xVB^v9Kr(Mmx$98j01~Z=k^G(EMnn(Z_KPVQKrc@6d{(MEMyy@?W$FIc z18D!6eQP*MgGt9uTd#wa1E@i+<4e$rFhW6UTVG-gKA&EY^|dczNZa-5Pi8gX^G$Bx znp|ZxVf|Eg!HuQ?^mO-0mw>|o1T}TW!Wo}mS=S@4y#3@yo*k*DobmY=tWa_B^s;aw zy?%FmQuhFwY^vQB+j;OjcN}d zw%1xo4~a67(=ub#jJNxV?5^2y$7Go3(9(-XH#$2JhohM-A}UN&mtE<{}Oq4cfOKRZlQ$%peua+g&OvJxN zL*zz`A9>aP&EhZi*#E_s4Lf#65`pp`XMH=&MBAO&9`4!{K-g!ojjYDaWWwrMb5`Sq zf4f94g)81n^k%N!=3I#oQvBxFLfs%Hs=u(wp5xgm;s*-rl8$B~w-eWQ_Lce*{)Vr9 z?Mz^z!I=6e!SN`vIXdN5MhedVPp$2^`Y3`Na*CAF&tM`hk^7otaU>zS+hJgN9uvub zzgHY589-dgb5@ZoWuk$=TDf}%LJ18W7aI?Jz3u<9`D>m^B)PEQTKd#Sd_8&_W3)Ns zH1Q$&#`%|BOcYXSulbVGj2Lo@k}MiwB2b+vA;>ztinSV%-oiFr;%KLAWOw3{MFWb~ z`Ho8()zo~pp>YKpJ*QhYpKFHOe@FE6EJ z+q7@5GbloiTehaKSQJ(MUP3IujYO`5;B;U5UvcP{v{ppiNkzCbv&7Hdy2Rh#GU zsoJbkUW3Q9W<*=qzNhl|WEAvbu!bcbHSF)HwOiLLNpizFIy$KQZUfb{>Q20gDb_n? zS!$UyP$lc4?prEgt>Ij(^5Y$~u;se*(}gs@JC%0*9c8$u%=EyIF$8x*)Gq40qf9o& zWIKJp`gx|>Xn#G`7Hv~O=3(s`rCoKop7LDQH0*F4>*Rc)RT}kFu%)h`B?D`QF0-Sj zj;eR=!q5F<-7c%TJg$z4^-b$ouZQ)8=I~PWI?7*X_Ewez*3qKkpL=SlPx((iuHnb} zr@rK|OSM#Eo1$LS$SBsi2~L`|l!jPawblo$uQKCm`fI4yw>=Bh^RX8Axa3Yk4Hde+ zz1a5_*3b8we$%U=6a(I^X3QF>&K%sp;);0B&Z#v4U=gsbt>+WD3Ehw#_Q%l`YnVI;kRVuPKdpMpw`1(tKf7 z)caS|x(Zg+9vQ4%_Yu#DSCqlro9?HVU~R9Gm~gv{idIusdCi7(a*nwiZyBZe^U@3B z@59)BoyWP8rBv;`rR+Qnv@VMBYAK;4rgH916krW?J2&c#on3K{*8XH@W-%q3 zz0m$dEUgWkhj=CeLB5krHhlTtrcuS0+9^gmvy*PHt}>R5}6TlrQMQqMH*NwP`M{EtBkQAo*aR$e}{2y4c6GT(}#5`WjN6k)?! zL*p`+N&&@--{C&mH-zab&!>DULXlRp?cO}wDIeXQoMEydbiM&wLO9@Vp< z;*|GetQlfm#f7=lvt=9k*lu85c;cyU>T@dJ^_f>@DAw@sDO*Gi^^9b$4?Rt5Rh^+z z&!}cOL#2oVw0-cgd3&>|Ry)z?+?`mr-)!5XpG6tg$*0UiSSKGbYEpPgHBShf2-}FY zed*6LB2Or}>^?EsWmt>syQ(|yF_obh(EMu-)=+ok=wt@<)Y+JSU<|+TSmX4zh~{*v zDoMh2_E-7^$bnz`$OqKwI~(_F<8Hf9D5w!~^B%SAYKM1PA+6aJtS_WdwaR7wG8tGi z-aGAeyF;lhNWS{ic>2g}M-X+0t0B^OTJ zlRvY-QEZyqDlHA5^iTGRu3JLuzM@%zp46qs3ukk1(R$q(ZVr2D+2n?C;dY!aLv+9h z^Ef)y>rk}fCat$9X|@C->G<=R0=OOmjG~g;Rk7%8`T+4`J3(p7;oO;i&%qdSgUjvs@n7G;KMyw7g`1H zn*9u2Z`*r+qbSXfD0wvFe0`&{y324MPVW`QS`Ts&`|%bCcttP^shXF8T%Py+kiB ze{`QJ!CJQbt;>cYB(^PxZ`CENca)|!&MHP5)tRLoj#vvQ5XGO0QP_nyPWmcXGtN3I z=arysvMX0_=f--lR!l9n6!|cR1G_)=V!dSD+aqP@))srYl^Iw^AME?7@d~9`*Q0NK zG+#DWxac(sJIT$|u)>=0!(6=QH4@$aAX;K0)*3(Am0!F;wZpLslG(6^jVt=Y%F%M- zq4^9NxTxtR9zXC4#e7Czd`9oC34@MGLW~2=DLdc zFW(|n{jTd}ideT-KW7WCLf=fCWK8B_9W7MIZBdP`H?ks4zjfpMU0Cqwyc*;Ul}S|; z)-|#7p1akcdBp|Ih4EOs2HkLeSA%{Dt-UXLissuDf|Y9#SHjbWV1l*&TBo^JYmtSz zW4D0>){M$_*3VjG&pvvIZ8p}2_BYz?szc(p+xM^U=)%{-h1{~abtpZ%<@D16tnIY~ zPAsWMMeDCP-A}}NN#C&zr|Xf`Y@y*fXRwyd_+`>ikN6h6|5$E`b-T=$0r_{x`I!kD z2z*0?&DNNs>^{V$ETdT<`weN@f_^U*L3qB4n+rR>p`1JKgVKB>iS*;fbHMc*GF)%& z-22;sc*Otd4=d^$nwq!EP}IkpBun?tfy{4+t+_@2(92Nr$ICsdpynHzA8T-Z1@b4< ze{gzGWwbmKe%?GQ`gMjy8U({m0tTyvZA_>KgZaOmj&mEZ$%=1SQ%Z&@j#`x zkdG?~Z%{w0x(lrc^Wr+eV2~cmHuSKVUFgxo^oa5K$Xr zg+R6&oga~X*7GNnymnGyHtgs|q>`gEMTHP$AzMYjwHvv6%^z{3!bpxYW8Ya(-N>q9 z%PkiOAwO}DYap{5@oaBb{RHR8(B*uctm$L50QeH2R0&eWL?0=>wx**aby*Bd5(yqE*By=ciX$(Z>fLws>epk){f}RzP$w67(nvV5cIJIdt|R((BtH(^zN z{|`&%J6cJsJ++GM4B3)*sflIs9VJNw9TH%jA+I^!SO>1(k-AAxjSdT!!xslnu%f=B ziQ=Nmr~h~pCKViYtc>rdL~?jQl(nDK*`ry>? z@5c|@puTpq!P`CoL_<_OPX9hMba5hLw*N^YVfFgeklBYg<}Eo8IdzDXkNEyrm#WOOCOd;$q`|cxY4g7{de87B@B6i^hUk8@XU|kzl=msppWXQb zE2AIzIIJ#R=^IX{=^Yh^nto*VnDyia+XZ4QTDz0Q?8o&I)3Q2nA>w{Kc+29)_1MEw zA{zd_Wb13wk1QG7Zo##@{3Uo1%*9)WStht0;z3;IUDgR={@&1T)^S|#E?_=B`NN8w z%epZiq6ZM$Yu>#Pi~v$w=*}lr#sJC|(Q25D+mdhk3N8a&Z@#$4(*7s=4zdK-#9=9?jSo}=H8N*bQ`(;AD3hi74vbbGw{>kBo=R=7&MZYl?t~YAEYJ8`O zLJ4QXd%s!7naJJq>WWeJeWYf|#Aj9%t|xY#9<4ohn(+D3j?*8v)7|z?Sju*syz%w) z3c&TW#}BKv51e-+7mQSZ}|4e7sZgO^(kynGl<5M^v_J5V-QnQ&66zVAS!fz@5;$>n!G%f#|r{O zXdmyffOm{Y!gjQCk|i^Qn95TJCaLR0Q$hpIe}~Yc9fGc>A%RG+SdH_)AvDnTrDI|$ zg4kd#`kNIsgifB=+#(Ax#4cisAY=}q3v9C(XME#{oEzdlS=B?xDLmqeG8slEq@{hj z*gS}))#78UBX;3o1wY0`achG~UQU0`3xmU`R8w}xPF5h9(Q)<{i+==N7u$Qm4bGFkb`SCSKY|wJ z1^TBllRW{5m$pZ$FE3*sl{}Hq^?TOTN6i05Y zYvq8<5%gKr!`0b0kg$65WQ-ilToxg<~aV9Hj6rGURG|a7$NN}VTbh0u=kvQLw%4R z21n5fvq0Vu-z!Az_7*`97(>b_2i+Vw?1{6YoQpwr3~|e^t*W9flZLJz`&cGpsPQHz z=Lz-@@}9XJlXZLy2_ASe?;Dj!2nu`?g6J_6&CEDo>T`j(sQzXaWR9UiK8lA+Gl?XZ z8mxqxF(j_H{+Go0t7OR8=Upu37~(kG&&ctKBmdv_h=1A{?5EoqIw|@|A%J#dY_SWs zGxVUdDdL!PQaw8Qi8cnqR598RCJA(T8V||1-mcly!p$r6I(OJ_<9mu>sUo&6Ym&YX zgYUS|bb_W_)>M9N10^!v{(m!?0Z zg435ZPz_F*H@*gZqo20Iy#uHH($w?xCA)%mRJ*!(?BV4cGqy18m!|eBiozV;QI!FP z`Z6CDY?-!g$8~$n_%=-M1_jA-yrVYp`t^@&ZJCL4Ax(2>s(;wRFtwhtkJ}kk_E==b zHkqbPG-X^(Xw|EyE;35r8SPp>Lm4!kpsC2`ypHZV>VsrX{3@>vGt_?C?Sqhf7yXOo{ukAl_DfUG>R+1Tb(BZ5nb^ABi}Ppp#g!sH!@TPd znISwwVcKsyrgtAa*yB`74c_%rC=(W*@!O7D5;SvZs{d#rjI)+HxSMmTz^J(Xd2&vX`zYp>OF6%<%h~&#N@4>v4v^BG)+!;H6B%o+h>vvnVb>*7j614+KyW> zG$&{pEp;lWsDg^h36sk-UOHo2NYnT)v|ontn~`$rVDwwG-#BmPxJssJE=@B#gvqt# zRPRgOmILAIXW~3foBoS3rv1`%$LO`)rf;Y(?_P2=)k@6R+E4raifN6AW3umSihJ^- zFO~n#zMA$+({=&&kO!|Q>uqKyqCftdHvJbBnf5y!^3pE;_hr<GYkZ@!$S~CeyaLG>u**^4qPL+GH+j6y!NHlM0M!zcd{b{%Lxt zh~g@76OxTsH{;iS+AmG*Wf~-MUQ(BD&tHxP4|V% zhk0h=JWZSai;7J9rD^*|xtKnR8dkWy$K3tzu>mwq=;r2KEQt^H0*Zh^iH(X9GXYP- zy#IjhxRpn<2}7Ymn<(?WBsIIh&f1D^<;?aJ(sY8R?b|jTyO>A4zHig5r1m#2o2KzS z|3Q;!+gzF^%e~hreoh^-lra+b{+q*2)29ETjA_3#weJkA8^kSKCl%hPT=|>h*wZw= z7gLehXwLF%%4%)^Ej(?KF+=qy7Fbw2-#VrD>u359?ED)WFSQMcv;U^JmTknl}9xO`i5U zP5&CB)~x3lGo#% zU;WM37&M%~P>2z0Z7C9s>c%@}+qC>sE}?0{AU1s?w%*_>Qsmn$pO^eMD+U_oVko51 z6=JKIjLaH`_7u)%+cJ|}xY19unKqSv_vl~>5}V3*Ke_mCS~6%jL7NsPExaFh7oFR% zyXMlFzZ`XxZP*r)lmqon0u``v`e$eq*9Of8Gpj$8Ck0 zO*FOFt=)X(F-p8K*V0!0pDPbdCurJUn>LpJ6g3)uytDW9-*d*Ert!m=CSNhy-II-0 zt+XX(AO0I>4VvcCw6K?_9&(U;|MhzjtNLb+FqrmBQ;l7=dlhm~vtItDJ@eQ8-5I#W zQFDT(_URF?kGw#ghsGrt(Fchf6fH%s&K>UTR`|QU z?YMPQGnb|U;RVK4Wyn*S*cev*Pq~ApO*D0N4-*_LL!Vb)>x`KHPx{hyf~GsP10zCS zAx^CrSAm27l)-75@B<%`YTq*oUZdKLJ@?JG%=x<`X_!aD@s{1Y-=O2HWNWeEIWsYa zX|EUxwTBY@vE}ISYRes3C;p!2?YR9(bAqNdHdY_xD$xDU4~-*~{+1gHX&OIH+iq43 zuBkxJ1kHDqZ2y~n$u!NSslZ^_uj7@dPV#c;iN2wkOv0X~O*D;um$~KfTcnh^Xk3Ez z_XO0K_B(Cc#hPbUg=9xH?L`HPX3{`}rU^gs2DN@oAFD$1BVRZ!T3tR9Fig9}P$={1 z(G~I4NcIz7d0)lynawT44YHa|G_^LeS9PmFF~?LzxdAh+k}4#Xw!aa zdT96lc&ZlVJ1PyKs9icI^Z>GOHVbQad3-aOsBw&1^oj&|JWs~JCuX>^PCU_l)k z<=C-p^4i~$Lm^FbX=-2pd1zrhdQPmaKily)0h4LkL{otR4=3k(^!oHr=3AHfGkb$U z(+Qf&Dx5Ygsz-bBXFoIFykv&jPy79aX?MfgtflYJ_4epK<*!^bRAbsNO<%heMYz2~ z?(dzrt3+k;r}IdWX}>g;6}r*><{e7;z_UC=M0$q8v|pMgtIm0}rUC7Gb>O^ff%Hts z?ceC_{f%kOYqN2$2E^2`9q3R0TXZO-X)dOvg7&*UHM2hQrtKT4uQr3z>vu+Zv~uJ1mAgt-c{r4##IysrC+RC{+wwv4+JF*7 zsBTRA8d5|Z9M~-;^Ry8CeE8T|Y#okI=Gy@~?$6ZC%DRK&pK+bG#v?y2pq&bRCp&M; zO{{-*Khk1rrvi`G$oDy5t--5Y`lpThd3FA!$+cLw=PYZQYNd{{SEtx?;aXtyE51jp z7V5dl>fM|Vu}+?vugcLvt#Vn+-|vQX`zCoO-j9^hPu^YJYFKAV?Uq^8Otq#v^qa6@ zZU1w(@9HM1#!El)c{#2}MZ0Ah$$p>)Ocb)zE@9n%Zz4pqkviOv@u`BY!9_34mb7?J zndN>pJ}8B?K;o^n&J9%Rhn4f$e&O0fvK_yp|2sA7A{ZJT9haKzCvTk#tfKthE~w_n!kV#V|LginYDb0l#>@*?CoeRdwV{IQG>Y55W-r$5 zCNe9$-%yVi1#PiZ#2Ofg9pB+r%~1cM%S&_4WoZg!RO{`xQv)5iLY@! z=TtMP2o0}4>X+#Cv$-xFzfbvZE-hbwlGdk9E}c!G_AH za5VRvC_q|0MkliQu(tQzQP=Vk?RTHo@+1Xcmy_FOJ@6<&J1gFIM)W**~MySaqI`$*?duI^V(dPoM zMSehWxz^9F5LnxPwqU#4gw_a@u{cL@)>TvQvZSCH$A96HHR)KlCqEjl`G~?sg zBzGYX$#ve}xIBr}o?Y_L-i313{Mhi7YadznB&(k#+>K=3+*zf|9zYiVo|FQ7nG#MI zzBM|2jM(8EKF&(&MpKszxXm~s$%Bh<$B_1Jq~rCfV1y%-)DfaOSi(Js5Sgc;IC+|^ zP1;k*vg<*%hm$!OEDzF2FK;s><2ztJeI+G7U5PtVxm~PJJ&0X5T(1e=am<*KZe$7f zBA-WZjV$<%ke65-Z7jQ9PO1A_!W1u2NR2ZNJqe>biNNMi&wF^lQDWf*g<3fweMqc%P>NS52=k- zV2__19Otuk=k+1^3j0MN8Q)R4s>AIc5Kekmj2(sc0W|8d=#nTSimW^*=?NnENo2EA zCz{Sjkvuwl8^E54T3Di@;HE(;G=z7vl9(v-?$qa&<?BCOH^c_4WZ#r&#gv~^gKmB?-xv=5?% zgQc7belg@&UdB(B@DM7>V;p=j8AhhZ7*QzUhw~wM2(iW%bMnA7 zLVxS)rO-Zvl5Ly6{_=|^UH1Or2izv2_3N5p&C~@l=0g&W|1k2ajXNXpC!A>hmMRX( z_>Mx@UbmC%uB4i}csuLUFq%kx(yZp4Ncau5jI)GCkhy7#i^9-3!ai*GAj@t9eV2P{ zXgTW!S@~$5G$fCp(<$}ay9V6}T)4il;@mZY6eiVtD3H7g^?@99evRAT%$54y; z>3|RZSBc|KgQr+_W4O*%BiF?pOy-TR9A+hrA$G45hmyvFiSvrTzq3A#A@v?v>1aj_ zsUWQ+FD(mj5;$<5V>TUNW1ojVK*oc=7zeZTfNd65xOXJ}51x3D02}O|`QG_i;J~f- zvk&mF;l<#H?N$LC1UCASLnmy9)lT5ReVpO&%qr^ve|ZoD%mIID5WsBaONiFAF$cKJ zbR0g8yF~;0d{+VdB|_Zg2{`C(&L?=-`03B4^YDuP&sWDZw`ytY=>CtM`3f0GdCG#d zEdCe?Ui%h1{&u8;%K;Gud8z5o$~)sY&f?&lJ%@{%hnH{eJbr=sf(sTdS}e3=>9XZ3 zR<05jS-ocMy7e1G#l$5fH%dv%{7>+kHWORak;aS{r$dL2I3GRca@^JJ#K}|c9;ZFe zoVD3wyVuVCzqyz9IUirfc|ZSvz@Xp@A)#U65s^{RF&AUwE?vHIH9jHn+VvYr$v1D^ zPPvnscK6=>2M^O9Wn?~n@-!>^S+0V% zyl?!_)cmofwe3^;=Z-I5JHK^x_w;`6>mOha4h@ftj{O+_IWhU`_tYO2P8vFS*ytbq z5C3Tn#_;gkbKo~Avf&>){?XI_SJTV?{iFH6^?jU7Zg><1PBuFntOX8R4{Hfz1MBT* zH%%9LcsbeV0lUQWop^>pe-D#^SDPOA{_pU20nhZn{=WX#9U3 z*f?gfbK+k4aXh>UoTfSk#@ag8I@&sBTADj_G_~|~w6^Q)+Nz^#f{~sv{;#KFy~9w` zz}U=OM^jr{(_C|_rkReRjuxIV)X_E9*TDhk>X@3+($GxDTFcNv8*_6Vqa7xunx?x9 z%#1Az4b3zyb#|B-7@O;unqgZ59W!GSa~c?I$0j;E%uO|o%``DEG1fGYC}8n;Pip;nj@wbd2%D9B+j=-U3Za17kfyldU>> z2F7?f9XTCiEgdrpV}qR*I;Oj<49xXSEO7WZmUvS%&1s{pI_6e7Iyg@H*x1y>Vh7$I z_z!QIriGz7p4ZVbF*L+Jv`n#uj+TWf4%FNZ?++aVBO@Jc13a@0Z=actskxD+xt2cO zXU3X_yLRjBFxAwq<8B-AcK(@=tv!q)Nf!)-~Iy} zse>Ft`VE3u0ex9SEcuSEefsuy^k7VD~MG$#C@TpP~qCK!3yx#?rtNSnUs1 z2c)HU>ywt20&{wg)Rb=hyY=nUub+qQ{wZlm{rWr7G7te+Nl)(KNJ&M&9w|)jN#MO( zQje6ra6qE_3}Og<`>)zPhY z-#&vN`L2BipqO+=7W7FU&<{8R9o+^DNE?`v1mfNM4C*>0B^`tvsi{LU`lh6z*bv9F zt9AN!IF4|HeHIqmrAR#zL_(k%5F2SsB1tSh9U4#CkPf6Xagc5#iKL@#A4syGhM_;m zA>&8^dWokMHH*w4&ywfJ60)4EAgjn4QbsDsM)C>S0`)ohlI$S6$pP{sIZl2iC&};R z61hTdK;0qt2v4=tm)3_0rD4=c?X($fN!!uxG>!H_!;(dxprdFmokXY88FUUUr7zIs zbQN7g-=G!rAM^wIDgBIoL3hzT^c(sEJxYI}ztfBKilSUp!|6F+t{xZ4S-D6qhKu9k zxwc#ft{a!arE&wfEN&#!7_NXT;-+)6xw+hYZXvgbTgt8Ah~tAbI{gYN%c3Bm4U}YF zl4P0JaFQqpyiSlMtpKxDE66-22$EjnWKq;gIzi%i83u$Bb&@CxqKtqr@G=4!v;qh~ z*MO2F14%225-(_dI8Klxjp%2PwIBxyf`EosBMDkjBk&9hlsPRg6UV{Vb^3-Ed3WQP zsOo=t?C4|JbDJlQIv^&OFx#aEzKP6AE+P7A#F5bNN{b$3W0Dzt9E_xU$~tFUzow%3@=%5ZPOwgI-}L}t6=4yh1@ z{jS?y!aVWRdU#S3Na{RxC0<-J0BZL@O=9-@uq-Zv#R`iPmNl@nNrZ*$Tit=dK@3sY za|4`~e0bJ)9^}{<2KP)8=WM*Ry2F9Lms3MmZl;E=TuY5m%9YfJOSz63V;WJ4-K~63_M3ecjh0 zd#>BP=)T6)b3Lf%+R3@@>kK{D6w)Kn`FGAQvzeL$Ay0Q24v1q$>@7Nx4q{we;pNN@{`s z`A|xZ0Gj$z@{}JXD_h{!BA6HUpd_a?C3VdxS=Nz~G4LylrDSO)!X;8-z$GmfxHW^@ zN#HJQPsyJ~N)Ev9EOc2QNGpFhPi~ zJM>4Ss{*-UhG{`1O-0>6}2xRq$H_I1hNe4QT2pIgM~Hf_@jcB_LhvTTrqB zGN|l8Ne_f?2wALzT;2tKH{f&xe?gG{c+eaHC;^R9xE%$ZVBl&1`Dv6SA}wiJN?wBO zGvT)k_L*?EAg_K!Jh$N17GbX-k2fQp+la&IgLuLFHXIO}*$Zg`Ux$&l{h;w`PfFGy z+yvm}fQCPKcmaI=iS+#fSOhJ8t?{3gC~R=27b?ihn>J*0>8cBIj~kEJ{YBEd2>Ryua#3F*@S#_K4RPj&dgDJ%-s#CKK~;9Qub&KM#JqAZgrv zC2Sl%ijDrB4G}aNtyZVg>kS4UAEVLN*JSeZ^Y;%3s8_Fk{lLJWpy1$;5VJWnG%Tz^ zgYa;R#cH+L8a8aysBz;aO(G)f_NGlEBcr04HH(gpiHVJE-W)fm;c7J8f`;qOaFZFX zC~M!|l*lFGePg3eAJBOKC8xl@2z|>FD8n#2=Dx4f*XIaNMBFSUD}z1dg;>%MM;ZJI zx;sV`=G?lM{YXGHtF;f`cCL;F^%9Lb10oN50d10>QBGHLsRg4 zld_#U9NQ`W9FB+xhog~x4p8*U0|QUNIWj^+ScX8MuV(-!3>*Bzt%)$hAu-~?lSup% z6HUxLk-(r177|GYri}%Xn32dRz*G6h&_OU0NA(9fy)My|kqAOlWt*k7>ZE^&orEgd*#H&LSTqARh#>lnsI6Y9+^MW=TaI)1X||z_OXnM6FQh5UUb0 z6}6Gs)dUjq0_bW@HqQu)eAmE(&t?b+nT|5S1ry@yj&}|44W^dvkq{xT2YB$+vj3;q zZFh4z9P#^3Y~~)Ip>slbQ?@z~WF+7Agill>xPe-W_#B%)!QjjgrV{gn9^_`;UCqdB z5c4znsvAiWekH`~5AoI@aNy{AZcf~3ONV#^Fzj2l8I)gYPD#D&&NVhV4j-Jwo1OC3U4kkK`;L~KVe>m$Ji zf`S^knz<9*JeO_J>3b|s@p!7UZ8rnmcB>lcf%q3MhG(NOXow=W>@)B<9|CCv%ra)VgzRmMV!8>IHg7j>D@7CjOCe?+VD4dRzjwYH!Kx(&SvaGCoXtyI546vr4; zNgs%#C*tsgp}_q-3cZ#;dR5T&&`SpX6YzTt8O7pf_#c4wcc6C$?yCULpx%!F4*>|P z0bR!9BJji7&aa@SYS1$xP8agiLxZB!QSf>O!e;G42;}P6^o36ETMwAAD1+h9+d+4n zhU+`%ywG;L|Cr%_@+cwyCjtI{!2M5iy^s5Q>`Z4RUaRx5qHuks)At#d!i(&+FcWe; zhEdeG6pdnUgM482{B&Na>fa7QyN3Lmm?s^aL1wYzlKdGYWoYt?ou>67Er>o+nFgMqD=3<2Z21fs zP5h@XYd81#!Tp+q&qCkWYF-IxNH9AzGQv*k#S}k@&Y<`$dxyxG-EH%`B*t-F--!r* z-ZZsL|LmJj%&A|$S*w0Ni-xxj=r$wnIVr4Y>f1fF^`@=rku)zoYedtR;>4-5p5e`X zry8v-UVU;!Bg^!)ll*ct%SxIH;mITA7h>iv&dh$}<&@9{qoWrLp6%PRaVK4qsW~|V zheQo1@6~_M(y1%`m-imF?v<&AwA7)cFZRiJw{t_s!l&`ks9idJYk}Zm)$x;%6X-qI zM}&M>ro4dVkk&Y!iyZ1q7LoaQqbDRRjd^t-&57ej1dT{yR=#2?HbdBL*0ZK9V~B{?Bz0!z3+;ahiYjQe5C4rV9%R z3;jR)gg+h~lUql(@|Zljy61}7R4m7zdm&m6ws+mJ-{=L z(0LCb5n&x}_FxE%3_)Z-g7x0*y1V!5?%pBJ8@KE3AX41F$HIcB%XPoD?#{j1+jXuw zFr++40+_p3TgOu4av}mmTz3yY5N1ice*?@QF2umNM~dreozA+ONG<3gu~4_~)Pe-) z->YRwLTt6pyI}kFt(&)9_wEpuH1-1u(ok1>kI4%bhz$G#pff>$M@2{w5K;yQ#xvYl z+#tY&&JYxxnPEgu5XJ~(&lqAw=2guna=FGNnIs#=?9WQyNrCcFjwvm*5szY+$4Qc? z`gr>{Q4=k|jCe;nfePFxeiZ+bTQwzxM4)+?uO{3$ZY6i0i{xMAzvP<;?S&1(S3(Oh z>Az4zfU{jZ0`(K!EH~76Y;ton$8&O8U`QalAO2J}>kamy7*HPAcV}7oHc`|8m%Bd6)Q;a7Scnl3Ues zwcOLjs zO2oH+_;;jaX}Z)(s*pBF!=?A6T~be}xAeXAqtst&CtZ}Q z(?Da^=rr%k&&tT{MZBZW@QCPX10#()85S$XzwPG>7CAO?OQXO|m9cW7Gs` z>S=mwtQtQ}x+Y8Wgl4GbNzHIgU(Hqdx;#viq8X_fr5UXmqsh^X)#Pb%HAR|yO@U^j zW|F2*Q>>Y+LEo(5HG*cU<{8a=%}UK0O??_jgXmS%zu&N)_6MLH-9b)3?+E=bwCY%J zpaCQjD`^b#1XkRRkfZoNfmJt)TW4klcIAHuMm*6Z1|gecl-Gi^B&|qmW#z9eE}d_W z(M$kgG462xyB_G5Ni||bLrT?u4fO9}Q|W1_H+pr%LhqAf*f25{Yl!2?QzVZ}z;2KN zw38FbB+{2?ks4pB$GYJcSUKEAz9LolAB6uQWEc3^ga1x+59vZffTyPJfLD6|DSaK$ zUM*HqWm9AuPutK4+T(#doCG61q>aheBa;r~S27q}XAvJ-L!QL{&MuzBju1=P@IkcM4I!yeKT5}Hdikkk&e27!=eM|XM-c$E`t zVSdJXKg$na`WAGxtg9(ua+?fkJ+#eGc+sK8VD@74%8S{qF9S_090$-c%LFgEKO=je#L8w7|%>zo|Y}h z-KMPqmzX@G*TK0H7=(6o$IwxcflAlpwPo{`Lo7d7oKvvBVhqx;j+8^5ftX}xIq!st zC}mbnZA+xDwNg%43)>hq-Wy$(iz;UwQM*`reu6y&5=e7P!8bWUgc8#pN#la6XRrtBBemL{2O_dzqj-}%=sOV zQa2@aIVjC-u@|Hz?G34Pqz&Bh+0cuqF;Bss6UM1`IUR#_tAk9E7nIVu9I;(c{MGUl@jot4 zJMguSi=bD7)`>}q$^JT#s7#rpLP#h$>8P|=S{khMrY{4BwIhtHD6i$|0!n8$1&85X z2bDyHW41GB2U;iAc3eQ57qAa#HA?du*tPT^@LFOEU~9LWbY9c*DmhC=;B7)pcYxRO zF$uA@1m~^kY_yZpk!NaKJPTZ`AcvLG_$+d72lyI;2`6vmiKPOP0GXw?j46m5HjV}@~6N4pO5xDpC?7%)e5A_&~Y`l%EZz*+!VFsXfSU`6Qc?0piiG13D z@%|1>X|h=BfuBIQc`E~rAH>FJ{kwE8MJZmWloBSX7XWGqwQlwV7xR?1V*yO6PhXT` zX7a#8`F3>IXy(I48^!Jrtj|&DskoDoduq$dFlPYM)9$K$;Sx6&ES*g9tlbNOV!Inv z3aoUqu#F*~NBbN#mi}Y_-h*~Lh|?1{8!NHCRi(59eGa2B1(-b_q=vb8fVC&f(Nd0d zQyQvBel<8+gII&`AA)pxsH>wYHqKMkpBPm!8L)9KbD4o{);ov;z!XQT6&<5D+DLKZ z+&&HI4a`>cBsa-+U~$+<$-tjJOy@vyo7m|cfXa~(;{ktago{5t36B1N{wq0$HykGU z#z?0`Z^IT!C&Q$2v>UC`4Dt)?o$&t)sRPX=_`eAm?E_Ey@If>Q|AE*G`U+`^R<0*@ zeEMV0=RN>a{*Oq0AZo!k*a;c{j_wdYdPy+{P}P)!cbI+fe}i;UIBH5iB~28UK*(sH zTSkrPDRA@+_7gDNQwmps`2QX144JLCtV5Y^Kz|`8@s`^=k%AS- zUmwVCKW2rR0wfYkza~r_!rLzcrcOg0hfTo$F>KW62x>Dxm%&V!eF4enfmmG%LP>5x zSCKxTJP&UoOHsm2aKXkbv94*2^LkPa2 z6Nwy~3?DT|i$IITISa8otE4Ll_zV^Rx&zdj)DgA3J3=+aGB&=hfw;OO#LI{$@Dcej zUIWoLvihYZKMl#@qs7)+3c60M#C- z)SV!-KFp7$YYFrvN^HJJ7j>tL<>L$`^#``}g=gbm@wc`lv1WZ?>+qk7Dnu{Qo<@QICatl;*$ldlauLUGRVk zYZ3o@zpnJ{$FLs_`|tc7#rs$0&oRUE|J47Q1lX#?0@U?t(vUW!DPktqT0BX6NSRzC zm}Qd5rHJWVCib{j&~Qn`j1U`eC+V-W3)i3Y$IkEOlu3F1I*6E@my5_;hTz*^Fa zrb%nb0$PN3hJhkW(Lm7~J(VBcD?bN>6xvfs)nW8pO=&x>9e18CC2>NW&{=Q@>D&f^ zrLjNl#(88ipDd)UxnTIzlJ%7RKc0$4)Ehs$`2)iD!U16o-NpZiGYu8=M|y$g(B4w1 z*D&9sHnY5M#7>U%i%w~mTT6_91yC73R*23S8{$N?2N947V$B} z#QAUr&JX#26Z!uxeTP0r^;|UiLsnm|d8O+uJ(*l@sRd-kVhAD{%5!_&am*$u++LS&f;6tquoi zU+y5xXd6KD1o;$s(p~DNv^+9d0UM7tB8k2UPMHL1z1DxTU>3rJ20|lJ2Rw#bL9@i) z_|Dv9x)vkbHMEoTBMlUrayvlP0;;v5inmSphP+B&cBkkR@s=XS70PF%W_#wMQ60>&(ei_sE{f`ac03=&VPnh|A4^q|A6o<-$|;Vlacdt$YOqjTXHet z=g1Ex&mLSiX(d@nHV86Ch8u)eq}A@0pLnhD+k_WTPa`0IB5_hnGM{HTuX3sW&w@?O zc^;gY1=geR?i#X&_a=XD>3Nv*TXC+d9`^ytN`#OphH+tBXX$nRSt^rp?v#xah@|om zDkN~*kelA+B$3aM;se4t$ZHlo%~#M@@Rs7FUkWkY?`XYe(lYp~v8y!KA>Cgo|o9VSY9U?1* zRtgToYk?Mb6aR|vvJ?n7f!66yI*9&2KH%GPExBsp5Zca*G=Y|*#px_HT`L!sr0^Ayk~?MP>w{s3l-m|>|H98Gr;9L%G69@D7}Oy12N-eZL){ojkZ4i=B1~v%sXkAoUO!e9ZWpbCvX}VKX@w}I0GG?Is23^Hv9YaqolP zSbiKop3mf#0=i1QFbB-Wm>-MRptpo9qNL-Pq0gf~WB+?^(wl6gLuh+t^*DiSqMu`% zTnAuQV736WZtgTb?Nt^PG=b}kcUe^yOTD%mZz^g08uaF(2mZ`!Inz@ePMCh|bwrrS zwL>p<6FnxA**UL0Q6~2`Qjv=H)ifyYGW!`4`vbG1>3FBEM}9+l`4`^x`j9Jh9nQA? zMIxjK;-V$|I@$+oSL^5s%oP+W^2y|KFgtOIXCsO|UelAw*~v>#8Bk9WyVrWtlfK5U z=5g48+@jN@%lx~13I7b@n~pW7Os*Pv`8#yh=e&dY2b0%yr*fHCnRGz)h4P+mM(;A& z%^yViqX4^DX~8;6Cbt}=^EKLo%ce)@WY~|<_2eH=nHWEv;y2=?@DQkOP?_8S%)h-1 zl_dlqlph_4IW)BT+-W!1RIW>5TMGekbpTK6Zk%O<0W?kZ|D{u?II8D@*L8^dP9#P;4XN zOz{p4;hJ-=3ETNPgosrnKSmJ97$HZP#peibA)Pz;T=-2FpQ0?Cso2w0h;z?LVjFQ5 ziFc=SI-Q4Jyg*za_LV*opAx5v2T6Bsi`WqU*NMP|h(6+2(I6VdF_M*5V!pHoXCSL^ zVx}v-O1jaTWS#i1Z*9)S(P4Bry-DK~j@}dnvO!!ewxN}x5xsqD@eTS6Jx^|nDo5Ld zB5^dmB_?yTNU@uvaZ(Wa+ZbuGcvEOD#Y#uWa_m7lin+vtw25?uG?K0flf)0{Zmiav z#v0J4;OHxIhHfWkF(HClA7_ymcrfjBv z59TC`g!?FCE&_lpnBkjs13&oh(n?j}wjWL^Cf^=Dg*=%1WNmI&25Z#J-+I;DCvJ&aq=^mG| zd^8I?glNhxJz?r$J~G=xo{Fkp2k!5Fx5-4luM+BENzHUqxlO33`gy2Q(c|~fg^JsR zM{Rcz>QVKfVsEDW|2w}skA-`b#=rA>6pxCP4|-C|GysiBx9%GL!Wk1Tes~MH^$lrm@eV6 zSpC3ek!;d^@m=v9>_~hI`zz;)Ys8h}tKxa_f_P3`AXST(fxAlFD;~#g#fLHcpD-O1 zkBLXIr}w0ISUe=|6TcC^759rlVm-0fBbPVw%{`t)_^6pf*pM2g=O-H3gWfQCb3gKk){_`YCV& zl$c6XQo(F36dQ9Jf)ublz6?84%9LIdUld;kCs$y12``H;VR!IrVmEQDG*%j|s8!M` z?1U{sZBSK_)JklH6x(1gdQ7?ns{JwP&3MetB;5Y7ketd_jgRyw&LI750svJ;9vS6RoM^R9;G;28Y+GY zpRHnj{Oc7Q+^16VzFvPr`xQ2vZbN6f-L0x%R@E+aqTfL6h1w1E9n|+w2eEO4sUNX~ zcO09ryeYOAc@l~(`u*Wne`0v1s^9Q3c1BVAvHbdQ+x=h4V&)|*pk9WmQkG9uehy&! z+W~AXV;AY%zy`Bx7}#H9YnK?}F-7SuY(==GECk=iKt#YGiGQH>VK`L-rNzq;Q=Yy^ zqD<+rok*s27)V@z`c+W|%G6y9Kx(1bawk(&7+BmPMhu3Ts=_8gRWTm@F+t~10XQOI z!m!&9uVXsADyr%P7R=d_`Qz$exz&FV>T&EqY$G=yrf_0Wak zKrr|V!=UXH7I!Z~HNZejm1Q^vWU7h%vu)E=#EPXArWz@#38olqPqXD@qN^ zrd%IVslsN(M{TM(8XIJqL&Z@as8)(%_LbyTDGXYvGuye z(8Hefi2dU~|85V2KTahoxGYQ~x1I6J>@jY7j25%Ea@(1iLAyun3Uo>|3lyE7KPWIZDl(?2y|EQ--)DM#z&Fvj90M>KFcj!Z0XWgE*ZU{D;BqQC>{q#(Clx;;+tx+~g;5HKa9OyWlSq1k9+{S^|M~c@4cGDn-4FEEE0crfw zpq|sV1pD~~uyf-;&K?A$PIwcW0(!{vg0$gr3 z40~bldBRUcVllz(H1jNhp@w!e|NY>-v(6pj)secbQ&r{Xzc1u{<;y+;#3YcaQ7l zO6G^~BQcXal%L9VBPTfnpX3_LC2=RXr}^!8s|@A4^ZqUiWq=m^f2;ZqZ=2t8+xVaO zzxd1iHU0+QNQe@;;|((f@75EAVqvQAv@lzEN%&3pL%1p27VZf;v9TB-wikzrPm06E zX&5sv5*K5v`J%W2?-Xmra&e>h5ylpu<9%X3-a@X6H^tlHU9nF5*Sx=mM!B(TAerTQ zpZ}9zjx;WH9pRIO0s*arJGE>Dx*d1VmFs%i#bcf|%cbWIIp?}=)7$iC*Hf-{$cG%> zsWB`4JZIulsXw2>&lDW|VIi1*i-hrK1TAmHTl_P07k7^G!hT*7Itr)w5yDtkgzzzU zTsS5CE*ulgVhdMiS9{DH^uqqAQFI#JNzc_T1YLW?LwL8FU3pkr39%~>?C!O5+z=$5*<>o)Lj}y zlcW@>my{}{N$Julyo=R{7K|7Jr64H;_!f!1wRMyRN>{|IqF!ns#Yk4X?ZireywqA6iqZQp z=}Bp%G+CO2QFbwAf>ufIN>im#7=6!>a;1gROz8z_uJn=gi8MkQFMT0xlQv0J(q8Es z>04>Dv{m|C`cnE-+9G`>eI@OXc1wGtebRpE0N%u=N%N&e(ofPUX@Zn5y)S(r9hMGC z|B&uWA4^B1O4Ny%3Zmd&z7w#qj7kaR-&PzsQZvPt%n{pI?yPS(ryaBPClE&A}fT1QuQJ8oX~gAM_k)r^?pv0&}Sy@csG*$kijnbV`{)tcT*agR$C8i_*aXf2gX4Gd^# zd;ms(FTe!w1NZ~jqc$Og1i)SofD4OAARq`33ai5 zk3AJPYT$N^v%M*~3wQx-=a4SA8w_p*zi2u=xm366hJvq=UXo%B&20a^c}p$-xi;qTk_$b3^yFySpZgm z4Zxb;hR_=U8UvaD(2Zfr9=HDiA^}l=W`Jlw3?LTJ91sU+0cZ(m1!xV32ebjS1+)XS z2PlIh$BA$B%C$;0&~xa6hcSicf&HHhitm5$CpQz-?kN(YW6yJD)=fE1@6+oiw|N3r zpJ8D0%abb9Ou!{M`IL-=Js0Lrpg#wwnT#vg2BJyB$4_yF8%N67sdRP(oE;@+=eAu6 z?P%p`mDbE)?AO;A6lAd^_e#$kI8=&{@0i#D|a+PynU~n+I(pe7F zD!LZB4|6cPJ$&6B=D_;Fl8h_431+7fnqSvCO$bH8O!VO)9OS+P_Ryi z+q-;y@fRAZ|5uk1fY}V?8J>|bE+apm{n#@UkTW_XBP%x}V{8U0@OOIs0xq-O2q|pT zNVC}=y5Gol<^br%kw&xS2^jK5hM4O^Pam0JZUFtMk%8u5lR-~?e1c6udRs%EY)zo$m2P zCh*zgbPrYfy6p?xTL?!qyLQVFo5dD zsKPGqa#Kq}_;GLvr}H&-lEe4lR$(9Aak>t#^bz0}z!{8d74tCs=YaFUpcRi$kqV7S zE|TU5j0eVJI6ohpw0Cng4(Gjx2?rI@}Ns4&%-v|+Sy*?u_$+ji$i7l|xHy$sI zDnyf9J{OXDOObb5n6hxcT0C?a;gaclTpIRcCnHR*As(r|q)2%`ULI2+>BaC#qqzuU zCr4;Q{6~{Ld>(Jt=TSR+?3nlO0lZ?QWDj=lMc^c11ojR`fWw>A&gX#(J8sU*gDHya zk)rV*4VUrQ0lo;PUNBz*7pdSU4P1c=UY?n|Op`(J9Q;e&Ar`~6mtu|rSGnLX4LE}^ zU6g=yr!x9*XJOb{JWJ7dxTb=eXwYEXrXp0df@!C@Zp?Cp;&|}OD5&(4VP@R4h2KL? zl8K7TQV<2nsN68KhZ|N-zgOt(K}@A^w*#1ODP@#Ks{9n$@sVe9L9wQhslreP^nPt1xzMImFHwPrd?z*Ui?UWsy~y}9^fQ% zGLLB&Mb_gL3F&ArAcvWES00WkyQei)hg5+6AEk~(bESPCD!en8kZ8b_nuGkA{ zhyvv(C6z3uGW%Yrzdvyl zxch*Upwxvud^sr=k$ml8>j%XC?M{6r(+5Ia-W8HSy?q24Ek}TjXo>B@w8<(-pr6(ub<1 zb#xH_9?la-<5a|AngwoG1G1F%IZ0{j6BK)UvL0nO68YzW=U{H4^f;x^v%q%>cFGL{ z&#Vt)6cR9i&qrIo1aW^xwvr*B&3sg-gS8}31f7VQ7_7Axk4Ykm`^a58^cP2tl1-#QkHH5cT>sbc(iNrm}1ZPr#$zR9*;gi z!B~l1XT>P9tVBGlYpk_tfmVf$C(wG6b0&6P>wi8bW;Ej!%IJ9@-Ey%9ImfHA$i+CQ zJzB~H=n2?)m4(UPEQ~sm`CKlUoWpkJY+;vOy9dPu5NOje{CkHx4x-1p!k8JM!nT=S4Si&27dG16pX zEnh5Nl=FRnvq!p#QI;>w1=zJ$c$Rlj=)W$Z7g~za%Sv0h$i^wGgbcvQb0x9!X*3T! zyazu6`m>erVdY~yMwY!0%Sx1;TyV~4^-@L#tlv8~A>Xi)oSSb?E=PUYCoaaWykv4B zyjEB(qd!I|+ebL`VOhx06Z~Rgma@>-Cy-iQA1;MY!u;MDyn8Vd8-p`=4*k)5)Q~f{ z|6RdQTrS`ph54-qj$&5Avha_>d_EFyAx!|g$bV}ZM$#n6kY^G_$)}}?&df~ZqAqyM zOlP)Il>bsFhHd9^QL^ml7gmd_4Yl+H&N1t`5DOhERrB#X;8D8+bV2D z8K{*L$cez!mTf^Cr!{_M*N4g;tj*XH#rjHin-ROoh%FJH!eZ{ZO`uElpw-ozfZa_shckG@Yoq#x5yFsHtievWzd zZP??s6JwFxs5N`BPktZH$Lz;GYniMz~Ib60R{cp+cJ7xPp28T`}yEdE)30bj~5Wd3p_<0H&#Ni!_=>|#UPCm)G-aB%G=Jh_IJ{QSCTKfoJ8C;=J8K=< z6m6O|U7MlJ)DG5;)Q;AU(H3eaYA0!@Yv*X^YUgX0YFB70wI6CXYroKbsokdCuHB>k zR{NdyfcBX77wswS@7lB48tpah4Q;KqPD^#X4%3Ia09}->r7m9APS;V_S?ACt>w4=l zbwhN+bYpadx=FfX-5lLK-F)3r-3nc$?nB)tx-WEJ>bB{&>-Olr(S57?PIpjuTKBu| ztgc3PRfi)&dQtDI570;HTk7NW9rc~`o%LPxiF$`VS>IcqrXQ#uq<=y`Tt8AjMqj9( zsGp>ttDmQzuV12Hu76d(M!#17w!T8YQNK-JrQfdKt>35rPJd8;On+8iqra}dtFP5l zgJ>`s{0&itcta;cXM@Ag!_dnxz%bY_!Z6Y>#xTt=+c4KK->}56+)!!w#PEgTOT#w9 zSB5IXZo?kKKEr;)5yMHtX~XY^vxch%&PVhy`uO{F^6Bj3@af^x%O}kz-Di-`2%nKY zV|)sIru)qBnd>v(XSL5qK41D&`Rw!g-scCOqdv!duKC>Xp~e=*md4h`F2+P-4`Z^i zk1@lTWt?Mt)>vv>WPHK+lJQmJo5pvI<;F_mKa3w3KQw-9{M`7ZvC6pHxYu~l_=^#Z zzOUdL;9JkPzHhLv+1KhD<=fgf-nWf!TivV5FEKSU#h6-{TAJFK+M7C35K8K!}z z!KN(JFjKZ^q$$UgYsxoGG!>iXn&z42n@UYDn3kB9nwFVXn_f4qHIwxHcX zdxE|RIvjK?=y=f2LB9n38gx3SCWr(}!E&%Z*cfaIwgz_#?jBqcygs-xcvCP5v4%7X zv4=#4#D%mBX%*5wBr&9WNRN<=kiw9XkWC@mL-vLo3^@^U65B$9%pvA*v(-GnJkUJI zJlH(KoNXRy9%arkPcTn5&o-Bsmzm4V<>m@A2^B)c(AJ^xp>0Atgmw(=6xubkTWC^f zMrc-OcIZ=~MWH33b3^Baz8U&f=(^B~(8|yaq3?xm4BZsEHS}!gxzO{W7egH+ZcHQb6%KUkYuBdt-^=GHiCduxKV zgSCs*VeM{Bvi7hhTl-r3S^HZDSch9jSjSrntrM+9)*05B)>+os))MO?>kHN;)@9Z* z>p|XIQ7(wl0?c?m@?N8bB>=W$y z_5ypMeWHDmeX_mCUTmLYf5~2D-(}xz-(x>(KW9I0zhJ*;|I2>Ke%W4azhbYkvkrV- z<6Fi1mpz#sROV{NewV;jXbj%^Yf z5o?ca8XFlK727;EE;b>yB=)sf(!2&2z)&1V#^ya-5UPpO#_8hpafUdbIAfe|oGH#P z&OfeRT>ZGfxS+V;xR5w=TxeWaT!XmqIBT3OE-Efo`7BbKxOQHFUxW%bJf%a*-!@SQvFINu@f8rJ!&Gp)0(i&~esE`MFYy25pn z*A=aswQlyhl67yaD_eJVUCp{{7(4})Tgw}k$CbA%FD#!}KD~Ta`NHz&%AYS^R$f+q zr@XejuAHmjE2Ij!LSJF1XjIXp!d}s|BCeuMMZ1dj6^Ru^6(tpOD?YB+S#hA^V8!W* zYZZ4Y>M$VFt&dnAyFPAx^7_8(hp!*8zGVHp_0O*VX#L0QH?QBi{)_cruHU}?`}GIc zpTy8Dva)$)i^^7&@s){{-733RrdFm^W>h{=Iij+#a$@Dw${CeUSC&+6s{Ekx!^%%8 zKdt<%a$99p<@U<2D}S#1wen=;>B_$K}ryk2>;@>V6?pxxlN!MdU8hN=yJ zY&f&w?1u9jE^PQ~!=(+EH`HLD`1Y|)$G*j2aQ4rxpIxWOY1bWcx3HGf1=KaD3$L@( zMb<^twX5q~msr=Wu1DR6bwAd1zTf43#{K2@YwqLLXxyJ{at1krogq%EGtSw<*~%I3 zY~yU}Z0Bt6OmKE^c64@fc6N4gCORF?uFh`G?#?7JaQ1f&b`Ei7Ifpu* za6aiA<{a)E=^W)8?HubI=N#`WbWXul%M@ECMVH2rdAi*Ll|k*JamT*F7xz`JmdeadrkN!I+rcVwQ+g2h%YUUWW4z_h|`NhS8{% zkK-MDSH2fcF}=Z;@q77$7=03h; z@;m5P61XAoy}*A2ZVcQMxH<6Cz^XuEC5;Ok!Uv({pININ9&H>J6iu(;IXh{@yFU6>vJsQ z*uY~2#|n=Nzsb>nJ#b!2tx>h{$gs=HPvSEp3> zu1>4YtnObusCr2C(CR0vhgWA;kFL(G&Z{n}o?bnxx}^H~>Nl(3t=>>wQ%!2(YRYOp zsM%g~sD@lMU5&e1c=ha6)3wBF8P~}5xa)1Nms~HqzWe&=>*Pk_jjlKD-oPUC&5WCM zH5w#!jO}C|y*VjZ-gku%BiwrNRQtGM8eR`#GG`c^>)_YNboD$#WUDJH80}3L1rz zKCyTsYmIY1aqKp4yqUG8uVcgJ8+eZ^1HEtz15B$Wi|Ut_Q{H zEY9rb;Us@|>VUZ$H=cIq!e|l~jx%;tm&7-wh1jn%1&i7Xcz>K7H{q?#OqcM%IIm;$ zdz&|EzlPuUyg(1)+|nU_2|a?nM@RW(^f*t0YxsrqD!+){;OEktdP&NVcd1fz9?KpQR+XqzfQqT2;(JSfn?EyTcxGE>)xJ@~fPckKIG;q>L#!k?vU z!$;7va5_R`d07)|d0A=-y&a$@%tI`%XvV_45U>hR0oY=BncHfa#eHFUiEguurh6^V z2>W3^4)+t5Y210}7cEn`OO{pij-`;RwM^pf!9T(_l1AB{pv`Q-Zz7Do4_lYmnUm=&GQl|<9g ze??Q_8tjSdXl%(k8r-m)8e5jr&eO}OFta?B&MK$6=gSFwp}ZGe2DdWUE6aOQo6D%I zwzJ(58l0>$5i@R0vSOYg0xez~TD(Neu_t47ID-^n#)qA*I)K?sRC{dHWm}Mc!p!o| zMCM0?HH~T(^+ne4tb4`VkNkGz_alEE;g4#Mh8%UA+Iq_I``f?Q{b9a(!`TUK@GMt} zYp$!*wam4~^@gj`wb@nW+UeTu+KV>$Allz^u8Xd#uG_9VXnocCLWn50le@{I(SSTU0-v3-}Q^vjW>GU$htA@#^M{BZfvuSH9OCwo-Rxcq|R>C>}E zjn2tYzxAD8z&`X{Jazhvg5vCP1;sai%YCZg%J|&;-?DSY6-+9gTr@U2=Ssm9WqXuD zWaJ;CN1w?#GbVTJ*l`!0%DaTmgqW&kr%Yv^3?ExGb$a%1zx|eT=FGT) zD_5=*-@G{;8!ulp=+Oo}cod${qIrE(1h3v`3h`h&0~vw8Y`{psD8Ohy4qyx*7cdqu z4lo|@6!vGlv%;VccsmcfP66Kn7UfV<1#s+y&xic{ZrIra=nT+fi`P%swrm}s##da7 z&nb<;NBjVlFrS0o0MHrcLC~{$sGl=ppEY8S`n6}P@~vy@w5Hbl>z1a?Hb>wW=Od?MbiseNHtXcsGPh!HP|Ca)AI_lM>E$4ZpHb< zlXDcOhi((54KK`AQ6JzH=3-;;03>|p_n0AvH$GZH!X(kljiDC|mTL^hjwY{E{Trpy3m z$H7(DYlMx{zc9TPpUZvKpw}==a5bAa-h(w%2I?#Q)K3etPYvE2^zd_oj)TC6!Uiu= z1SodLJq7wyz%;;gzzo3CfM*cnrqu?$vekzgs&If;edaP&;9<_jXN}l{LP;J+xWf=K zl2ruu|2RU~Yd{#~rl25bN))*tr(_}Pol+(M8! z4ufxF?B(*1nWI^Tc8z&I2k(+M;Qd!S48%SN zKFz$BAao7$E_WpL$I!g~|*ZsGQRz}jAq_wh%)S-Da|*ZhBtooS3yRTzfb8IXY)Y{D2L zO$?|Z0ZUad5FsXV{e_8tj4?5qATv-V)y`sPQDm_oN-_MX22H_Hstiz8%WByRj8HZy zic%cZf(u1KDj*g_hej>381j6`F*vS8 zPZ+&s(RXn&Bq=X|oQ6~W3?_qil%8`S#2iJ|TVT@o;qb0mVt5I}_mC8Y<2ojnE-N4j z%I%O-l*c?|i*}as`;haLJ7tdQt!A@AOCf&%IReMs|KZZy#wUk^KoUTCF92F6Jv}5D z=3a=T9FzK?Y1&y!EK-aiDIAA*@=kd3=;=9Q@X9_!QUZ>v3nDk3e)MrlQ~n5Ymhv^3 z{8p7r?dNn zyBx|2v6a|Y;g4IDvaA#He{hKCO0<6!GOvc=$r10)!SzBtP}s}(|DrOWhS2gIglp}S3;6lcR419z^c+n_~0#-!PR7R2DV+aEMCn6hNYAAsQW zK!E>dliT%x0)grNQosnILoXqkKnpHTGyn?vF_A#3-hvdIe^s#2pz(@4oG1ufkPC{L zKPkW_0FgBSHuov5Kn0QnW=W9D5;iL(+VxNtnzkb|@vp35>M0A*BEr0&!a#b}B%TfLA^C(al{7 z5L*wRO>Qfg&N%E=fHZ-rHlR}h3It}_fKL@5x&c6|4cMaqDFRb$z+MH&6PRHG(h3mS z2;dPsigDvl_F|s`BniCbE(P{0Kn_6p8D1gI($gnw5nkB=&V^4fpYtP2bK_qbLp}&O zq02G~#)UqG!-z7ks988}E`#M}%02Z@4hMly5nq1 zts&(&F4^4ph@#7R%AY|lQuaP-lOvlTABG%*6g3%!lpmK@#;`E$roIIiJxC9vcK{b7Cj4=h|v+aA9b5k<=-C{9fz_?78B)goA_`LX;NpP{0#dA#+DB}VJoXZ>MWFV3U#lYif#wR{4B>Tiz~mm zSv96^M(Lx?!t zc$FXy1_rkwzsjk|4io?s2*l(r)(-pzAi5L4D2+Xqhbg5E7XYLP488$y z5kQ{600Hz-P)fK2AhHWn)Q|Ip)=p2q!H>f$`yEM1CJjSwtb- zMT&nQDZCr?Mx|@|h=oH&H_($JfCPaf_Zw*f;P<`8Mj%UIo!pw}!KuI#qHI?HggOE2 zQsWQ6EP+6rz#$a~N1W!pJq>T5rGte;Q~ngP6Nkx zI{@+oR*y51%m#ALjX-2CfaL;61k3^)j6jmWG+T(wAQQ+Dn4<#9F#Qc6oCZ*(_CgsZ z0to_l*m_|!5tAN3mcVRVn9Vj22<^i-ye$Bm@8melN2(JCaGdJh6{SJYc8Z$8-fgOS zUZwraUL8Ui%;a8>2#>G<%f2qElgGhj2tUc-JC>~+{1u-ULM3{R9=5e3K*f?@~bA|$4* z%~YAMx|rv5wbTUh+x_Pz`S_9%Uvq`WRo~iDQycJk>jI6ncw2|3x&iCagEdV}{)Rg5 zq^6cee?yC>vbHtwg16pR-x!?isc81sHF)tp505M8o8$}n@CAXF-xxG|`qZ}uTKsK% lm5Rru Date: Mon, 1 Jun 2026 20:03:47 +0300 Subject: [PATCH 06/16] fix(auth): check server before Google sign-in --- app/lib/auth/auth_api_client.dart | 16 ++++++++++++++++ app/lib/auth/auth_repository.dart | 2 ++ app/test/auth/auth_api_client_test.dart | 17 +++++++++++++++++ 3 files changed, 35 insertions(+) diff --git a/app/lib/auth/auth_api_client.dart b/app/lib/auth/auth_api_client.dart index 6463997..83b0fa4 100644 --- a/app/lib/auth/auth_api_client.dart +++ b/app/lib/auth/auth_api_client.dart @@ -16,11 +16,27 @@ class AuthApiException implements Exception { } class AuthApiClient { + static const serverUnavailableMessage = 'Unable to connect. Please try again.'; + final PapyrusApiConfig config; final http.Client _httpClient; AuthApiClient({required this.config, http.Client? httpClient}) : _httpClient = httpClient ?? http.Client(); + Future ensureServerReachable() async { + try { + final response = await _httpClient.get(config.serverBaseUri.resolve('/health')); + + if (response.statusCode >= 200 && response.statusCode < 300) { + return; + } + } catch (_) { + throw const AuthApiException(statusCode: 0, message: serverUnavailableMessage); + } + + throw const AuthApiException(statusCode: 0, message: serverUnavailableMessage); + } + Uri googleOAuthStartUri(String redirectUri) { return config.endpoint('/auth/oauth/google/start', {'redirect_uri': redirectUri}); } diff --git a/app/lib/auth/auth_repository.dart b/app/lib/auth/auth_repository.dart index fb37ace..aa67a36 100644 --- a/app/lib/auth/auth_repository.dart +++ b/app/lib/auth/auth_repository.dart @@ -99,6 +99,8 @@ class AuthRepository { } Future signInWithGoogle({required String clientType, String? deviceLabel}) async { + await apiClient.ensureServerReachable(); + final redirectUri = kIsWeb ? _webOAuthRedirectUri() : _usesDesktopLoopbackOAuth diff --git a/app/test/auth/auth_api_client_test.dart b/app/test/auth/auth_api_client_test.dart index c0e4f88..5242866 100644 --- a/app/test/auth/auth_api_client_test.dart +++ b/app/test/auth/auth_api_client_test.dart @@ -79,6 +79,23 @@ void main() { expect(uri.queryParameters['redirect_uri'], 'papyrus://auth/callback'); }); + test('ensureServerReachable maps network failures to auth errors before OAuth redirect', () async { + final client = AuthApiClient( + config: PapyrusApiConfig(serverBaseUri: Uri.parse('http://server.test')), + httpClient: MockClient((request) async { + expect(request.url.path, '/health'); + throw http.ClientException('Connection refused', request.url); + }), + ); + + await expectLater( + client.ensureServerReachable(), + throwsA( + isA().having((error) => error.message, 'message', 'Unable to connect. Please try again.'), + ), + ); + }); + test('powerSyncToken maps Papyrus token response', () async { final client = AuthApiClient( config: PapyrusApiConfig(serverBaseUri: Uri.parse('http://server.test')), From 81353a97048c742a52f2eb56466dd3a50b51497b Mon Sep 17 00:00:00 2001 From: Eoic Date: Mon, 1 Jun 2026 20:03:52 +0300 Subject: [PATCH 07/16] feat(auth): add offline entry points --- app/lib/auth/offline_link.dart | 33 ++++++++++++++++++++++++++++++ app/lib/pages/auth/actions.dart | 17 ++++++++++++++++ app/lib/pages/login_page.dart | 16 ++++++++++----- app/lib/pages/register_page.dart | 16 ++++++++++----- app/lib/pages/welcome_page.dart | 35 +++----------------------------- 5 files changed, 75 insertions(+), 42 deletions(-) create mode 100644 app/lib/auth/offline_link.dart create mode 100644 app/lib/pages/auth/actions.dart diff --git a/app/lib/auth/offline_link.dart b/app/lib/auth/offline_link.dart new file mode 100644 index 0000000..d4f1237 --- /dev/null +++ b/app/lib/auth/offline_link.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:papyrus/providers/auth_provider.dart'; +import 'package:provider/provider.dart'; + +class OfflineModeLink extends StatelessWidget { + final bool center; + + const OfflineModeLink({super.key, required this.center}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Align( + alignment: Alignment.center, + child: TextButton( + onPressed: () { + context.read().setOfflineMode(true); + context.goNamed('LIBRARY'); + }, + style: TextButton.styleFrom( + foregroundColor: theme.colorScheme.primary, + textStyle: theme.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w600), + ), + child: const Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [Text('Continue offline'), Icon(Icons.arrow_right_alt)], + ), + ), + ); + } +} diff --git a/app/lib/pages/auth/actions.dart b/app/lib/pages/auth/actions.dart new file mode 100644 index 0000000..f1744ed --- /dev/null +++ b/app/lib/pages/auth/actions.dart @@ -0,0 +1,17 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:papyrus/providers/auth_provider.dart'; +import 'package:provider/provider.dart'; + +void navigateToLogin(BuildContext context) { + context.go('/login'); +} + +void navigateToRegister(BuildContext context) { + context.go('/register'); +} + +void navigateToOffline(BuildContext context) { + context.read().setOfflineMode(true); + context.goNamed('LIBRARY'); +} diff --git a/app/lib/pages/login_page.dart b/app/lib/pages/login_page.dart index 52e786e..773dd1a 100644 --- a/app/lib/pages/login_page.dart +++ b/app/lib/pages/login_page.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; +import 'package:papyrus/pages/auth/actions.dart'; import 'package:papyrus/providers/auth_provider.dart'; import 'package:papyrus/themes/design_tokens.dart'; import 'package:papyrus/utils/responsive.dart'; @@ -90,16 +91,21 @@ class _LoginPageState extends State { ); } - void _navigateToRegister() { - context.go('/register'); - } - List _buildFooter() { return [ const TitledDivider(title: 'Or continue with'), const GoogleSignInButton(title: 'Sign in with Google'), const SizedBox(height: Spacing.md), - AuthSwitchLink(promptText: "Don't have an account?", actionText: 'Sign up', onPressed: _navigateToRegister), + AuthSwitchLink( + promptText: "Don't have an account?", + actionText: 'Sign up', + onPressed: () => navigateToRegister(context), + ), + AuthSwitchLink( + promptText: "No internet?", + actionText: 'Continue offline', + onPressed: () => navigateToOffline(context), + ), ]; } diff --git a/app/lib/pages/register_page.dart b/app/lib/pages/register_page.dart index a71e177..2846fc8 100644 --- a/app/lib/pages/register_page.dart +++ b/app/lib/pages/register_page.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; +import 'package:papyrus/pages/auth/actions.dart'; import 'package:papyrus/providers/auth_provider.dart'; import 'package:papyrus/themes/design_tokens.dart'; import 'package:papyrus/utils/responsive.dart'; @@ -100,10 +101,6 @@ class _RegisterPageState extends State { ); } - void _navigateToLogin() { - context.go('/login'); - } - String? _validateConfirmPassword(String? value) { if (value != _passwordController.text) { return 'Passwords do not match'; @@ -125,7 +122,16 @@ class _RegisterPageState extends State { const TitledDivider(title: 'Or continue with'), const GoogleSignInButton(title: 'Sign up with Google'), const SizedBox(height: Spacing.md), - AuthSwitchLink(promptText: 'Already have an account?', actionText: 'Sign in', onPressed: _navigateToLogin), + AuthSwitchLink( + promptText: 'Already have an account?', + actionText: 'Sign in', + onPressed: () => navigateToLogin(context), + ), + AuthSwitchLink( + promptText: 'No internet?', + actionText: 'Continue offline', + onPressed: () => navigateToOffline(context), + ), ]; } diff --git a/app/lib/pages/welcome_page.dart b/app/lib/pages/welcome_page.dart index 83f1df9..4e7209c 100644 --- a/app/lib/pages/welcome_page.dart +++ b/app/lib/pages/welcome_page.dart @@ -1,9 +1,8 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; -import 'package:papyrus/providers/auth_provider.dart'; +import 'package:papyrus/auth/offline_link.dart'; import 'package:papyrus/themes/design_tokens.dart'; import 'package:papyrus/widgets/titled_divider.dart'; -import 'package:provider/provider.dart'; class WelcomePage extends StatelessWidget { const WelcomePage({super.key}); @@ -200,11 +199,9 @@ class _WelcomeCopy extends StatelessWidget { crossAxisAlignment: center ? CrossAxisAlignment.center : CrossAxisAlignment.start, children: [ Image.asset('assets/images/logo.png', width: logoSize, height: logoSize, fit: BoxFit.contain), - SizedBox(height: titleSpacing), Text('Papyrus', textAlign: center ? TextAlign.center : TextAlign.left, style: titleStyle), - SizedBox(height: subtitleSpacing), Text( - 'Manage your library, track reading progress, and read anywhere.', + 'Your personal library, always within reach', textAlign: center ? TextAlign.center : TextAlign.left, style: subtitleStyle, ), @@ -237,7 +234,7 @@ class _WelcomeActions extends StatelessWidget { _SignInButton(isDesktop: isDesktop, useSurfaceColors: useSurfaceColors), SizedBox(height: compact ? Spacing.sm : Spacing.sm), TitledDivider(title: 'or', verticalPadding: compact ? Spacing.sm : Spacing.md), - _OfflineModeLink(center: center), + OfflineModeLink(center: center), ], ); } @@ -303,29 +300,3 @@ class _SignInButton extends StatelessWidget { ); } } - -class _OfflineModeLink extends StatelessWidget { - final bool center; - - const _OfflineModeLink({required this.center}); - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - - return Align( - alignment: center ? Alignment.center : Alignment.centerLeft, - child: TextButton( - onPressed: () { - context.read().setOfflineMode(true); - context.goNamed('LIBRARY'); - }, - style: TextButton.styleFrom( - foregroundColor: theme.colorScheme.primary, - textStyle: theme.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w600), - ), - child: const Text('Use offline mode'), - ), - ); - } -} From 53dd66ce9b6b868c2b6fbb24dc462ec369ef3e64 Mon Sep 17 00:00:00 2001 From: Eoic Date: Mon, 1 Jun 2026 20:03:56 +0300 Subject: [PATCH 08/16] docs: document local backend sync setup --- README.md | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index fa62da7..7a63fc8 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,7 @@ Many reading applications offer partial solutions but fall short on essential fe 2. **Install dependencies** ```bash - cd client + cd app flutter pub get ``` @@ -87,6 +87,23 @@ Many reading applications offer partial solutions but fall short on essential fe flutter run -d windows # or: macos, linux ``` +### Running with the back-end + +When the user signs in, the Flutter client communicates with the Papyrus server for auth and asks the server for PowerSync credentials. + +Run the server and PowerSync locally, then start Flutter with: + +```bash +cd app +flutter run \ + --dart-define=PAPYRUS_API_BASE_URL=http://localhost:8080 \ + --dart-define=POWERSYNC_SERVICE_URL=http://localhost:8081 +``` + +See +[`server/docs/flutter-auth-integration.md`](../server/docs/flutter-auth-integration.md) +for the full integration guide. + ## Documentation See [PapyrusReader/docs](https://github.com/PapyrusReader/docs). From 378b44d718482c5bcad0bcb0130324a228cb251c Mon Sep 17 00:00:00 2001 From: Eoic Date: Mon, 1 Jun 2026 20:11:30 +0300 Subject: [PATCH 09/16] refactor(auth): simplify auth form layouts --- app/lib/forms/login_form.dart | 251 ++++++++++---------- app/lib/widgets/auth/auth_branding.dart | 3 +- app/lib/widgets/auth/auth_page_layouts.dart | 5 +- 3 files changed, 133 insertions(+), 126 deletions(-) diff --git a/app/lib/forms/login_form.dart b/app/lib/forms/login_form.dart index c69d473..4989bd1 100644 --- a/app/lib/forms/login_form.dart +++ b/app/lib/forms/login_form.dart @@ -1,135 +1,142 @@ -import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; -import 'package:papyrus/providers/auth_provider.dart'; -import 'package:papyrus/widgets/buttons/google_sign_in.dart'; -import 'package:papyrus/widgets/input/email_input.dart'; -import 'package:papyrus/widgets/input/password_input.dart'; -import 'package:papyrus/widgets/titled_divider.dart'; -import 'package:provider/provider.dart'; +// import 'package:flutter/material.dart'; +// import 'package:go_router/go_router.dart'; +// import 'package:papyrus/providers/auth_provider.dart'; +// import 'package:papyrus/widgets/buttons/google_sign_in.dart'; +// import 'package:papyrus/widgets/input/email_input.dart'; +// import 'package:papyrus/widgets/input/password_input.dart'; +// import 'package:papyrus/widgets/titled_divider.dart'; +// import 'package:provider/provider.dart'; -class LoginForm extends StatefulWidget { - const LoginForm({super.key}); +// class LoginForm extends StatefulWidget { +// const LoginForm({super.key}); - @override - State createState() => _LoginForm(); -} +// @override +// State createState() => _LoginForm(); +// } -class _LoginForm extends State { - bool isLoginDisabled = false; +// class _LoginForm extends State { +// bool isLoginDisabled = false; - final formKey = GlobalKey(); - final emailController = TextEditingController(text: "karolis.strazdas.sso@gmail.com"); - final passwordController = TextEditingController(text: ""); +// final formKey = GlobalKey(); +// final emailController = TextEditingController(text: "karolis.strazdas.sso@gmail.com"); +// final passwordController = TextEditingController(text: ""); - Future signIn() async { - return context.read().login( - email: emailController.text.trim(), - password: passwordController.text.trim(), - ); - } +// Future signIn() async { +// return context.read().login( +// email: emailController.text.trim(), +// password: passwordController.text.trim(), +// ); +// } - Future _handleLogin() async { - if (!formKey.currentState!.validate()) return; +// Future _handleLogin() async { +// if (!formKey.currentState!.validate()) return; - ScaffoldMessenger.of(context).hideCurrentSnackBar(); - setState(() => isLoginDisabled = true); +// ScaffoldMessenger.of(context).hideCurrentSnackBar(); +// setState(() => isLoginDisabled = true); - showDialog( - context: context, - barrierDismissible: false, - builder: (context) => - const Center(child: SizedBox(width: 150, height: 150, child: CircularProgressIndicator(strokeWidth: 8))), - ); +// showDialog( +// context: context, +// barrierDismissible: false, +// builder: (context) => +// const Center(child: SizedBox(width: 150, height: 150, child: CircularProgressIndicator(strokeWidth: 8))), +// ); - try { - final success = await signIn(); - if (!mounted) return; - setState(() => isLoginDisabled = false); - Navigator.of(context).pop(); +// try { +// final success = await signIn(); +// if (!mounted) return; +// setState(() => isLoginDisabled = false); +// Navigator.of(context).pop(); - if (success) { - context.goNamed('LIBRARY'); - return; - } +// if (success) { +// context.goNamed('LIBRARY'); +// return; +// } - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - duration: const Duration(seconds: 5), - content: Text(context.read().error ?? 'Incorrect username or password.'), - backgroundColor: Theme.of(context).colorScheme.error, - ), - ); - } catch (e) { - if (!mounted) return; - setState(() => isLoginDisabled = false); - Navigator.of(context).pop(); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - duration: const Duration(seconds: 5), - content: const Text("Incorrect username or password."), - backgroundColor: Theme.of(context).colorScheme.error, - ), - ); - } - } +// ScaffoldMessenger.of(context).showSnackBar( +// SnackBar( +// duration: const Duration(seconds: 5), +// content: Text(context.read().error ?? 'Incorrect username or password.'), +// backgroundColor: Theme.of(context).colorScheme.error, +// ), +// ); +// } catch (e) { +// if (!mounted) return; +// setState(() => isLoginDisabled = false); +// Navigator.of(context).pop(); +// ScaffoldMessenger.of(context).showSnackBar( +// SnackBar( +// duration: const Duration(seconds: 5), +// content: const Text("Incorrect username or password."), +// backgroundColor: Theme.of(context).colorScheme.error, +// ), +// ); +// } +// } - @override - Widget build(BuildContext context) { - return Form( - key: formKey, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 26.0, vertical: 16.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Align( - alignment: Alignment.centerLeft, - child: Text("Sign in", style: Theme.of(context).textTheme.headlineMedium), - ), - const SizedBox(height: 16), - EmailInput(labelText: "Email address", controller: emailController), - const SizedBox(height: 24), - PasswordInput(labelText: "Password", controller: passwordController), - Align( - alignment: Alignment.centerRight, - child: TextButton(onPressed: () {}, child: const Text("Forgot your password?")), - ), - ElevatedButton( - onPressed: isLoginDisabled ? null : _handleLogin, - style: const ButtonStyle( - minimumSize: WidgetStatePropertyAll(Size.fromHeight(50)), - elevation: WidgetStatePropertyAll(2.0), - ), - child: const Row(children: [Spacer(), Text("Continue"), Spacer(), Icon(Icons.arrow_right)]), - ), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - const Spacer(), - const TitledDivider(title: "Or continue with"), - const GoogleSignInButton(title: "Sign in with Google"), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text("Don't have an account?"), - TextButton(onPressed: () => context.go("/register"), child: const Text("Sign up")), - ], - ), - ], - ), - ), - ], - ), - ), - ); - } +// @override +// Widget build(BuildContext context) { +// return Form( +// key: formKey, +// child: Padding( +// padding: const EdgeInsets.symmetric(horizontal: 26.0, vertical: 16.0), +// child: Column( +// mainAxisAlignment: MainAxisAlignment.start, +// mainAxisSize: MainAxisSize.min, +// children: [ +// Align( +// alignment: Alignment.centerLeft, +// child: Text("Sign in", style: Theme.of(context).textTheme.headlineMedium), +// ), +// const SizedBox(height: 16), +// EmailInput(labelText: "Email address", controller: emailController), +// const SizedBox(height: 24), +// PasswordInput(labelText: "Password", controller: passwordController), +// Align( +// alignment: Alignment.centerRight, +// child: TextButton(onPressed: () {}, child: const Text("Forgot your password?")), +// ), +// ElevatedButton( +// onPressed: isLoginDisabled ? null : _handleLogin, +// style: const ButtonStyle( +// minimumSize: WidgetStatePropertyAll(Size.fromHeight(50)), +// elevation: WidgetStatePropertyAll(2.0), +// ), +// child: const Row(children: [Spacer(), Text("Continue"), Spacer(), Icon(Icons.arrow_right)]), +// ), +// Expanded( +// child: Column( +// crossAxisAlignment: CrossAxisAlignment.stretch, +// children: [ +// const Spacer(), +// const TitledDivider(title: "Or continue with"), +// const GoogleSignInButton(title: "Sign in with Google"), +// Row( +// mainAxisAlignment: MainAxisAlignment.center, +// children: [ +// const Text("Don't have an accountsss?"), +// TextButton(onPressed: () => context.go("/register"), child: const Text("Sign up")), +// ], +// ), +// Row( +// mainAxisAlignment: MainAxisAlignment.center, +// children: [ +// const Text("Don't need an account?"), +// TextButton(onPressed: () => {}, child: const Text("Continue offline")), +// ], +// ), +// ], +// ), +// ), +// ], +// ), +// ), +// ); +// } - @override - void dispose() { - emailController.dispose(); - passwordController.dispose(); - super.dispose(); - } -} +// @override +// void dispose() { +// emailController.dispose(); +// passwordController.dispose(); +// super.dispose(); +// } +// } diff --git a/app/lib/widgets/auth/auth_branding.dart b/app/lib/widgets/auth/auth_branding.dart index ad26937..6e5988f 100644 --- a/app/lib/widgets/auth/auth_branding.dart +++ b/app/lib/widgets/auth/auth_branding.dart @@ -9,7 +9,8 @@ class AuthBranding extends StatelessWidget { @override Widget build(BuildContext context) { return Row( - mainAxisSize: MainAxisSize.min, + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, children: [ SvgPicture.asset('assets/images/logo-icon-light.svg', width: 56, height: 56), const SizedBox(width: 16), diff --git a/app/lib/widgets/auth/auth_page_layouts.dart b/app/lib/widgets/auth/auth_page_layouts.dart index 62d422d..ac3ab55 100644 --- a/app/lib/widgets/auth/auth_page_layouts.dart +++ b/app/lib/widgets/auth/auth_page_layouts.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'package:papyrus/themes/design_tokens.dart'; -import 'package:papyrus/widgets/auth/auth_branding.dart'; import 'package:papyrus/widgets/auth/auth_hero_panel.dart'; /// Mobile auth layout: compact hero header + scrollable form panel. @@ -48,7 +47,7 @@ class MobileAuthLayout extends StatelessWidget { children: [ const SizedBox(height: Spacing.xl), if (showHeader) ...[ - const AuthBranding(), + // const AuthBranding(), const SizedBox(height: Spacing.md), Text( heading, @@ -138,7 +137,7 @@ class _DesktopAuthLayoutState extends State { crossAxisAlignment: CrossAxisAlignment.stretch, children: [ if (widget.showHeader) ...[ - const AuthBranding(), + // const AuthBranding(), const SizedBox(height: Spacing.md), Text( widget.heading, From 20e234ee2ddca8ec6f457b2c186836f6c9dfa2a3 Mon Sep 17 00:00:00 2001 From: Eoic Date: Mon, 1 Jun 2026 20:17:05 +0300 Subject: [PATCH 10/16] fix: element focus order in auth forms --- app/lib/widgets/auth/auth_page_layouts.dart | 106 ++++++++++-------- .../widgets/auth/auth_page_layouts_test.dart | 65 +++++++++++ 2 files changed, 124 insertions(+), 47 deletions(-) create mode 100644 app/test/widgets/auth/auth_page_layouts_test.dart diff --git a/app/lib/widgets/auth/auth_page_layouts.dart b/app/lib/widgets/auth/auth_page_layouts.dart index ac3ab55..11160bf 100644 --- a/app/lib/widgets/auth/auth_page_layouts.dart +++ b/app/lib/widgets/auth/auth_page_layouts.dart @@ -120,41 +120,48 @@ class _DesktopAuthLayoutState extends State { Widget _buildFormPanel(ThemeData theme) { final isSwapped = DesktopAuthLayout._isSwapped; - return Container( - decoration: BoxDecoration( - color: theme.colorScheme.surface, - boxShadow: [ - BoxShadow(color: Colors.black.withValues(alpha: 0.08), blurRadius: 24, offset: Offset(isSwapped ? 4 : -4, 0)), - ], - ), - child: Center( - child: SingleChildScrollView( - padding: const EdgeInsets.all(ComponentSizes.authFormPanelPadding), - child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: ComponentSizes.authFormPanelMaxWidth), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - if (widget.showHeader) ...[ - // const AuthBranding(), - const SizedBox(height: Spacing.md), - Text( - widget.heading, - style: theme.textTheme.headlineLarge?.copyWith( - fontWeight: FontWeight.w600, - color: theme.colorScheme.onSurface, + return FocusTraversalOrder( + order: const NumericFocusOrder(1), + child: Container( + decoration: BoxDecoration( + color: theme.colorScheme.surface, + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.08), + blurRadius: 24, + offset: Offset(isSwapped ? 4 : -4, 0), + ), + ], + ), + child: Center( + child: SingleChildScrollView( + padding: const EdgeInsets.all(ComponentSizes.authFormPanelPadding), + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: ComponentSizes.authFormPanelMaxWidth), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (widget.showHeader) ...[ + // const AuthBranding(), + const SizedBox(height: Spacing.md), + Text( + widget.heading, + style: theme.textTheme.headlineLarge?.copyWith( + fontWeight: FontWeight.w600, + color: theme.colorScheme.onSurface, + ), ), - ), - Text( - widget.subtitle, - style: theme.textTheme.bodyMedium?.copyWith(color: theme.colorScheme.onSurfaceVariant), - ), - const SizedBox(height: Spacing.xl), + Text( + widget.subtitle, + style: theme.textTheme.bodyMedium?.copyWith(color: theme.colorScheme.onSurfaceVariant), + ), + const SizedBox(height: Spacing.xl), + ], + widget.form, + ...widget.footer, ], - widget.form, - ...widget.footer, - ], + ), ), ), ), @@ -171,20 +178,25 @@ class _DesktopAuthLayoutState extends State { final hero = Expanded(flex: 6, child: AuthHeroPanel(isDark: isDark)); final form = Expanded(flex: 4, child: _buildFormPanel(theme)); - return Scaffold( - backgroundColor: theme.colorScheme.surface, - body: Stack( - children: [ - Row(children: isSwapped ? [form, hero] : [hero, form]), - // Swap button at the hero/form boundary - Positioned( - left: isSwapped ? null : 0, - right: isSwapped ? 0 : null, - top: 0, - bottom: 0, - child: _SwapPanelsButton(isSwapped: isSwapped, onPressed: _toggleSwap), - ), - ], + return FocusTraversalGroup( + policy: OrderedTraversalPolicy(), + child: Scaffold( + backgroundColor: theme.colorScheme.surface, + body: Stack( + children: [ + Row(children: isSwapped ? [form, hero] : [hero, form]), + Positioned( + left: isSwapped ? null : 0, + right: isSwapped ? 0 : null, + top: 0, + bottom: 0, + child: FocusTraversalOrder( + order: const NumericFocusOrder(2), + child: _SwapPanelsButton(isSwapped: isSwapped, onPressed: _toggleSwap), + ), + ), + ], + ), ), ); } diff --git a/app/test/widgets/auth/auth_page_layouts_test.dart b/app/test/widgets/auth/auth_page_layouts_test.dart new file mode 100644 index 0000000..bd20fca --- /dev/null +++ b/app/test/widgets/auth/auth_page_layouts_test.dart @@ -0,0 +1,65 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:papyrus/widgets/auth/auth_page_layouts.dart'; + +void main() { + testWidgets('desktop swap button is focused after form controls', (tester) async { + final firstFocusNode = FocusNode(debugLabel: 'first'); + final secondFocusNode = FocusNode(debugLabel: 'second'); + final submitFocusNode = FocusNode(debugLabel: 'submit'); + final footerFocusNode = FocusNode(debugLabel: 'footer'); + + addTearDown(() { + firstFocusNode.dispose(); + secondFocusNode.dispose(); + submitFocusNode.dispose(); + footerFocusNode.dispose(); + }); + + await tester.pumpWidget( + MaterialApp( + home: MediaQuery( + data: const MediaQueryData(size: Size(1200, 800)), + child: DesktopAuthLayout( + heading: 'Heading', + subtitle: 'Subtitle', + form: Column( + children: [ + TextField(focusNode: firstFocusNode), + TextField(focusNode: secondFocusNode), + ElevatedButton(focusNode: submitFocusNode, onPressed: () {}, child: const Text('Continue')), + ], + ), + footer: [TextButton(focusNode: footerFocusNode, onPressed: () {}, child: const Text('Switch form'))], + ), + ), + ), + ); + + firstFocusNode.requestFocus(); + await tester.pump(); + + await tester.sendKeyEvent(LogicalKeyboardKey.tab); + await tester.pump(); + expect(secondFocusNode.hasFocus, isTrue); + + await tester.sendKeyEvent(LogicalKeyboardKey.tab); + await tester.pump(); + expect(submitFocusNode.hasFocus, isTrue); + + await tester.sendKeyEvent(LogicalKeyboardKey.tab); + await tester.pump(); + expect(footerFocusNode.hasFocus, isTrue); + + await tester.sendKeyEvent(LogicalKeyboardKey.tab); + await tester.pump(); + + final focusedContext = FocusManager.instance.primaryFocus?.context; + expect(focusedContext, isNotNull); + expect( + find.descendant(of: find.byWidget(focusedContext!.widget), matching: find.byIcon(Icons.swap_horiz)), + findsOneWidget, + ); + }); +} From 61063448f90d266fc1351d2ddc29cd9d44bb7367 Mon Sep 17 00:00:00 2001 From: Eoic Date: Thu, 4 Jun 2026 23:13:42 +0300 Subject: [PATCH 11/16] Refine auth hero branding --- app/lib/widgets/auth/auth_branding.dart | 108 ++++++++++++++++-- app/lib/widgets/auth/auth_hero_panel.dart | 37 +++++- .../widgets/auth/auth_page_layouts_test.dart | 78 ++++++++++--- 3 files changed, 195 insertions(+), 28 deletions(-) diff --git a/app/lib/widgets/auth/auth_branding.dart b/app/lib/widgets/auth/auth_branding.dart index 6e5988f..ab1338c 100644 --- a/app/lib/widgets/auth/auth_branding.dart +++ b/app/lib/widgets/auth/auth_branding.dart @@ -1,30 +1,120 @@ +import 'dart:ui' as ui; + import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; /// Branding element with logo and app name. -/// Used at the top of auth form panels (login, register). class AuthBranding extends StatelessWidget { - const AuthBranding({super.key}); + final Color? textColor; + final Color? iconOutlineColor; + final double iconSize; + final double fontSize; + final double iconOutlineWidth; + final Color? shadowColor; + final Offset shadowOffset; + final double shadowBlurRadius; + + const AuthBranding({ + super.key, + this.textColor, + this.iconOutlineColor, + this.iconSize = 56, + this.fontSize = 36, + this.iconOutlineWidth = 0, + this.shadowColor, + this.shadowOffset = const Offset(0, 1), + this.shadowBlurRadius = 4, + }); @override Widget build(BuildContext context) { + final foregroundColor = textColor ?? Theme.of(context).colorScheme.onSurface; + return Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, children: [ - SvgPicture.asset('assets/images/logo-icon-light.svg', width: 56, height: 56), - const SizedBox(width: 16), + _LogoIcon( + size: iconSize, + outlineColor: iconOutlineColor, + outlineWidth: iconOutlineWidth, + shadowColor: shadowColor, + shadowOffset: shadowOffset, + shadowBlurRadius: shadowBlurRadius, + ), + const SizedBox(width: 12), Text( 'Papyrus', style: TextStyle( fontFamily: 'MadimiOne', - fontSize: 36, + fontSize: fontSize, fontWeight: FontWeight.normal, - color: Theme.of(context).colorScheme.onSurface, - letterSpacing: -0.5, + color: foregroundColor, + letterSpacing: 0, + shadows: shadowColor == null + ? null + : [Shadow(color: shadowColor!, offset: shadowOffset, blurRadius: shadowBlurRadius)], ), ), ], ); } } + +class _LogoIcon extends StatelessWidget { + final double size; + final Color? outlineColor; + final double outlineWidth; + final Color? shadowColor; + final Offset shadowOffset; + final double shadowBlurRadius; + + const _LogoIcon({ + required this.size, + this.outlineColor, + required this.outlineWidth, + this.shadowColor, + required this.shadowOffset, + required this.shadowBlurRadius, + }); + + @override + Widget build(BuildContext context) { + if (outlineColor == null && shadowColor == null) { + return SvgPicture.asset('assets/images/logo-icon-light.svg', width: size, height: size); + } + + final outlineSize = size + outlineWidth * 2; + + return SizedBox( + width: outlineSize, + height: outlineSize, + child: Stack( + alignment: Alignment.center, + clipBehavior: Clip.none, + children: [ + if (shadowColor != null) + Transform.translate( + offset: shadowOffset, + child: ImageFiltered( + imageFilter: ui.ImageFilter.blur(sigmaX: shadowBlurRadius / 2, sigmaY: shadowBlurRadius / 2), + child: SvgPicture.asset( + 'assets/images/logo-icon-light.svg', + width: outlineSize, + height: outlineSize, + colorFilter: ColorFilter.mode(shadowColor!, BlendMode.srcIn), + ), + ), + ), + if (outlineColor != null && outlineWidth > 0) + SvgPicture.asset( + 'assets/images/logo-icon-light.svg', + width: outlineSize, + height: outlineSize, + colorFilter: ColorFilter.mode(outlineColor!, BlendMode.srcIn), + ), + SvgPicture.asset('assets/images/logo-icon-light.svg', width: size, height: size), + ], + ), + ); + } +} diff --git a/app/lib/widgets/auth/auth_hero_panel.dart b/app/lib/widgets/auth/auth_hero_panel.dart index 16c5da0..74488c7 100644 --- a/app/lib/widgets/auth/auth_hero_panel.dart +++ b/app/lib/widgets/auth/auth_hero_panel.dart @@ -1,4 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:papyrus/themes/design_tokens.dart'; +import 'package:papyrus/widgets/auth/auth_branding.dart'; import 'package:papyrus/widgets/auth/curved_bottom_clipper.dart'; /// Hero gradient colors for auth pages. @@ -14,8 +16,7 @@ class AuthColors { static const Color gradientEndDark = Color(0xFF272377); } -/// Desktop hero panel with illustration background (no branding overlay). -/// Branding should be placed on the form side for better UX. +/// Desktop hero panel with illustration background and branding overlay. class AuthHeroPanel extends StatelessWidget { final bool isDark; @@ -39,17 +40,27 @@ class AuthHeroPanel extends StatelessWidget { ), ), ), - // Illustration filling the panel - clean, no overlays Positioned.fill( child: Image.asset('assets/images/auth-illustration.png', fit: BoxFit.cover, alignment: Alignment.center), ), + const Positioned( + top: Spacing.xl, + left: Spacing.xl, + child: AuthBranding( + textColor: Colors.white, + iconOutlineColor: Color(0x998F89FF), + iconSize: 48, + fontSize: 32, + iconOutlineWidth: 2.5, + shadowColor: Color(0x40000000), + ), + ), ], ); } } -/// Compact hero header for mobile auth pages (no branding overlay). -/// Branding should be placed in the form area below. +/// Compact hero header for mobile auth pages with branding overlay. class CompactAuthHeader extends StatelessWidget { final bool isDark; final double height; @@ -78,7 +89,6 @@ class CompactAuthHeader extends StatelessWidget { ), ), ), - // Illustration - clean, no overlays Positioned.fill( child: Image.asset( 'assets/images/auth-illustration.png', @@ -86,6 +96,21 @@ class CompactAuthHeader extends StatelessWidget { alignment: Alignment.topCenter, ), ), + const Positioned( + top: Spacing.lg, + left: Spacing.lg, + right: Spacing.lg, + child: Center( + child: AuthBranding( + textColor: Colors.white, + iconOutlineColor: Color(0x998F89FF), + iconSize: 40, + fontSize: 28, + iconOutlineWidth: 2.0, + shadowColor: Color(0x40000000), + ), + ), + ), ], ), ), diff --git a/app/test/widgets/auth/auth_page_layouts_test.dart b/app/test/widgets/auth/auth_page_layouts_test.dart index bd20fca..c2e4a03 100644 --- a/app/test/widgets/auth/auth_page_layouts_test.dart +++ b/app/test/widgets/auth/auth_page_layouts_test.dart @@ -1,10 +1,65 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:papyrus/themes/design_tokens.dart'; +import 'package:papyrus/widgets/auth/auth_branding.dart'; import 'package:papyrus/widgets/auth/auth_page_layouts.dart'; void main() { + void setViewport(WidgetTester tester, Size size) { + tester.view.devicePixelRatio = 1; + tester.view.physicalSize = size; + addTearDown(tester.view.resetPhysicalSize); + addTearDown(tester.view.resetDevicePixelRatio); + } + + testWidgets('desktop auth layout shows branding over the hero image', (tester) async { + setViewport(tester, const Size(1200, 800)); + + await tester.pumpWidget( + MaterialApp( + home: DesktopAuthLayout( + heading: 'Heading', + subtitle: 'Subtitle', + form: const SizedBox.shrink(), + footer: const [], + ), + ), + ); + + final branding = find.byType(AuthBranding); + + expect(find.text('Papyrus'), findsOneWidget); + expect(branding, findsOneWidget); + expect(tester.getTopLeft(branding).dx, Spacing.xl); + expect(tester.getTopLeft(branding).dy, Spacing.xl); + }); + + testWidgets('mobile auth layout shows branding over the compact image header', (tester) async { + setViewport(tester, const Size(390, 844)); + + await tester.pumpWidget( + MaterialApp( + home: MobileAuthLayout( + heading: 'Heading', + subtitle: 'Subtitle', + form: const SizedBox.shrink(), + footer: const [], + ), + ), + ); + + final branding = find.byType(AuthBranding); + + expect(find.text('Papyrus'), findsOneWidget); + expect(branding, findsOneWidget); + expect(tester.getCenter(branding).dx, moreOrLessEquals(195)); + expect(tester.getTopLeft(branding).dy, Spacing.lg); + }); + testWidgets('desktop swap button is focused after form controls', (tester) async { + setViewport(tester, const Size(1200, 800)); + final firstFocusNode = FocusNode(debugLabel: 'first'); final secondFocusNode = FocusNode(debugLabel: 'second'); final submitFocusNode = FocusNode(debugLabel: 'submit'); @@ -19,20 +74,17 @@ void main() { await tester.pumpWidget( MaterialApp( - home: MediaQuery( - data: const MediaQueryData(size: Size(1200, 800)), - child: DesktopAuthLayout( - heading: 'Heading', - subtitle: 'Subtitle', - form: Column( - children: [ - TextField(focusNode: firstFocusNode), - TextField(focusNode: secondFocusNode), - ElevatedButton(focusNode: submitFocusNode, onPressed: () {}, child: const Text('Continue')), - ], - ), - footer: [TextButton(focusNode: footerFocusNode, onPressed: () {}, child: const Text('Switch form'))], + home: DesktopAuthLayout( + heading: 'Heading', + subtitle: 'Subtitle', + form: Column( + children: [ + TextField(focusNode: firstFocusNode), + TextField(focusNode: secondFocusNode), + ElevatedButton(focusNode: submitFocusNode, onPressed: () {}, child: const Text('Continue')), + ], ), + footer: [TextButton(focusNode: footerFocusNode, onPressed: () {}, child: const Text('Switch form'))], ), ), ); From 2d375a9245cf9d720e6e1706e90932d67cf267a4 Mon Sep 17 00:00:00 2001 From: Eoic Date: Thu, 4 Jun 2026 23:21:52 +0300 Subject: [PATCH 12/16] Adjust auth branding contrast --- app/lib/widgets/auth/auth_branding.dart | 10 ++++++++-- app/lib/widgets/auth/auth_hero_panel.dart | 23 ++++++++++++++++------- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/app/lib/widgets/auth/auth_branding.dart b/app/lib/widgets/auth/auth_branding.dart index ab1338c..133b853 100644 --- a/app/lib/widgets/auth/auth_branding.dart +++ b/app/lib/widgets/auth/auth_branding.dart @@ -13,6 +13,9 @@ class AuthBranding extends StatelessWidget { final Color? shadowColor; final Offset shadowOffset; final double shadowBlurRadius; + final Color? textShadowColor; + final Offset textShadowOffset; + final double textShadowBlurRadius; const AuthBranding({ super.key, @@ -24,6 +27,9 @@ class AuthBranding extends StatelessWidget { this.shadowColor, this.shadowOffset = const Offset(0, 1), this.shadowBlurRadius = 4, + this.textShadowColor, + this.textShadowOffset = const Offset(0, 1), + this.textShadowBlurRadius = 3, }); @override @@ -50,9 +56,9 @@ class AuthBranding extends StatelessWidget { fontWeight: FontWeight.normal, color: foregroundColor, letterSpacing: 0, - shadows: shadowColor == null + shadows: textShadowColor == null ? null - : [Shadow(color: shadowColor!, offset: shadowOffset, blurRadius: shadowBlurRadius)], + : [Shadow(color: textShadowColor!, offset: textShadowOffset, blurRadius: textShadowBlurRadius)], ), ), ], diff --git a/app/lib/widgets/auth/auth_hero_panel.dart b/app/lib/widgets/auth/auth_hero_panel.dart index 74488c7..ee403ed 100644 --- a/app/lib/widgets/auth/auth_hero_panel.dart +++ b/app/lib/widgets/auth/auth_hero_panel.dart @@ -53,6 +53,9 @@ class AuthHeroPanel extends StatelessWidget { fontSize: 32, iconOutlineWidth: 2.5, shadowColor: Color(0x40000000), + textShadowColor: Color(0x59000000), + textShadowOffset: Offset(0, 2), + textShadowBlurRadius: 6, ), ), ], @@ -101,13 +104,19 @@ class CompactAuthHeader extends StatelessWidget { left: Spacing.lg, right: Spacing.lg, child: Center( - child: AuthBranding( - textColor: Colors.white, - iconOutlineColor: Color(0x998F89FF), - iconSize: 40, - fontSize: 28, - iconOutlineWidth: 2.0, - shadowColor: Color(0x40000000), + child: FittedBox( + fit: BoxFit.scaleDown, + child: AuthBranding( + textColor: Colors.white, + iconOutlineColor: Color(0x998F89FF), + iconSize: 70, + fontSize: 40, + iconOutlineWidth: 3, + shadowColor: Color(0x40000000), + textShadowColor: Color(0x59000000), + textShadowOffset: Offset(0, 2), + textShadowBlurRadius: 6, + ), ), ), ), From f093cf9ca561c7ee32299a592400a68f3d803d33 Mon Sep 17 00:00:00 2001 From: Eoic Date: Fri, 5 Jun 2026 01:34:29 +0300 Subject: [PATCH 13/16] fix: password reset flow --- README.md | 11 +- app/.dart_defines.example | 4 +- app/lib/config/app_router.dart | 11 +- app/lib/forms/login_form.dart | 2 +- app/lib/pages/forgot_password_page.dart | 185 +++++++++++------- app/lib/pages/login_page.dart | 2 +- app/lib/pages/register_page.dart | 2 +- app/lib/widgets/auth/auth_switch_link.dart | 5 +- app/lib/widgets/buttons/google_sign_in.dart | 11 +- app/test/config/app_router_test.dart | 2 + .../pages/auth_password_submission_test.dart | 117 +++++++++++ app/test/pages/forgot_password_page_test.dart | 121 ++++++++++++ 12 files changed, 386 insertions(+), 87 deletions(-) create mode 100644 app/test/pages/auth_password_submission_test.dart create mode 100644 app/test/pages/forgot_password_page_test.dart diff --git a/README.md b/README.md index 7a63fc8..a237bb6 100644 --- a/README.md +++ b/README.md @@ -95,9 +95,14 @@ Run the server and PowerSync locally, then start Flutter with: ```bash cd app -flutter run \ - --dart-define=PAPYRUS_API_BASE_URL=http://localhost:8080 \ - --dart-define=POWERSYNC_SERVICE_URL=http://localhost:8081 +flutter run -d chrome --web-hostname papyrus.localhost --web-port 3000 --dart-define-from-file=.dart_defines +``` + +For local web auth links, add these entries to `/etc/hosts`: + +```text +127.0.0.1 papyrus.localhost +::1 papyrus.localhost ``` See diff --git a/app/.dart_defines.example b/app/.dart_defines.example index f946c57..e9d0735 100644 --- a/app/.dart_defines.example +++ b/app/.dart_defines.example @@ -1,4 +1,4 @@ { - "SUPABASE_URL": "https://your-project-ref.supabase.co", - "SUPABASE_ANON_KEY": "your-anon-key" + "PAPYRUS_API_BASE_URL": "http://papyrus.localhost:8080", + "POWERSYNC_SERVICE_URL": "http://localhost:8081" } diff --git a/app/lib/config/app_router.dart b/app/lib/config/app_router.dart index c463c08..3de412f 100644 --- a/app/lib/config/app_router.dart +++ b/app/lib/config/app_router.dart @@ -58,6 +58,14 @@ class AppRouter { path: 'forgot-password', pageBuilder: (context, state) => NoTransitionPage(key: state.pageKey, child: const ForgotPasswordPage()), ), + GoRoute( + name: 'RESET_PASSWORD', + path: 'reset-password', + pageBuilder: (context, state) => NoTransitionPage( + key: state.pageKey, + child: ForgotPasswordPage(resetToken: state.uri.queryParameters['token'], isResetLink: true), + ), + ), GoRoute( name: 'AUTH_CALLBACK', path: 'auth/callback', @@ -204,6 +212,7 @@ class AppRouter { location == '/login' || location == '/register' || location == '/forgot-password' || + location == '/reset-password' || location == '/auth/callback'; if (authProvider.status == AuthStatus.bootstrapping) { @@ -218,7 +227,7 @@ class AppRouter { return '/'; } - if (location == '/' || location == '/login' || location == '/register') { + if (location == '/' || location == '/login' || location == '/register' || location == '/reset-password') { return '/library/books'; } diff --git a/app/lib/forms/login_form.dart b/app/lib/forms/login_form.dart index 4989bd1..50e84b4 100644 --- a/app/lib/forms/login_form.dart +++ b/app/lib/forms/login_form.dart @@ -24,7 +24,7 @@ // Future signIn() async { // return context.read().login( // email: emailController.text.trim(), -// password: passwordController.text.trim(), +// password: passwordController.text, // ); // } diff --git a/app/lib/pages/forgot_password_page.dart b/app/lib/pages/forgot_password_page.dart index e92c0e9..d334a76 100644 --- a/app/lib/pages/forgot_password_page.dart +++ b/app/lib/pages/forgot_password_page.dart @@ -13,7 +13,10 @@ import 'package:provider/provider.dart'; /// Forgot password page for the Papyrus book management application. /// Provides responsive layouts for mobile and desktop displays. class ForgotPasswordPage extends StatefulWidget { - const ForgotPasswordPage({super.key}); + final String? resetToken; + final bool isResetLink; + + const ForgotPasswordPage({super.key, this.resetToken, this.isResetLink = false}); @override State createState() => _ForgotPasswordPageState(); @@ -23,11 +26,9 @@ class _ForgotPasswordPageState extends State { final _formKey = GlobalKey(); final _resetFormKey = GlobalKey(); final _emailController = TextEditingController(); - final _tokenController = TextEditingController(); final _passwordController = TextEditingController(); final _confirmPasswordController = TextEditingController(); final _emailFocusNode = FocusNode(); - final _tokenFocusNode = FocusNode(); final _passwordFocusNode = FocusNode(); final _confirmPasswordFocusNode = FocusNode(); @@ -38,11 +39,9 @@ class _ForgotPasswordPageState extends State { @override void dispose() { _emailController.dispose(); - _tokenController.dispose(); _passwordController.dispose(); _confirmPasswordController.dispose(); _emailFocusNode.dispose(); - _tokenFocusNode.dispose(); _passwordFocusNode.dispose(); _confirmPasswordFocusNode.dispose(); super.dispose(); @@ -84,6 +83,13 @@ class _ForgotPasswordPageState extends State { FocusScope.of(context).unfocus(); + final resetToken = widget.resetToken; + + if (resetToken == null || resetToken.isEmpty) { + _showErrorSnackBar('Password reset link is invalid.'); + return; + } + if (!_resetFormKey.currentState!.validate()) return; setState(() => _isLoading = true); @@ -91,7 +97,7 @@ class _ForgotPasswordPageState extends State { try { final message = await context.read().resetPassword( - token: _tokenController.text.trim(), + token: resetToken, password: _passwordController.text, ); @@ -160,14 +166,15 @@ class _ForgotPasswordPageState extends State { return const _PasswordResetConfirmation(); } - if (_emailSent) { - return _EmailSentConfirmation( - email: _emailController.text.trim(), + if (widget.isResetLink) { + if (widget.resetToken == null || widget.resetToken!.isEmpty) { + return const _InvalidResetLink(); + } + + return _SetNewPasswordForm( formKey: _resetFormKey, - tokenController: _tokenController, passwordController: _passwordController, confirmPasswordController: _confirmPasswordController, - tokenFocusNode: _tokenFocusNode, passwordFocusNode: _passwordFocusNode, confirmPasswordFocusNode: _confirmPasswordFocusNode, isLoading: _isLoading, @@ -177,6 +184,10 @@ class _ForgotPasswordPageState extends State { validateConfirmPassword: _validateConfirmPassword, ); } + + if (_emailSent) { + return _EmailSentConfirmation(email: _emailController.text.trim()); + } return _ForgotPasswordForm( formKey: _formKey, emailController: _emailController, @@ -196,18 +207,24 @@ class _ForgotPasswordPageState extends State { @override Widget build(BuildContext context) { + final heading = widget.isResetLink ? 'Create new password' : 'Reset password'; + final subtitle = widget.isResetLink + ? 'Enter a new password for your account' + : 'Enter your email to receive a password reset link'; + final showHeader = !_emailSent && !_passwordReset; + return ResponsiveBuilder( mobile: (context) => MobileAuthLayout( - heading: 'Reset password', - subtitle: 'Enter your email to receive a password reset link', - showHeader: !_emailSent, + heading: heading, + subtitle: subtitle, + showHeader: showHeader, form: _buildForm(isDesktop: false), footer: _buildFooter(), ), desktop: (context) => DesktopAuthLayout( - heading: 'Reset password', - subtitle: 'Enter your email to receive a password reset link', - showHeader: !_emailSent, + heading: heading, + subtitle: subtitle, + showHeader: showHeader, form: _buildForm(isDesktop: true), footer: _buildFooter(), ), @@ -267,11 +284,45 @@ class _ForgotPasswordForm extends StatelessWidget { /// Confirmation view shown after the reset email has been sent. class _EmailSentConfirmation extends StatelessWidget { final String email; + + const _EmailSentConfirmation({required this.email}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.mark_email_read_outlined, size: 72, color: theme.colorScheme.primary), + const SizedBox(height: Spacing.sm), + Text( + 'Check your email', + style: theme.textTheme.titleLarge?.copyWith(fontWeight: FontWeight.w600, color: theme.colorScheme.onSurface), + textAlign: TextAlign.center, + ), + const SizedBox(height: Spacing.sm), + Text( + 'We sent a password reset link to $email', + style: theme.textTheme.bodyMedium?.copyWith(color: theme.colorScheme.onSurfaceVariant), + textAlign: TextAlign.center, + ), + const SizedBox(height: Spacing.sm), + Text( + "If you don't see the email, check your spam folder.", + style: theme.textTheme.bodyMedium?.copyWith(color: theme.colorScheme.onSurfaceVariant), + textAlign: TextAlign.center, + ), + ], + ); + } +} + +class _SetNewPasswordForm extends StatelessWidget { final GlobalKey formKey; - final TextEditingController tokenController; final TextEditingController passwordController; final TextEditingController confirmPasswordController; - final FocusNode tokenFocusNode; final FocusNode passwordFocusNode; final FocusNode confirmPasswordFocusNode; final bool isLoading; @@ -280,13 +331,10 @@ class _EmailSentConfirmation extends StatelessWidget { final String? Function(String?) validatePasswordStrength; final String? Function(String?) validateConfirmPassword; - const _EmailSentConfirmation({ - required this.email, + const _SetNewPasswordForm({ required this.formKey, - required this.tokenController, required this.passwordController, required this.confirmPasswordController, - required this.tokenFocusNode, required this.passwordFocusNode, required this.confirmPasswordFocusNode, required this.isLoading, @@ -296,6 +344,42 @@ class _EmailSentConfirmation extends StatelessWidget { required this.validateConfirmPassword, }); + @override + Widget build(BuildContext context) { + return Form( + key: formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.min, + children: [ + PasswordInput( + labelText: 'New password', + controller: passwordController, + focusNode: passwordFocusNode, + textInputAction: TextInputAction.next, + extraValidator: validatePasswordStrength, + onEditingComplete: () => confirmPasswordFocusNode.requestFocus(), + ), + const SizedBox(height: Spacing.md), + PasswordInput( + labelText: 'Confirm new password', + controller: confirmPasswordController, + focusNode: confirmPasswordFocusNode, + textInputAction: TextInputAction.done, + extraValidator: validateConfirmPassword, + onFieldSubmitted: (_) => onSubmit(), + ), + const SizedBox(height: Spacing.lg), + AuthContinueButton(isLoading: isLoading, onPressed: onSubmit, isDesktop: isDesktop), + ], + ), + ); + } +} + +class _InvalidResetLink extends StatelessWidget { + const _InvalidResetLink(); + @override Widget build(BuildContext context) { final theme = Theme.of(context); @@ -304,68 +388,21 @@ class _EmailSentConfirmation extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisSize: MainAxisSize.min, children: [ - Icon(Icons.mark_email_read_outlined, size: 72, color: theme.colorScheme.primary), + Icon(Icons.link_off_outlined, size: 72, color: theme.colorScheme.error), const SizedBox(height: Spacing.sm), Text( - 'Check your email', + 'Invalid reset link', style: theme.textTheme.titleLarge?.copyWith(fontWeight: FontWeight.w600, color: theme.colorScheme.onSurface), textAlign: TextAlign.center, ), const SizedBox(height: Spacing.sm), Text( - 'We sent a password reset token to $email', - style: theme.textTheme.bodyMedium?.copyWith(color: theme.colorScheme.onSurfaceVariant), - textAlign: TextAlign.center, - ), - const SizedBox(height: Spacing.sm), - Text( - "If you don't see the email, check your spam folder.", + 'Request a new password reset email and use the latest link.', style: theme.textTheme.bodyMedium?.copyWith(color: theme.colorScheme.onSurfaceVariant), textAlign: TextAlign.center, ), const SizedBox(height: Spacing.lg), - Form( - key: formKey, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - TextFormField( - controller: tokenController, - focusNode: tokenFocusNode, - textInputAction: TextInputAction.next, - decoration: const InputDecoration(labelText: 'Reset token'), - validator: (value) { - if (value == null || value.trim().isEmpty) { - return 'Reset token is required'; - } - - return null; - }, - onEditingComplete: () => passwordFocusNode.requestFocus(), - ), - const SizedBox(height: Spacing.md), - PasswordInput( - labelText: 'New password', - controller: passwordController, - focusNode: passwordFocusNode, - textInputAction: TextInputAction.next, - extraValidator: validatePasswordStrength, - onEditingComplete: () => confirmPasswordFocusNode.requestFocus(), - ), - const SizedBox(height: Spacing.md), - PasswordInput( - labelText: 'Confirm new password', - controller: confirmPasswordController, - focusNode: confirmPasswordFocusNode, - textInputAction: TextInputAction.done, - extraValidator: validateConfirmPassword, - onFieldSubmitted: (_) => onSubmit(), - ), - const SizedBox(height: Spacing.lg), - AuthContinueButton(isLoading: isLoading, onPressed: onSubmit, isDesktop: isDesktop), - ], - ), - ), + FilledButton(onPressed: () => context.go('/forgot-password'), child: const Text('Request new link')), ], ); } diff --git a/app/lib/pages/login_page.dart b/app/lib/pages/login_page.dart index 773dd1a..21277f7 100644 --- a/app/lib/pages/login_page.dart +++ b/app/lib/pages/login_page.dart @@ -55,7 +55,7 @@ class _LoginPageState extends State { try { final success = await context.read().login( email: _emailController.text.trim(), - password: _passwordController.text.trim(), + password: _passwordController.text, ); if (!mounted) { diff --git a/app/lib/pages/register_page.dart b/app/lib/pages/register_page.dart index 2846fc8..3e6e43e 100644 --- a/app/lib/pages/register_page.dart +++ b/app/lib/pages/register_page.dart @@ -64,7 +64,7 @@ class _RegisterPageState extends State { try { final success = await context.read().register( email: _emailController.text.trim(), - password: _passwordController.text.trim(), + password: _passwordController.text, displayName: _displayNameController.text.trim(), ); diff --git a/app/lib/widgets/auth/auth_switch_link.dart b/app/lib/widgets/auth/auth_switch_link.dart index 80fba06..fce69e9 100644 --- a/app/lib/widgets/auth/auth_switch_link.dart +++ b/app/lib/widgets/auth/auth_switch_link.dart @@ -17,8 +17,9 @@ class AuthSwitchLink extends StatelessWidget { Widget build(BuildContext context) { final theme = Theme.of(context); - return Row( - mainAxisAlignment: MainAxisAlignment.center, + return Wrap( + alignment: WrapAlignment.center, + crossAxisAlignment: WrapCrossAlignment.center, children: [ Text(promptText, style: theme.textTheme.bodyMedium?.copyWith(color: theme.colorScheme.onSurfaceVariant)), TextButton( diff --git a/app/lib/widgets/buttons/google_sign_in.dart b/app/lib/widgets/buttons/google_sign_in.dart index dc2a875..cba8b95 100644 --- a/app/lib/widgets/buttons/google_sign_in.dart +++ b/app/lib/widgets/buttons/google_sign_in.dart @@ -96,12 +96,19 @@ class _GoogleSignInButtonState extends State { ), onPressed: _handleSignIn, child: Row( - mainAxisSize: MainAxisSize.min, + mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.center, children: [ const Image(image: AssetImage('assets/images/google_logo.png'), height: 24.0), const SizedBox(width: Spacing.sm), - Text(widget.title, style: theme.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w500)), + Flexible( + child: Text( + widget.title, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: theme.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w500), + ), + ), ], ), ); diff --git a/app/test/config/app_router_test.dart b/app/test/config/app_router_test.dart index bc91b84..cb04046 100644 --- a/app/test/config/app_router_test.dart +++ b/app/test/config/app_router_test.dart @@ -55,6 +55,7 @@ void main() { expect(appRouter.redirectForPath('/library/books'), '/'); expect(appRouter.redirectForPath('/login'), isNull); + expect(appRouter.redirectForPath('/reset-password'), isNull); }); test('redirects signed-in users away from auth routes', () async { @@ -67,6 +68,7 @@ void main() { final appRouter = AppRouter(authProvider: provider); expect(appRouter.redirectForPath('/login'), '/library/books'); + expect(appRouter.redirectForPath('/reset-password'), '/library/books'); expect(appRouter.redirectForPath('/library/books'), isNull); }); diff --git a/app/test/pages/auth_password_submission_test.dart b/app/test/pages/auth_password_submission_test.dart new file mode 100644 index 0000000..5a35049 --- /dev/null +++ b/app/test/pages/auth_password_submission_test.dart @@ -0,0 +1,117 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:papyrus/auth/auth_api_client.dart'; +import 'package:papyrus/auth/auth_models.dart'; +import 'package:papyrus/auth/auth_repository.dart'; +import 'package:papyrus/auth/papyrus_api_config.dart'; +import 'package:papyrus/auth/token_store.dart'; +import 'package:papyrus/pages/login_page.dart'; +import 'package:papyrus/pages/register_page.dart'; +import 'package:papyrus/providers/auth_provider.dart'; +import 'package:provider/provider.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class MemoryRefreshTokenStorage implements RefreshTokenStorage { + String? value; + + @override + Future delete() async { + value = null; + } + + @override + Future read() async => value; + + @override + Future write(String refreshToken) async { + value = refreshToken; + } +} + +class CapturingAuthRepository extends AuthRepository { + CapturingAuthRepository() + : super( + apiClient: AuthApiClient(config: PapyrusApiConfig(serverBaseUri: Uri.parse('http://server.test'))), + tokenStore: TokenStore(MemoryRefreshTokenStorage()), + ); + + String? loginPassword; + String? registerPassword; + + @override + Future login({ + required String email, + required String password, + required String clientType, + String? deviceLabel, + }) async { + loginPassword = password; + throw const AuthApiException(statusCode: 401, message: 'Incorrect email or password.'); + } + + @override + Future register({ + required String email, + required String password, + required String displayName, + required String clientType, + String? deviceLabel, + }) async { + registerPassword = password; + throw const AuthApiException(statusCode: 409, message: 'Registration failed.'); + } +} + +void main() { + setUp(() { + SharedPreferences.setMockInitialValues({}); + }); + + void setViewport(WidgetTester tester) { + tester.view.devicePixelRatio = 1; + tester.view.physicalSize = const Size(390, 844); + addTearDown(tester.view.resetPhysicalSize); + addTearDown(tester.view.resetDevicePixelRatio); + } + + Future pumpAuthPage(WidgetTester tester, Widget page) async { + final prefs = await SharedPreferences.getInstance(); + final repository = CapturingAuthRepository(); + final provider = AuthProvider(prefs, repository: repository, bootstrapOnCreate: false); + + await tester.pumpWidget( + ChangeNotifierProvider( + create: (_) => provider, + child: MaterialApp(home: page), + ), + ); + + return repository; + } + + testWidgets('login submits password exactly as typed', (tester) async { + setViewport(tester); + final repository = await pumpAuthPage(tester, const LoginPage()); + + await tester.enterText(find.widgetWithText(TextFormField, 'Email address'), 'reader@example.com'); + await tester.enterText(find.widgetWithText(TextFormField, 'Password'), ' NewSecureP@ss123 '); + await tester.tap(find.text('Continue')); + await tester.pump(); + + expect(repository.loginPassword, ' NewSecureP@ss123 '); + }); + + testWidgets('register submits password exactly as typed', (tester) async { + setViewport(tester); + final repository = await pumpAuthPage(tester, const RegisterPage()); + + await tester.enterText(find.widgetWithText(TextFormField, 'Display name'), 'Reader'); + await tester.enterText(find.widgetWithText(TextFormField, 'Email address'), 'reader@example.com'); + await tester.enterText(find.widgetWithText(TextFormField, 'Password'), ' NewSecureP@ss123 '); + await tester.enterText(find.widgetWithText(TextFormField, 'Confirm password'), ' NewSecureP@ss123 '); + await tester.tap(find.text('Continue')); + await tester.pump(); + + expect(repository.registerPassword, ' NewSecureP@ss123 '); + }); +} diff --git a/app/test/pages/forgot_password_page_test.dart b/app/test/pages/forgot_password_page_test.dart new file mode 100644 index 0000000..2b5971a --- /dev/null +++ b/app/test/pages/forgot_password_page_test.dart @@ -0,0 +1,121 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:papyrus/auth/auth_api_client.dart'; +import 'package:papyrus/auth/auth_repository.dart'; +import 'package:papyrus/auth/papyrus_api_config.dart'; +import 'package:papyrus/auth/token_store.dart'; +import 'package:papyrus/pages/forgot_password_page.dart'; +import 'package:papyrus/providers/auth_provider.dart'; +import 'package:provider/provider.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class MemoryRefreshTokenStorage implements RefreshTokenStorage { + String? value; + + @override + Future delete() async { + value = null; + } + + @override + Future read() async => value; + + @override + Future write(String refreshToken) async { + value = refreshToken; + } +} + +class FakeAuthRepository extends AuthRepository { + FakeAuthRepository() + : super( + apiClient: AuthApiClient(config: PapyrusApiConfig(serverBaseUri: Uri.parse('http://server.test'))), + tokenStore: TokenStore(MemoryRefreshTokenStorage()), + ); + + String? forgotPasswordEmail; + String? resetToken; + String? resetPasswordValue; + + @override + Future forgotPassword(String email) async { + forgotPasswordEmail = email; + return 'If the email is registered, a reset link has been sent'; + } + + @override + Future resetPassword({required String token, required String password}) async { + resetToken = token; + resetPasswordValue = password; + return 'Password has been reset successfully'; + } +} + +void main() { + setUp(() { + SharedPreferences.setMockInitialValues({}); + }); + + void setViewport(WidgetTester tester, Size size) { + tester.view.devicePixelRatio = 1; + tester.view.physicalSize = size; + addTearDown(tester.view.resetPhysicalSize); + addTearDown(tester.view.resetDevicePixelRatio); + } + + Future pumpPage(WidgetTester tester, {String? resetToken, bool isResetLink = false}) async { + final prefs = await SharedPreferences.getInstance(); + final repository = FakeAuthRepository(); + final provider = AuthProvider(prefs, repository: repository, bootstrapOnCreate: false); + + await tester.pumpWidget( + ChangeNotifierProvider( + create: (_) => provider, + child: MaterialApp( + home: ForgotPasswordPage(resetToken: resetToken, isResetLink: isResetLink), + ), + ), + ); + + return repository; + } + + testWidgets('forgot password request shows check email state without token field', (tester) async { + setViewport(tester, const Size(390, 844)); + final repository = await pumpPage(tester); + + await tester.enterText(find.byType(TextFormField), 'reader@example.com'); + await tester.tap(find.text('Continue')); + await tester.pump(); + + expect(repository.forgotPasswordEmail, 'reader@example.com'); + expect(find.text('Check your email'), findsOneWidget); + expect(find.text('We sent a password reset link to reader@example.com'), findsOneWidget); + expect(find.text('Reset token'), findsNothing); + expect(find.text('New password'), findsNothing); + }); + + testWidgets('reset link page submits URL token with new password', (tester) async { + setViewport(tester, const Size(390, 844)); + final repository = await pumpPage(tester, resetToken: 'reset-token-123', isResetLink: true); + + await tester.enterText(find.widgetWithText(TextFormField, 'New password'), ' NewSecureP@ss123 '); + await tester.enterText(find.widgetWithText(TextFormField, 'Confirm new password'), ' NewSecureP@ss123 '); + await tester.tap(find.text('Continue')); + await tester.pump(); + + expect(repository.resetToken, 'reset-token-123'); + expect(repository.resetPasswordValue, ' NewSecureP@ss123 '); + expect(find.text('Password reset'), findsOneWidget); + }); + + testWidgets('reset link page shows invalid state without token', (tester) async { + setViewport(tester, const Size(390, 844)); + await pumpPage(tester, isResetLink: true); + + expect(find.text('Invalid reset link'), findsOneWidget); + expect(find.text('Request new link'), findsOneWidget); + expect(find.text('Reset token'), findsNothing); + expect(find.text('New password'), findsNothing); + }); +} From 41ba83e037e444a789238c2633667860c86ccc82 Mon Sep 17 00:00:00 2001 From: Eoic Date: Thu, 25 Jun 2026 00:20:52 +0300 Subject: [PATCH 14/16] feat: update environment configuration for local testing --- README.md | 8 +- .../powersync_books_integration_test.dart | 121 +++++++ app/lib/data/data_store.dart | 70 +++-- .../data/repositories/book_repository.dart | 47 +++ app/lib/main.dart | 26 +- app/lib/pages/profile_page.dart | 36 ++- app/lib/powersync/papyrus_schema.dart | 67 ++-- app/lib/powersync/powersync_service.dart | 295 +++++++++++++----- app/lib/powersync/sync_state.dart | 23 ++ app/pubspec.lock | 55 +++- app/pubspec.yaml | 4 +- .../data/book_repository_data_store_test.dart | 71 +++++ app/test/data/data_store_sync_test.dart | 57 ---- .../powersync/papyrus_schema_mode_test.dart | 18 ++ .../powersync/powersync_service_test.dart | 70 +++++ 15 files changed, 743 insertions(+), 225 deletions(-) create mode 100644 app/integration_test/powersync_books_integration_test.dart create mode 100644 app/lib/data/repositories/book_repository.dart create mode 100644 app/lib/powersync/sync_state.dart create mode 100644 app/test/data/book_repository_data_store_test.dart delete mode 100644 app/test/data/data_store_sync_test.dart create mode 100644 app/test/powersync/papyrus_schema_mode_test.dart create mode 100644 app/test/powersync/powersync_service_test.dart diff --git a/README.md b/README.md index a237bb6..62a734f 100644 --- a/README.md +++ b/README.md @@ -94,10 +94,16 @@ When the user signs in, the Flutter client communicates with the Papyrus server Run the server and PowerSync locally, then start Flutter with: ```bash -cd app +cd ../server +./scripts/bootstrap_local.sh +cd ../client/app flutter run -d chrome --web-hostname papyrus.localhost --web-port 3000 --dart-define-from-file=.dart_defines ``` +Authenticated books use `papyrus-account.db` and synchronize through +PowerSync. Guest mode uses the separate local-only `papyrus-guest.db`; guest +books remain on that device and are not merged into an account. + For local web auth links, add these entries to `/etc/hosts`: ```text diff --git a/app/integration_test/powersync_books_integration_test.dart b/app/integration_test/powersync_books_integration_test.dart new file mode 100644 index 0000000..29f791a --- /dev/null +++ b/app/integration_test/powersync_books_integration_test.dart @@ -0,0 +1,121 @@ +import 'dart:io'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:papyrus/auth/auth_api_client.dart'; +import 'package:papyrus/auth/auth_repository.dart'; +import 'package:papyrus/auth/papyrus_api_config.dart'; +import 'package:papyrus/auth/token_store.dart'; +import 'package:papyrus/models/book.dart'; +import 'package:papyrus/powersync/papyrus_powersync_connector.dart'; +import 'package:papyrus/powersync/powersync_service.dart'; +import 'package:path/path.dart' as path; +import 'package:uuid/uuid.dart'; + +const runIntegration = bool.fromEnvironment('RUN_POWERSYNC_INTEGRATION'); + +class MemoryRefreshTokenStorage implements RefreshTokenStorage { + String? value; + + @override + Future delete() async => value = null; + + @override + Future read() async => value; + + @override + Future write(String refreshToken) async => value = refreshToken; +} + +Future waitForBook(PapyrusPowerSyncService service, String id, {bool present = true, String? title}) async { + final deadline = DateTime.now().add(const Duration(seconds: 20)); + while (DateTime.now().isBefore(deadline)) { + final book = await service.getById(id); + if ((book != null) == present && (title == null || book?.title == title)) { + return; + } + await Future.delayed(const Duration(milliseconds: 200)); + } + fail('Book $id did not reach expected presence=$present'); +} + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + testWidgets('two clients sync books, reconnect offline writes, and isolate users', (tester) async { + final config = PapyrusApiConfig.fromEnvironment(); + final email = 'powersync-${DateTime.now().microsecondsSinceEpoch}@example.com'; + const password = 'SecureP@ss123'; + final root = await Directory.systemTemp.createTemp('papyrus-e2e-'); + + AuthRepository repository(String label) { + return AuthRepository( + apiClient: AuthApiClient(config: config), + tokenStore: TokenStore(MemoryRefreshTokenStorage()), + ); + } + + PapyrusPowerSyncService service(AuthRepository repository, String label) { + return PapyrusPowerSyncService( + connectorFactory: () => PapyrusPowerSyncConnector(authRepository: repository, config: config), + pathResolver: (mode) async => path.join(root.path, '$label-${mode.name}.db'), + ); + } + + final firstAuth = repository('first'); + final firstTokens = await firstAuth.register( + email: email, + password: password, + displayName: 'PowerSync Test', + clientType: 'desktop', + ); + final secondAuth = repository('second'); + await secondAuth.login(email: email, password: password, clientType: 'desktop'); + final otherAuth = repository('other'); + final otherTokens = await otherAuth.register( + email: 'other-$email', + password: password, + displayName: 'Other User', + clientType: 'desktop', + ); + + final first = service(firstAuth, 'first'); + final second = service(secondAuth, 'second'); + final other = service(otherAuth, 'other'); + + try { + await Future.wait([ + first.activateAuthenticated(firstTokens.user.userId), + second.activateAuthenticated(firstTokens.user.userId), + other.activateAuthenticated(otherTokens.user.userId), + ]); + await Future.wait([ + first.syncStates.firstWhere((state) => state.connected), + second.syncStates.firstWhere((state) => state.connected), + other.syncStates.firstWhere((state) => state.connected), + ]).timeout(const Duration(seconds: 20)); + + final book = Book( + id: const Uuid().v4(), + title: 'Realtime book', + author: 'Papyrus', + addedAt: DateTime.now().toUtc(), + ); + await first.upsert(book); + await waitForBook(second, book.id, title: book.title); + expect(await other.getById(book.id), isNull); + + await first.setOnline(false); + await first.upsert(book.copyWith(title: 'Offline edit')); + await first.setOnline(true); + await waitForBook(second, book.id, title: 'Offline edit'); + expect((await second.getById(book.id))?.title, 'Offline edit'); + + await second.delete(book.id); + await waitForBook(first, book.id, present: false); + } finally { + await Future.wait([first.close(), second.close(), other.close()]); + await root.delete(recursive: true); + } + }, skip: !runIntegration); +} diff --git a/app/lib/data/data_store.dart b/app/lib/data/data_store.dart index cd3ae98..4deb034 100644 --- a/app/lib/data/data_store.dart +++ b/app/lib/data/data_store.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; +import 'package:papyrus/data/repositories/book_repository.dart'; import 'package:papyrus/models/annotation.dart'; import 'package:papyrus/models/book.dart'; import 'package:papyrus/models/book_shelf_relation.dart'; @@ -13,15 +14,15 @@ import 'package:papyrus/models/series.dart'; import 'package:papyrus/models/shelf.dart'; import 'package:papyrus/models/tag.dart'; -abstract class BookSyncWriter { - Future upsertBook(Book book); - - Future deleteBook(String id); -} - /// Central in-memory data store - the single source of truth. /// All repositories read from and write to this store. class DataStore extends ChangeNotifier { + DataStore({BookRepository? bookRepository}) { + final repository = bookRepository ?? InMemoryBookRepository(); + _bookRepository = repository; + _bookSubscription = repository.watchAll().listen(replaceBooksFromSync); + } + // Primary data collections (keyed by ID) final Map _books = {}; final Map _shelves = {}; @@ -38,7 +39,8 @@ class DataStore extends ChangeNotifier { final List _bookTagRelations = []; bool _isLoaded = false; - BookSyncWriter? _bookSyncWriter; + BookRepository? _bookRepository; + StreamSubscription>? _bookSubscription; // ============================================================ // Getters for read access @@ -71,33 +73,47 @@ class DataStore extends ChangeNotifier { Book? getBook(String id) => _books[id]; - void attachBookSyncWriter(BookSyncWriter? writer) { - _bookSyncWriter = writer; + Future attachBookRepository(BookRepository repository) async { + await _bookSubscription?.cancel(); + _bookRepository = repository; + _bookSubscription = repository.watchAll().listen( + replaceBooksFromSync, + onError: (Object error, StackTrace stackTrace) { + FlutterError.reportError( + FlutterErrorDetails(exception: error, stack: stackTrace, library: 'papyrus book repository'), + ); + }, + ); + } + + Future disposeBookRepository() async { + await _bookSubscription?.cancel(); + _bookSubscription = null; + _bookRepository = null; } void addBook(Book book) { - _books[book.id] = book; - unawaited(_bookSyncWriter?.upsertBook(book)); - notifyListeners(); + final repository = _bookRepository; + if (repository == null) { + throw StateError('Book repository is not initialized'); + } + unawaited(repository.upsert(book)); } void updateBook(Book book) { - _books[book.id] = book; - unawaited(_bookSyncWriter?.upsertBook(book)); - notifyListeners(); + final repository = _bookRepository; + if (repository == null) { + throw StateError('Book repository is not initialized'); + } + unawaited(repository.upsert(book)); } void deleteBook(String id) { - _books.remove(id); - // Also remove related data - _bookShelfRelations.removeWhere((r) => r.bookId == id); - _bookTagRelations.removeWhere((r) => r.bookId == id); - _annotations.removeWhere((key, a) => a.bookId == id); - _notes.removeWhere((key, n) => n.bookId == id); - _bookmarks.removeWhere((key, b) => b.bookId == id); - _readingSessions.removeWhere((key, s) => s.bookId == id); - unawaited(_bookSyncWriter?.deleteBook(id)); - notifyListeners(); + final repository = _bookRepository; + if (repository == null) { + throw StateError('Book repository is not initialized'); + } + unawaited(repository.delete(id)); } void replaceBooksFromSync(List books) { @@ -449,6 +465,10 @@ class DataStore extends ChangeNotifier { for (final book in books) { _books[book.id] = book; } + final repository = _bookRepository; + if (repository is InMemoryBookRepository) { + repository.replaceAll(books); + } } if (shelves != null) { _shelves.clear(); diff --git a/app/lib/data/repositories/book_repository.dart b/app/lib/data/repositories/book_repository.dart new file mode 100644 index 0000000..506b63e --- /dev/null +++ b/app/lib/data/repositories/book_repository.dart @@ -0,0 +1,47 @@ +import 'dart:async'; + +import 'package:papyrus/models/book.dart'; + +abstract interface class BookRepository { + Stream> watchAll(); + + Future getById(String id); + + Future upsert(Book book); + + Future delete(String id); +} + +class InMemoryBookRepository implements BookRepository { + final Map _books = {}; + final StreamController> _changes = StreamController>.broadcast(sync: true); + + @override + Stream> watchAll() async* { + yield _snapshot; + yield* _changes.stream; + } + + @override + Future getById(String id) async => _books[id]; + + @override + Future upsert(Book book) async { + _books[book.id] = book; + _changes.add(_snapshot); + } + + @override + Future delete(String id) async { + _books.remove(id); + _changes.add(_snapshot); + } + + void replaceAll(Iterable books) { + _books + ..clear() + ..addEntries(books.map((book) => MapEntry(book.id, book))); + } + + List get _snapshot => List.unmodifiable(_books.values); +} diff --git a/app/lib/main.dart b/app/lib/main.dart index e9c5f04..8a24d4a 100644 --- a/app/lib/main.dart +++ b/app/lib/main.dart @@ -8,6 +8,8 @@ import 'package:papyrus/auth/papyrus_api_config.dart'; import 'package:papyrus/auth/token_store.dart'; import 'package:papyrus/data/data_store.dart'; import 'package:papyrus/powersync/powersync_service.dart'; +import 'package:papyrus/powersync/papyrus_powersync_connector.dart'; +import 'package:papyrus/powersync/sync_state.dart'; import 'package:papyrus/providers/auth_provider.dart'; import 'package:papyrus/providers/library_provider.dart'; import 'package:papyrus/providers/preferences_provider.dart'; @@ -55,10 +57,9 @@ class _PapyrusState extends State { _dataStore = DataStore(); _authProvider = AuthProvider(widget.prefs, repository: authRepository); _powerSyncService = PapyrusPowerSyncService( - authRepository: authRepository, - config: apiConfig, - dataStore: _dataStore, + connectorFactory: () => PapyrusPowerSyncConnector(authRepository: authRepository, config: apiConfig), ); + unawaited(_dataStore.attachBookRepository(_powerSyncService)); _appRouter = AppRouter(authProvider: _authProvider); _authProvider.addListener(_syncPowerSyncAuthState); _syncPowerSyncAuthState(); @@ -67,24 +68,31 @@ class _PapyrusState extends State { @override void dispose() { _authProvider.removeListener(_syncPowerSyncAuthState); - unawaited(_powerSyncService.close()); + unawaited(_disposeDataServices()); _authProvider.dispose(); super.dispose(); } + Future _disposeDataServices() async { + await _dataStore.disposeBookRepository(); + await _powerSyncService.close(); + } + void _syncPowerSyncAuthState() { - if (_authProvider.isSignedIn) { - unawaited(_powerSyncService.connect()); + final user = _authProvider.user; + if (user != null && !_authProvider.isOfflineMode) { + final userId = user.userId; + unawaited(_powerSyncService.activateAuthenticated(userId)); return; } if (_authProvider.isOfflineMode) { - unawaited(_powerSyncService.showOfflineSampleData()); + unawaited(_powerSyncService.activateGuest()); return; } if (!_authProvider.isBootstrapping) { - unawaited(_powerSyncService.disconnectAndClear()); + unawaited(_powerSyncService.deactivate()); } } @@ -94,6 +102,8 @@ class _PapyrusState extends State { providers: [ // Core data store - single source of truth ChangeNotifierProvider.value(value: _dataStore), + Provider.value(value: _powerSyncService), + StreamProvider.value(value: _powerSyncService.syncStates, initialData: _powerSyncService.syncState), // Auth and UI state providers ChangeNotifierProvider.value(value: _authProvider), ChangeNotifierProvider(create: (_) => SidebarProvider()), diff --git a/app/lib/pages/profile_page.dart b/app/lib/pages/profile_page.dart index e46bb9b..c89a995 100644 --- a/app/lib/pages/profile_page.dart +++ b/app/lib/pages/profile_page.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:papyrus/providers/auth_provider.dart'; import 'package:papyrus/providers/preferences_provider.dart'; +import 'package:papyrus/powersync/sync_state.dart'; import 'package:papyrus/themes/design_tokens.dart'; import 'package:papyrus/widgets/settings/settings_row.dart'; import 'package:papyrus/widgets/settings/settings_section.dart'; @@ -195,6 +196,8 @@ class _ProfilePageState extends State { Widget _buildMobileStorageSyncSection(BuildContext context) { final prefs = context.watch(); + final auth = context.watch(); + final sync = context.watch(); return Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -206,6 +209,7 @@ class _ProfilePageState extends State { value: prefs.serverUrl.isEmpty ? 'Not connected' : prefs.serverUrl, onTap: () {}, ), + SettingsRow(label: 'Current status', value: _syncStatusLabel(auth, sync)), SettingsToggleRow( label: 'Sync enabled', value: prefs.syncEnabled, @@ -886,6 +890,9 @@ class _ProfilePageState extends State { final colorScheme = Theme.of(context).colorScheme; final textTheme = Theme.of(context).textTheme; final prefs = context.watch(); + final auth = context.watch(); + final sync = context.watch(); + final connected = sync.connected; return Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -930,13 +937,13 @@ class _ProfilePageState extends State { Container( padding: const EdgeInsets.symmetric(horizontal: Spacing.sm, vertical: Spacing.xs), decoration: BoxDecoration( - color: prefs.serverUrl.isEmpty ? colorScheme.errorContainer : colorScheme.primaryContainer, + color: connected ? colorScheme.primaryContainer : colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(AppRadius.sm), ), child: Text( - prefs.serverUrl.isEmpty ? 'Offline' : 'Connected', + _syncStatusLabel(auth, sync), style: textTheme.labelSmall?.copyWith( - color: prefs.serverUrl.isEmpty ? colorScheme.onErrorContainer : colorScheme.onPrimaryContainer, + color: connected ? colorScheme.onPrimaryContainer : colorScheme.onSurfaceVariant, ), ), ), @@ -981,10 +988,7 @@ class _ProfilePageState extends State { const SizedBox(height: Spacing.md), Padding( padding: const EdgeInsets.symmetric(horizontal: Spacing.sm, vertical: Spacing.xs), - child: Text( - 'Last sync: 2 min ago', - style: textTheme.bodySmall?.copyWith(color: colorScheme.onSurfaceVariant), - ), + child: Text(_syncDetail(sync), style: textTheme.bodySmall?.copyWith(color: colorScheme.onSurfaceVariant)), ), const SizedBox(height: Spacing.sm), Align( @@ -997,6 +1001,24 @@ class _ProfilePageState extends State { ); } + String _syncStatusLabel(AuthProvider auth, SyncState sync) { + if (auth.isOfflineMode) return 'Guest local'; + if (sync.uploadError != null || sync.downloadError != null) return 'Error'; + if (sync.connecting) return 'Connecting'; + if (sync.uploading || sync.downloading) return 'Syncing'; + if (sync.connected) return sync.hasPendingWrites ? 'Pending upload' : 'Connected'; + return 'Offline'; + } + + String _syncDetail(SyncState sync) { + final error = sync.uploadError ?? sync.downloadError; + if (error != null) return 'Sync error: $error'; + if (sync.hasPendingWrites) return 'Local changes are waiting to upload'; + final lastSyncedAt = sync.lastSyncedAt; + if (lastSyncedAt == null) return 'No completed sync yet'; + return 'Last sync: ${lastSyncedAt.toLocal()}'; + } + // -- Privacy & data --------------------------------------------------------- Widget _buildPrivacyDataContent(BuildContext context) { diff --git a/app/lib/powersync/papyrus_schema.dart b/app/lib/powersync/papyrus_schema.dart index a6e14be..a289a00 100644 --- a/app/lib/powersync/papyrus_schema.dart +++ b/app/lib/powersync/papyrus_schema.dart @@ -1,34 +1,37 @@ import 'package:powersync/powersync.dart'; -const papyrusPowerSyncSchema = Schema([ - Table( - 'books', - [ - Column.text('owner_user_id'), - Column.text('title'), - Column.text('subtitle'), - Column.text('author'), - Column.text('co_authors'), - Column.text('isbn'), - Column.text('isbn13'), - Column.text('publisher'), - Column.text('language'), - Column.integer('page_count'), - Column.text('description'), - Column.text('cover_image_url'), - Column.text('reading_status'), - Column.integer('current_page'), - Column.real('current_position'), - Column.text('current_cfi'), - Column.integer('is_favorite'), - Column.integer('rating'), - Column.text('custom_metadata'), - Column.text('added_at'), - Column.text('updated_at'), - ], - indexes: [ - Index('books_added_at', [IndexedColumn('added_at')]), - Index('books_title', [IndexedColumn('title')]), - ], - ), -]); +const _bookColumns = [ + Column.text('owner_user_id'), + Column.text('title'), + Column.text('subtitle'), + Column.text('author'), + Column.text('co_authors'), + Column.text('isbn'), + Column.text('isbn13'), + Column.text('publisher'), + Column.text('language'), + Column.integer('page_count'), + Column.text('description'), + Column.text('cover_image_url'), + Column.text('reading_status'), + Column.integer('current_page'), + Column.real('current_position'), + Column.text('current_cfi'), + Column.integer('is_favorite'), + Column.integer('rating'), + Column.text('custom_metadata'), + Column.text('added_at'), + Column.text('updated_at'), +]; + +const _bookIndexes = [ + Index('books_added_at', [IndexedColumn('added_at')]), + Index('books_title', [IndexedColumn('title')]), +]; + +const papyrusAccountSchema = Schema([Table('books', _bookColumns, indexes: _bookIndexes)]); + +const papyrusGuestSchema = Schema([Table.localOnly('books', _bookColumns, indexes: _bookIndexes)]); + +@Deprecated('Use papyrusAccountSchema') +const papyrusPowerSyncSchema = papyrusAccountSchema; diff --git a/app/lib/powersync/powersync_service.dart b/app/lib/powersync/powersync_service.dart index 787a442..0fa3574 100644 --- a/app/lib/powersync/powersync_service.dart +++ b/app/lib/powersync/powersync_service.dart @@ -1,133 +1,148 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; -import 'package:papyrus/auth/auth_repository.dart'; -import 'package:papyrus/auth/papyrus_api_config.dart'; -import 'package:papyrus/data/data_store.dart'; -import 'package:papyrus/data/sample_data.dart'; +import 'package:papyrus/data/repositories/book_repository.dart'; import 'package:papyrus/models/book.dart'; -import 'package:papyrus/powersync/papyrus_powersync_connector.dart'; import 'package:papyrus/powersync/papyrus_schema.dart'; import 'package:papyrus/powersync/powersync_book_mapper.dart'; +import 'package:papyrus/powersync/sync_state.dart'; import 'package:path/path.dart' as path; import 'package:path_provider/path_provider.dart'; import 'package:powersync/powersync.dart'; -class PapyrusPowerSyncService implements BookSyncWriter { - final AuthRepository authRepository; - final PapyrusApiConfig config; - final DataStore dataStore; +typedef PowerSyncConnectorFactory = PowerSyncBackendConnector Function(); +typedef LibraryDatabasePathResolver = Future Function(LibraryDatabaseMode mode); + +class PapyrusPowerSyncService implements BookRepository { + final PowerSyncConnectorFactory connectorFactory; + final LibraryDatabasePathResolver? pathResolver; + final bool connectAuthenticated; + + final StreamController> _booksController = StreamController>.broadcast(); + final StreamController _syncStateController = StreamController.broadcast(); PowerSyncDatabase? _database; StreamSubscription? _booksSubscription; - Future? _connectOperation; - bool _isConnected = false; + StreamSubscription? _statusSubscription; + Future? _modeOperation; + LibraryDatabaseMode? _mode; + String? _authenticatedUserId; + SyncState _syncState = const SyncState(); - PapyrusPowerSyncService({required this.authRepository, required this.config, required this.dataStore}); + PapyrusPowerSyncService({required this.connectorFactory, this.pathResolver, this.connectAuthenticated = true}); - Future connect() { - if (_isConnected) { - return Future.value(); - } + LibraryDatabaseMode? get mode => _mode; + SyncState get syncState => _syncState; + Stream get syncStates => _syncStateController.stream; - _connectOperation ??= _connect().whenComplete(() { - _connectOperation = null; - }); + Future activateGuest() => _switchMode(LibraryDatabaseMode.guest); - return _connectOperation!; - } - - Future showOfflineSampleData() async { - await disconnectAndClear(); - dataStore.loadData( - books: SampleData.books, - shelves: SampleData.shelves, - tags: SampleData.tags, - series: SampleData.seriesList, - annotations: SampleData.annotations, - notes: SampleData.notes, - bookmarks: SampleData.bookmarks, - readingSessions: SampleData.readingSessions, - readingGoals: SampleData.readingGoals, - bookShelfRelations: SampleData.bookShelfRelations, - bookTagRelations: SampleData.bookTagRelations, - ); + Future activateAuthenticated(String userId) { + return _switchMode(LibraryDatabaseMode.authenticated, authenticatedUserId: userId); } - Future disconnectAndClear() async { - _isConnected = false; - dataStore.attachBookSyncWriter(null); - await _booksSubscription?.cancel(); - _booksSubscription = null; - - final database = _database; - - if (database != null) { - await database.disconnectAndClear(); + Future setOnline(bool online) async { + await _modeOperation; + if (_mode != LibraryDatabaseMode.authenticated) { + throw StateError('Only authenticated libraries can connect to PowerSync'); } + final database = _requireDatabase(); + if (online) { + await database.connect(connector: connectorFactory()); + } else { + await database.disconnect(); + } + } - dataStore.clear(); + Future deactivate({bool clearAuthenticated = true}) async { + await _modeOperation; + final previousMode = _mode; + await _closeActive(clearAuthenticated: clearAuthenticated); + if (clearAuthenticated && previousMode != LibraryDatabaseMode.authenticated) { + await _clearStoredAuthenticatedDatabase(); + } + _mode = null; + _authenticatedUserId = null; + _booksController.add(const []); + _setSyncState(const SyncState()); } - Future close() async { - _isConnected = false; - dataStore.attachBookSyncWriter(null); - await _booksSubscription?.cancel(); - await _database?.close(); + @override + Stream> watchAll() => _booksController.stream; + + @override + Future getById(String id) async { + final database = _requireDatabase(); + final row = await database.getOptional('SELECT * FROM books WHERE id = ?', [id]); + return row == null ? null : PowerSyncBookMapper.fromRow(Map.from(row)); } @override - Future upsertBook(Book book) async { - final database = await _openDatabase(); + Future upsert(Book book) async { + final database = _requireDatabase(); final row = PowerSyncBookMapper.toRow(book); final existing = await database.getOptional('SELECT id FROM books WHERE id = ?', [book.id]); - if (existing == null) { await database.execute(PowerSyncBookMapper.insertSql(), PowerSyncBookMapper.rowParameters(row)); - return; + } else { + await database.execute(PowerSyncBookMapper.updateSql(), PowerSyncBookMapper.updateParameters(row)); } - - await database.execute(PowerSyncBookMapper.updateSql(), PowerSyncBookMapper.updateParameters(row)); + await _refreshPendingWrites(); } @override - Future deleteBook(String id) async { - final database = await _openDatabase(); + Future delete(String id) async { + final database = _requireDatabase(); await database.execute('DELETE FROM books WHERE id = ?', [id]); + await _refreshPendingWrites(); } - Future _connect() async { - final database = await _openDatabase(); - dataStore.clear(); - dataStore.attachBookSyncWriter(this); - _watchBooks(database); - - await database.connect( - connector: PapyrusPowerSyncConnector(authRepository: authRepository, config: config), - ); - _isConnected = true; + Future close() async { + await _modeOperation; + await _closeActive(clearAuthenticated: false); + await _booksController.close(); + await _syncStateController.close(); } - Future _openDatabase() async { - final existing = _database; + Future _switchMode(LibraryDatabaseMode mode, {String? authenticatedUserId}) async { + await _modeOperation; + if (_mode == mode && + _database != null && + (mode == LibraryDatabaseMode.guest || _authenticatedUserId == authenticatedUserId)) { + return; + } - if (existing != null) { - return existing; + final operation = _performModeSwitch(mode, authenticatedUserId); + _modeOperation = operation; + try { + await operation; + } finally { + if (identical(_modeOperation, operation)) { + _modeOperation = null; + } } + } - final database = PowerSyncDatabase(schema: papyrusPowerSyncSchema, path: await _databasePath()); + Future _performModeSwitch(LibraryDatabaseMode mode, String? authenticatedUserId) async { + await _closeActive(clearAuthenticated: _mode == LibraryDatabaseMode.authenticated); + _mode = mode; + _authenticatedUserId = authenticatedUserId; + _booksController.add(const []); + + final database = PowerSyncDatabase( + schema: mode == LibraryDatabaseMode.guest ? papyrusGuestSchema : papyrusAccountSchema, + path: await _databasePath(mode), + ); await database.initialize(); _database = database; - return database; - } + _watchBooks(database); - Future _databasePath() async { - if (kIsWeb) { - return 'papyrus-powersync.db'; + if (mode == LibraryDatabaseMode.authenticated && connectAuthenticated) { + _watchStatus(database); + await database.connect(connector: connectorFactory()); + } else { + _setSyncState(const SyncState()); } - - final directory = await getApplicationSupportDirectory(); - return path.join(directory.path, 'papyrus-powersync.db'); } void _watchBooks(PowerSyncDatabase database) { @@ -135,8 +150,116 @@ class PapyrusPowerSyncService implements BookSyncWriter { _booksSubscription = database .watch('SELECT * FROM books ORDER BY added_at DESC', triggerOnTables: ['books']) .listen((rows) { - final books = rows.map((row) => PowerSyncBookMapper.fromRow(Map.from(row))).toList(); - dataStore.replaceBooksFromSync(books); + _booksController.add(rows.map((row) => PowerSyncBookMapper.fromRow(Map.from(row))).toList()); }); } + + void _watchStatus(PowerSyncDatabase database) { + unawaited(_statusSubscription?.cancel()); + _statusSubscription = database.statusStream.listen((status) async { + await _setStatusFromPowerSync(status); + }); + unawaited(_setStatusFromPowerSync(database.currentStatus)); + } + + Future _setStatusFromPowerSync(SyncStatus status) async { + final pending = await _hasPendingWrites(); + _setSyncState( + SyncState( + connected: status.connected, + connecting: status.connecting, + uploading: status.uploading, + downloading: status.downloading, + hasPendingWrites: pending, + lastSyncedAt: status.lastSyncedAt, + uploadError: status.uploadError, + downloadError: status.downloadError, + ), + ); + } + + Future _refreshPendingWrites() async { + final current = _syncState; + _setSyncState( + SyncState( + connected: current.connected, + connecting: current.connecting, + uploading: current.uploading, + downloading: current.downloading, + hasPendingWrites: await _hasPendingWrites(), + lastSyncedAt: current.lastSyncedAt, + uploadError: current.uploadError, + downloadError: current.downloadError, + ), + ); + } + + Future _hasPendingWrites() async { + final database = _database; + if (database == null || _mode != LibraryDatabaseMode.authenticated) { + return false; + } + final row = await database.get('SELECT EXISTS(SELECT 1 FROM ps_crud) AS pending'); + return row['pending'] == 1; + } + + void _setSyncState(SyncState state) { + _syncState = state; + if (!_syncStateController.isClosed) { + _syncStateController.add(state); + } + } + + PowerSyncDatabase _requireDatabase() { + final database = _database; + if (database == null) { + throw StateError('Library database is not active'); + } + return database; + } + + Future _closeActive({required bool clearAuthenticated}) async { + await _booksSubscription?.cancel(); + await _statusSubscription?.cancel(); + _booksSubscription = null; + _statusSubscription = null; + + final database = _database; + final mode = _mode; + _database = null; + if (database == null) { + return; + } + + if (mode == LibraryDatabaseMode.authenticated && clearAuthenticated) { + await database.disconnectAndClear(clearLocal: true); + } else if (mode == LibraryDatabaseMode.authenticated) { + await database.disconnect(); + } + await database.close(); + } + + Future _clearStoredAuthenticatedDatabase() async { + final database = PowerSyncDatabase( + schema: papyrusAccountSchema, + path: await _databasePath(LibraryDatabaseMode.authenticated), + ); + await database.initialize(); + await database.disconnectAndClear(clearLocal: true); + await database.close(); + } + + Future _databasePath(LibraryDatabaseMode mode) async { + final customResolver = pathResolver; + if (customResolver != null) { + return customResolver(mode); + } + + final fileName = mode == LibraryDatabaseMode.guest ? 'papyrus-guest.db' : 'papyrus-account.db'; + if (kIsWeb) { + return fileName; + } + final directory = await getApplicationSupportDirectory(); + return path.join(directory.path, fileName); + } } diff --git a/app/lib/powersync/sync_state.dart b/app/lib/powersync/sync_state.dart new file mode 100644 index 0000000..54cf911 --- /dev/null +++ b/app/lib/powersync/sync_state.dart @@ -0,0 +1,23 @@ +enum LibraryDatabaseMode { guest, authenticated } + +class SyncState { + final bool connected; + final bool connecting; + final bool uploading; + final bool downloading; + final bool hasPendingWrites; + final DateTime? lastSyncedAt; + final Object? uploadError; + final Object? downloadError; + + const SyncState({ + this.connected = false, + this.connecting = false, + this.uploading = false, + this.downloading = false, + this.hasPendingWrites = false, + this.lastSyncedAt, + this.uploadError, + this.downloadError, + }); +} diff --git a/app/pubspec.lock b/app/pubspec.lock index 4732b3a..a69813f 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -222,6 +222,11 @@ packages: url: "https://pub.dev" source: hosted version: "3.4.1" + flutter_driver: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" flutter_lints: dependency: "direct dev" description: @@ -320,6 +325,11 @@ packages: description: flutter source: sdk version: "0.0.0" + fuchsia_remote_debug_protocol: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" glob: dependency: transitive description: @@ -376,6 +386,11 @@ packages: url: "https://pub.dev" source: hosted version: "4.8.0" + integration_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" intl: dependency: "direct main" description: @@ -596,10 +611,10 @@ packages: dependency: "direct main" description: name: powersync - sha256: bd1f1b2ee9b79b6f821f2cc77e7ae6ef1a7159d2a9779990e1b8dc791e97e91c + sha256: fd497501e9adbfcafb65870311c6c2b48f6146cbf67bb4e2d92f3f8da23bd9ab url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.3.1" powersync_flutter_libs: dependency: transitive description: @@ -608,6 +623,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.5.0+eol" + process: + dependency: transitive + description: + name: process + sha256: c6248e4526673988586e8c00bb22a49210c258dc91df5227d5da9748ecf79744 + url: "https://pub.dev" + source: hosted + version: "5.0.5" provider: dependency: "direct main" description: @@ -769,10 +792,10 @@ packages: dependency: transitive description: name: sqlite3 - sha256: "56da3e13ed7d28a66f930aa2b2b29db6736a233f08283326e96321dd812030f5" + sha256: "37356bcb56ce0d9404d602c41e4bdb7765e7e9732a3e47adb3d98c556a6abdad" url: "https://pub.dev" source: hosted - version: "3.3.1" + version: "3.3.3" sqlite3_connection_pool: dependency: transitive description: @@ -793,18 +816,18 @@ packages: dependency: transitive description: name: sqlite3_web - sha256: d876398a9f2cbf115d93fc34901f8fa129b58b13b5fa9377156ed3a9a05695e3 + sha256: a7023d26a31e2783ef2cdb91cf0f066385742a2cba6c6da4de69ad56cc8d1079 url: "https://pub.dev" source: hosted - version: "0.7.1" + version: "0.9.1" sqlite_async: dependency: transitive description: name: sqlite_async - sha256: "4c243c5386eba3a7102f98999388a7e0a7f2632e4e06dafb3b4f5a44170a26f6" + sha256: "17176f00a10e5b8ba6e0205e42de1c15a94ff8762745fed19750b44b6bbd5649" url: "https://pub.dev" source: hosted - version: "0.14.1" + version: "0.14.3" stack_trace: dependency: transitive description: @@ -829,6 +852,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.1" + sync_http: + dependency: transitive + description: + name: sync_http + sha256: "7f0cd72eca000d2e026bcd6f990b81d0ca06022ef4e32fb257b30d3d1014a961" + url: "https://pub.dev" + source: hosted + version: "0.3.1" syncfusion_flutter_core: dependency: transitive description: @@ -1005,6 +1036,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" + webdriver: + dependency: transitive + description: + name: webdriver + sha256: "2f3a14ca026957870cfd9c635b83507e0e51d8091568e90129fbf805aba7cade" + url: "https://pub.dev" + source: hosted + version: "3.1.0" win32: dependency: transitive description: diff --git a/app/pubspec.yaml b/app/pubspec.yaml index b6c469d..d691ec2 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -34,7 +34,7 @@ dependencies: path: ^1.9.1 crypto: ^3.0.6 path_provider: ^2.1.5 - powersync: ^2.1.0 + powersync: ^2.3.0 web: ^1.1.1 mobile_scanner: ^7.0.1 shared_preferences: ^2.5.4 @@ -44,6 +44,8 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter + integration_test: + sdk: flutter flutter_lints: ^6.0.0 path_provider_platform_interface: ^2.1.2 diff --git a/app/test/data/book_repository_data_store_test.dart b/app/test/data/book_repository_data_store_test.dart new file mode 100644 index 0000000..2b9ead1 --- /dev/null +++ b/app/test/data/book_repository_data_store_test.dart @@ -0,0 +1,71 @@ +import 'dart:async'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:papyrus/data/data_store.dart'; +import 'package:papyrus/data/repositories/book_repository.dart'; +import 'package:papyrus/models/book.dart'; + +class FakeBookRepository implements BookRepository { + final StreamController> controller = StreamController>.broadcast(); + final List upserts = []; + final List deletes = []; + + @override + Future delete(String id) async { + deletes.add(id); + } + + @override + Future getById(String id) async { + return upserts.where((book) => book.id == id).firstOrNull; + } + + @override + Future upsert(Book book) async { + upserts.add(book); + } + + @override + Stream> watchAll() => controller.stream; +} + +Book _book(String id, String title) { + return Book(id: id, title: title, author: 'Author', addedAt: DateTime.utc(2026, 1, 1)); +} + +void main() { + test('repository stream is the source of the DataStore book snapshot', () async { + final repository = FakeBookRepository(); + final store = DataStore(); + + await store.attachBookRepository(repository); + repository.controller.add([_book('one', 'First')]); + await pumpEventQueue(); + + expect(store.books.map((book) => book.title), ['First']); + + repository.controller.add([_book('two', 'Second')]); + await pumpEventQueue(); + + expect(store.books.map((book) => book.title), ['Second']); + await store.disposeBookRepository(); + await repository.controller.close(); + }); + + test('book mutations delegate to the active repository', () async { + final repository = FakeBookRepository(); + final store = DataStore(); + final book = _book('one', 'First'); + + await store.attachBookRepository(repository); + store.addBook(book); + store.updateBook(book.copyWith(title: 'Updated')); + store.deleteBook(book.id); + await pumpEventQueue(); + + expect(repository.upserts.map((item) => item.title), ['First', 'Updated']); + expect(repository.deletes, ['one']); + await store.disposeBookRepository(); + await repository.controller.close(); + }); +} diff --git a/app/test/data/data_store_sync_test.dart b/app/test/data/data_store_sync_test.dart deleted file mode 100644 index e2aa5c5..0000000 --- a/app/test/data/data_store_sync_test.dart +++ /dev/null @@ -1,57 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:papyrus/data/data_store.dart'; -import 'package:papyrus/models/book.dart'; - -class FakeBookSyncWriter implements BookSyncWriter { - final upserts = []; - final deletes = []; - - @override - Future deleteBook(String id) async { - deletes.add(id); - } - - @override - Future upsertBook(Book book) async { - upserts.add(book); - } -} - -void main() { - test('DataStore delegates local book writes to sync writer', () { - final dataStore = DataStore(); - final writer = FakeBookSyncWriter(); - final book = Book( - id: '11111111-1111-1111-1111-111111111111', - title: 'Book', - author: 'Author', - addedAt: DateTime(2026, 5, 9), - ); - - dataStore.attachBookSyncWriter(writer); - dataStore.addBook(book); - dataStore.updateBook(book.copyWith(title: 'Updated')); - dataStore.deleteBook(book.id); - - expect(writer.upserts.map((book) => book.title), ['Book', 'Updated']); - expect(writer.deletes, [book.id]); - }); - - test('replaceBooksFromSync updates books without delegating writes', () { - final dataStore = DataStore(); - final writer = FakeBookSyncWriter(); - final book = Book( - id: '11111111-1111-1111-1111-111111111111', - title: 'Synced Book', - author: 'Author', - addedAt: DateTime(2026, 5, 9), - ); - - dataStore.attachBookSyncWriter(writer); - dataStore.replaceBooksFromSync([book]); - - expect(dataStore.books.single.title, 'Synced Book'); - expect(writer.upserts, isEmpty); - expect(writer.deletes, isEmpty); - }); -} diff --git a/app/test/powersync/papyrus_schema_mode_test.dart b/app/test/powersync/papyrus_schema_mode_test.dart new file mode 100644 index 0000000..ad3f381 --- /dev/null +++ b/app/test/powersync/papyrus_schema_mode_test.dart @@ -0,0 +1,18 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:papyrus/powersync/papyrus_schema.dart'; + +void main() { + test('guest books table is local-only', () { + final table = papyrusGuestSchema.tables.single; + + expect(table.name, 'books'); + expect(table.localOnly, isTrue); + }); + + test('authenticated books table participates in synchronization', () { + final table = papyrusAccountSchema.tables.single; + + expect(table.name, 'books'); + expect(table.localOnly, isFalse); + }); +} diff --git a/app/test/powersync/powersync_service_test.dart b/app/test/powersync/powersync_service_test.dart new file mode 100644 index 0000000..7c31386 --- /dev/null +++ b/app/test/powersync/powersync_service_test.dart @@ -0,0 +1,70 @@ +import 'dart:io'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:papyrus/models/book.dart'; +import 'package:papyrus/powersync/powersync_service.dart'; +import 'package:papyrus/powersync/sync_state.dart'; +import 'package:path/path.dart' as path; +import 'package:powersync/powersync.dart'; + +class OfflineConnector extends PowerSyncBackendConnector { + @override + Future fetchCredentials() async => null; + + @override + Future uploadData(PowerSyncDatabase database) async {} +} + +Book _book(String id) { + return Book(id: id, title: 'Persistent guest book', author: 'Author', addedAt: DateTime.utc(2026, 1, 1)); +} + +void main() { + late Directory directory; + + setUp(() async { + directory = await Directory.systemTemp.createTemp('papyrus-powersync-test-'); + }); + + tearDown(() async { + if (directory.existsSync()) { + await directory.delete(recursive: true); + } + }); + + PapyrusPowerSyncService service() { + return PapyrusPowerSyncService( + connectorFactory: OfflineConnector.new, + connectAuthenticated: false, + pathResolver: (mode) async => + path.join(directory.path, mode == LibraryDatabaseMode.guest ? 'guest.db' : 'account.db'), + ); + } + + test('guest books persist when the service is reopened', () async { + final first = service(); + await first.activateGuest(); + await first.upsert(_book('guest-book')); + await first.close(); + + final second = service(); + await second.activateGuest(); + + expect((await second.getById('guest-book'))?.title, 'Persistent guest book'); + await second.close(); + }); + + test('authenticated books are cleared on deactivation', () async { + final first = service(); + await first.activateAuthenticated('user-one'); + await first.upsert(_book('account-book')); + await first.deactivate(); + await first.close(); + + final second = service(); + await second.activateAuthenticated('user-one'); + + expect(await second.getById('account-book'), isNull); + await second.close(); + }); +} From 9564ecedc02aa59a4628693c95a8b916c5870675 Mon Sep 17 00:00:00 2001 From: Eoic Date: Fri, 26 Jun 2026 23:39:59 +0300 Subject: [PATCH 15/16] Fix auto assign workflow trigger --- .github/workflows/auto-assign.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/auto-assign.yml b/.github/workflows/auto-assign.yml index 10a719c..f70fbfd 100644 --- a/.github/workflows/auto-assign.yml +++ b/.github/workflows/auto-assign.yml @@ -2,8 +2,6 @@ name: Auto Assign on: issues: types: [opened] - pull_request: - types: [opened] jobs: run: runs-on: ubuntu-latest @@ -16,4 +14,3 @@ jobs: with: repo-token: ${{ secrets.GITHUB_TOKEN }} assignees: Eoic - numOfAssignee: 1 \ No newline at end of file From 9f62591914d0052ce448ea95ccab9d5ef10af204 Mon Sep 17 00:00:00 2001 From: Eoic Date: Fri, 26 Jun 2026 23:43:48 +0300 Subject: [PATCH 16/16] Fix Flutter CI app path --- .github/workflows/ci.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 389204e..22f8596 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,12 +4,12 @@ on: push: branches: [master, main] paths: - - 'client/**' + - 'app/**' - '.github/workflows/**' pull_request: branches: [master, main] paths: - - 'client/**' + - 'app/**' - '.github/workflows/**' workflow_dispatch: @@ -26,7 +26,7 @@ jobs: runs-on: ubuntu-latest defaults: run: - working-directory: client + working-directory: app steps: - name: Checkout repository @@ -45,7 +45,7 @@ jobs: run: dart format --set-exit-if-changed . - name: Analyze code - run: dart analyze + run: flutter analyze --no-fatal-warnings --no-fatal-infos - name: Run tests with coverage run: flutter test --coverage @@ -53,6 +53,6 @@ jobs: - name: Upload coverage to Codecov uses: codecov/codecov-action@v4 with: - files: client/coverage/lcov.info + files: app/coverage/lcov.info flags: client token: ${{ secrets.CODECOV_TOKEN }}