[READ ONLY MIRROR] Open Source TikTok alternative built on AT Protocol github.com/sprksocial/client
flutter atproto video dart
10
fork

Configure Feed

Select the types of activity you want to include in your feed.

chore: agents.md and cleanup

+246 -1244
-201
.cursor/rules/flutter.mdc
··· 1 - --- 2 - description: 3 - globs: 4 - alwaysApply: false 5 - --- 6 - You are a senior Dart programmer with experience in the Flutter framework. 7 - 8 - Generate code, corrections, and refactorings that comply with the basic principles and nomenclature. 9 - 10 - ## Dart General Guidelines 11 - 12 - ### Basic Principles 13 - 14 - - Use English for all code and documentation. 15 - - Always declare the type of each variable and function (parameters and return value). 16 - - Avoid using any. 17 - - Create necessary types. 18 - - Don't leave blank lines within a function. 19 - - One export per file. 20 - 21 - ### Nomenclature 22 - 23 - - Use PascalCase for classes. 24 - - Use camelCase for variables, functions, and methods. 25 - - Use underscores_case for file and directory names. 26 - - Use UPPERCASE for environment variables. 27 - - Avoid magic numbers and define constants. 28 - - Start each function with a verb. 29 - - Use verbs for boolean variables. Example: isLoading, hasError, canDelete, etc. 30 - - Use complete words instead of abbreviations and correct spelling. 31 - - Except for standard abbreviations like API, URL, etc. 32 - - Except for well-known abbreviations: 33 - - i, j for loops 34 - - err for errors 35 - - ctx for contexts 36 - - req, res, next for middleware function parameters 37 - 38 - ### Functions 39 - 40 - - In this context, what is understood as a function will also apply to a method. 41 - - Write short functions with a single purpose. Less than 20 instructions. 42 - - Name functions with a verb and something else. 43 - - If it returns a boolean, use isX or hasX, canX, etc. 44 - - If it doesn't return anything, use executeX or saveX, etc. 45 - - Avoid nesting blocks by: 46 - - Early checks and returns. 47 - - Extraction to utility functions. 48 - - Use higher-order functions (map, filter, reduce, etc.) to avoid function nesting. 49 - - Use arrow functions for simple functions (less than 3 instructions). 50 - - Use named functions for non-simple functions. 51 - - Use default parameter values instead of checking for null or undefined. 52 - - Reduce function parameters using RO-RO 53 - - Use an object to pass multiple parameters. 54 - - Use an object to return results. 55 - - Declare necessary types for input arguments and output. 56 - - Use a single level of abstraction. 57 - 58 - ### Data 59 - 60 - - Don't abuse primitive types and encapsulate data in composite types. 61 - - Avoid data validations in functions and use classes with internal validation. 62 - - Prefer immutability for data. 63 - - Use readonly for data that doesn't change. 64 - - Use as const for literals that don't change. 65 - 66 - ### Classes 67 - 68 - - Follow SOLID principles. 69 - - Prefer composition over inheritance. 70 - - Declare interfaces to define contracts. 71 - - Write small classes with a single purpose. 72 - - Less than 200 instructions. 73 - - Less than 10 public methods. 74 - - Less than 10 properties. 75 - 76 - ### Exceptions 77 - 78 - - Use exceptions to handle errors you don't expect. 79 - - If you catch an exception, it should be to: 80 - - Fix an expected problem. 81 - - Add context. 82 - - Otherwise, use a global handler. 83 - 84 - ## Specific to Flutter 85 - 86 - ### Basic Principles 87 - 88 - - Use the following architecture structure: 89 - ``` 90 - lib/ 91 - ├── main.dart # App entry point 92 - └── src/ 93 - ├── sprk_app.dart # Main MaterialApp 94 - ├── core/ # Shared code across features 95 - │ ├── config/ # Application-wide configurations 96 - │ ├── di/ # Dependency injection setup 97 - │ ├── network/ # ATProto client, API base 98 - │ ├── routing/ # AutoRoute setup 99 - │ ├── storage/ # Local storage utilities 100 - │ │ ├── cache/ # Cache management 101 - │ │ └── preferences/ # Settings and preferences 102 - │ ├── theme/ # Theme definitions 103 - │ ├── auth/ # Authentication system 104 - │ │ └── data/ 105 - │ │ └── repositories/ 106 - │ ├── feed_algorithms/ # Feed algorithms 107 - │ ├── l10n/ # Localization (when implemented) 108 - │ ├── widgets/ # Common widgets 109 - │ └── utils/ # Shared utilities 110 - │ └── logging/ # Logging framework 111 - └── features/ # Feature modules 112 - └── feature/ # (auth, feed, profile, etc.) 113 - ├── data/ # Data layer for this feature 114 - │ ├── repositories/ 115 - │ └── models/ 116 - ├── providers/ # Riverpod providers 117 - └── ui/ # UI components 118 - ├── pages/ 119 - └── widgets/ 120 - ``` 121 - 122 - Folder structure explanation: 123 - - `lib/src/core/`: Contains all shared code that's used across multiple features. 124 - - `config/`: Holds application-wide configurations, such as environment settings, API keys, or feature flags. 125 - - `di/`: Manages dependency injection setup using GetIt to register and resolve dependencies. 126 - - `network/`: Handles all network-related functionality, including the ATProto client, API base classes, and repositories. Use only methods included in the ATProto API. 127 - - `routing/`: Contains the AutoRoute setup, including route definitions, guards, and navigation helpers. 128 - - `storage/`: Contains utilities for local storage. 129 - - `cache/`: Cache management with SQL-based implementation, download manager, and cache interfaces. 130 - - `preferences/`: Settings, storage manager, secure storage, and shared preferences. 131 - - `theme/`: Defines the application's themes, including colors, typography, and component styles for different modes (e.g., light/dark). 132 - - `auth/`: Centralized authentication system with repositories for auth operations. 133 - - `feed_algorithms/`: Contains feed algorithms and related logic. 134 - - `l10n/`: Manages localization and internationalization (when implemented). 135 - - `widgets/`: Houses reusable UI components that are used across multiple features. 136 - - `utils/`: Includes utility functions and helpers. 137 - - `logging/`: Complete logging framework with different log levels, outputs, and predefined loggers. 138 - 139 - - `lib/src/features/`: Contains feature-specific modules, where each feature is a self-contained unit. 140 - - Current features: `auth`, `feed`, `profile`, `splash`, `settings`, `comments`, `search`, `messages`, `home` 141 - - Each feature follows the same structure: 142 - - `data/`: Handles all data-related concerns for the feature. 143 - - `providers/`: Contains Riverpod providers that manage the state and business logic. 144 - - `ui/`: Contains all UI components with `pages/` and `widgets/` subdirectories. 145 - 146 - - The backend is using AT Protocol. Any interfacing between frontend and backend will use the AT Protocol API 147 - - Use repository pattern for data persistence 148 - - Use the cache manager: `CacheManagerInterface` from GetIt 149 - - Use SQL cache: `SQLCacheInterface` from GetIt 150 - - Use download manager: `DownloadManagerInterface` from GetIt 151 - - Use Riverpod to manage state 152 - - see keepAlive if you need to keep the state alive 153 - - use riverpod annotations 154 - - Use freezed to manage UI states 155 - - Use GetIt to manage dependencies 156 - - Use singleton for services and repositories 157 - - Use factory for use cases 158 - - Use lazy singleton for controllers 159 - - Use AutoRoute to manage routes 160 - - Use extras to pass data between pages 161 - - Use extensions to manage reusable code 162 - - Use ThemeData to manage themes 163 - - Use AppLocalizations to manage translations (when implemented) 164 - - Use constants to manage constants values 165 - - Use `StorageManager` from GetIt to manage storage 166 - - When a widget tree becomes too deep, it can lead to longer build times and increased memory usage. Flutter needs to traverse the entire tree to render the UI, so a flatter structure improves efficiency 167 - - A flatter widget structure makes it easier to understand and modify the code. Reusable components also facilitate better code organization 168 - - Avoid Nesting Widgets Deeply in Flutter. Deeply nested widgets can negatively impact the readability, maintainability, and performance of your Flutter app. Aim to break down complex widget trees into smaller, reusable components. This not only makes your code cleaner but also enhances the performance by reducing the build complexity 169 - - Deeply nested widgets can make state management more challenging. By keeping the tree shallow, it becomes easier to manage state and pass data between widgets 170 - - Break down large widgets into smaller, focused widgets 171 - - Utilize const constructors wherever possible to reduce rebuilds 172 - - Use Flutter 3.x features. 173 - - Always use withAlpha instead of withOpacity 174 - - Never use relative paths in imports 175 - - Register pages in `lib/src/core/routing/pages.dart` and routes in `lib/src/core/routing/app_router.dart` 176 - - Avoid functions that return widgets 177 - - Use `lib/src/core/utils/logging/` for logging - get `LogService` from GetIt 178 - - Storage keys are always stored at `lib/src/core/storage/preferences/storage_constants.dart` 179 - - Repositories should always have an interface and an implementation (thing_repository.dart, thing_repository_impl.dart) 180 - - For storage, get a `StorageManager` from GetIt 181 - - For cache, get a `CacheManagerInterface` from GetIt 182 - - For auth, get an `AuthRepository` from GetIt 183 - - For API calls, check if `lib/src/core/network/data/repositories/sprk_repository.dart` already has that method in its contract 184 - - If it does, get `SprkRepository` from GetIt 185 - - Available repositories in service locator: 186 - - `LogService` - Logging service 187 - - `SQLCacheInterface` - SQL-based cache 188 - - `CacheManagerInterface` - Cache manager 189 - - `DownloadManagerInterface` - Download manager 190 - - `StorageManager` - Storage management 191 - - `AuthRepository` - Authentication 192 - - `SprkRepository` - Main API repository 193 - - `IdentityRepository` - User identity 194 - - `ThemeRepository` - Theme management 195 - - `ActorRepository` - Actor operations 196 - - `GraphRepository` - Graph operations 197 - - `SettingsRepository` - Settings management 198 - - `OnboardingRepository` - Onboarding flow 199 - - Don't forget to add new repositories to the service locator at `lib/src/core/di/service_locator.dart` 200 - 201 -
-61
.cursor/rules/logging.mdc
··· 1 - --- 2 - description: 3 - globs: 4 - alwaysApply: false 5 - --- 6 - # Spark Logging Framework 7 - 8 - A lightweight, flexible logging system for Spark Social with multiple outputs and log levels. 9 - 10 - ## Features 11 - - 6 log levels: verbose, debug, info, warning, error, fatal 12 - - Console and file outputs 13 - - Automatic stack traces 14 - - Color-coded console logs 15 - - File rotation 16 - 17 - ## Basic Usage 18 - 19 - ```dart 20 - 21 - import 'package:spark/src/core/utils/logging/logger.dart'; 22 - 23 - // Get a logger 24 - final logger = GetIt.instance<LogService>().getLogger('MyFeature'); 25 - 26 - // Log at different levels 27 - logger.v('Verbose message'); 28 - logger.d('Debug message'); 29 - logger.i('Info message'); 30 - logger.w('Warning message'); 31 - logger.e('Error message', error: exception, stackTrace: stackTrace); 32 - logger.f('Fatal message'); 33 - ``` 34 - 35 - ## Predefined Loggers 36 - ```dart 37 - final logService = GetIt.instance<LogService>(); 38 - 39 - // Use predefined loggers 40 - logService.appLogger.i('Application message'); 41 - logService.networkLogger.d('Network operation'); 42 - logService.uiLogger.w('UI warning'); 43 - ``` 44 - 45 - ## Configure Log Levels 46 - 47 - ```dart 48 - // Set minimum log level 49 - LoggerFactory.setGlobalLogLevel(LogLevel.warning); // production 50 - LoggerFactory.setGlobalLogLevel(LogLevel.debug); // development 51 - ``` 52 - 53 - ## Log Levels 54 - 55 - - **VERBOSE**: Detailed debugging information 56 - - **DEBUG**: Debugging information 57 - - **INFO**: General application flow 58 - - **WARNING**: Potential issues 59 - - **ERROR**: Errors that impact functionality 60 - - **FATAL**: Critical errors 61 - - **NOTHING**: Special level to suppress logs
-107
.cursor/rules/project-overview.mdc
··· 1 - --- 2 - description: 3 - globs: 4 - alwaysApply: false 5 - --- 6 - # Spark Social - Project Overview 7 - 8 - This is a Flutter application for a social media platform inspired by TikTok, integrated with the AT Protocol. 9 - 10 - ## Main Entry Points 11 - - [lib/main.dart](mdc:lib/main.dart) - Main application entry point 12 - - [lib/src/sprk_app.dart](mdc:lib/src/sprk_app.dart) - Main MaterialApp configuration 13 - - [pubspec.yaml](mdc:pubspec.yaml) - Project dependencies and configuration 14 - 15 - ## Key Architecture Files 16 - - [lib/src/core/di/service_locator.dart](mdc:lib/src/core/di/service_locator.dart) - Dependency injection setup 17 - - [lib/src/core/routing/app_router.dart](mdc:lib/src/core/routing/app_router.dart) - AutoRoute configuration 18 - - [lib/src/core/storage/preferences/storage_manager.dart](mdc:lib/src/core/storage/preferences/storage_manager.dart) - Storage management 19 - - [lib/src/core/utils/logging/](mdc:lib/src/core/utils/logging) - Logging framework 20 - 21 - ## Current Features 22 - - **Authentication** (`lib/src/features/auth/`) - User login/logout with AT Protocol 23 - - **Feed** (`lib/src/features/feed/`) - Main social media feed with posts and videos 24 - - **Profile** (`lib/src/features/profile/`) - User profiles and profile management 25 - - **Comments** (`lib/src/features/comments/`) - Comment system for posts 26 - - **Search** (`lib/src/features/search/`) - Search functionality 27 - - **Messages** (`lib/src/features/messages/`) - Direct messaging 28 - - **Settings** (`lib/src/features/settings/`) - App settings and preferences 29 - - **Home** (`lib/src/features/home/`) - Home navigation 30 - - **Splash** (`lib/src/features/splash/`) - App initialization screen 31 - 32 - ## Project Structure 33 - ``` 34 - lib/ 35 - ├── main.dart # App entry point 36 - └── src/ 37 - ├── sprk_app.dart # Main MaterialApp 38 - ├── core/ # Shared code across features 39 - │ ├── config/ # Application-wide configurations 40 - │ ├── di/ # Dependency injection setup (GetIt) 41 - │ ├── network/ # ATProto client, API repositories 42 - │ │ └── data/ 43 - │ │ ├── models/ 44 - │ │ └── repositories/ 45 - │ ├── routing/ # AutoRoute setup 46 - │ ├── storage/ # Local storage utilities 47 - │ │ ├── cache/ # SQL-based cache system 48 - │ │ └── preferences/ # Settings and secure storage 49 - │ ├── theme/ # Theme system with repositories 50 - │ │ ├── data/ 51 - │ │ │ ├── models/ 52 - │ │ │ └── repositories/ 53 - │ │ └── domain/ 54 - │ ├── auth/ # Centralized authentication 55 - │ │ └── data/ 56 - │ │ └── repositories/ 57 - │ ├── feed_algorithms/ # Feed algorithms 58 - │ ├── widgets/ # Common reusable widgets 59 - │ └── utils/ # Shared utilities 60 - │ └── logging/ # Complete logging framework 61 - └── features/ # Feature modules 62 - ├── auth/ # Authentication UI and logic 63 - ├── feed/ # Feed display and interactions 64 - ├── profile/ # User profiles 65 - ├── comments/ # Comment system 66 - ├── search/ # Search functionality 67 - ├── messages/ # Direct messaging 68 - ├── settings/ # App settings 69 - ├── home/ # Home navigation 70 - └── splash/ # App initialization 71 - ``` 72 - 73 - ## Tech Stack 74 - - **Flutter 3.7+** with Dart 75 - - **AT Protocol** for backend communication 76 - - **Riverpod** for state management 77 - - **AutoRoute** for navigation 78 - - **GetIt** for dependency injection 79 - - **Freezed** for immutable data classes 80 - - **SQLite** for local caching 81 - - **Secure Storage** for sensitive data 82 - - **Shared Preferences** for app settings 83 - 84 - ## Key Dependencies 85 - - `atproto: ^0.13.3` - AT Protocol client 86 - - `bluesky: ^0.18.10` - Bluesky integration 87 - - `flutter_riverpod: ^2.4.9` - State management 88 - - `auto_route: ^10.1.0+1` - Navigation 89 - - `get_it: ^7.6.7` - Dependency injection 90 - - `sqflite: ^2.4.2` - SQL database 91 - - `flutter_secure_storage: ^9.2.4` - Secure storage 92 - - `cached_network_image: ^3.3.1` - Image caching 93 - - `video_player: ^2.9.3` - Video playback 94 - 95 - ## Available Services (GetIt) 96 - - `LogService` - Comprehensive logging system 97 - - `StorageManager` - Local storage management 98 - - `SQLCacheInterface` - SQL-based caching 99 - - `CacheManagerInterface` - File cache management 100 - - `DownloadManagerInterface` - Download operations 101 - - `AuthRepository` - Authentication services 102 - - `SprkRepository` - Main API operations 103 - - `ThemeRepository` - Theme management 104 - - `SettingsRepository` - App settings 105 - - `ActorRepository` - User actor operations 106 - - `GraphRepository` - Social graph operations 107 - - `OnboardingRepository` - User onboarding
+58
AGENTS.md
··· 1 + # AGENTS.md 2 + 3 + ## Project at a glance 4 + - Root Flutter app: `spark` 5 + - Workspace members: `widgetbook`, `fonts`, `assets` 6 + - Stack: feature-first + Riverpod + GetIt + Freezed + AutoRoute 7 + - Generated files in use: `*.g.dart`, `*.freezed.dart`, `*.gr.dart` 8 + 9 + ## Setup 10 + 1. Use Flutter `3.38.4` (stable, CI-aligned) 11 + 2. Ensure `.env` exists: `touch .env` 12 + 3. Install deps: `flutter pub get --enforce-lockfile` 13 + 14 + ## Common commands (repo root) 15 + - Deps: `flutter pub get --enforce-lockfile` 16 + - Codegen: `dart run build_runner build --delete-conflicting-outputs` 17 + - Format: `dart format .` 18 + - Format check: `dart format --set-exit-if-changed .` 19 + - Analyze all: `flutter analyze .` 20 + - Run app: `flutter run` 21 + 22 + ## Code conventions 23 + - Prefer `package:spark/...` imports; avoid deep cross-feature relative imports 24 + - Import order: Dart SDK, third-party, project; keep `part` after imports 25 + - Use strong explicit types; avoid `dynamic` unless required at boundaries 26 + - Use Freezed for immutable models and `@riverpod` for providers 27 + - Model async state consistently with `AsyncValue` 28 + - Naming: types `PascalCase`, members/providers `lowerCamelCase`, private `_name` 29 + - Keep feature flow: external/API/storage -> repository -> provider -> widget 30 + - Use GetIt (`GetIt.I` / `sl`) for DI-managed services 31 + - Never hand-edit generated files; regenerate instead 32 + 33 + ## Reliability and logging 34 + - Wrap fallible async work in `try/catch` 35 + - After `await`: check `mounted` in widgets, `ref.mounted` in providers 36 + - Prefer graceful failures over crashes (`AsyncValue.error`, typed/null fallback) 37 + - Use `LogService` / `SparkLogger`, not `print` 38 + - Log context + stack traces; use proper levels (`d`, `i`, `w`, `e`, `f`) 39 + 40 + ## Agent workflow 41 + 1. Read nearby feature files for local patterns 42 + 2. Edit source files; run codegen when annotations/models change 43 + 3. Format touched code (`dart format .`) 44 + 4. Analyze (`flutter analyze lib`, or `flutter analyze .` for wider impact) 45 + 5. Run targeted tests first, then broader tests 46 + 6. Keep comments minimal and only when needed 47 + 48 + ## References 49 + - `analysis_options.yaml` 50 + - `lib/src/features/README.md` 51 + - `lib/src/core/utils/logging/README.md` 52 + - `.github/workflows/flutter_lint.yml` 53 + - `.github/workflows/android-internal-release.yml` 54 + 55 + ## Safety 56 + - Never commit secrets (`.env`, platform credentials) 57 + - Do not revert unrelated local changes 58 + - Keep diffs scoped to the feature/task
+83 -105
README.md
··· 1 - # Spark Social 1 + # Spark Client 2 2 3 - A decentralized social network for video sharing built on the AT Protocol, putting users in control of their digital presence and data. 3 + Flutter client for Spark social. This repository contains the production mobile app, 4 + plus workspace packages used by the app (assets, fonts, and widgetbook). 4 5 5 - ![Spark Logo](https://static.sprk.so/branding/logo-horizontal-t6.png) 6 + ## What This Repo Contains 6 7 7 - ## Spark Your Creativity 8 + - `spark` app package at repo root (`pubspec.yaml`) 9 + - Flutter workspace members: 10 + - `widgetbook` (component/dev preview package) 11 + - `fonts` (shared font package) 12 + - `assets` (shared assets package) 8 13 9 - Share videos freely while maintaining complete control over your data. Break free from corporate control and gain digital autonomy. 14 + The app is organized with a feature-first structure and uses Riverpod + GetIt + 15 + Freezed + AutoRoute. 10 16 11 - ## About Spark 17 + ## Tech Stack 12 18 13 - We are building a decentralized social network on the AT Protocol, empowering users to share content without compromising privacy or control. With Spark, you own your data and decide how it's used. 19 + - Flutter / Dart 20 + - Riverpod (with code generation) 21 + - GetIt for dependency injection 22 + - Freezed + json_serializable for immutable models 23 + - AutoRoute for navigation 24 + - AT Protocol client libraries (`atproto`, `bluesky`) 14 25 15 - ## Core Principles 16 - 17 - - **Decentralized Network**: Built on the AT Protocol, giving you full control over your digital presence 18 - - **User-First Approach**: Your data belongs to you, share content freely without compromising privacy 19 - - **Digital Autonomy**: Break free from corporate control and take charge of your online experience 20 - 21 - ## Features 26 + ## Prerequisites 22 27 23 - - **Content Filters**: Customize your feed with advanced content filters 24 - - **Moderation Lists**: Create and subscribe to moderation lists for a healthier online environment 25 - - **Custom Feeds**: Build personalized feeds based on your interests and favorite creators 26 - - **Music & Audio Gallery**: Platform for musicians to reach wider audiences and listeners to discover new talent 27 - - **Built-in Video Editor**: Create and edit professional-quality videos directly in the app 28 - - **Creative Effects**: Share your creativity with Spark effects or design your own 29 - - **Full Content Control**: You decide what to share and with whom 30 - - **Social Media Detox**: Tools to reduce social media addiction and improve focus 31 - - **Community Building**: Connect with like-minded individuals and build genuine communities 32 - - **Human-first Discovery**: Find real creators and build genuine connections 33 - 34 - ## What Makes Spark Different? 35 - 36 - - **Authenticity**: Rediscover genuine connections with real creators 37 - - **Decentralization**: Break free from corporate control and gain digital autonomy 38 - - **Custom Lexicon**: Our own lexicon provides more flexibility for content creators 39 - - **Higher Content Limits**: Increased limits for video length and image quality 40 - - **User Control**: You own your data and decide how it's used 41 - - **Community Focus**: Build meaningful relationships in a supportive environment 28 + - Flutter SDK (CI uses stable `3.38.4`) 29 + - Dart SDK matching Flutter toolchain 30 + - Xcode (for iOS builds) and/or Android SDK 42 31 43 - ## Screenshots 32 + ## Quick Start 44 33 45 - (Screenshots coming soon) 34 + From repository root: 46 35 47 - ## Getting Started 36 + ```bash 37 + touch .env 38 + flutter pub get --enforce-lockfile 39 + dart run build_runner build --delete-conflicting-outputs 40 + flutter run 41 + ``` 48 42 49 - ### Prerequisites 43 + ## Common Commands 50 44 51 - - Flutter SDK 52 - - Dart SDK 53 - - iOS/Android development environment 45 + ### Dependencies and codegen 54 46 55 - ### Installation 47 + ```bash 48 + flutter pub get --enforce-lockfile 49 + dart run build_runner build --delete-conflicting-outputs 50 + dart run build_runner watch --delete-conflicting-outputs 51 + ``` 56 52 57 - 1. Clone this repository 53 + ### Lint and format 58 54 59 55 ```bash 60 - git clone https://github.com/sprksocial/spark-front-end.git 56 + flutter analyze lib 57 + flutter analyze . 58 + dart format . 59 + dart format --set-exit-if-changed . 61 60 ``` 62 61 63 - 2. Navigate to the project directory 62 + ### Tests 63 + 64 + No tests are currently committed, but these are the standard commands: 64 65 65 66 ```bash 66 - cd spark-front-end 67 + flutter test 68 + flutter test test/path/to/some_test.dart 69 + flutter test test/path/to/some_test.dart --plain-name "does something specific" 67 70 ``` 68 71 69 - 3. Install dependencies 72 + For `widgetbook` (run inside `widgetbook/`): 70 73 71 74 ```bash 72 - flutter pub get 75 + flutter test 73 76 ``` 74 77 75 - 4. Generate code 78 + ### Builds 76 79 77 80 ```bash 78 - dart run build_runner build --delete-conflicting-outputs 81 + flutter build appbundle 82 + flutter build apk 83 + flutter build ios --no-codesign 79 84 ``` 80 85 81 - 5. Run the app 86 + ## Project Layout 82 87 83 - ```bash 84 - flutter run 88 + ```text 89 + lib/ 90 + main.dart 91 + src/ 92 + core/ # shared infrastructure (network, routing, utils, theme, etc.) 93 + features/ # feature modules 94 + <feature>/ 95 + data/ 96 + providers/ 97 + ui/ 98 + widgetbook/ # widgetbook workspace package 99 + fonts/ # local font package 100 + assets/ # local assets package 85 101 ``` 86 102 87 - ## Technologies Used 103 + ## Architecture Notes 88 104 89 - - Flutter 90 - - AT Protocol for decentralized social networking 91 - - Cupertino (iOS-style) widgets 92 - - Riverpod for state management 93 - - AutoRoute for routing 94 - - GetIt for dependency injection 95 - - Freezed for data models 96 - - Logger for logging 97 - - Ionicons for beautiful icons 98 - - Video player for media playback 99 - - Camera for video recording 100 - - Animation for smooth transitions 105 + - Prefer package imports (`package:spark/...`) for app code. 106 + - Typical flow is: external/API/storage -> repository -> provider -> widget. 107 + - Providers are generated with `@riverpod`; immutable state is typically Freezed. 108 + - Generated files (`*.g.dart`, `*.freezed.dart`, `*.gr.dart`) should not be edited manually. 101 109 102 - ## Project Structure 110 + ## CI Overview 103 111 104 - ``` 105 - lib/ 106 - ├── main.dart # App entry point 107 - └── src/ 108 - ├── sprk_app.dart # Main MaterialApp 109 - ├── core/ # Shared code across features 110 - │ ├── config/ # Application-wide configurations 111 - │ ├── di/ # Dependency injection setup 112 - │ ├── network/ # ATProto client, API base 113 - │ ├── routing/ # AutoRoute setup 114 - │ ├── storage/ # Local storage utilities 115 - │ ├── theme/ # Theme definitions 116 - │ ├── l10n/ # Localization 117 - │ ├── widgets/ # Common widgets 118 - │ └── utils/ # Shared utilities 119 - └── features/ # Feature modules 120 - └── feature/ 121 - ├── data/ # Data layer for this feature 122 - │ ├── repositories/ 123 - │ └── models/ 124 - ├── providers/ # Riverpod providers 125 - └── ui/ # UI components 126 - ├── pages/ 127 - └── widgets/ 128 - ``` 112 + - Lint workflow runs codegen, then `flutter analyze`. 113 + - Android internal release workflow runs codegen, config setup, then `flutter build appbundle`. 129 114 130 - ## Future Enhancements 115 + See: 131 116 132 - - Enhanced data portability 133 - - Custom server hosting options 134 - - Advanced content creation tools 135 - - Cross-platform federation 136 - - Community moderation tools 137 - - Expanded creative effects library 117 + - `.github/workflows/flutter_lint.yml` 118 + - `.github/workflows/android-internal-release.yml` 138 119 139 120 ## Contributing 140 121 141 - Contributions are welcome! Feel free to submit a Pull Request. 122 + 1. Keep changes scoped to the feature you are editing. 123 + 2. Run format, codegen (if needed), and analyze before opening a PR. 124 + 3. Do not commit secrets (`.env`, signing keys, service credentials). 142 125 143 126 ## License 144 127 145 - This project is licensed under the MIT License - see the LICENSE file for details. 146 - 147 - ## Connect With Us 148 - 149 - - [Subscribe to Newsletter](https://spark-social-link-to-newsletter.com) 150 - - [Learn More](https://spark-social-learn-more.com) 128 + MIT. See `LICENSE`.
+1 -13
lib/main.dart
··· 1 - import 'package:flutter/foundation.dart'; 2 1 import 'package:flutter/material.dart'; 3 2 import 'package:flutter/services.dart'; 4 3 import 'package:flutter_dotenv/flutter_dotenv.dart'; ··· 48 47 49 48 /// Setup logging framework based on environment 50 49 void _setupLogging() { 51 - final logService = sl<LogService>(); 52 - 53 - // Set log level based on debug mode 54 - if (kDebugMode) { 55 - logService.setGlobalLogLevel(LogLevel.debug); 56 - logService.appLogger.i('Debug logging enabled'); 57 - } else { 58 - logService.setGlobalLogLevel(LogLevel.info); 59 - logService.appLogger.i('Production logging enabled'); 60 - } 50 + sl<LogService>().setGlobalLogLevel(LogLevel.warning); 61 51 } 62 52 63 53 /// Initialize auth repository and wait for it to be ready ··· 69 59 final authRepository = sl<AuthRepository>(); 70 60 71 61 if (authRepository is AuthRepositoryImpl) { 72 - logger.d('Waiting for AuthRepository initialization...'); 73 62 await authRepository.initializationComplete; 74 - logger.d('AuthRepository initialized successfully'); 75 63 } 76 64 } catch (e) { 77 65 logger.e('AuthRepository initialization failed', error: e);
-49
lib/src/core/auth/data/repositories/auth_repository_impl.dart
··· 24 24 /// Implementation of the authentication repository for AT Protocol using OAuth 25 25 class AuthRepositoryImpl implements AuthRepository { 26 26 AuthRepositoryImpl() { 27 - _logger.i('Initializing AuthRepository'); 28 27 _initialize(); 29 28 } 30 29 ··· 87 86 /// Fetches a DID document, handling both did:plc and did:web methods. 88 87 Future<Map<String, dynamic>> _fetchDidDocument(String did) async { 89 88 final url = DidUtils.buildDidDocumentUrl(did); 90 - _logger.d('Fetching DID document from: $url'); 91 89 final response = await http.get(url); 92 90 93 91 if (response.statusCode != 200) { 94 - _logger.e('Failed to fetch DID document: ${response.statusCode}'); 95 92 throw Exception('Failed to fetch DID document: ${response.statusCode}'); 96 93 } 97 94 ··· 118 115 119 116 Future<void> _loadSavedSession() async { 120 117 try { 121 - _logger.d('Loading saved account'); 122 - 123 118 // Load account as single JSON object - much faster than multiple reads 124 119 final accountJson = await StorageManager.instance.secure.getString( 125 120 StorageKeys.account, 126 121 ); 127 122 128 123 if (accountJson == null) { 129 - _logger.d('No saved account found'); 130 124 return; 131 125 } 132 126 ··· 160 154 if (tokenNeedsRefresh && _oauthServer != null) { 161 155 final metadata = await _getCachedClientMetadata(); 162 156 _oauthClient = OAuthClient(metadata, service: _oauthServer!); 163 - _logger.d('Access token expired or expiring soon, refreshing'); 164 157 final refreshed = await refreshToken(); 165 158 if (!refreshed) { 166 - _logger.w('Token refresh failed during restore, clearing session'); 167 159 await _clearSavedSession(); 168 160 _oauthSession = null; 169 161 _atProto = null; ··· 174 166 _oauthClient = null; 175 167 return; 176 168 } 177 - _logger.i('Token refreshed successfully during restore'); 178 169 } 179 170 180 171 // Extract just the host from the PDS endpoint ··· 186 177 _oauthSession!, 187 178 service: pdsHost, 188 179 ); 189 - 190 - _logger.i('Account loaded successfully for user: $_handle'); 191 180 } catch (e) { 192 181 _logger.e('Error loading saved account', error: e); 193 182 } ··· 197 186 if (_oauthSession == null) return; 198 187 199 188 try { 200 - _logger.d('Saving account for user: $_handle'); 201 - 202 189 final account = Account( 203 190 accessToken: _oauthSession!.accessToken, 204 191 refreshToken: _oauthSession!.refreshToken, ··· 216 203 StorageKeys.account, 217 204 account.toJsonString(), 218 205 ); 219 - 220 - _logger.d('Account saved successfully'); 221 206 } catch (e) { 222 207 _logger.e('Failed to save account', error: e); 223 208 } ··· 225 210 226 211 Future<void> _clearSavedSession() async { 227 212 try { 228 - _logger.d('Clearing saved account'); 229 213 await StorageManager.instance.secure.remove(StorageKeys.account); 230 214 await StorageManager.instance.secure.remove( 231 215 StorageKeys.pendingAuthContext, 232 216 ); 233 217 // Also clear old session format if exists 234 218 await StorageManager.instance.secure.remove(StorageKeys.userSession); 235 - _logger.d('Account cleared successfully'); 236 219 } catch (e) { 237 220 _logger.e('Failed to clear account', error: e); 238 221 } ··· 241 224 @override 242 225 Future<String> initiateOAuth(String handle) async { 243 226 try { 244 - _logger.i('Initiating OAuth for handle: $handle'); 245 - 246 227 // Resolve handle to DID 247 228 final at = ATProto.anonymous(service: 'public.api.bsky.app'); 248 - _logger.d('Resolving handle: $handle'); 249 229 final didRes = await at.identity.resolveHandle(handle: handle); 250 230 final resolvedDid = didRes.data.did; 251 - _logger.d('Resolved DID: $resolvedDid'); 252 231 253 232 final didDoc = await _fetchDidDocument(resolvedDid); 254 233 final pdsEndpoint = _extractPdsEndpoint(didDoc); ··· 258 237 throw Exception('PDS endpoint not found in DID document'); 259 238 } 260 239 261 - _logger.d('Found PDS endpoint: $pdsEndpoint'); 262 - 263 240 // Store user info for later 264 241 _did = resolvedDid; 265 242 _handle = handle; ··· 269 246 final metadata = await _getCachedClientMetadata(); 270 247 // Resolve OAuth server from PDS endpoint 271 248 _oauthServer = await resolveOAuthServer(pdsEndpoint); 272 - _logger.d('Resolved OAuth server: $_oauthServer'); 273 249 _oauthClient = OAuthClient(metadata, service: _oauthServer!); 274 250 275 251 // Start OAuth authorization ··· 292 268 }), 293 269 ); 294 270 295 - _logger.i('OAuth authorization URL generated'); 296 271 return authUrl.toString(); 297 272 } catch (e) { 298 273 _logger.e('Failed to initiate OAuth', error: e); ··· 303 278 @override 304 279 Future<String> initiateOAuthWithService(String service) async { 305 280 try { 306 - _logger.i('Initiating OAuth with service: $service'); 307 - 308 281 // Store service for later 309 282 _pdsEndpoint = 'https://$service'; 310 283 _oauthServer = service; 311 284 312 285 // Get client metadata (cached) 313 286 final metadata = await _getCachedClientMetadata(); 314 - _logger.d('Using OAuth server: $service'); 315 287 _oauthClient = OAuthClient(metadata, service: _oauthServer!); 316 288 317 289 // Start OAuth authorization without login hint ··· 332 304 }), 333 305 ); 334 306 335 - _logger.i('OAuth authorization URL generated'); 336 307 return authUrl.toString(); 337 308 } catch (e) { 338 309 _logger.e('Failed to initiate OAuth with service', error: e); ··· 427 398 final sessionResponse = await _atProto!.server.getSession(); 428 399 _did = sessionResponse.data.did; 429 400 _handle = sessionResponse.data.handle; 430 - _logger.d('Fetched session info - DID: $_did, Handle: $_handle'); 431 401 } catch (e) { 432 402 _logger.e('Failed to fetch session info', error: e); 433 403 return LoginResult.failed('Failed to get session info: $e'); ··· 443 413 ); 444 414 _pendingContext = null; 445 415 446 - _logger.i('OAuth login successful for user: $_handle'); 447 416 return LoginResult.success(); 448 417 } catch (e, stackTrace) { 449 418 _logger.e('OAuth callback failed', error: e, stackTrace: stackTrace); ··· 454 423 @override 455 424 Future<void> logout() async { 456 425 try { 457 - _logger.i('Logging out user: $_handle'); 458 426 await _clearSavedSession(); 459 427 _oauthSession = null; 460 428 _atProto = null; ··· 464 432 _oauthServer = null; 465 433 _oauthClient = null; 466 434 _pendingContext = null; 467 - _logger.i('Logout successful'); 468 435 } catch (e) { 469 436 _logger.e('Logout failed', error: e); 470 437 } ··· 476 443 await initializationComplete; 477 444 478 445 if (_atProto == null || _oauthSession == null) { 479 - _logger.d('No session to validate'); 480 446 return false; 481 447 } 482 448 483 449 try { 484 - _logger.d('Validating OAuth session for user: $_handle'); 485 450 await _atProto!.identity.resolveHandle(handle: _handle ?? ''); 486 - _logger.d('Session validation successful'); 487 451 return true; 488 452 } catch (e) { 489 - _logger.w( 490 - 'Session validation failed, attempting token refresh', 491 - error: e, 492 - ); 493 - 494 453 // Try to refresh the token before giving up 495 454 final refreshed = await refreshToken(); 496 455 if (refreshed) { 497 - _logger.i('Token refresh successful, session is now valid'); 498 456 return true; 499 457 } 500 458 501 - _logger.w('Token refresh failed, logging out'); 502 459 await logout(); 503 460 return false; 504 461 } ··· 508 465 Future<bool> refreshToken() async { 509 466 try { 510 467 if (_oauthSession == null || _oauthClient == null) { 511 - _logger.w('No OAuth session or client to refresh'); 512 - 513 468 // Try to recreate OAuth client if we have a session but no client 514 469 if (_oauthSession != null && _oauthServer != null) { 515 470 final metadata = await _getCachedClientMetadata(); 516 471 _oauthClient = OAuthClient(metadata, service: _oauthServer!); 517 - _logger.d('OAuthClient recreated with service: $_oauthServer'); 518 472 } else { 519 - _logger.w('Cannot refresh: missing session or OAuth server'); 520 473 return false; 521 474 } 522 475 } 523 476 524 - _logger.i('Refreshing OAuth token'); 525 477 final refreshedSession = await _oauthClient!.refresh(_oauthSession!); 526 478 _oauthSession = refreshedSession; 527 479 ··· 534 486 ); 535 487 536 488 await _saveSession(); 537 - _logger.i('OAuth token refresh successful'); 538 489 return true; 539 490 } catch (e) { 540 491 _logger.e('OAuth token refresh failed', error: e);
+3 -3
lib/src/core/design_system/templates/recording_page_template.dart
··· 47 47 @override 48 48 Widget build(BuildContext context) { 49 49 final size = MediaQuery.sizeOf(context); 50 - final footerHeight = kBottomNavigationBarHeight + 12; 50 + const footerHeight = kBottomNavigationBarHeight + 12; 51 51 const borderRadius = BorderRadius.all(Radius.circular(20)); 52 52 53 53 // Calculate scale based on camera aspect ratio and screen aspect ratio ··· 100 100 ), 101 101 ), 102 102 ), 103 - SizedBox( 103 + const SizedBox( 104 104 height: footerHeight, 105 - child: const ColoredBox(color: Colors.black), 105 + child: ColoredBox(color: Colors.black), 106 106 ), 107 107 ], 108 108 ),
-2
lib/src/core/l10n/app_localizations_en.dart
··· 1 - // ignore: unused_import 2 - import 'package:intl/intl.dart' as intl; 3 1 import 'app_localizations.dart'; 4 2 5 3 // ignore_for_file: type=lint
+30 -22
lib/src/core/media/create_media_actions.dart
··· 5 5 import 'package:get_it/get_it.dart'; 6 6 import 'package:image_picker/image_picker.dart'; 7 7 import 'package:pro_video_editor/pro_video_editor.dart'; 8 + import 'package:spark/src/core/pro_video_editor/models/video_editor_result.dart'; 8 9 import 'package:spark/src/core/pro_video_editor/pro_video_editor_repository.dart'; 9 10 import 'package:spark/src/core/routing/app_router.dart'; 10 11 import 'package:spark/src/features/posting/ui/pages/recording_page.dart'; ··· 48 49 source: ImageSource.gallery, 49 50 maxDuration: const Duration(seconds: 180), 50 51 ); 51 - if (pickedVideo != null && context.mounted) { 52 - final editorVideo = EditorVideo.file(File(pickedVideo.path)); 53 - final repository = GetIt.I<ProVideoEditorRepository>(); 54 - final result = storyMode 55 - ? await repository.openStoryVideoEditor(context, editorVideo) 56 - : await repository.openVideoEditor(context, editorVideo); 57 - if (result != null && context.mounted) { 58 - if (storyMode) { 59 - // For stories, post directly 60 - await context.router.push( 61 - StoryPostRoute(videoPath: result.video.path), 62 - ); 63 - } else { 64 - // For posts, go to review 65 - await context.router.push( 66 - VideoReviewRoute( 67 - videoPath: result.video.path, 68 - storyMode: storyMode, 69 - soundRef: result.soundRef, 70 - ), 71 - ); 72 - } 52 + if (pickedVideo == null) return; 53 + if (!context.mounted) return; 54 + 55 + final editorVideo = EditorVideo.file(File(pickedVideo.path)); 56 + final repository = GetIt.I<ProVideoEditorRepository>(); 57 + VideoEditorResult? result; 58 + if (storyMode) { 59 + if (!context.mounted) return; 60 + result = await repository.openStoryVideoEditor(context, editorVideo); 61 + } else { 62 + if (!context.mounted) return; 63 + result = await repository.openVideoEditor(context, editorVideo); 64 + } 65 + 66 + if (result != null && context.mounted) { 67 + if (storyMode) { 68 + // For stories, post directly 69 + await context.router.push( 70 + StoryPostRoute(videoPath: result.video.path), 71 + ); 72 + } else { 73 + // For posts, go to review 74 + await context.router.push( 75 + VideoReviewRoute( 76 + videoPath: result.video.path, 77 + storyMode: storyMode, 78 + soundRef: result.soundRef, 79 + ), 80 + ); 73 81 } 74 82 } 75 83 };
+1 -1
lib/src/core/network/atproto/data/repositories/feed_repository_impl.dart
··· 79 79 80 80 if (hasMedia(parsedPost)) { 81 81 posts.add(parsedPost); 82 - } else {} 82 + } 83 83 } catch (e) { 84 84 _logger.w('Failed to parse $source post, skipping: $e'); 85 85 }
-14
lib/src/core/network/messages/data/repository/messages_repository_xrpc.dart
··· 31 31 queryParameters: params.isEmpty ? null : params, 32 32 ); 33 33 34 - _logger.d('GET $url'); 35 - 36 34 final response = await http.get( 37 35 url, 38 36 headers: { ··· 40 38 'Authorization': 'Bearer $token', 41 39 }, 42 40 ); 43 - 44 - _logger.d('Response: ${response.statusCode}'); 45 41 46 42 if (response.statusCode == 200) { 47 43 return jsonDecode(response.body) as Map<String, dynamic>; ··· 65 61 final token = await _serviceAuthHelper.getServiceToken(nsid); 66 62 final url = Uri.parse('$_baseUrl/xrpc/$nsid'); 67 63 68 - _logger 69 - ..d('POST $url') 70 - ..d('Body: ${jsonEncode(body)}'); 71 - 72 64 final response = await http.post( 73 65 url, 74 66 headers: { ··· 78 70 }, 79 71 body: jsonEncode(body), 80 72 ); 81 - 82 - _logger.d('Response: ${response.statusCode}'); 83 73 84 74 if (response.statusCode == 204) { 85 75 // No content, but considered success ··· 149 139 'so.sprk.chat.convo.getConvoForMembers', 150 140 ); 151 141 152 - _logger.d('GET $url'); 153 - 154 142 final response = await http.get( 155 143 url, 156 144 headers: { ··· 158 146 'Authorization': 'Bearer $token', 159 147 }, 160 148 ); 161 - 162 - _logger.d('Response: ${response.statusCode}'); 163 149 164 150 if (response.statusCode == 200) { 165 151 final data = jsonDecode(response.body) as Map<String, dynamic>;
-8
lib/src/core/network/xrpc/service_auth_helper.dart
··· 29 29 DateTime.now().isBefore( 30 30 cached.expiry.subtract(const Duration(seconds: 30)), 31 31 )) { 32 - _logger.d('Using cached service token for $nsid'); 33 32 return cached.token; 34 33 } 35 34 ··· 39 38 throw Exception('Not authenticated - ATProto client not available'); 40 39 } 41 40 42 - _logger.d( 43 - 'Requesting service auth token for $nsid with aud: $serviceDid', 44 - ); 45 - 46 41 // Calculate expiration (60 seconds from now) 47 42 final exp = 48 43 DateTime.now() ··· 65 60 expiry: DateTime.fromMillisecondsSinceEpoch(exp * 1000), 66 61 ); 67 62 68 - _logger.d('Successfully obtained service auth token for $nsid'); 69 63 return token; 70 64 } catch (e) { 71 65 _logger.e('Failed to get service auth token for $nsid', error: e); ··· 76 70 /// Clears the token cache 77 71 void clearCache() { 78 72 _tokenCache.clear(); 79 - _logger.d('Service auth token cache cleared'); 80 73 } 81 74 82 75 /// Clears a specific token from cache 83 76 void clearToken(String nsid) { 84 77 _tokenCache.remove(nsid); 85 - _logger.d('Cleared service auth token for $nsid'); 86 78 } 87 79 }
+4 -44
lib/src/core/notifications/push_notification_service.dart
··· 11 11 12 12 /// Service for managing push notifications via Firebase Cloud Messaging 13 13 class PushNotificationService { 14 - PushNotificationService() { 15 - _logger.v('PushNotificationService initialized'); 16 - } 14 + PushNotificationService(); 17 15 18 16 late final FirebaseMessaging _messaging; 19 17 final SparkLogger _logger = GetIt.instance<LogService>().getLogger( ··· 31 29 /// Initializes Firebase without requesting permissions 32 30 /// Permissions should be requested via [requestPermissionAndGetToken] 33 31 Future<void> initialize() async { 34 - _logger.i('Initializing push notification service'); 35 - 36 32 try { 37 33 await Firebase.initializeApp( 38 34 options: DefaultFirebaseOptions.currentPlatform, 39 35 ); 40 36 _messaging = FirebaseMessaging.instance; 41 - _logger.d('Firebase initialized'); 42 37 43 38 // Listen for token refresh 44 39 _messaging.onTokenRefresh.listen(_onTokenRefresh); 45 40 46 41 // Check if badge is supported on this device 47 42 _badgeSupported = await AppBadgePlus.isSupported(); 48 - _logger.d('Badge support: $_badgeSupported'); 49 43 50 44 _initialized = true; 51 - _logger.i('Push notification service initialized successfully'); 52 45 53 46 // Set up message handlers for deep linking 54 47 await _setupMessageHandlers(); ··· 69 62 // Handle notification tap when app was terminated 70 63 final initialMessage = await _messaging.getInitialMessage(); 71 64 if (initialMessage != null) { 72 - _logger.i('App opened from terminated state via notification'); 73 65 // Queue the navigation - will be processed after auth completes 74 66 _pendingNotification = initialMessage; 75 67 } 76 68 77 69 // Handle foreground messages (for badge updates) 78 70 FirebaseMessaging.onMessage.listen(_handleForegroundMessage); 79 - 80 - _logger.d('FCM message handlers set up'); 81 71 } 82 72 83 73 /// Handles notification tap when app is in background or foreground 84 74 void _handleNotificationTap(RemoteMessage message) { 85 - _logger.i('Notification tapped: ${message.data}'); 86 - 87 75 final data = message.data; 88 76 final reason = data['reason'] as String?; 89 77 final author = data['author'] as String?; ··· 91 79 final reasonSubject = data['reasonSubject'] as String?; 92 80 93 81 if (!GetIt.instance.isRegistered<AppRouter>()) { 94 - _logger.w('AppRouter not registered, queueing navigation'); 95 82 _pendingNotification = message; 96 83 return; 97 84 } ··· 100 87 101 88 if (reason == 'follow' && author != null) { 102 89 // Navigate to profile for follow notifications 103 - _logger.d('Navigating to profile: $author'); 104 90 router.push(ProfileRoute(did: author)); 105 91 } else if (reasonSubject != null) { 106 92 // For likes/reposts, navigate to the subject (the post being liked/reposted) 107 - _logger.d('Navigating to post (reasonSubject): $reasonSubject'); 108 93 router.push(StandalonePostRoute(postUri: reasonSubject)); 109 94 } else if (recordUri != null) { 110 95 // For replies/mentions, navigate to the record itself 111 - _logger.d('Navigating to post (recordUri): $recordUri'); 112 96 router.push(StandalonePostRoute(postUri: recordUri)); 113 97 } else if (author != null) { 114 98 // Fallback to author profile 115 - _logger.d('Navigating to author profile (fallback): $author'); 116 99 router.push(ProfileRoute(did: author)); 117 - } else { 118 - _logger.w('No valid navigation target in notification data'); 119 100 } 120 101 } 121 102 122 103 /// Handles foreground messages (updates badge count) 123 104 void _handleForegroundMessage(RemoteMessage message) { 124 - _logger.d('Foreground message received: ${message.notification?.title}'); 125 - 126 105 // Badge is already set by the server in the APNS payload 127 106 // We could optionally show an in-app notification here 128 107 } ··· 133 112 /// Processes pending notification navigation (call after auth completes) 134 113 void processPendingNotification() { 135 114 if (_pendingNotification != null) { 136 - _logger.i('Processing pending notification navigation'); 137 115 _handleNotificationTap(_pendingNotification!); 138 116 _pendingNotification = null; 139 117 } ··· 158 136 Future<bool> requestPermission() async { 159 137 if (!_initialized) return false; 160 138 161 - _logger.d('Requesting notification permissions'); 162 - 163 139 try { 164 140 final settings = await _messaging.requestPermission(); 165 141 166 - if (settings.authorizationStatus == AuthorizationStatus.authorized) { 167 - _logger.i('User granted notification permission'); 168 - return true; 169 - } else if (settings.authorizationStatus == 170 - AuthorizationStatus.provisional) { 171 - _logger.i('User granted provisional notification permission'); 172 - return true; 173 - } else { 174 - _logger.w('User denied notification permission'); 175 - return false; 176 - } 142 + return settings.authorizationStatus == AuthorizationStatus.authorized || 143 + settings.authorizationStatus == AuthorizationStatus.provisional; 177 144 } catch (e, stackTrace) { 178 145 _logger.e( 179 146 'Failed to request permission', ··· 201 168 202 169 /// Handles FCM token refresh 203 170 void _onTokenRefresh(String token) { 204 - _logger.d('FCM token refreshed: ${token.substring(0, 10)}...'); 205 171 _currentToken = token; 206 172 // Token refresh registration is handled by the auth flow 207 173 // which will call registerPush with the new token ··· 213 179 if (!_initialized) return null; 214 180 215 181 try { 216 - _currentToken ??= await _messaging.getToken(); 217 - if (_currentToken != null) { 218 - _logger.d('FCM token obtained: ${_currentToken!.substring(0, 10)}...'); 219 - } 220 - return _currentToken; 182 + return _currentToken ??= await _messaging.getToken(); 221 183 } catch (e) { 222 184 _logger.e('Failed to get FCM token', error: e); 223 185 return null; ··· 236 198 237 199 try { 238 200 await AppBadgePlus.updateBadge(0); 239 - _logger.d('Badge cleared'); 240 201 } catch (e, stackTrace) { 241 202 _logger.e('Failed to clear badge', error: e, stackTrace: stackTrace); 242 203 } ··· 248 209 249 210 try { 250 211 await AppBadgePlus.updateBadge(count); 251 - _logger.d('Badge updated to $count'); 252 212 } catch (e, stackTrace) { 253 213 _logger.e('Failed to update badge', error: e, stackTrace: stackTrace); 254 214 }
-1
lib/src/core/pro_image_editor/story_image_editor_configs.dart
··· 84 84 stream: rebuildStream, 85 85 ), 86 86 wrapBody: (editor, rebuildStream, content) { 87 - // Fill behind content so no letterboxing shows as dark lines on sides 88 87 return ClipRRect( 89 88 borderRadius: _storyEditorBorderRadius, 90 89 child: Container(
+4 -10
lib/src/core/pro_image_editor/ui/story_image_editor_page.dart
··· 181 181 return Scaffold( 182 182 backgroundColor: Colors.black, 183 183 body: ProImageEditor.file( 184 - _croppedImageFile!, 184 + _croppedImageFile, 185 185 key: _editorKey, 186 186 callbacks: ProImageEditorCallbacks( 187 187 onImageEditingComplete: _onImageEditingComplete, 188 188 onCloseEditor: _onCloseEditor, 189 189 stickerEditorCallbacks: StickerEditorCallbacks( 190 - onSearchChanged: (value) { 191 - debugPrint('Sticker search: $value'); 192 - }, 190 + onSearchChanged: (_) {}, 193 191 ), 194 192 ), 195 193 configs: _configs, ··· 270 268 }); 271 269 _addBackgroundImageLayer(); 272 270 } 273 - } catch (e) { 274 - debugPrint('Failed to load image size: $e'); 275 - } 271 + } catch (_) {} 276 272 } 277 273 278 274 void _initializeEditor() { ··· 372 368 onAfterViewInit: _addBackgroundImageLayer, 373 369 ), 374 370 stickerEditorCallbacks: StickerEditorCallbacks( 375 - onSearchChanged: (value) { 376 - debugPrint('Sticker search: $value'); 377 - }, 371 + onSearchChanged: (_) {}, 378 372 ), 379 373 ), 380 374 configs: _configs.copyWith(
+1 -1
lib/src/core/pro_image_editor/utils/story_image_cropper.dart
··· 53 53 54 54 // Draw the cropped portion scaled to target size 55 55 final srcRect = Rect.fromLTWH(cropX, cropY, cropWidth, cropHeight); 56 - final dstRect = Rect.fromLTWH(0, 0, targetWidth, targetHeight); 56 + const dstRect = Rect.fromLTWH(0, 0, targetWidth, targetHeight); 57 57 58 58 canvas.drawImageRect(image, srcRect, dstRect, Paint()); 59 59
+3 -2
lib/src/core/pro_video_editor/pro_video_editor_repository_impl.dart
··· 103 103 }) { 104 104 return StoryBlankCanvasEditorPage.open( 105 105 context, 106 - backgroundImage: 107 - backgroundImage != null ? File(backgroundImage.path) : null, 106 + backgroundImage: backgroundImage != null 107 + ? File(backgroundImage.path) 108 + : null, 108 109 backgroundColor: backgroundColor, 109 110 ); 110 111 }
+1 -2
lib/src/core/pro_video_editor/services/audio_waveform_extractor.dart
··· 38 38 ); 39 39 40 40 return _normalizeWaveform(waveformData); 41 - } catch (e) { 42 - debugPrint('Failed to extract waveform: $e'); 41 + } catch (_) { 43 42 return []; 44 43 } 45 44 }
+2 -7
lib/src/core/pro_video_editor/ui/video_editor_grounded_page.dart
··· 296 296 ), 297 297 ), 298 298 ); 299 - } catch (e) { 300 - debugPrint('Failed to fetch trending audios: $e'); 301 - } 299 + } catch (_) {} 302 300 return audioTracks; 303 301 } 304 302 ··· 639 637 ), 640 638 mainEditorCallbacks: const MainEditorCallbacks(), 641 639 stickerEditorCallbacks: StickerEditorCallbacks( 642 - onSearchChanged: (value) { 643 - /// Filter your stickers 644 - debugPrint(value); 645 - }, 640 + onSearchChanged: (_) {}, 646 641 ), 647 642 ), 648 643 configs: _configs,
+3 -1
lib/src/core/pro_video_editor/ui/widgets/player/video_player_widget.dart
··· 56 56 if (path != null && path.isNotEmpty) { 57 57 return VideoPlayerController.networkUrl(Uri.file(path)); 58 58 } 59 - } catch (_) {} 59 + } catch (_) { 60 + // File doesn't expose path, will fall back to network URL 61 + } 60 62 } 61 63 // Fallback controller (should not happen when a valid video is provided) 62 64 return VideoPlayerController.networkUrl(Uri.parse('about:blank'));
-10
lib/src/core/routing/app_router.dart
··· 9 9 import 'package:spark/src/core/auth/data/repositories/onboarding_repository.dart'; 10 10 import 'package:spark/src/core/network/atproto/atproto.dart'; 11 11 import 'package:spark/src/core/routing/pages.dart'; 12 - import 'package:spark/src/core/utils/logging/log_service.dart'; 13 - import 'package:spark/src/core/utils/logging/logger.dart'; 14 12 import 'package:spark/src/features/profile/ui/pages/user_list_page.dart'; 15 13 16 14 part 'app_router.gr.dart'; 17 15 18 16 class AuthGuard extends AutoRouteGuard { 19 - final SparkLogger _logger = GetIt.instance<LogService>().getLogger( 20 - 'AuthGuard', 21 - ); 22 - 23 17 @override 24 18 Future<void> onNavigation( 25 19 NavigationResolver resolver, ··· 32 26 final hasSpark = await onboardingRepository.hasSparkProfile(); 33 27 34 28 if (!hasSpark) { 35 - _logger.d('No Spark profile found, redirecting to register'); 36 29 resolver.redirectUntil(const RegisterRoute()); 37 30 return; 38 31 } ··· 40 33 final isSessionValid = await authRepository.validateSession(); 41 34 42 35 if (!isSessionValid) { 43 - _logger.d('Session invalid, redirecting to login'); 44 36 resolver.redirectUntil(const LoginRoute()); 45 37 return; 46 38 } 47 39 48 - _logger.d('Authentication valid, continuing to route'); 49 40 resolver.next(); 50 41 } catch (e) { 51 - _logger.e('Error during auth check', error: e); 52 42 resolver.redirectUntil(const RegisterRoute()); 53 43 } 54 44 }
+2 -31
lib/src/core/storage/cache/download_manager_impl.dart
··· 23 23 type: 'timeline', 24 24 config: SavedFeed(type: 'timeline', value: 'following', pinned: true), 25 25 ); 26 - _logger.d('DownloadManager initialized with default feed'); 27 26 } 28 27 29 28 @override ··· 44 43 45 44 @override 46 45 void setActiveFeed(Feed feed) { 47 - _logger.d('Setting active feed to: ${feed.config.id}'); 48 46 _activeFeed = feed; 49 47 _updateTaskPriorities(); 50 48 _processQueue(); // Re-evaluate queue processing if active feed changed ··· 74 72 void submitTask(DownloadTask task) { 75 73 // Prevent duplicate tasks for the same post if one is already pending/active 76 74 if (_tasks.contains(task)) { 77 - _logger.d('Task for ${task.uri} already in queue. Skipping.'); 78 75 return; 79 76 } 80 77 ··· 88 85 Future<void> _processQueue() async { 89 86 if (_isProcessing) return; // Already processing 90 87 _isProcessing = true; 91 - _logger.d('Processing queue. Queue size: ${_tasks.length}'); 92 88 final newTasks = <DownloadTask>[]; 93 89 94 90 while (_tasks.isNotEmpty) { ··· 124 120 } 125 121 _tasks.remove(task); // Ensure removal on unhandled pool error 126 122 }); 127 - _logger.d('Task ${task.uri} submitted to pool for execution.'); 128 123 } 129 124 if (task.status != DownloadTaskStatus.completed && 130 125 task.status != DownloadTaskStatus.failed) { ··· 135 130 _tasks.addAll(newTasks); 136 131 137 132 _isProcessing = false; 138 - _logger.d('Finished a processing pass. Queue size: ${_tasks.length}'); 139 133 } 140 134 141 135 @override 142 136 Future<void> dispose() async { 143 - _logger.d('Disposing DownloadManager...'); 144 137 _cancelAllPendingTasks(); // Attempt to clean up 145 138 await _pool 146 139 .close(); // Closes the pool and waits for active tasks to complete 147 - _logger.d('DownloadManager disposed.'); 148 140 } 149 141 150 142 bool _areTherePendingActiveFeedTasks() { ··· 156 148 } 157 149 158 150 Future<void> _executeTask(DownloadTask task) async { 159 - _logger.d( 160 - 'Executing task: ${task.uri} for feed ${task.feed.config.id} ' 161 - 'with priority ${task.priority}', 162 - ); 163 151 if (_activeFeed != task.feed && 164 152 task.priority > activeFeedPriority && 165 153 _areTherePendingActiveFeedTasks()) { 166 - _logger.d( 167 - 'Task ${task.uri} is for an inactive feed, but there are still ' 168 - 'pending active feed tasks. Skipping.', 169 - ); 170 154 return; 171 155 } 172 156 try { ··· 174 158 // This is a softer check than full cancellation. 175 159 176 160 task.status = DownloadTaskStatus.active; 177 - 178 - _logger.d('Caching media for post: ${task.uri}'); 179 161 // Actual caching work - start downloading the media 180 162 switch (task.post.media) { 181 163 case MediaViewVideo(): ··· 191 173 ), 192 174 ), 193 175 ); 194 - _logger.d('Video file successfully cached: ${task.post.videoUrl}'); 195 176 case MediaViewImages(): 196 177 for (final url in task.post.imageUrls) { 197 178 // Download the image and verify it's cached ··· 222 203 ), 223 204 ), 224 205 ); 225 - _logger.d( 226 - 'Video file successfully cached: ${task.post.videoUrl}', 227 - ); 228 206 case MediaViewImage() || MediaViewImages() || MediaViewBskyImages(): 229 207 for (final url in task.post.imageUrls) { 230 208 // Download the image and verify it's cached 231 - final fileInfo = await CachedNetworkImageProvider 232 - .defaultCacheManager 209 + await CachedNetworkImageProvider.defaultCacheManager 233 210 .downloadFile(url, key: url); 234 - if (fileInfo.statusCode != 200) { 235 - _logger.w( 236 - 'Image file was not properly cached after download: $url', 237 - ); 238 - } 239 211 } 240 212 default: 241 213 throw Exception('Unsupported media type: ${media.runtimeType}'); ··· 263 235 } 264 236 265 237 task.status = DownloadTaskStatus.completed; 266 - _logger.d('Task ${task.uri} completed successfully.'); 267 238 task.onComplete(task); 268 239 } catch (e, s) { 269 240 task.status = DownloadTaskStatus.failed; 270 - _logger.e('Task ${task.uri} failed: $e', error: e, stackTrace: s); 241 + _logger.w('Failed to cache media for ${task.uri}'); 271 242 task.onError(task, e, s); 272 243 } finally { 273 244 // Remove from the main queue regardless of outcome, as it's processed.
+2 -2
lib/src/core/utils/logging/logger_factory.dart
··· 9 9 /// Factory for creating logger instances 10 10 class LoggerFactory { 11 11 /// Global minimum log level 12 - static LogLevel _globalMinLevel = kDebugMode ? LogLevel.debug : LogLevel.info; 12 + static LogLevel _globalMinLevel = LogLevel.warning; 13 13 14 14 /// List of default outputs 15 15 static final List<LogOutput> _defaultOutputs = [ ··· 72 72 if (!kIsWeb) { 73 73 _defaultOutputs.add(FileOutput()); 74 74 } 75 - _globalMinLevel = kDebugMode ? LogLevel.debug : LogLevel.info; 75 + _globalMinLevel = LogLevel.warning; 76 76 } 77 77 }
+7 -331
lib/src/core/utils/logging/riverpod_logger.dart
··· 1 1 import 'package:flutter_riverpod/flutter_riverpod.dart'; 2 - import 'package:get_it/get_it.dart'; 3 - 4 - import 'package:spark/src/core/utils/logging/logging.dart'; 5 2 6 3 /// A ProviderObserver that logs provider changes 4 + /// 5 + /// Logging is currently disabled to reduce console noise. 6 + /// Re-enable by uncommenting the logging logic in didUpdateProvider. 7 7 final class SparkRiverpodLogger extends ProviderObserver { 8 8 /// Constructor 9 - SparkRiverpodLogger({LogService? logService}) 10 - : _logService = logService ?? GetIt.instance<LogService>() { 11 - _logger = _logService.getLogger('Riverpod'); 12 - } 13 - final LogService _logService; 14 - late final SparkLogger _logger; 9 + SparkRiverpodLogger(); 15 10 16 11 @override 17 12 void didAddProvider(ProviderObserverContext context, Object? value) { 18 - _logger.d( 19 - '${context.provider.name ?? context.provider.runtimeType} ' 20 - 'added: ${_truncateIfNeeded(value.toString())}', 21 - ); 13 + // Provider lifecycle logging disabled to reduce noise 22 14 } 23 15 24 16 @override 25 17 void didDisposeProvider(ProviderObserverContext context) { 26 - _logger.d( 27 - '${context.provider.name ?? context.provider.runtimeType} was disposed.', 28 - ); 18 + // Provider lifecycle logging disabled to reduce noise 29 19 } 30 20 31 21 @override ··· 34 24 Object? previousValue, 35 25 Object? newValue, 36 26 ) { 37 - // Skip logging if previous and new values are identical 38 - if (previousValue == newValue) return; 39 - 40 - // Also skip if they have the same string representation 41 - if (previousValue.toString() == newValue.toString()) return; 42 - 43 - final diff = _generateDiff(previousValue, newValue); 44 - if (diff.isNotEmpty && diff != 'No changes') { 45 - _logger.d( 46 - '${context.provider.name ?? context.provider.runtimeType} ' 47 - 'changed: $diff', 48 - ); 49 - } 50 - } 51 - 52 - /// Generates a diff showing what changed between two values 53 - String _generateDiff(Object? previous, Object? current) { 54 - // Handle null cases 55 - if (previous == null && current == null) return 'No changes'; 56 - if (previous == null) { 57 - return 'Added: ${_truncateIfNeeded(current.toString())}'; 58 - } 59 - if (current == null) { 60 - return 'Removed: ${_truncateIfNeeded(previous.toString())}'; 61 - } 62 - 63 - // Handle Map types 64 - if (previous is Map && current is Map) { 65 - return _generateMapDiff(previous, current); 66 - } 67 - 68 - // Handle List types 69 - if (previous is List && current is List) { 70 - return _generateListDiff(previous, current); 71 - } 72 - 73 - // Handle custom objects with structured toString() 74 - final previousStr = previous.toString(); 75 - final currentStr = current.toString(); 76 - 77 - if (previousStr == currentStr) return 'No visible changes'; 78 - 79 - final structuredDiff = _tryParseStructuredObjectDiff( 80 - previousStr, 81 - currentStr, 82 - ); 83 - if (structuredDiff != null && structuredDiff.isNotEmpty) { 84 - return structuredDiff; 85 - } 86 - 87 - // For very long objects, try to show just class name and indicate change 88 - if (previousStr.length > 1000 || currentStr.length > 1000) { 89 - final className = _extractClassName(previousStr); 90 - return className != null 91 - ? '$className fields changed' 92 - : 'Object changed (too large to diff)'; 93 - } 94 - 95 - // Fallback to simple before → after for smaller objects 96 - return '${_truncateIfNeeded(previousStr)} → ' 97 - '${_truncateIfNeeded(currentStr)}'; 98 - } 99 - 100 - /// Generates diff for Map objects 101 - String _generateMapDiff(Map previous, Map current) { 102 - final changes = <String>[]; 103 - final allKeys = {...previous.keys, ...current.keys}; 104 - 105 - for (final key in allKeys) { 106 - final prevValue = previous[key]; 107 - final currValue = current[key]; 108 - 109 - if (!previous.containsKey(key)) { 110 - changes.add('+ $key: ${_truncateIfNeeded(currValue.toString())}'); 111 - } else if (!current.containsKey(key)) { 112 - changes.add('- $key: ${_truncateIfNeeded(prevValue.toString())}'); 113 - } else if (prevValue != currValue) { 114 - changes.add( 115 - '~ $key: ${_truncateIfNeeded(prevValue.toString())} → ' 116 - '${_truncateIfNeeded(currValue.toString())}', 117 - ); 118 - } 119 - } 120 - 121 - return changes.isEmpty ? 'No changes' : changes.join(', '); 122 - } 123 - 124 - /// Generates diff for List objects 125 - String _generateListDiff(List previous, List current) { 126 - final changes = <String>[]; 127 - 128 - // Find added items (items in current but not in previous) 129 - final added = <dynamic>[]; 130 - for (final item in current) { 131 - if (!previous.contains(item)) { 132 - added.add(item); 133 - } 134 - } 135 - 136 - // Find removed items (items in previous but not in current) 137 - final removed = <dynamic>[]; 138 - for (final item in previous) { 139 - if (!current.contains(item)) { 140 - removed.add(item); 141 - } 142 - } 143 - 144 - // Add length change if different 145 - if (previous.length != current.length) { 146 - changes.add('length: ${previous.length} → ${current.length}'); 147 - } 148 - 149 - // Add removed items 150 - if (removed.isNotEmpty) { 151 - if (removed.length <= 3) { 152 - changes.add( 153 - 'removed: [${removed.map((e) => _truncateIfNeeded( 154 - e.toString(), 155 - maxLength: 50, 156 - )).join(', ')}]', 157 - ); 158 - } else { 159 - changes.add('removed: ${removed.length} items'); 160 - } 161 - } 162 - 163 - // Add added items 164 - if (added.isNotEmpty) { 165 - if (added.length <= 3) { 166 - changes.add( 167 - 'added: [${added.map((e) => _truncateIfNeeded( 168 - e.toString(), 169 - maxLength: 50, 170 - )).join(', ')}]', 171 - ); 172 - } else { 173 - changes.add('added: ${added.length} items'); 174 - } 175 - } 176 - 177 - // If no adds/removes but lists are different, check for positional changes 178 - if (changes.isEmpty && previous.length == current.length) { 179 - for (var i = 0; i < previous.length; i++) { 180 - if (previous[i] != current[i]) { 181 - changes.add( 182 - '[$i]: ${_truncateIfNeeded(previous[i].toString(), maxLength: 50)} ' 183 - '→ ${_truncateIfNeeded(current[i].toString(), maxLength: 50)}', 184 - ); 185 - if (changes.length >= 3) { 186 - changes.add('... and ${previous.length - i - 1} more changes'); 187 - break; 188 - } 189 - } 190 - } 191 - } 192 - 193 - return changes.isEmpty ? 'No changes' : changes.join(', '); 194 - } 195 - 196 - String? _tryParseStructuredObjectDiff(String previous, String current) { 197 - final prevFields = _parseStructuredObject(previous); 198 - final currFields = _parseStructuredObject(current); 199 - 200 - if (prevFields == null || currFields == null) return null; 201 - 202 - final changes = <String>[]; 203 - final allKeys = {...prevFields.keys, ...currFields.keys}; 204 - 205 - for (final key in allKeys) { 206 - final prevValue = prevFields[key]; 207 - final currValue = currFields[key]; 208 - 209 - if (!prevFields.containsKey(key)) { 210 - changes.add('+ $key: ${_truncateIfNeeded(currValue!, maxLength: 200)}'); 211 - } else if (!currFields.containsKey(key)) { 212 - changes.add('- $key: ${_truncateIfNeeded(prevValue!, maxLength: 200)}'); 213 - } else if (prevValue != currValue) { 214 - // Skip logging very large nested objects - just show field name changed 215 - if (prevValue!.length > 500 || currValue!.length > 500) { 216 - changes.add('$key: <complex object changed>'); 217 - } else { 218 - // Check if the field values are lists and handle them specially 219 - final listDiff = _tryParseFieldAsListDiff(prevValue, currValue); 220 - if (listDiff != null) { 221 - changes.add('$key: $listDiff'); 222 - } else { 223 - changes.add( 224 - '$key: ${_truncateIfNeeded(prevValue, maxLength: 100)} → ' 225 - '${_truncateIfNeeded(currValue, maxLength: 100)}', 226 - ); 227 - } 228 - } 229 - } 230 - } 231 - 232 - final result = changes.join(', '); 233 - return result.isEmpty ? null : result; 234 - } 235 - 236 - /// Extracts the class name from a structured object string 237 - String? _extractClassName(String objStr) { 238 - final match = RegExp(r'^([^(]+)\(').firstMatch(objStr.trim()); 239 - return match?.group(1)?.trim(); 240 - } 241 - 242 - Map<String, String>? _parseStructuredObject(String objStr) { 243 - // Match pattern like ClassName(...) 244 - final match = RegExp(r'^[^(]+\((.+)\)$').firstMatch(objStr.trim()); 245 - if (match == null) return null; 246 - 247 - final content = match.group(1)!; 248 - final fields = <String, String>{}; 249 - 250 - // Split by comma, but be careful about nested structures 251 - final parts = _splitFields(content); 252 - 253 - for (final part in parts) { 254 - final colonIndex = part.indexOf(':'); 255 - if (colonIndex == -1) continue; 256 - 257 - final key = part.substring(0, colonIndex).trim(); 258 - final value = part.substring(colonIndex + 1).trim(); 259 - fields[key] = value; 260 - } 261 - 262 - return fields.isEmpty ? null : fields; 263 - } 264 - 265 - /// Splits field strings by comma, handling nested structures 266 - List<String> _splitFields(String content) { 267 - final parts = <String>[]; 268 - final buffer = StringBuffer(); 269 - var depth = 0; 270 - var inString = false; 271 - String? currentQuote; 272 - 273 - for (var i = 0; i < content.length; i++) { 274 - final char = content[i]; 275 - 276 - // Handle string literals 277 - if (!inString && (char == '"' || char == "'")) { 278 - inString = true; 279 - currentQuote = char; 280 - } else if (inString && char == currentQuote) { 281 - // Check if it's not escaped 282 - if (i == 0 || content[i - 1] != r'\') { 283 - inString = false; 284 - currentQuote = null; 285 - } 286 - } else if (!inString) { 287 - // Handle nesting 288 - if (char == '(' || char == '[' || char == '{') { 289 - depth++; 290 - } else if (char == ')' || char == ']' || char == '}') { 291 - depth--; 292 - } else if (char == ',' && depth == 0) { 293 - // Found a top-level comma separator 294 - final part = buffer.toString().trim(); 295 - if (part.isNotEmpty) { 296 - parts.add(part); 297 - } 298 - buffer.clear(); 299 - continue; 300 - } 301 - } 302 - 303 - buffer.write(char); 304 - } 305 - 306 - // Add the last part 307 - final lastPart = buffer.toString().trim(); 308 - if (lastPart.isNotEmpty) { 309 - parts.add(lastPart); 310 - } 311 - 312 - return parts; 313 - } 314 - 315 - /// Tries to parse field values as lists and generate list diffs 316 - String? _tryParseFieldAsListDiff(String prevValue, String currValue) { 317 - // Check if both values look like lists [...] 318 - if (!prevValue.startsWith('[') || 319 - !prevValue.endsWith(']') || 320 - !currValue.startsWith('[') || 321 - !currValue.endsWith(']')) { 322 - return null; 323 - } 324 - 325 - // Parse the list contents 326 - final prevList = _parseListString(prevValue); 327 - final currList = _parseListString(currValue); 328 - 329 - if (prevList == null || currList == null) return null; 330 - 331 - return _generateListDiff(prevList, currList); 332 - } 333 - 334 - /// Parses a list string like "[item1, item2, item3]" into a List 335 - List<String>? _parseListString(String listStr) { 336 - if (!listStr.startsWith('[') || !listStr.endsWith(']')) return null; 337 - 338 - final content = listStr.substring(1, listStr.length - 1).trim(); 339 - if (content.isEmpty) return <String>[]; 340 - 341 - // Split by comma, handling nested structures 342 - return _splitFields(content); 343 - } 344 - 345 - /// Truncates long strings to avoid excessive logging 346 - String _truncateIfNeeded(String text, {int maxLength = 700}) { 347 - if (text.length <= maxLength) { 348 - return text; 349 - } 350 - return '${text.substring(0, maxLength)}... ' 351 - '(${text.length - maxLength} more characters)'; 27 + // Provider update logging disabled to reduce noise 352 28 } 353 29 }
-3
lib/src/core/utils/oauth_resolver.dart
··· 32 32 /// ```dart 33 33 /// // From PDS URL 34 34 /// final authServer = await resolveOAuthServer('https://suillus.us-west.host.bsky.network'); 35 - /// print('Authorization server: $authServer'); // 'bsky.social' 36 35 /// 37 36 /// // From handle 38 37 /// final authServer = await resolveOAuthServer('user.bsky.social'); 39 - /// print('Authorization server: $authServer'); // 'bsky.social' 40 38 /// 41 39 /// // From DID 42 40 /// final authServer = await resolveOAuthServer('did:plc:abc123'); 43 - /// print('Authorization server: $authServer'); // 'bsky.social' 44 41 /// ``` 45 42 Future<String> resolveOAuthServer( 46 43 String input, {
-22
lib/src/features/auth/providers/auth_providers.dart
··· 74 74 /// 75 75 /// Returns the authorization URL that the user should be redirected to 76 76 Future<String> initiateOAuth(String handle) async { 77 - _logger.i('Initiating OAuth for handle: $handle'); 78 - 79 77 state = state.copyWith(isLoading: true, error: null); 80 78 81 79 try { ··· 98 96 /// 99 97 /// Returns the authorization URL that the user should be redirected to 100 98 Future<String> initiateOAuthWithService(String service) async { 101 - _logger.i('Initiating OAuth with service: $service'); 102 - 103 99 state = state.copyWith(isLoading: true, error: null); 104 100 105 101 try { ··· 129 125 /// 130 126 /// Returns the result of the login attempt 131 127 Future<LoginResult> completeOAuth(String callbackUrl) async { 132 - _logger.i('Completing OAuth with callback'); 133 - 134 128 try { 135 129 final result = await _authRepository.completeOAuth(callbackUrl); 136 130 ··· 157 151 158 152 /// Logs out the current user 159 153 Future<void> logout() async { 160 - _logger.i('Logout attempt by service layer'); 161 154 state = state.copyWith(isLoading: true, error: null); 162 155 163 156 try { ··· 176 169 /// Validates if the current session is still active 177 170 /// Returns true if valid, false otherwise 178 171 Future<bool> validateSession() async { 179 - _logger.d('Session validation by service layer'); 180 - 181 172 try { 182 173 final result = await _authRepository.validateSession(); 183 174 _updateState(); ··· 192 183 /// Refreshes the authentication token 193 184 /// Returns true if the session was successfully refreshed 194 185 Future<bool> refreshToken() async { 195 - _logger.i('Token refresh by service layer'); 196 - 197 186 try { 198 187 final result = await _authRepository.refreshToken(); 199 188 _updateState(); ··· 220 209 } else { 221 210 // Permission not granted yet, defer until main screen 222 211 _pendingPushRegistration = true; 223 - _logger.i( 224 - 'Push permission not granted, deferring registration to main screen', 225 - ); 226 212 } 227 213 } catch (e, stackTrace) { 228 214 // Don't fail login if push registration fails ··· 245 231 platform: pushService.platform, 246 232 appId: 'so.sprk.app', 247 233 ); 248 - _logger.i('Push notifications registered successfully'); 249 234 250 235 // Set up listener for token refresh 251 236 await _setupTokenRefreshListener(pushService, notificationRepo); 252 237 _pendingPushRegistration = false; 253 - } else { 254 - _logger.w('No push token available'); 255 238 } 256 239 } 257 240 ··· 262 245 /// Call this from the main screen after login 263 246 Future<bool> requestPushPermissionAndRegister() async { 264 247 if (!_pendingPushRegistration) { 265 - _logger.d('No pending push registration'); 266 248 return true; 267 249 } 268 250 ··· 274 256 await _doRegisterPush(pushService); 275 257 return true; 276 258 } else { 277 - _logger.w('User denied push notification permission'); 278 259 _pendingPushRegistration = false; 279 260 return false; 280 261 } ··· 298 279 299 280 _tokenRefreshSubscription = pushService.onTokenRefresh.listen( 300 281 (newToken) async { 301 - _logger.i('FCM token refreshed, re-registering push notifications'); 302 282 try { 303 283 await notificationRepo.registerPush( 304 284 token: newToken, 305 285 platform: pushService.platform, 306 286 appId: 'so.sprk.app', 307 287 ); 308 - _logger.i('Push notifications re-registered with new token'); 309 288 } catch (e, stackTrace) { 310 289 _logger.e( 311 290 'Failed to re-register push notifications after token refresh', ··· 341 320 platform: pushService.platform, 342 321 appId: 'so.sprk.app', 343 322 ); 344 - _logger.i('Push notifications unregistered successfully'); 345 323 } 346 324 } catch (e, stackTrace) { 347 325 // Don't fail logout if push unregistration fails
+1 -2
lib/src/features/auth/ui/pages/onboarding_page.dart
··· 96 96 97 97 // Navigate to main screen 98 98 context.router.replaceAll([const MainRoute()]); 99 - } catch (e) { 99 + } catch (_) { 100 100 if (!mounted) return; 101 - // Error handling - snackbar removed 102 101 } finally { 103 102 if (mounted) { 104 103 setState(() {
+1 -27
lib/src/features/feed/providers/feed_provider.dart
··· 45 45 _logger = GetIt.instance<LogService>().getLogger( 46 46 'FeedNotifier ${feed.config.id}', 47 47 ); 48 - } else { 49 - _logger.d( 50 - 'Build called again for ${feed.config.id}, ' 51 - 'hasBeenBuilt: $_hasBeenBuilt', 52 - ); 53 48 } 54 49 55 50 listenSelf((previous, next) { ··· 97 92 98 93 Future<void> loadAndUpdateFirstLoad() async { 99 94 if (_isLoadingInProgress || state.loadingFirstLoad) { 100 - _logger.w('Load already in progress, skipping duplicate call'); 101 95 return; 102 96 } 103 97 104 98 if (_lastErrorTime != null && 105 99 DateTime.now().difference(_lastErrorTime!) < _errorCooldown) { 106 - _logger.w('In error cooldown, skipping load'); 107 100 return; 108 101 } 109 102 ··· 147 140 // We just need to merge them with self-labels and process them 148 141 final allLabels = <Label>[]; 149 142 final postsWithMergedLabels = <PostView>[]; 150 - var postsWithoutLabels = 0; 151 143 152 144 for (final post in posts) { 153 145 final key = post.uri.toString(); ··· 168 160 } 169 161 } 170 162 171 - // Check if post has no labels (after adding self-labels check original) 172 - if (post.labels == null || post.labels!.isEmpty) { 173 - postsWithoutLabels++; 174 - } 175 - 176 163 allLabels.addAll(postLabels); 177 164 postsWithMergedLabels.add(post.copyWith(labels: postLabels)); 178 165 } 179 166 180 - // Log warning if many posts missing labels (may indicate header issue) 181 - if (postsWithoutLabels > 0 && postsWithoutLabels == posts.length) { 182 - _logger.w( 183 - 'All ${posts.length} posts are missing labels - ' 184 - 'check if labelers header is being sent', 185 - ); 186 - } else if (postsWithoutLabels > posts.length / 2) { 187 - _logger.w( 188 - '$postsWithoutLabels/${posts.length} posts are missing labels - some labels may not be included', 189 - ); 190 - } 191 - 192 167 final extraInfo = LinkedHashMap<AtUri, ({List<Label> postLabels})>.from( 193 168 state.extraInfo, 194 169 ); ··· 264 239 uri: post.uri, 265 240 post: post, 266 241 feed: _feed, 267 - onComplete: (task) => _logger.d('Media cached for ${task.uri}'), 242 + onComplete: (_) {}, 268 243 onError: (task, error, stackTrace) => _logger.e( 269 244 'Error caching media for ${task.uri}: $error', 270 245 error: error, ··· 477 452 final labelPreference = await settings.getLabelPreference(label.val); 478 453 if (labelPreference.setting == Setting.hide || 479 454 labelPreference.adultOnly) { 480 - _logger.d('Hiding post $uri due to label: ${label.val}'); 481 455 return true; 482 456 } 483 457 } catch (e) {
-3
lib/src/features/feed/ui/widgets/action_buttons/share_panel.dart
··· 41 41 } 42 42 }); 43 43 44 - // Snackbar removed 45 - 46 44 Future.delayed(const Duration(seconds: 2), () { 47 45 if (mounted) { 48 46 setState(() { ··· 249 247 250 248 navigator.maybePop(); 251 249 } catch (e) { 252 - // Error handling - snackbar removed 253 250 logger.d( 254 251 'Failed to share video to conversation', 255 252 error: e,
+2 -10
lib/src/features/feed/ui/widgets/action_buttons/side_action_bar.dart
··· 162 162 _isLiked = wasLiked; 163 163 _likeCount += wasLiked ? 1 : -1; 164 164 }); 165 - 166 - // Error handling - snackbar removed 167 165 } 168 166 } 169 167 ··· 230 228 _isReposted = wasReposted; 231 229 _repostCount += wasReposted ? 1 : -1; 232 230 }); 233 - 234 - // Error handling - snackbar removed 235 231 } 236 232 } 237 233 ··· 373 369 ..invalidate(profileFeedProvider(profileUri, false, false)) 374 370 ..invalidate(profileFeedProvider(profileUri, true, false)); 375 371 } 376 - } catch (e) { 377 - // Error handling - snackbar removed 378 - } 372 + } catch (_) {} 379 373 } 380 374 381 375 Future<void> _handleBlock() async { ··· 426 420 ); 427 421 controller?.onAdvanceAndRemove(); 428 422 } 429 - } catch (e) { 430 - // Error handling - snackbar removed 431 - } 423 + } catch (_) {} 432 424 } 433 425 434 426 // Future<void> _handleCurate() async {
+3 -1
lib/src/features/feed/ui/widgets/videos/video_progress_bar.dart
··· 174 174 setState(() {}); 175 175 try { 176 176 await _vp?.seekTo(finalPos); 177 - } catch (_) {} 177 + } catch (_) { 178 + // Video controller may be disposed during seek, ignore error 179 + } 178 180 widget.onSeekEnd?.call(finalPos); 179 181 _dragging = false; 180 182 _pendingSeek = null;
+1 -3
lib/src/features/messages/ui/pages/chat_page.dart
··· 62 62 conversationProvider(widget.conversationId).notifier, 63 63 ); 64 64 await chatService.sendMessage(widget.conversationId, content); 65 - } catch (e) { 66 - // Error handling - snackbar removed 67 - } 65 + } catch (_) {} 68 66 } 69 67 70 68 @override
+1 -2
lib/src/features/messages/ui/pages/new_chat_search_page.dart
··· 198 198 otherUserAvatar: actor.avatar?.toString(), 199 199 ), 200 200 ); 201 - } catch (e) { 201 + } catch (_) { 202 202 if (!mounted) return; 203 - // Error handling - snackbar removed 204 203 } 205 204 } 206 205
-15
lib/src/features/notifications/providers/notification_provider.dart
··· 43 43 bool refresh = false, 44 44 }) async { 45 45 if (_isLoading && !refresh) { 46 - _logger.w('Load already in progress, skipping'); 47 46 return; 48 47 } 49 48 ··· 59 58 final response = await _notificationRepository.listNotifications( 60 59 priority: priority, 61 60 reasons: reasons, 62 - ); 63 - 64 - _logger.d( 65 - 'Loaded ${response.notifications.length} notifications, ' 66 - 'cursor: ${response.cursor}', 67 61 ); 68 62 69 63 state = state.copyWith( ··· 96 90 List<String>? reasons, 97 91 }) async { 98 92 if (_isLoading || state.isLoadingMore || !state.hasMore) { 99 - _logger.w( 100 - 'Cannot load more: isLoading=$_isLoading, ' 101 - 'isLoadingMore=${state.isLoadingMore}, hasMore=${state.hasMore}', 102 - ); 103 93 return; 104 94 } 105 95 ··· 111 101 cursor: state.cursor, 112 102 priority: priority, 113 103 reasons: reasons, 114 - ); 115 - 116 - _logger.d( 117 - 'Loaded ${response.notifications.length} more notifications, ' 118 - 'cursor: ${response.cursor}', 119 104 ); 120 105 121 106 state = state.copyWith(
+1 -1
lib/src/features/posting/providers/camera_provider.dart
··· 246 246 247 247 _logger.d('Stopping video recording'); 248 248 249 - // Update state optimistically BEFORE the native call so UI responds immediately 249 + // Update state optimistically BEFORE native call so UI responds immediately 250 250 state = AsyncValue.data(currentState.copyWith(isRecording: false)); 251 251 252 252 try {
-11
lib/src/features/posting/providers/recording_provider.dart
··· 1 1 import 'dart:async'; 2 2 3 - import 'package:get_it/get_it.dart'; 4 3 import 'package:riverpod_annotation/riverpod_annotation.dart'; 5 - import 'package:spark/src/core/utils/logging/logging.dart'; 6 4 import 'package:spark/src/features/posting/providers/recording_state.dart'; 7 5 8 6 part 'recording_provider.g.dart'; 9 7 10 8 @riverpod 11 9 class Recording extends _$Recording { 12 - late final SparkLogger _logger; 13 10 Timer? _timer; 14 11 15 12 @override 16 13 RecordingState build() { 17 - _logger = GetIt.instance<LogService>().getLogger('Recording'); 18 14 ref.onDispose(_dispose); 19 15 return const RecordingState(); 20 16 } 21 17 22 18 void startRecording() { 23 19 if (state.isRecording) { 24 - _logger.w('Recording already started'); 25 20 return; 26 21 } 27 22 28 - _logger.d('Starting recording timer'); 29 23 state = state.copyWith( 30 24 isRecording: true, 31 25 elapsedDuration: Duration.zero, ··· 37 31 state.elapsedDuration + const Duration(milliseconds: 100); 38 32 39 33 if (newDuration >= state.maxDuration) { 40 - _logger.i('Max duration reached, stopping timer'); 41 34 stopTimer(); 42 35 state = state.copyWith( 43 36 elapsedDuration: state.maxDuration, ··· 52 45 53 46 void stopRecording() { 54 47 if (!state.isRecording) { 55 - _logger.w('Not currently recording'); 56 48 return; 57 49 } 58 50 59 - _logger.d('Stopping recording timer'); 60 51 stopTimer(); 61 52 state = state.copyWith(isRecording: false); 62 53 } 63 54 64 55 void reset() { 65 - _logger.d('Resetting recording state'); 66 56 stopTimer(); 67 57 state = const RecordingState(); 68 58 } ··· 73 63 } 74 64 75 65 void _dispose() { 76 - _logger.d('Disposing recording provider'); 77 66 stopTimer(); 78 67 } 79 68 }
+1 -2
lib/src/features/posting/ui/pages/image_review_page.dart
··· 102 102 _altTexts[file.path] = ''; 103 103 } 104 104 }); 105 - } catch (e) { 105 + } catch (_) { 106 106 if (!mounted) return; 107 - // Error handling - snackbar removed 108 107 } 109 108 } 110 109
+15 -25
lib/src/features/posting/ui/pages/recording_page.dart
··· 7 7 import 'package:get_it/get_it.dart'; 8 8 import 'package:pro_video_editor/pro_video_editor.dart'; 9 9 import 'package:spark/src/core/design_system/templates/recording_page_template.dart'; 10 + import 'package:spark/src/core/pro_video_editor/models/video_editor_result.dart'; 10 11 import 'package:spark/src/core/pro_video_editor/pro_video_editor_repository.dart'; 11 12 import 'package:spark/src/core/routing/app_router.dart'; 12 13 import 'package:spark/src/core/utils/logging/logging.dart'; ··· 105 106 if (_isProcessing) return; 106 107 107 108 final cameraNotifier = ref.read(cameraProvider.notifier); 108 - _logger.d('Taking photo'); 109 - 110 109 setState(() { 111 110 _isProcessing = true; 112 111 }); ··· 128 127 if (!mounted) return; 129 128 130 129 try { 131 - _logger.d('Processing photo: ${photoFile.path}'); 132 - 133 130 // Open the story image editor 134 131 final editedImage = await GetIt.I<ProVideoEditorRepository>() 135 132 .openStoryImageEditor(context, photoFile); ··· 151 148 editedImage, 152 149 ); 153 150 if (result != null && mounted) { 154 - _logger.i('Story posted successfully'); 155 151 // Exit the recording flow completely 156 152 if (mounted) context.router.maybePop(); 157 153 return; ··· 201 197 202 198 void _startRecording() { 203 199 final cameraNotifier = ref.read(cameraProvider.notifier); 204 - final recordingNotifier = ref.read(recordingProvider.notifier); 205 - 206 - _logger.d('Starting video recording'); 207 - 208 - // Start timer optimistically so UI responds immediately 209 - recordingNotifier.startRecording(); 200 + final recordingNotifier = ref.read(recordingProvider.notifier) 201 + // Start timer optimistically so UI responds immediately 202 + ..startRecording(); 210 203 211 204 // Start native recording; revert timer if it fails 212 205 cameraNotifier.startVideoRecording().then((success) { ··· 224 217 }); 225 218 226 219 final cameraNotifier = ref.read(cameraProvider.notifier); 227 - final recordingNotifier = ref.read(recordingProvider.notifier); 228 - 229 - _logger.d('Stopping video recording'); 230 - 231 - recordingNotifier.stopRecording(); 220 + ref.read(recordingProvider.notifier).stopRecording(); 232 221 233 222 // Defer heavy stop so the "processing" frame paints before blocking 234 223 WidgetsBinding.instance.addPostFrameCallback((_) async { ··· 250 239 if (!mounted) return; 251 240 252 241 try { 253 - _logger.d('Processing recorded video: ${videoFile.path}'); 254 - 255 242 final cameraNotifier = ref.read(cameraProvider.notifier); 256 243 await cameraNotifier.disposeCamera(); 257 244 258 - if (!mounted) return; 245 + if (!mounted || !context.mounted) return; 259 246 260 247 final editorVideo = EditorVideo.file(File(videoFile.path)); 261 248 final repository = GetIt.I<ProVideoEditorRepository>(); 262 - final result = widget.storyMode 263 - ? await repository.openStoryVideoEditor(context, editorVideo) 264 - : await repository.openVideoEditor(context, editorVideo); 249 + VideoEditorResult? result; 250 + if (widget.storyMode) { 251 + if (!context.mounted) return; 252 + result = await repository.openStoryVideoEditor(context, editorVideo); 253 + } else { 254 + if (!context.mounted) return; 255 + result = await repository.openVideoEditor(context, editorVideo); 256 + } 265 257 266 258 if (!mounted) return; 267 259 ··· 269 261 setState(() { 270 262 _isProcessing = false; 271 263 }); 272 - _logger.d('User cancelled video editing'); 273 264 await cameraNotifier.reinitializeCamera(); 274 265 return; 275 266 } ··· 289 280 soundRef: result.soundRef, 290 281 ); 291 282 if (postResult != null && mounted) { 292 - _logger.i('Video story posted successfully'); 293 283 // Exit the recording flow completely 294 284 if (mounted) context.router.maybePop(); 295 285 return; ··· 500 490 501 491 @override 502 492 void dispose() { 503 - // Defer provider modification to avoid modifying during widget tree finalization 493 + // Defer modifying provider to avoid modifying while finalizing widget tree 504 494 final notifier = _recordingNotifier; 505 495 if (notifier != null) { 506 496 Future(notifier.reset);
+3 -3
lib/src/features/posting/ui/pages/story_post_page.dart
··· 19 19 this.videoPath, 20 20 super.key, 21 21 }) : assert( 22 - imageFile != null || videoPath != null, 23 - 'Either imageFile or videoPath must be provided', 24 - ); 22 + imageFile != null || videoPath != null, 23 + 'Either imageFile or videoPath must be provided', 24 + ); 25 25 26 26 final XFile? imageFile; 27 27 final String? videoPath;
+1 -13
lib/src/features/posting/utils/story_direct_post.dart
··· 1 1 import 'package:atproto/com_atproto_repo_strongref.dart'; 2 - import 'package:flutter/foundation.dart'; 3 2 import 'package:flutter/material.dart'; 4 3 import 'package:flutter_riverpod/flutter_riverpod.dart'; 5 4 import 'package:get_it/get_it.dart'; ··· 22 21 WidgetRef ref, 23 22 XFile imageFile, 24 23 ) async { 25 - debugPrint('[StoryDirectPost] Starting photo story post'); 26 - debugPrint('[StoryDirectPost] Image path: ${imageFile.path}'); 27 - 28 24 // Show loading overlay 29 25 final navigator = Navigator.of(context); 30 26 ··· 37 33 try { 38 34 final feedRepository = GetIt.I<SprkRepository>().feed; 39 35 40 - debugPrint('[StoryDirectPost] Uploading image...'); 41 36 // Upload the image 42 37 final uploadedImages = await feedRepository.uploadImages( 43 38 imageFiles: [imageFile], 44 39 altTexts: {imageFile.path: ''}, 45 40 ); 46 - 47 - debugPrint('[StoryDirectPost] Upload result: ${uploadedImages.length} images'); 48 41 49 42 if (uploadedImages.isEmpty) { 50 43 throw Exception('Failed to upload image'); 51 44 } 52 45 53 46 final uploadedImage = uploadedImages.first; 54 - debugPrint('[StoryDirectPost] Image uploaded, posting story...'); 55 47 56 48 // Post the story 57 49 final result = await ref.read( ··· 64 56 throw Exception('Failed to post story'); 65 57 } 66 58 67 - debugPrint('[StoryDirectPost] Story posted: ${result.uri}'); 68 - 69 59 // Dismiss loading 70 60 if (navigator.mounted) { 71 61 navigator.pop(); 72 62 } 73 63 74 64 return result; 75 - } catch (e, stackTrace) { 76 - debugPrint('[StoryDirectPost] Error: $e'); 77 - debugPrint('[StoryDirectPost] StackTrace: $stackTrace'); 65 + } catch (_) { 78 66 // Dismiss loading 79 67 if (navigator.mounted) { 80 68 navigator.pop();
+1 -2
lib/src/features/profile/ui/pages/edit_profile_page.dart
··· 79 79 80 80 // Go back to previous screen 81 81 context.router.pop(); 82 - } catch (e) { 82 + } catch (_) { 83 83 if (!mounted) return; 84 - // Error handling - snackbar removed 85 84 } finally { 86 85 if (mounted) { 87 86 setState(() {
+1 -3
lib/src/features/profile/ui/pages/profile_page.dart
··· 14 14 import 'package:spark/src/core/network/atproto/data/models/actor_models.dart' 15 15 as actor_models; 16 16 import 'package:spark/src/core/routing/app_router.dart'; 17 - import 'package:spark/src/features/posting/ui/pages/recording_page.dart'; 18 17 import 'package:spark/src/core/ui/widgets/options_panel.dart'; 19 18 import 'package:spark/src/core/ui/widgets/report_dialog.dart'; 20 19 import 'package:spark/src/core/utils/blocking_utils.dart'; ··· 22 21 import 'package:spark/src/core/utils/logging/logger.dart'; 23 22 import 'package:spark/src/core/utils/text_formatter.dart'; 24 23 import 'package:spark/src/features/auth/providers/auth_providers.dart'; 24 + import 'package:spark/src/features/posting/ui/pages/recording_page.dart'; 25 25 import 'package:spark/src/features/profile/providers/profile_feed_provider.dart'; 26 26 import 'package:spark/src/features/profile/providers/profile_likes_provider.dart'; 27 27 import 'package:spark/src/features/profile/providers/profile_provider.dart'; ··· 152 152 final cleanUsername = username.startsWith('@') 153 153 ? username.substring(1) 154 154 : username; 155 - _logger.d('Username clicked: $cleanUsername'); 156 155 157 156 final didRes = await _identityRepository.resolveHandleToDid( 158 157 cleanUsername, 159 158 ); 160 159 if (didRes == null) { 161 - _logger.w('Could not resolve handle to DID for $cleanUsername'); 162 160 return; 163 161 } 164 162 if (mounted) {
-9
lib/src/features/search/providers/post_search_provider.dart
··· 115 115 .cast<PostView>() 116 116 .toList(); 117 117 118 - _logger.d( 119 - 'Successfully converted ${bskyPosts.length}/${bskyResponse.data.posts.length} bsky posts', 120 - ); 121 - 122 118 final filteredSprkPosts = _filterHiddenPosts(sprkResponse.posts); 123 119 final filteredBskyPosts = _filterHiddenPosts(bskyPosts); 124 120 ··· 129 125 sprkNextCursor: sprkResponse.cursor, 130 126 bskyNextCursor: bskyResponse.data.cursor, 131 127 isLoading: false, 132 - ); 133 - _logger.d( 134 - 'Search completed with ${combinedPosts.length} results, ' 135 - 'sprkNextCursor: ${sprkResponse.cursor}, ' 136 - 'bskyNextCursor: ${bskyResponse.data.cursor}', 137 128 ); 138 129 139 130 // If we have very few results, try to load more immediately
-17
lib/src/features/search/providers/search_provider.dart
··· 77 77 isLoading: false, 78 78 isLoadingMore: false, 79 79 ); 80 - 81 - _logger.d( 82 - 'Search completed with ${response.actors.length} results, ' 83 - 'nextCursor: ${response.cursor}', 84 - ); 85 80 } catch (e) { 86 81 _logger.e('Failed to search users', error: e); 87 82 state = state.copyWith(error: 'Failed to search users', isLoading: false); ··· 108 103 nextCursor: response.cursor, 109 104 isLoadingMore: false, 110 105 ); 111 - 112 - _logger.d( 113 - 'Loaded more users: +${response.actors.length}, ' 114 - 'nextCursor: ${response.cursor}', 115 - ); 116 106 } catch (e) { 117 107 _logger.e('Failed to load more users', error: e); 118 108 state = state.copyWith(isLoadingMore: false); ··· 124 114 try { 125 115 final authRepo = _authRepository; 126 116 if (!authRepo.isAuthenticated) { 127 - _logger.w('User not authenticated, cannot follow'); 128 117 return; 129 118 } 130 119 ··· 147 136 updatedResults[userIndex] = updatedUser; 148 137 state = state.copyWith(searchResults: updatedResults); 149 138 } 150 - 151 - _logger.i('Successfully followed user: $userDid'); 152 139 } catch (e) { 153 140 _logger.e('Failed to follow user', error: e); 154 141 } ··· 159 146 try { 160 147 final authRepo = _authRepository; 161 148 if (!authRepo.isAuthenticated) { 162 - _logger.w('User not authenticated, cannot unfollow'); 163 149 return; 164 150 } 165 151 ··· 180 166 updatedResults[userIndex] = updatedUser; 181 167 state = state.copyWith(searchResults: updatedResults); 182 168 } 183 - 184 - _logger.i('Successfully unfollowed user: $userDid'); 185 169 } catch (e) { 186 170 _logger.e('Failed to unfollow user', error: e); 187 171 } ··· 190 174 bool isCurrentUser(String did) { 191 175 final authRepo = _authRepository; 192 176 if (!authRepo.isAuthenticated) { 193 - _logger.w('User not authenticated, cannot check if current user'); 194 177 return false; 195 178 } 196 179 final currentDid = authRepo.did;
-4
lib/src/features/search/providers/suggested_feeds_provider.dart
··· 18 18 @override 19 19 Future<List<GeneratorView>> build() async { 20 20 try { 21 - _logger.d('Fetching suggested Spark feeds...'); 22 21 // Only fetch Spark feeds 23 22 final feeds = await _feedRepository.getSuggestedFeeds(); 24 - _logger.d('Fetched ${feeds.length} suggested Spark feeds'); 25 23 return feeds; 26 24 } catch (e, stackTrace) { 27 25 _logger.e( ··· 38 36 state = const AsyncValue.loading(); 39 37 40 38 try { 41 - _logger.d('Refreshing suggested Spark feeds...'); 42 39 // Only fetch Spark feeds 43 40 final feeds = await _feedRepository.getSuggestedFeeds(); 44 - _logger.d('Refreshed ${feeds.length} suggested Spark feeds'); 45 41 state = AsyncValue.data(feeds); 46 42 } catch (e, stackTrace) { 47 43 _logger.e(
-9
lib/src/features/settings/providers/preferences_provider.dart
··· 28 28 _sprkRepository = GetIt.instance<SprkRepository>(); 29 29 _logger = GetIt.instance<LogService>().getLogger('UserPreferences'); 30 30 31 - _logger.d('Loading preferences...'); 32 - 33 31 // Wait for auth to be initialized 34 32 await _sprkRepository.authRepository.initializationComplete; 35 33 36 34 if (!_sprkRepository.authRepository.isAuthenticated) { 37 - _logger.w('Not authenticated, returning empty preferences'); 38 35 return Preferences(preferences: []); 39 36 } 40 37 41 38 try { 42 39 final preferences = await _prefRepository.getPreferences(); 43 - _logger.d('Preferences loaded successfully'); 44 40 return preferences; 45 41 } catch (e) { 46 42 _logger.e('Error loading preferences: $e'); ··· 55 51 /// Refreshes preferences from the server. 56 52 /// This should be called when logging in or when syncing from another device. 57 53 Future<void> refresh() async { 58 - _logger.d('Refreshing preferences from server...'); 59 54 state = const AsyncValue.loading(); 60 55 61 56 try { 62 57 final preferences = await _prefRepository.getPreferences(); 63 58 state = AsyncValue.data(preferences); 64 - _logger.d('Preferences refreshed successfully'); 65 59 } catch (e, st) { 66 60 _logger.e('Error refreshing preferences: $e'); 67 61 state = AsyncValue.error(e, st); ··· 71 65 /// Updates preferences on the server and in local state. 72 66 /// This should be called whenever preferences are modified. 73 67 Future<void> updatePreferences(Preferences preferences) async { 74 - _logger.d('Updating preferences...'); 75 - 76 68 try { 77 69 await _prefRepository.putPreferences(preferences); 78 70 state = AsyncValue.data(preferences); 79 - _logger.d('Preferences updated successfully'); 80 71 } catch (e, st) { 81 72 _logger.e('Error updating preferences: $e'); 82 73 state = AsyncValue.error(e, st);
+3 -9
lib/src/features/settings/ui/pages/feed_list_page.dart
··· 98 98 await ref 99 99 .read(settingsProvider.notifier) 100 100 .reorderFeed(actualOldIndex, actualNewIndex); 101 - } catch (e) { 102 - // Error handling - snackbar removed 103 - } 101 + } catch (_) {} 104 102 }, 105 103 proxyDecorator: (child, index, animation) { 106 104 return AnimatedBuilder( ··· 161 159 await ref 162 160 .read(settingsProvider.notifier) 163 161 .removeFeed(feed); 164 - } catch (e) { 165 - // Error handling - snackbar removed 166 - } 162 + } catch (_) {} 167 163 } 168 164 : null, 169 165 onPin: _isEditMode ··· 208 204 await ref 209 205 .read(settingsProvider.notifier) 210 206 .likeFeed(feed); 211 - } catch (e) { 212 - // Error handling - snackbar removed 213 - } 207 + } catch (_) {} 214 208 } 215 209 : null, 216 210 onUnlike: feed.view?.viewer?.like != null
+1 -3
lib/src/features/settings/ui/pages/labeler_management_page.dart
··· 64 64 } 65 65 66 66 setState(() => _isLoading = false); 67 - _logger.d('Loaded ${labelerDids.length} labelers'); 68 67 } catch (e) { 69 68 _logger.e('Error loading labelers: $e'); 70 69 setState(() => _isLoading = false); ··· 82 81 (p) => p.did == did, 83 82 ); 84 83 profileMap[did] = profile; 85 - } catch (e) { 86 - _logger.w('Profile not found for DID: $did'); 84 + } catch (_) { 87 85 profileMap[did] = null; 88 86 } 89 87 }
+1 -13
lib/src/features/stories/providers/story_auto_delete_provider.dart
··· 5 5 import 'package:spark/src/core/storage/preferences/storage_constants.dart'; 6 6 import 'package:spark/src/core/storage/preferences/storage_manager.dart'; 7 7 import 'package:spark/src/core/utils/logging/log_service.dart'; 8 - import 'package:spark/src/core/utils/logging/logger.dart'; 9 8 import 'package:spark/src/features/stories/providers/story_manager_provider.dart'; 10 9 11 10 part 'story_auto_delete_provider.g.dart'; ··· 13 12 /// Holds the auto delete preference state (bool) 14 13 @riverpod 15 14 class StoryAutoDeletePref extends _$StoryAutoDeletePref { 16 - late final SparkLogger _logger; 17 - 18 15 @override 19 16 Future<bool> build() async { 20 - _logger = GetIt.I<LogService>().getLogger('StoryAutoDeletePref'); 21 17 final prefs = StorageManager.instance.preferences; 22 18 var stored = await prefs.getBool(StorageKeys.storyAutoDeleteEnabled); 23 19 if (stored == null) { 24 20 stored = true; 25 21 await prefs.setBool(StorageKeys.storyAutoDeleteEnabled, true); 26 - _logger.d('Auto delete preference not found. Setting default to true.'); 27 - } else { 28 - _logger.d('Loaded auto delete preference: $stored'); 29 22 } 30 23 return stored; 31 24 } ··· 33 26 Future<void> setEnabled(bool value) async { 34 27 final prefs = StorageManager.instance.preferences; 35 28 await prefs.setBool(StorageKeys.storyAutoDeleteEnabled, value); 36 - _logger.d('Set auto delete preference to $value'); 37 29 // Update state immutably 38 30 state = AsyncData(value); 39 31 } ··· 77 69 cursor = page.data.cursor; 78 70 } while (cursor != null); 79 71 80 - if (expiredUris.isEmpty) { 81 - logger.d('Auto delete: no expired stories'); 82 - return; 83 - } 72 + if (expiredUris.isEmpty) return; 84 73 85 - logger.d('Auto delete: deleting ${expiredUris.length} expired stories'); 86 74 for (final uri in expiredUris) { 87 75 try { 88 76 await sprk.repo.deleteRecord(uri: uri);
+1 -1
lib/src/features/stories/providers/story_manager_provider.dart
··· 43 43 _sprk = GetIt.I<SprkRepository>(); 44 44 _storyRepo = GetIt.I<StoryRepository>(); 45 45 _logger = GetIt.I<LogService>().getLogger('StoryManager'); 46 - ref.read(storyAutoDeleteExecutorProvider.future).catchError((_) {}); 46 + ref.read(storyAutoDeleteExecutorProvider.future).ignore(); 47 47 return _loadInitial(); 48 48 } 49 49
+3 -3
lib/src/features/stories/ui/pages/story_page.dart
··· 105 105 106 106 @override 107 107 Widget build(BuildContext context) { 108 - final footerHeight = kBottomNavigationBarHeight + 12; 108 + const footerHeight = kBottomNavigationBarHeight + 12; 109 109 const borderRadius = BorderRadius.all(Radius.circular(20)); 110 110 111 111 // Determine the main media widget (video or image) first. ··· 228 228 ), 229 229 ), 230 230 ), 231 - SizedBox( 231 + const SizedBox( 232 232 height: footerHeight, 233 - child: const ColoredBox(color: Colors.black), 233 + child: ColoredBox(color: Colors.black), 234 234 ), 235 235 ], 236 236 );