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 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 }} 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..b8421a6 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -1,22 +1,29 @@ { "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", - ".dart_defines" + ".dart_defines", + "--web-port", + "3000" ] }, { "label": "Papyrus (android)", "type": "shell", - "command": "flutter run", + "command": "flutter", "args": [ + "run", "-d", "android", "--dart-define-from-file", @@ -26,8 +33,9 @@ { "label": "Papyrus (iOS)", "type": "shell", - "command": "flutter run", + "command": "flutter", "args": [ + "run", "-d", "ios", "--dart-define-from-file", @@ -35,12 +43,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" ] diff --git a/README.md b/README.md index fa62da7..62a734f 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,34 @@ 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 ../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 +127.0.0.1 papyrus.localhost +::1 papyrus.localhost +``` + +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). 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/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/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/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..83b0fa4 --- /dev/null +++ b/app/lib/auth/auth_api_client.dart @@ -0,0 +1,207 @@ +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 { + 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}); + } + + 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 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)); + + 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..63e851e --- /dev/null +++ b/app/lib/auth/auth_models.dart @@ -0,0 +1,78 @@ +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), + ); + } +} + +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; + } + + 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..aa67a36 --- /dev/null +++ b/app/lib/auth/auth_repository.dart @@ -0,0 +1,239 @@ +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'; + static const desktopOAuthRedirectUri = 'http://localhost:43821/auth/callback'; + + final AuthApiClient apiClient; + final TokenStore tokenStore; + + Future? _refreshOperation; + + AuthRepository({required this.apiClient, required this.tokenStore}); + + 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(); + + 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 { + await apiClient.ensureServerReachable(); + + final redirectUri = kIsWeb + ? _webOAuthRedirectUri() + : _usesDesktopLoopbackOAuth + ? desktopOAuthRedirectUri + : nativeOAuthRedirectUri; + + final startUri = apiClient.googleOAuthStartUri(redirectUri); + + if (kIsWeb) { + redirectTo(startUri.toString()); + return null; + } + + 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); + } + + 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 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(); + } + + 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); + } + + 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; + + return Uri( + scheme: base.scheme, + host: base.host, + port: base.hasPort ? base.port : null, + path: '/auth/callback', + ).toString(); + } +} 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/auth/papyrus_api_config.dart b/app/lib/auth/papyrus_api_config.dart new file mode 100644 index 0000000..ca18757 --- /dev/null +++ b/app/lib/auth/papyrus_api_config.dart @@ -0,0 +1,36 @@ +class PapyrusApiConfig { + static const _defaultBaseUrl = 'http://localhost:8080'; + static const _defaultPowerSyncServiceUrl = 'http://localhost:8081'; + + final Uri serverBaseUri; + final Uri powerSyncServiceUri; + final String apiPrefix; + + 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), + powerSyncServiceUri: Uri.parse(rawPowerSyncServiceUrl), + ); + } + + 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..3de412f 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,32 @@ 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()), + ), + GoRoute( + name: 'FORGOT_PASSWORD', + 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: const RegisterPage(), + child: ForgotPasswordPage(resetToken: state.uri.queryParameters['token'], isResetLink: true), ), ), GoRoute( - name: 'FORGOT_PASSWORD', - path: 'forgot-password', + name: 'AUTH_CALLBACK', + path: 'auth/callback', pageBuilder: (context, state) => NoTransitionPage( key: state.pageKey, - child: const ForgotPasswordPage(), + child: OAuthCallbackPage(callbackUri: state.uri), ), ), ], @@ -74,40 +87,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 +124,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 +169,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 +195,42 @@ 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 == '/reset-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' || location == '/reset-password') { + return '/library/books'; + } + + return null; + } } diff --git a/app/lib/data/data_store.dart b/app/lib/data/data_store.dart index dae8ae5..4deb034 100644 --- a/app/lib/data/data_store.dart +++ b/app/lib/data/data_store.dart @@ -1,4 +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'; @@ -14,6 +17,12 @@ import 'package:papyrus/models/tag.dart'; /// 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 = {}; @@ -30,6 +39,8 @@ class DataStore extends ChangeNotifier { final List _bookTagRelations = []; bool _isLoaded = false; + BookRepository? _bookRepository; + StreamSubscription>? _bookSubscription; // ============================================================ // Getters for read access @@ -53,10 +64,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 @@ -64,25 +73,61 @@ class DataStore extends ChangeNotifier { Book? getBook(String id) => _books[id]; + 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; - 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; - 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); + final repository = _bookRepository; + if (repository == null) { + throw StateError('Book repository is not initialized'); + } + unawaited(repository.delete(id)); + } + + 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(); } @@ -94,10 +139,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 +160,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 +185,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 +212,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 +369,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 +396,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 +422,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) { @@ -458,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/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..50e84b4 100644 --- a/app/lib/forms/login_form.dart +++ b/app/lib/forms/login_form.dart @@ -1,148 +1,142 @@ -import 'package:flutter/material.dart'; -import 'package:go_router/go_router.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: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 Supabase.instance.client.auth.signInWithPassword( - email: emailController.text.trim(), - password: passwordController.text.trim(), - ); - } +// Future signIn() async { +// return context.read().login( +// email: emailController.text.trim(), +// password: passwordController.text, +// ); +// } - 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 { - await signIn(); - if (!mounted) return; - setState(() => isLoginDisabled = false); - Navigator.of(context).pop(); - context.goNamed('LIBRARY'); - } 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, - ), - ); - } - } +// try { +// final success = await signIn(); +// if (!mounted) return; +// setState(() => isLoginDisabled = false); +// Navigator.of(context).pop(); - @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"), - ), - ], - ), - ], - ), - ), - ], - ), - ), - ); - } +// if (success) { +// context.goNamed('LIBRARY'); +// return; +// } - @override - void dispose() { - emailController.dispose(); - passwordController.dispose(); - super.dispose(); - } -} +// 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 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(); +// } +// } 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..8a24d4a 100644 --- a/app/lib/main.dart +++ b/app/lib/main.dart @@ -1,6 +1,15 @@ +import 'dart:async'; + 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'; +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/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'; @@ -8,16 +17,13 @@ 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'), - ); + usePathUrlStrategy(); + final prefs = await SharedPreferences.getInstance(); runApp(Papyrus(prefs: prefs)); } @@ -32,36 +38,77 @@ class Papyrus extends StatefulWidget { } class _PapyrusState extends State { - late final AppRouter _appRouter = AppRouter(); + late final DataStore _dataStore; + late final AuthProvider _authProvider; + late final PapyrusPowerSyncService _powerSyncService; + 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, + ); + + _dataStore = DataStore(); + _authProvider = AuthProvider(widget.prefs, repository: authRepository); + _powerSyncService = PapyrusPowerSyncService( + connectorFactory: () => PapyrusPowerSyncConnector(authRepository: authRepository, config: apiConfig), + ); + unawaited(_dataStore.attachBookRepository(_powerSyncService)); + _appRouter = AppRouter(authProvider: _authProvider); + _authProvider.addListener(_syncPowerSyncAuthState); + _syncPowerSyncAuthState(); + } + + @override + void dispose() { + _authProvider.removeListener(_syncPowerSyncAuthState); + unawaited(_disposeDataServices()); + _authProvider.dispose(); + super.dispose(); + } + + Future _disposeDataServices() async { + await _dataStore.disposeBookRepository(); + await _powerSyncService.close(); + } + + void _syncPowerSyncAuthState() { + final user = _authProvider.user; + if (user != null && !_authProvider.isOfflineMode) { + final userId = user.userId; + unawaited(_powerSyncService.activateAuthenticated(userId)); + return; + } + + if (_authProvider.isOfflineMode) { + unawaited(_powerSyncService.activateGuest()); + return; + } + + if (!_authProvider.isBootstrapping) { + unawaited(_powerSyncService.deactivate()); + } + } @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), + Provider.value(value: _powerSyncService), + StreamProvider.value(value: _powerSyncService.syncStates, initialData: _powerSyncService.syncState), // 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/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/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..d334a76 100644 --- a/app/lib/pages/forgot_password_page.dart +++ b/app/lib/pages/forgot_password_page.dart @@ -1,17 +1,22 @@ 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. 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(); @@ -19,16 +24,26 @@ class ForgotPasswordPage extends StatefulWidget { class _ForgotPasswordPageState extends State { final _formKey = GlobalKey(); + final _resetFormKey = GlobalKey(); final _emailController = TextEditingController(); + final _passwordController = TextEditingController(); + final _confirmPasswordController = TextEditingController(); final _emailFocusNode = FocusNode(); + final _passwordFocusNode = FocusNode(); + final _confirmPasswordFocusNode = FocusNode(); bool _isLoading = false; bool _emailSent = false; + bool _passwordReset = false; @override void dispose() { _emailController.dispose(); + _passwordController.dispose(); + _confirmPasswordController.dispose(); _emailFocusNode.dispose(); + _passwordFocusNode.dispose(); + _confirmPasswordFocusNode.dispose(); super.dispose(); } @@ -44,28 +59,61 @@ 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(); + + final resetToken = widget.resetToken; + + if (resetToken == null || resetToken.isEmpty) { + _showErrorSnackBar('Password reset link is invalid.'); + 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 (!_resetFormKey.currentState!.validate()) return; + + setState(() => _isLoading = true); + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + + try { + final message = await context.read().resetPassword( + token: resetToken, + password: _passwordController.text, + ); + + if (!mounted) return; + + 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 +121,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,6 +162,29 @@ class _ForgotPasswordPageState extends State { } Widget _buildForm({required bool isDesktop}) { + if (_passwordReset) { + return const _PasswordResetConfirmation(); + } + + if (widget.isResetLink) { + if (widget.resetToken == null || widget.resetToken!.isEmpty) { + return const _InvalidResetLink(); + } + + return _SetNewPasswordForm( + formKey: _resetFormKey, + passwordController: _passwordController, + confirmPasswordController: _confirmPasswordController, + passwordFocusNode: _passwordFocusNode, + confirmPasswordFocusNode: _confirmPasswordFocusNode, + isLoading: _isLoading, + isDesktop: isDesktop, + onSubmit: _handleSetNewPassword, + validatePasswordStrength: _validatePasswordStrength, + validateConfirmPassword: _validateConfirmPassword, + ); + } + if (_emailSent) { return _EmailSentConfirmation(email: _emailController.text.trim()); } @@ -104,28 +201,30 @@ 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), ]; } @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(), ), @@ -171,11 +270,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), ], ), ); @@ -200,34 +295,141 @@ 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, - ), + 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, + ), + ], + ); + } +} + +class _SetNewPasswordForm extends StatelessWidget { + final GlobalKey formKey; + final TextEditingController passwordController; + final TextEditingController confirmPasswordController; + 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 _SetNewPasswordForm({ + required this.formKey, + required this.passwordController, + required this.confirmPasswordController, + 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) { + 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); + + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.link_off_outlined, size: 72, color: theme.colorScheme.error), + const SizedBox(height: Spacing.sm), + Text( + 'Invalid reset link', + style: theme.textTheme.titleLarge?.copyWith(fontWeight: FontWeight.w600, color: theme.colorScheme.onSurface), + textAlign: TextAlign.center, + ), + const SizedBox(height: Spacing.sm), + Text( + '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), + FilledButton(onPressed: () => context.go('/forgot-password'), child: const Text('Request new link')), + ], + ); + } +} + +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..21277f7 100644 --- a/app/lib/pages/login_page.dart +++ b/app/lib/pages/login_page.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; -import 'package:supabase_flutter/supabase_flutter.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'; import 'package:papyrus/widgets/auth/auth_continue_button.dart'; @@ -10,9 +11,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 +39,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(), + password: _passwordController.text, ); - 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) { @@ -94,10 +91,6 @@ class _LoginPageState extends State { ); } - void _navigateToRegister() { - context.go('/register'); - } - List _buildFooter() { return [ const TitledDivider(title: 'Or continue with'), @@ -106,7 +99,12 @@ class _LoginPageState extends State { AuthSwitchLink( promptText: "Don't have an account?", actionText: 'Sign up', - onPressed: _navigateToRegister, + onPressed: () => navigateToRegister(context), + ), + AuthSwitchLink( + promptText: "No internet?", + actionText: 'Continue offline', + onPressed: () => navigateToOffline(context), ), ]; } @@ -148,11 +146,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 +197,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..c89a995 100644 --- a/app/lib/pages/profile_page.dart +++ b/app/lib/pages/profile_page.dart @@ -3,7 +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:supabase_flutter/supabase_flutter.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'; @@ -104,11 +104,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 +116,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), @@ -204,21 +196,20 @@ 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, 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, onTap: () {}, ), + SettingsRow(label: 'Current status', value: _syncStatusLabel(auth, sync)), SettingsToggleRow( label: 'Sync enabled', value: prefs.syncEnabled, @@ -376,12 +367,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 +416,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 +433,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 +443,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 +552,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 +581,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 +606,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 +632,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 +641,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 +683,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 +695,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 +703,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 +719,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 +732,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 +744,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 +769,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 +788,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 +813,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 +821,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 +841,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), @@ -975,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, @@ -1010,39 +928,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: 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, ), ), ), @@ -1053,10 +954,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 +982,18 @@ 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, - ), - child: Text( - 'Last sync: 2 min ago', - style: textTheme.bodySmall?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), + padding: const EdgeInsets.symmetric(horizontal: Spacing.sm, vertical: Spacing.xs), + child: Text(_syncDetail(sync), 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')), ), ], ), @@ -1118,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) { @@ -1139,9 +1040,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 +1078,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 +1093,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 +1144,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 +1168,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 +1188,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 +1206,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 +1227,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 +1257,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 +1315,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 +1345,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 +1395,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 +1406,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 +1417,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 +1480,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 +1504,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 +1523,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 +1539,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 +1548,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 +1564,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..3e6e43e 100644 --- a/app/lib/pages/register_page.dart +++ b/app/lib/pages/register_page.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; -import 'package:supabase_flutter/supabase_flutter.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'; import 'package:papyrus/widgets/auth/auth_continue_button.dart'; @@ -11,9 +12,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 +48,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()}, + password: _passwordController.text, + displayName: _displayNameController.text.trim(), ); - if (!mounted) return; - context.goNamed('LIBRARY'); - } on AuthException catch (e) { - if (!mounted) return; + if (!mounted) { + 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.'; + if (!success) { + _showErrorSnackBar(context.read().error ?? 'Registration failed. Please try again.'); + return; + } + + 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) { @@ -103,14 +101,11 @@ class _RegisterPageState extends State { ); } - void _navigateToLogin() { - context.go('/login'); - } - String? _validateConfirmPassword(String? value) { if (value != _passwordController.text) { return 'Passwords do not match'; } + return null; } @@ -118,6 +113,7 @@ class _RegisterPageState extends State { if (value != null && value.length < 8) { return 'Minimum 8 characters'; } + return null; } @@ -129,7 +125,12 @@ class _RegisterPageState extends State { AuthSwitchLink( promptText: 'Already have an account?', actionText: 'Sign in', - onPressed: _navigateToLogin, + onPressed: () => navigateToLogin(context), + ), + AuthSwitchLink( + promptText: 'No internet?', + actionText: 'Continue offline', + onPressed: () => navigateToOffline(context), ), ]; } @@ -138,7 +139,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 +160,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 +184,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 +259,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..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}); @@ -33,15 +32,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 +45,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 +56,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 +64,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 +104,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 +152,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,25 +196,12 @@ 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, - ), - SizedBox(height: titleSpacing), + Image.asset('assets/images/logo.png', width: logoSize, height: logoSize, fit: BoxFit.contain), + 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.', + 'Your personal library, always within reach', textAlign: center ? TextAlign.center : TextAlign.left, style: subtitleStyle, ), @@ -292,11 +233,8 @@ 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, - ), - _OfflineModeLink(center: center), + TitledDivider(title: 'or', verticalPadding: compact ? Spacing.sm : Spacing.md), + OfflineModeLink(center: center), ], ); } @@ -315,25 +253,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 +273,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,59 +283,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), - ), - ); - } -} - -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'), - ), + child: const Text('Sign in', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500)), ); } } 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/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..a289a00 --- /dev/null +++ b/app/lib/powersync/papyrus_schema.dart @@ -0,0 +1,37 @@ +import 'package:powersync/powersync.dart'; + +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_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..0fa3574 --- /dev/null +++ b/app/lib/powersync/powersync_service.dart @@ -0,0 +1,265 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:papyrus/data/repositories/book_repository.dart'; +import 'package:papyrus/models/book.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'; + +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; + StreamSubscription? _statusSubscription; + Future? _modeOperation; + LibraryDatabaseMode? _mode; + String? _authenticatedUserId; + SyncState _syncState = const SyncState(); + + PapyrusPowerSyncService({required this.connectorFactory, this.pathResolver, this.connectAuthenticated = true}); + + LibraryDatabaseMode? get mode => _mode; + SyncState get syncState => _syncState; + Stream get syncStates => _syncStateController.stream; + + Future activateGuest() => _switchMode(LibraryDatabaseMode.guest); + + Future activateAuthenticated(String userId) { + return _switchMode(LibraryDatabaseMode.authenticated, authenticatedUserId: userId); + } + + 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(); + } + } + + 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()); + } + + @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 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)); + } else { + await database.execute(PowerSyncBookMapper.updateSql(), PowerSyncBookMapper.updateParameters(row)); + } + await _refreshPendingWrites(); + } + + @override + Future delete(String id) async { + final database = _requireDatabase(); + await database.execute('DELETE FROM books WHERE id = ?', [id]); + await _refreshPendingWrites(); + } + + Future close() async { + await _modeOperation; + await _closeActive(clearAuthenticated: false); + await _booksController.close(); + await _syncStateController.close(); + } + + Future _switchMode(LibraryDatabaseMode mode, {String? authenticatedUserId}) async { + await _modeOperation; + if (_mode == mode && + _database != null && + (mode == LibraryDatabaseMode.guest || _authenticatedUserId == authenticatedUserId)) { + return; + } + + final operation = _performModeSwitch(mode, authenticatedUserId); + _modeOperation = operation; + try { + await operation; + } finally { + if (identical(_modeOperation, operation)) { + _modeOperation = null; + } + } + } + + 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; + _watchBooks(database); + + if (mode == LibraryDatabaseMode.authenticated && connectAuthenticated) { + _watchStatus(database); + await database.connect(connector: connectorFactory()); + } else { + _setSyncState(const SyncState()); + } + } + + void _watchBooks(PowerSyncDatabase database) { + unawaited(_booksSubscription?.cancel()); + _booksSubscription = database + .watch('SELECT * FROM books ORDER BY added_at DESC', triggerOnTables: ['books']) + .listen((rows) { + _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/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..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 = {}; @@ -64,8 +62,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']); @@ -123,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(); @@ -131,9 +128,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 +144,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 +173,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 +202,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 +227,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 +246,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 +257,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 +270,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/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/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..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 } @@ -29,16 +30,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 +107,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 +127,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 +171,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!); } } @@ -231,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), @@ -259,9 +247,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 +266,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 +295,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 +307,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 +338,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 +347,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 +365,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 +376,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 +398,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 +469,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 +502,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 +519,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..133b853 100644 --- a/app/lib/widgets/auth/auth_branding.dart +++ b/app/lib/widgets/auth/auth_branding.dart @@ -1,33 +1,126 @@ +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; + final Color? textShadowColor; + final Offset textShadowOffset; + final double textShadowBlurRadius; + + 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, + this.textShadowColor, + this.textShadowOffset = const Offset(0, 1), + this.textShadowBlurRadius = 3, + }); @override Widget build(BuildContext context) { + final foregroundColor = textColor ?? Theme.of(context).colorScheme.onSurface; + return Row( mainAxisSize: MainAxisSize.min, children: [ - SvgPicture.asset( - 'assets/images/logo-icon-light.svg', - width: 56, - height: 56, + _LogoIcon( + size: iconSize, + outlineColor: iconOutlineColor, + outlineWidth: iconOutlineWidth, + shadowColor: shadowColor, + shadowOffset: shadowOffset, + shadowBlurRadius: shadowBlurRadius, ), - const SizedBox(width: 16), + 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: textShadowColor == null + ? null + : [Shadow(color: textShadowColor!, offset: textShadowOffset, blurRadius: textShadowBlurRadius)], ), ), ], ); } } + +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_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..ee403ed 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; @@ -23,12 +24,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, @@ -43,12 +40,22 @@ 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), + ), + 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), + textShadowColor: Color(0x59000000), + textShadowOffset: Offset(0, 2), + textShadowBlurRadius: 6, ), ), ], @@ -56,8 +63,7 @@ class AuthHeroPanel extends StatelessWidget { } } -/// 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; @@ -66,12 +72,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), @@ -90,7 +92,6 @@ class CompactAuthHeader extends StatelessWidget { ), ), ), - // Illustration - clean, no overlays Positioned.fill( child: Image.asset( 'assets/images/auth-illustration.png', @@ -98,6 +99,27 @@ class CompactAuthHeader extends StatelessWidget { alignment: Alignment.topCenter, ), ), + const Positioned( + top: Spacing.lg, + left: Spacing.lg, + right: Spacing.lg, + child: Center( + 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, + ), + ), + ), + ), ], ), ), diff --git a/app/lib/widgets/auth/auth_page_layouts.dart b/app/lib/widgets/auth/auth_page_layouts.dart index 6899a66..11160bf 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. @@ -35,10 +34,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: [ @@ -51,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, @@ -62,9 +58,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), ], @@ -126,49 +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, + 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: 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, + ], + ), + 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, + Text( + widget.subtitle, + style: theme.textTheme.bodyMedium?.copyWith(color: theme.colorScheme.onSurfaceVariant), ), - ), - const SizedBox(height: Spacing.xl), + const SizedBox(height: Spacing.xl), + ], + widget.form, + ...widget.footer, ], - widget.form, - ...widget.footer, - ], + ), ), ), ), @@ -185,23 +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), + ), ), - ), - ], + ], + ), ), ); } @@ -229,12 +224,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 +235,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..fce69e9 100644 --- a/app/lib/widgets/auth/auth_switch_link.dart +++ b/app/lib/widgets/auth/auth_switch_link.dart @@ -11,33 +11,22 @@ 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) { 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, - ), - ), + 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..cba8b95 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,32 +90,23 @@ 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, + mainAxisSize: MainAxisSize.max, 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, + Flexible( + child: Text( + widget.title, + maxLines: 1, + overflow: TextOverflow.ellipsis, + 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..a69813f 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: @@ -105,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: @@ -161,14 +129,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 +137,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: @@ -262,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: @@ -278,128 +243,125 @@ 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: "direct main" + description: flutter + source: sdk + version: "0.0.0" + fuchsia_remote_debug_protocol: 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: 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: @@ -424,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: @@ -432,14 +399,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.19.0" - jwt_decode: + json_annotation: dependency: transitive description: - name: jwt_decode - sha256: d2e9f68c052b2225130977429d30f187aa1981d789c76ad104a32243cfdebfbb + name: json_annotation + sha256: cb09e7dac6210041fad964ed7fbee004f14258b4eca4040f72d1234062ace4c8 url: "https://pub.dev" source: hosted - version: "0.3.1" + version: "4.11.0" leak_tracker: dependency: transitive description: @@ -504,14 +471,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: @@ -524,10 +483,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: @@ -640,14 +599,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 +607,30 @@ packages: url: "https://pub.dev" source: hosted version: "6.5.0" - postgrest: + powersync: + dependency: "direct main" + description: + name: powersync + sha256: fd497501e9adbfcafb65870311c6c2b48f6146cbf67bb4e2d92f3f8da23bd9ab + url: "https://pub.dev" + source: hosted + version: "2.3.1" + powersync_flutter_libs: dependency: transitive description: - name: postgrest - sha256: f4b6bb24b465c47649243ef0140475de8a0ec311dc9c75ebe573b2dcabb10460 + name: powersync_flutter_libs + sha256: "24324947bffee25912abd3534203d7ab070b99d3186a8de658205ffb44902971" url: "https://pub.dev" source: hosted - version: "2.6.0" + 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: @@ -680,22 +647,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.0" - realtime_client: + pubspec_parse: dependency: transitive description: - name: realtime_client - sha256: "5268afc208d02fb9109854d262c1ebf6ece224cd285199ae1d2f92d2ff49dbf1" + name: pubspec_parse + sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" url: "https://pub.dev" source: hosted - version: "2.7.0" - retry: + version: "1.5.0" + record_use: dependency: transitive description: - name: retry - sha256: "822e118d5b3aafed083109c72d5f484c6dc66707885e07c0fbcb8b986bba7efc" + name: record_use + sha256: "2551bd8eecfe95d14ae75f6021ad0248be5c27f138c2ec12fcb52b500b3ba1ed" url: "https://pub.dev" source: hosted - version: "3.1.2" + version: "0.6.0" rxdart: dependency: transitive description: @@ -813,22 +780,62 @@ packages: url: "https://pub.dev" source: hosted version: "2.4.0" - stack_trace: + sqlcipher_flutter_libs: dependency: transitive description: - name: stack_trace - sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + name: sqlcipher_flutter_libs + sha256: "38d62d659d2fb8739bf25a42c9a350d1fdd6c29a5a61f13a946778ec75d27929" url: "https://pub.dev" source: hosted - version: "1.12.1" - storage_client: + version: "0.7.0+eol" + sqlite3: dependency: transitive description: - name: storage_client - sha256: "1c61b19ed9e78f37fdd1ca8b729ab8484e6c8fe82e15c87e070b861951183657" + name: sqlite3 + sha256: "37356bcb56ce0d9404d602c41e4bdb7765e7e9732a3e47adb3d98c556a6abdad" url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "3.3.3" + 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: a7023d26a31e2783ef2cdb91cf0f066385742a2cba6c6da4de69ad56cc8d1079 + url: "https://pub.dev" + source: hosted + version: "0.9.1" + sqlite_async: + dependency: transitive + description: + name: sqlite_async + sha256: "17176f00a10e5b8ba6e0205e42de1c15a94ff8762745fed19750b44b6bbd5649" + url: "https://pub.dev" + source: hosted + version: "0.14.3" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" stream_channel: dependency: transitive description: @@ -845,22 +852,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.1" - supabase: + sync_http: 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" + name: sync_http + sha256: "7f0cd72eca000d2e026bcd6f990b81d0ca06022ef4e32fb257b30d3d1014a961" url: "https://pub.dev" source: hosted - version: "2.12.0" + version: "0.3.1" syncfusion_flutter_core: dependency: transitive description: @@ -982,7 +981,7 @@ packages: source: hosted version: "3.1.5" uuid: - dependency: transitive + dependency: "direct main" description: name: uuid sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489" @@ -1037,22 +1036,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" - web_socket: + webdriver: dependency: transitive description: - name: web_socket - sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" + name: webdriver + sha256: "2f3a14ca026957870cfd9c635b83507e0e51d8091568e90129fbf805aba7cade" 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" + version: "3.1.0" win32: dependency: transitive description: @@ -1061,6 +1052,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 +1092,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..d691ec2 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -9,13 +9,15 @@ environment: dependencies: flutter: sdk: flutter + flutter_web_plugins: + sdk: flutter cupertino_icons: ^1.0.8 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 @@ -32,14 +34,18 @@ dependencies: path: ^1.9.1 crypto: ^3.0.6 path_provider: ^2.1.5 + powersync: ^2.3.0 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 + uuid: ^4.5.1 dev_dependencies: flutter_test: sdk: flutter + integration_test: + sdk: flutter flutter_lints: ^6.0.0 path_provider_platform_interface: ^2.1.2 @@ -62,4 +68,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..5242866 --- /dev/null +++ b/app/test/auth/auth_api_client_test.dart @@ -0,0 +1,146 @@ +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'); + }); + + 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')), + 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/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..cb04046 --- /dev/null +++ b/app/test/config/app_router_test.dart @@ -0,0 +1,104 @@ +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); + expect(appRouter.redirectForPath('/reset-password'), 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('/reset-password'), '/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/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/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/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); + }); +} 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/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/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_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/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(); + }); +} 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..1b85f3e 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); @@ -20,6 +18,8 @@ class _FakePathProvider extends Fake } 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; @@ -53,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); @@ -67,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); @@ -80,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); @@ -124,9 +124,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); }); @@ -143,21 +141,16 @@ 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'); }); 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/auth/auth_page_layouts_test.dart b/app/test/widgets/auth/auth_page_layouts_test.dart new file mode 100644 index 0000000..c2e4a03 --- /dev/null +++ b/app/test/widgets/auth/auth_page_layouts_test.dart @@ -0,0 +1,117 @@ +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'); + final footerFocusNode = FocusNode(debugLabel: 'footer'); + + addTearDown(() { + firstFocusNode.dispose(); + secondFocusNode.dispose(); + submitFocusNode.dispose(); + footerFocusNode.dispose(); + }); + + await tester.pumpWidget( + MaterialApp( + 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'))], + ), + ), + ); + + 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, + ); + }); +} 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/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 +#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