diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..bd1093a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,47 @@ +# ============================================================================ +# Bug Report Template +# ============================================================================ +--- +name: Bug Report +about: Create a report to help us improve +title: '[BUG]' +labels: bug +assignees: '' +--- + +## ๐Ÿ› Bug Description + + +## Environment +- **Device** +- **OS Version**: +- **App Version**: +- **Build**: Dev/Staging/Production + +## ๐Ÿ”„ Steps To Reproduce +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +## โœ… Expected Behavior + + +## โŒ Actual Behavior + + +## ๐Ÿ“ธ Screenshots + + +## ๐Ÿ“‹ Logs + + +``` +Paste logs here +``` + +## ๐Ÿ’ก Possible Solution + + +## ๐Ÿ“ Additional Context + \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..14ed8d4 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,28 @@ +# ============================================================================ +# Feature Request Template +# ============================================================================ +--- +name: Feature Request +about: Suggest a feature for this project +title: '[FEATURE] ' +labels: enhancement +assignees: '' +--- + +## โœจ Feature Description + + +## ๐ŸŽฏ Problem/Motivation + + +## ๐Ÿ’ก Proposed Solution + + +## ๐Ÿ”„ Alternatives Considered + + +## ๐Ÿ“ธ Mockups/Examples + + +## ๐Ÿ“ Additional Context + diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..6c12a64 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,50 @@ +#======================================================================= +# Pull Request Template +#======================================================================= +## ๐Ÿ“‹ Description + + +## ๐ŸŽฏ Type of Change +- [ ] ๐Ÿ› Bug fix (non-breaking change that fixes an issue) +- [ ] โœจ New feature (non-breaking change that adds functionality) +- [ ] ๐Ÿ’ฅ Breaking change (fix or feature that causes existing functionality to change) +- [ ] ๐Ÿ“ Documentation update +- [ ] ๐ŸŽจ UI/UX improvement +- [ ] โšก Performance improvement +- [ ] ๐Ÿ”’ Security fix + +## ๐Ÿงช Testing +- [ ] Unit tests pass locally +- [ ] Integration tests pass (if applicable) +- [ ] Manual testing completed +- [ ] Tested on physical device + +## Test Coverage +- [ ] Added tests for new features +- [ ] Updated existing tests +- [ ] Coverage increased/maintained + +## ๐Ÿ“ฑ Tested On +- [ ] Android (API level: ___) +- [ ] iOS (version: ___) +- [ ] Physical device (model: ___) +- [ ] Emulator/Simulator + +## ๐Ÿ“ธ Screenshots (if UI changes) + + +## โœ… Checklist +- [ ] Code follows project style guidelines +- [ ] Self-reviewed the code +- [ ] Commented complex code sections +- [ ] Updated documentation (if needed) +- [ ] No new warnings generated +- [ ] Added tests that prove fix/feature works +- [ ] All tests pass locally +- [ ] Conflicts resolved with base branch + +## ๐Ÿ”— Related Issues + + +## ๐Ÿ“ Additional Notes + \ No newline at end of file diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..5578626 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,23 @@ +# ============================================================================ +# .github/dependabot.yml +# Automated dependency updates +# ============================================================================ +version: 2 +updates: + # GitHub Actions + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + labels: + - "dependencies" + - "github-actions" + + # Gradle (Android) + - package-ecosystem: "gradle" + directory: "/android" + schedule: + interval: "weekly" + labels: + - "dependencies" + - "android" \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e69de29..5978a31 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -0,0 +1,153 @@ +# ============================================================================ +# CONTINUOUS INTEGRATION PIPELINE +# Runs on every PR and push to develop/main +# ============================================================================ +name: Continuous Integration + +on: + pull_request: + branches: [develop, main] + push: + branches: [develop, main] + workflow_dispatch: + +env: + FLUTTER_VERSION: '3.35.7' + JAVA_VERSION: '17' + +jobs: + # ============================================================================ + # CODE QUALITY CHECKS + # ============================================================================ + code-quality: + name: Code Quality Analysis + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - name: ๐Ÿ“ฆ Checkout Code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: ๐ŸŽฏ Setup Flutter + uses: subosito/flutter-action@v2 + with: + flutter-version: ${{ env.FLUTTER_VERSION }} + channel: 'stable' + cache: true + + - name: ๐Ÿ“Š Get Dependencies + run: flutter pub get + + - name: ๐Ÿ” Run Flutter Analyzer + run: flutter analyze --no-fatal-infos + + - name: ๐ŸŽจ Check Code Formatting + run: dart format --set-exit-if-changed . + + - name: ๐Ÿ” Check for Secrets + uses: trufflesecurity/trufflehog@main + with: + path: ./ + base: ${{ github.event.repository.default_branch }} + head: HEAD + + + # ============================================================================ + # UNIT & WIDGET TESTS + # ============================================================================ + tests: + name: Run Tests + runs-on: ubuntu-latest + needs: code-quality + timeout-minutes: 15 + + steps: + - name: ๐Ÿ“ฆ Checkout Code + uses: actions/checkout@v4 + + - name: ๐ŸŽฏ Setup Flutter + uses: subosito/flutter-action@v2 + with: + flutter-version: ${{ env.FLUTTER_VERSION }} + channel: 'stable' + cache: true + + - name: ๐Ÿ“Š Get Dependencies + run: flutter pub get + + - name: ๐Ÿงช Run Tests with Coverage + run: flutter test --coverage --reporter expanded + + - name: ๐Ÿ“ˆ Upload Coverage to Codecov + uses: codecov/codecov-action@v3 + with: + files: ./coverage/lcov.info + fail_ci_if_error: false + verbose: true + + - name: ๐Ÿ“Š Generate Coverage Report + run: | + flutter pub global activate coverage + flutter pub global run coverage:format_coverage --lcov --in=coverage --out=coverage/lcov.info --report-on=lib + + + # ============================================================================ + # BUILD VERIFICATION (Debug APK) + # ============================================================================ + build-verification: + name: Build Verification + runs-on: ubuntu-latest + needs: tests + timeout-minutes: 20 + + strategy: + matrix: + flavor: [dev, staging, prod] + + steps: + - name: ๐Ÿ“ฆ Checkout Code + uses: actions/checkout@v4 + + - name: ๐ŸŽฏ Setup Flutter + uses: subosito/flutter-action@v2 + with: + flutter-version: ${{ env.FLUTTER_VERSION }} + channel: 'stable' + cache: true + + - name: โ˜• Setup Java + uses: actions/setup-java@v4 + with: + distribution: 'zulu' + java-version: ${{ env.JAVA_VERSION }} + cache: 'gradle' + + - name: ๐Ÿ“ฆ Get Dependencies + run: flutter pub get + + - name: ๐Ÿ”จ Run Code Generation + run: flutter pub run build_runner build --delete-conflicting-outputs + + - name: ๐Ÿ“ Create Environment File + run: | + mkdir -p assets/config + echo "APP_NAME=Password Manager ${{ matrix.flavor }}" > assets/config/.env.${{ matrix.flavor }} + echo "APP_ENV=${{ matrix.flavor }}" >> assets/config/.env.${{ matrix.flavor }} + echo "API_BASE_URL=${{ secrets[format('{0}_API_BASE_URL', matrix.flavor)] }}" >> assets/config/.env.${{ matrix.flavor }} + + - name: ๐Ÿ”จ Build Debug APK + run: | + flutter build apk \ + -t lib/main_${{ matrix.flavor }}.dart \ + --debug \ + --flavor ${{ matrix.flavor }} \ + --dart-define=FLAVOR=${{ matrix.flavor }} + + - name: ๐Ÿ“ค Upload Build Artifact + uses: actions/upload-artifact@v4 + with: + name: apk-${{ matrix.flavor }}-debug + path: build/app/outputs/flutter-apk/app-${{ matrix.flavor }}-debug.apk + retention-days: 7 diff --git a/.github/workflows/deploy_dev.yml b/.github/workflows/deploy_dev.yml index 8ef3de7..9b5b4c1 100644 --- a/.github/workflows/deploy_dev.yml +++ b/.github/workflows/deploy_dev.yml @@ -354,16 +354,17 @@ jobs:
${{ needs.deploy-android-dev.result == 'success' && 'Deployed' || 'Failed' }}
+

Development Build #${{ github.run_number }}

- Branch + Branch ${{ github.ref_name }}
- Commit + Commit ${{ github.sha }}
diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png index ec826fd..16afabb 100644 Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png index 49477c8..2537235 100644 Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png index 3c9cf09..4e73f43 100644 Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png index 3713ba1..f3b505e 100644 Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png index 28d335a..2ce5d49 100644 Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/assets/logo/app_logo.png b/assets/logo/app_logo.png index 10beb10..5711861 100644 Binary files a/assets/logo/app_logo.png and b/assets/logo/app_logo.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png index 3fda7f7..2f9ac6c 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png index 8c6ed0b..e5ddf5d 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png index 0cd034a..dcf1bc1 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png index d3cf792..e17e1aa 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png index 16d8472..b149f64 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png index 9638b54..4e4dfa7 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png index 35d71b7..d7c524d 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png index 0cd034a..dcf1bc1 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png index 3abaa05..5b039c5 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png index dbc0baa..aac70a7 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png index f61f19e..90bd46a 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png index e821bb9..536106b 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png index 76f519c..2571431 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png index 729803c..a2a804d 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png index dbc0baa..aac70a7 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png index 56c8eee..1d03075 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png index ec826fd..16afabb 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png index 3713ba1..f3b505e 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png index 9357e4c..d4896ed 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png index c0eb1b8..bc5403c 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png index 3bd6186..f625bb3 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/lib/config/routes/app_router.dart b/lib/config/routes/app_router.dart index b3007fb..6593154 100644 --- a/lib/config/routes/app_router.dart +++ b/lib/config/routes/app_router.dart @@ -8,6 +8,7 @@ import 'package:password_manager/feature/auth/presentation/pages/master-password import 'package:password_manager/feature/auth/presentation/pages/master-password-success/master_password_success.dart'; import 'package:password_manager/feature/auth/presentation/pages/unlock-vault/unlock_vault.dart'; import 'package:password_manager/feature/home/domain/entities/password_entry.dart'; +import 'package:password_manager/feature/home/presentation/mainscreen/pages/camera_screen.dart'; import 'package:password_manager/feature/home/presentation/mainscreen/pages/main_screen.dart'; import 'package:password_manager/feature/home/presentation/password_detail/password_add_screen.dart'; import 'package:password_manager/feature/home/presentation/password_detail/password_detail_screen.dart'; @@ -179,15 +180,15 @@ class AppRouter { // // ====================================================================== // // CATEGORIES // // ====================================================================== - // GoRoute( - // path: Routes.categories, - // name: RouteNames.categories, - // pageBuilder: (context, state) => CustomTransitionPage( - // key: state.pageKey, - // child: const CategoriesPage(), - // transitionsBuilder: RouteTransitions.slide, - // ), - // ), + GoRoute( + path: Routes.camera, + name: RouteNames.camera, + pageBuilder: (context, state) => CustomTransitionPage( + key: state.pageKey, + child: SnapCameraPro(), + transitionsBuilder: RouteTransitions.slide, + ), + ), // GoRoute( // path: Routes.addCategory, diff --git a/lib/config/routes/route_guards.dart b/lib/config/routes/route_guards.dart index 5fb7b0e..470b795 100644 --- a/lib/config/routes/route_guards.dart +++ b/lib/config/routes/route_guards.dart @@ -203,6 +203,7 @@ class AuthGuard { Routes.masterPasswordSetup, Routes.masterPasswordSuccess, Routes.unlock, + Routes.camera, ]; return publicRoutes.contains(path); diff --git a/lib/config/routes/route_names.dart b/lib/config/routes/route_names.dart index cab0b30..1ac141e 100644 --- a/lib/config/routes/route_names.dart +++ b/lib/config/routes/route_names.dart @@ -2,6 +2,8 @@ class Routes { Routes._(); + static const String camera ='/camera'; + // Core App Routes static const String splash = '/'; static const String onboarding = '/onboarding'; @@ -43,6 +45,8 @@ class Routes { class RouteNames { RouteNames._(); + static const String camera= 'camera'; + static const String splash = 'splash'; static const String onboarding = 'onboarding'; static const String masterPasswordSetup = 'masterPasswordSetup'; diff --git a/lib/feature/home/presentation/mainscreen/pages/camera_screen.dart b/lib/feature/home/presentation/mainscreen/pages/camera_screen.dart new file mode 100644 index 0000000..00aa928 --- /dev/null +++ b/lib/feature/home/presentation/mainscreen/pages/camera_screen.dart @@ -0,0 +1,544 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:math'; +import 'package:camera/camera.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +// --------------------------------------------------------- +// CUSTOM PAINTER โ€” Optimized progress drawing +// --------------------------------------------------------- +class RecordProgressPainter extends CustomPainter { + final double progress; + final Color color; + + RecordProgressPainter({required this.progress, required this.color}); + + @override + void paint(Canvas canvas, Size size) { + const strokeWidth = 6.0; + + final paint = Paint() + ..color = color + ..strokeWidth = strokeWidth + ..style = PaintingStyle.stroke + ..strokeCap = StrokeCap.round; + + canvas.drawArc( + Rect.fromLTWH(0, 0, size.width, size.height).deflate(strokeWidth / 2), + -pi / 2, + 2 * pi * progress, + false, + paint, + ); + } + + @override + bool shouldRepaint(covariant RecordProgressPainter oldDelegate) { + return oldDelegate.progress != progress; + } +} + +// --------------------------------------------------------- +// MAIN CAMERA WIDGET +// --------------------------------------------------------- +class SnapCameraPro extends StatefulWidget { + const SnapCameraPro({super.key}); + + @override + State createState() => _SnapCameraProState(); +} + +class _SnapCameraProState extends State + with SingleTickerProviderStateMixin, WidgetsBindingObserver { + CameraController? _controller; + List _cameras = []; + + bool _isRecording = false; + bool _isInitialized = false; + bool _isSwitchingCamera = false; + String? _errorMessage; + + double _zoomLevel = 1.0; + double _minZoom = 1.0; + double _maxZoom = 1.0; + + // Recording timer & progress + Timer? _recordTimer; + double _progress = 0; + static const int _maxSeconds = 10; + int _currentSeconds = 0; + + late AnimationController _scaleController; + late Animation _scaleAnimation; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addObserver(this); + _initializeAnimations(); + _initCamera(); + } + + @override + void dispose() { + WidgetsBinding.instance.removeObserver(this); + _recordTimer?.cancel(); + _controller?.dispose(); + _scaleController.dispose(); + super.dispose(); + } + + // Handle app lifecycle changes + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + final controller = _controller; + if (controller == null || !controller.value.isInitialized) { + return; + } + + if (state == AppLifecycleState.inactive) { + controller.dispose(); + } else if (state == AppLifecycleState.resumed) { + _initCamera(_controller?.description); + } + } + + // --------------------------------------------------------- + // ANIMATION SETUP + // --------------------------------------------------------- + void _initializeAnimations() { + _scaleController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 250), + ); + + _scaleAnimation = Tween(begin: 1.0, end: 1.3).animate( + CurvedAnimation(parent: _scaleController, curve: Curves.easeOutBack), + ); + } + + // --------------------------------------------------------- + // CAMERA INITIALIZATION - Optimized with error handling + // --------------------------------------------------------- + Future _initCamera([CameraDescription? cam]) async { + try { + setState(() { + _isInitialized = false; + _errorMessage = null; + }); + + // Get available cameras + _cameras = await availableCameras(); + + if (_cameras.isEmpty) { + throw CameraException('NO_CAMERA', 'No cameras available'); + } + + // Dispose previous controller if exists + await _controller?.dispose(); + + // Initialize new controller + final camera = cam ?? _cameras.first; + final controller = CameraController( + camera, + ResolutionPreset + .high, // Use 'high' instead of 'max' for better performance + enableAudio: true, + imageFormatGroup: ImageFormatGroup.jpeg, + ); + + _controller = controller; + await controller.initialize(); + + // Get zoom levels + _minZoom = await controller.getMinZoomLevel(); + _maxZoom = await controller.getMaxZoomLevel(); + _zoomLevel = _minZoom; + + if (mounted) { + setState(() => _isInitialized = true); + } + } catch (e) { + _handleError('Failed to initialize camera: $e'); + } + } + + // --------------------------------------------------------- + // ERROR HANDLING + // --------------------------------------------------------- + void _handleError(String message) { + debugPrint('Camera Error: $message'); + if (mounted) { + setState(() => _errorMessage = message); + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(message), + backgroundColor: Colors.red, + duration: const Duration(seconds: 3), + ), + ); + } + } + + // --------------------------------------------------------- + // PHOTO CAPTURE - Optimized + // --------------------------------------------------------- + Future _takePhoto() async { + if (_isRecording || !_isInitialized) return; + + try { + await HapticFeedback.mediumImpact(); + final file = await _controller!.takePicture(); + debugPrint('๐Ÿ“ธ Photo saved: ${file.path}'); + + // Show feedback + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Photo saved: ${file.path}'), + duration: const Duration(seconds: 2), + ), + ); + } + } catch (e) { + _handleError('Failed to take photo: $e'); + } + } + + // --------------------------------------------------------- + // VIDEO RECORDING - START + // --------------------------------------------------------- + Future _startRecording() async { + if (_isRecording || !_isInitialized) return; + + try { + await HapticFeedback.mediumImpact(); + + setState(() { + _isRecording = true; + _progress = 0; + _currentSeconds = 0; + }); + + _scaleController.forward(); + + await _controller!.startVideoRecording(); + + // Use 100ms intervals for smooth progress + _recordTimer = Timer.periodic(const Duration(milliseconds: 100), (_) { + if (!mounted) return; + + setState(() { + _progress += 0.01; // 0.01 * 100 calls = 1.0 in 10 seconds + _currentSeconds = (_progress * _maxSeconds).toInt(); + }); + + if (_progress >= 1.0) { + _stopRecording(); + } + }); + } catch (e) { + _handleError('Failed to start recording: $e'); + setState(() => _isRecording = false); + _scaleController.reverse(); + } + } + + // --------------------------------------------------------- + // VIDEO RECORDING - STOP + // --------------------------------------------------------- + Future _stopRecording() async { + if (!_isRecording) return; + + try { + // Cancel timer and update UI first + _recordTimer?.cancel(); + _recordTimer = null; + + // Update state before stopping recording + if (mounted) { + setState(() => _isRecording = false); + } + + await HapticFeedback.mediumImpact(); + _scaleController.reverse(); + + // Check if controller is still recording + if (_controller != null && _controller!.value.isRecordingVideo) { + final file = await _controller!.stopVideoRecording(); + debugPrint('๐ŸŽฅ Video saved: ${file.path}'); + + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Video saved: ${file.path}'), + duration: const Duration(seconds: 2), + ), + ); + } + } + } catch (e) { + debugPrint('Error stopping recording: $e'); + // Don't show error to user for stop recording issues + // Just ensure state is cleaned up + if (mounted) { + setState(() => _isRecording = false); + } + } + } + + // --------------------------------------------------------- + // CAMERA SWITCH - Smooth with loading state + // --------------------------------------------------------- + Future _switchCamera() async { + if (_cameras.length < 2 || _isSwitchingCamera || _isRecording) return; + + try { + setState(() => _isSwitchingCamera = true); + await HapticFeedback.lightImpact(); + + final isFront = + _controller!.description.lensDirection == CameraLensDirection.front; + + final newCamera = _cameras.firstWhere( + (cam) => + cam.lensDirection == + (isFront ? CameraLensDirection.back : CameraLensDirection.front), + orElse: () => _cameras.first, + ); + + await _initCamera(newCamera); + } catch (e) { + _handleError('Failed to switch camera: $e'); + } finally { + if (mounted) { + setState(() => _isSwitchingCamera = false); + } + } + } + + // --------------------------------------------------------- + // ZOOM CONTROL - Optimized + // --------------------------------------------------------- + void _setZoom(double value) { + final clampedValue = value.clamp(_minZoom, _maxZoom); + setState(() => _zoomLevel = clampedValue); + _controller?.setZoomLevel(clampedValue); + } + + // --------------------------------------------------------- + // UI BUILD + // --------------------------------------------------------- + @override + Widget build(BuildContext context) { + return Scaffold(backgroundColor: Colors.black, body: _buildBody()); + } + + Widget _buildBody() { + if (_errorMessage != null) { + return _buildErrorView(); + } + + if (!_isInitialized || _controller == null) { + return _buildLoadingView(); + } + + return Stack( + fit: StackFit.expand, + children: [ + _buildCameraPreview(), + _buildZoomSlider(), + _buildTopControls(), + _buildBottomControls(), + if (_isSwitchingCamera) _buildSwitchingOverlay(), + ], + ); + } + + // Full-screen camera preview + Widget _buildCameraPreview() { + return SizedBox.expand( + child: FittedBox( + fit: BoxFit.cover, + child: SizedBox( + width: _controller!.value.previewSize?.height ?? 1, + height: _controller!.value.previewSize?.width ?? 1, + child: CameraPreview(_controller!), + ), + ), + ); + } + + // Error view + Widget _buildErrorView() { + return Center( + child: Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.error_outline, color: Colors.red, size: 64), + const SizedBox(height: 16), + Text( + _errorMessage ?? 'Unknown error', + style: const TextStyle(color: Colors.white, fontSize: 16), + textAlign: TextAlign.center, + ), + const SizedBox(height: 24), + ElevatedButton( + onPressed: () => _initCamera(), + child: const Text('Retry'), + ), + ], + ), + ), + ); + } + + // Loading view + Widget _buildLoadingView() { + return const Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CircularProgressIndicator(color: Colors.white), + SizedBox(height: 16), + Text('Initializing camera...', style: TextStyle(color: Colors.white)), + ], + ), + ); + } + + // Switching camera overlay + Widget _buildSwitchingOverlay() { + return Container( + color: Colors.black54, + child: const Center( + child: CircularProgressIndicator(color: Colors.white), + ), + ); + } + + // Zoom slider + Widget _buildZoomSlider() { + return Positioned( + right: 20, + top: 350, + bottom: 250, + child: RotatedBox( + quarterTurns: 3, + child: SliderTheme( + data: SliderThemeData( + activeTrackColor: Colors.white, + inactiveTrackColor: Colors.white30, + thumbColor: Colors.white, + overlayColor: Colors.white24, + ), + child: Slider( + value: _zoomLevel, + min: _minZoom, + max: _maxZoom, + onChanged: _setZoom, + ), + ), + ), + ); + } + + // Top controls (switch camera, timer) + Widget _buildTopControls() { + return Positioned( + top: MediaQuery.of(context).padding.top + 10, + left: 0, + right: 0, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + if (_isRecording) + Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 6, + ), + decoration: BoxDecoration( + color: Colors.red, + borderRadius: BorderRadius.circular(20), + ), + child: Row( + children: [ + const Icon( + Icons.fiber_manual_record, + color: Colors.white, + size: 12, + ), + const SizedBox(width: 6), + Text( + '${_currentSeconds}s / ${_maxSeconds}s', + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ) + else + const SizedBox.shrink(), + ], + ), + ), + ); + } + + // Bottom controls (capture button) + Widget _buildBottomControls() { + return Positioned( + bottom: 40, + left: 0, + right: 0, + child: Center( + child: GestureDetector( + onTap: !_isRecording ? _takePhoto : null, + onLongPressStart: (_) => _startRecording(), + onLongPressEnd: (_) => _stopRecording(), + child: AnimatedBuilder( + animation: _scaleAnimation, + builder: (context, child) { + return Transform.scale( + scale: _scaleAnimation.value, + child: Container( + width: 80, + height: 80, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.black26, + ), + child: CustomPaint( + painter: _isRecording + ? RecordProgressPainter( + progress: _progress, + color: Colors.red, + ) + : null, + child: Container( + margin: const EdgeInsets.all(8), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: _isRecording ? Colors.red : Colors.white, + border: Border.all(color: Colors.white, width: 3), + ), + ), + ), + ), + ); + }, + ), + ), + ), + ); + } +} diff --git a/lib/feature/home/presentation/mainscreen/pages/widgets/main_screen_appbar.dart b/lib/feature/home/presentation/mainscreen/pages/widgets/main_screen_appbar.dart index 6ddc82f..688e479 100644 --- a/lib/feature/home/presentation/mainscreen/pages/widgets/main_screen_appbar.dart +++ b/lib/feature/home/presentation/mainscreen/pages/widgets/main_screen_appbar.dart @@ -1,7 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; import 'package:iconsax/iconsax.dart'; -import 'package:password_manager/config/routes/route_names.dart'; import 'package:password_manager/core/extension/context_extension.dart'; import 'package:password_manager/core/extension/font_extension.dart'; import 'package:password_manager/core/widgets/appbar.dart'; @@ -12,12 +10,7 @@ class MainScreenAppBar extends StatelessWidget { Widget build(BuildContext context) { return Appbar( title: Text('My Vault', style: context.textTheme.titleLarge?.bold), - actions: [ - GestureDetector( - onTap: () => context.go(Routes.addPassword), - child: Icon(Iconsax.setting), - ), - ], + actions: [GestureDetector(onTap: () {}, child: Icon(Iconsax.setting))], ); } } diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 3308b6f..ca53a60 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -6,6 +6,7 @@ import FlutterMacOS import Foundation import connectivity_plus +import firebase_core import flutter_secure_storage_macos import local_auth_darwin import path_provider_foundation @@ -14,6 +15,7 @@ import sqflite_darwin func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin")) + FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) LocalAuthPlugin.register(with: registry.registrar(forPlugin: "LocalAuthPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) diff --git a/pubspec.lock b/pubspec.lock index ce6877b..3119531 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -145,6 +145,46 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.1" + camera: + dependency: "direct main" + description: + name: camera + sha256: eefad89f262a873f38d21e5eec853461737ea074d7c9ede39f3ceb135d201cab + url: "https://pub.dev" + source: hosted + version: "0.11.3" + camera_android_camerax: + dependency: transitive + description: + name: camera_android_camerax + sha256: d96bf774152dd2a0aee64c59ba6b914b5efb04ec5a25b56e17b7e898869cc07c + url: "https://pub.dev" + source: hosted + version: "0.6.24+2" + camera_avfoundation: + dependency: transitive + description: + name: camera_avfoundation + sha256: "035b90c1e33c2efad7548f402572078f6e514d4f82be0a315cd6c6af7e855aa8" + url: "https://pub.dev" + source: hosted + version: "0.9.22+6" + camera_platform_interface: + dependency: transitive + description: + name: camera_platform_interface + sha256: "98cfc9357e04bad617671b4c1f78a597f25f08003089dd94050709ae54effc63" + url: "https://pub.dev" + source: hosted + version: "2.12.0" + camera_web: + dependency: transitive + description: + name: camera_web + sha256: "77e53acb64d9de8917424eeb32b5c7c73572d1e00954bbf54a1e609d79a751a2" + url: "https://pub.dev" + source: hosted + version: "0.3.5+1" characters: dependency: transitive description: @@ -225,6 +265,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.2" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: "701dcfc06da0882883a2657c445103380e53e647060ad8d9dfb710c100996608" + url: "https://pub.dev" + source: hosted + version: "0.3.5+1" crypto: dependency: "direct main" description: @@ -329,6 +377,30 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.1" + firebase_core: + dependency: "direct main" + description: + name: firebase_core + sha256: "1f2dfd9f535d81f8b06d7a50ecda6eac1e6922191ed42e09ca2c84bd2288927c" + url: "https://pub.dev" + source: hosted + version: "4.2.1" + firebase_core_platform_interface: + dependency: transitive + description: + name: firebase_core_platform_interface + sha256: cccb4f572325dc14904c02fcc7db6323ad62ba02536833dddb5c02cac7341c64 + url: "https://pub.dev" + source: hosted + version: "6.0.2" + firebase_core_web: + dependency: transitive + description: + name: firebase_core_web + sha256: ff18fabb0ad0ed3595d2f2c85007ecc794aadecdff5b3bb1460b7ee47cded398 + url: "https://pub.dev" + source: hosted + version: "3.3.0" fixnum: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index a21d476..597a439 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -59,6 +59,8 @@ dependencies: local_auth_android: ^1.0.56 flutter_svg: ^2.2.2 rxdart: ^0.28.0 + firebase_core: ^4.2.1 + camera: ^0.11.3 # ------------------------------------------------------------ # Dev Dependencies diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index d7240f1..d785ab2 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -7,12 +7,15 @@ #include "generated_plugin_registrant.h" #include +#include #include #include void RegisterPlugins(flutter::PluginRegistry* registry) { ConnectivityPlusWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin")); + FirebaseCorePluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FirebaseCorePluginCApi")); FlutterSecureStorageWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin")); LocalAuthPluginRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 9b83ab5..572ceb4 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST connectivity_plus + firebase_core flutter_secure_storage_windows local_auth_windows )