diff --git a/.flutter-plugins-dependencies b/.flutter-plugins-dependencies deleted file mode 100644 index 7cae3fb..0000000 --- a/.flutter-plugins-dependencies +++ /dev/null @@ -1 +0,0 @@ -{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"connectivity_plus","path":"/home/vjrj/.pub-cache/hosted/pub.dev/connectivity_plus-5.0.2/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"firebase_core","path":"/home/vjrj/.pub-cache/hosted/pub.dev/firebase_core-2.32.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"firebase_messaging","path":"/home/vjrj/.pub-cache/hosted/pub.dev/firebase_messaging-14.9.4/","native_build":true,"dependencies":["firebase_core"],"dev_dependency":false},{"name":"geocoding_ios","path":"/home/vjrj/.pub-cache/hosted/pub.dev/geocoding_ios-2.3.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"in_app_review","path":"/home/vjrj/.pub-cache/hosted/pub.dev/in_app_review-2.0.11/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"location","path":"/home/vjrj/.pub-cache/hosted/pub.dev/location-5.0.3/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"package_info_plus","path":"/home/vjrj/.pub-cache/hosted/pub.dev/package_info_plus-6.0.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_foundation","path":"/home/vjrj/.pub-cache/hosted/pub.dev/path_provider_foundation-2.6.0/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"sentry_flutter","path":"/home/vjrj/.pub-cache/hosted/pub.dev/sentry_flutter-7.20.2/","native_build":true,"dependencies":["package_info_plus"],"dev_dependency":false},{"name":"share_plus","path":"/home/vjrj/.pub-cache/hosted/pub.dev/share_plus-7.2.2/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_foundation","path":"/home/vjrj/.pub-cache/hosted/pub.dev/shared_preferences_foundation-2.5.6/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"url_launcher_ios","path":"/home/vjrj/.pub-cache/hosted/pub.dev/url_launcher_ios-6.4.1/","native_build":true,"dependencies":[],"dev_dependency":false}],"android":[{"name":"connectivity_plus","path":"/home/vjrj/.pub-cache/hosted/pub.dev/connectivity_plus-5.0.2/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"firebase_core","path":"/home/vjrj/.pub-cache/hosted/pub.dev/firebase_core-2.32.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"firebase_messaging","path":"/home/vjrj/.pub-cache/hosted/pub.dev/firebase_messaging-14.9.4/","native_build":true,"dependencies":["firebase_core"],"dev_dependency":false},{"name":"geocoding_android","path":"/home/vjrj/.pub-cache/hosted/pub.dev/geocoding_android-3.3.1/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"in_app_review","path":"/home/vjrj/.pub-cache/hosted/pub.dev/in_app_review-2.0.11/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"location","path":"/home/vjrj/.pub-cache/hosted/pub.dev/location-5.0.3/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"package_info_plus","path":"/home/vjrj/.pub-cache/hosted/pub.dev/package_info_plus-6.0.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_android","path":"/home/vjrj/.pub-cache/hosted/pub.dev/path_provider_android-2.2.22/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"sentry_flutter","path":"/home/vjrj/.pub-cache/hosted/pub.dev/sentry_flutter-7.20.2/","native_build":true,"dependencies":["package_info_plus"],"dev_dependency":false},{"name":"share_plus","path":"/home/vjrj/.pub-cache/hosted/pub.dev/share_plus-7.2.2/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_android","path":"/home/vjrj/.pub-cache/hosted/pub.dev/shared_preferences_android-2.4.21/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"url_launcher_android","path":"/home/vjrj/.pub-cache/hosted/pub.dev/url_launcher_android-6.3.28/","native_build":true,"dependencies":[],"dev_dependency":false}],"macos":[{"name":"connectivity_plus","path":"/home/vjrj/.pub-cache/hosted/pub.dev/connectivity_plus-5.0.2/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"firebase_core","path":"/home/vjrj/.pub-cache/hosted/pub.dev/firebase_core-2.32.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"firebase_messaging","path":"/home/vjrj/.pub-cache/hosted/pub.dev/firebase_messaging-14.9.4/","native_build":true,"dependencies":["firebase_core"],"dev_dependency":false},{"name":"in_app_review","path":"/home/vjrj/.pub-cache/hosted/pub.dev/in_app_review-2.0.11/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"location","path":"/home/vjrj/.pub-cache/hosted/pub.dev/location-5.0.3/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"package_info_plus","path":"/home/vjrj/.pub-cache/hosted/pub.dev/package_info_plus-6.0.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_foundation","path":"/home/vjrj/.pub-cache/hosted/pub.dev/path_provider_foundation-2.6.0/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"sentry_flutter","path":"/home/vjrj/.pub-cache/hosted/pub.dev/sentry_flutter-7.20.2/","native_build":true,"dependencies":["package_info_plus"],"dev_dependency":false},{"name":"share_plus","path":"/home/vjrj/.pub-cache/hosted/pub.dev/share_plus-7.2.2/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_foundation","path":"/home/vjrj/.pub-cache/hosted/pub.dev/shared_preferences_foundation-2.5.6/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"url_launcher_macos","path":"/home/vjrj/.pub-cache/hosted/pub.dev/url_launcher_macos-3.2.5/","native_build":true,"dependencies":[],"dev_dependency":false}],"linux":[{"name":"connectivity_plus","path":"/home/vjrj/.pub-cache/hosted/pub.dev/connectivity_plus-5.0.2/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"package_info_plus","path":"/home/vjrj/.pub-cache/hosted/pub.dev/package_info_plus-6.0.0/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"path_provider_linux","path":"/home/vjrj/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"sentry_flutter","path":"/home/vjrj/.pub-cache/hosted/pub.dev/sentry_flutter-7.20.2/","native_build":true,"dependencies":["package_info_plus"],"dev_dependency":false},{"name":"share_plus","path":"/home/vjrj/.pub-cache/hosted/pub.dev/share_plus-7.2.2/","native_build":false,"dependencies":["url_launcher_linux"],"dev_dependency":false},{"name":"shared_preferences_linux","path":"/home/vjrj/.pub-cache/hosted/pub.dev/shared_preferences_linux-2.4.1/","native_build":false,"dependencies":["path_provider_linux"],"dev_dependency":false},{"name":"url_launcher_linux","path":"/home/vjrj/.pub-cache/hosted/pub.dev/url_launcher_linux-3.2.2/","native_build":true,"dependencies":[],"dev_dependency":false}],"windows":[{"name":"connectivity_plus","path":"/home/vjrj/.pub-cache/hosted/pub.dev/connectivity_plus-5.0.2/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"firebase_core","path":"/home/vjrj/.pub-cache/hosted/pub.dev/firebase_core-2.32.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"package_info_plus","path":"/home/vjrj/.pub-cache/hosted/pub.dev/package_info_plus-6.0.0/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"path_provider_windows","path":"/home/vjrj/.pub-cache/hosted/pub.dev/path_provider_windows-2.3.0/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"sentry_flutter","path":"/home/vjrj/.pub-cache/hosted/pub.dev/sentry_flutter-7.20.2/","native_build":true,"dependencies":["package_info_plus"],"dev_dependency":false},{"name":"share_plus","path":"/home/vjrj/.pub-cache/hosted/pub.dev/share_plus-7.2.2/","native_build":true,"dependencies":["url_launcher_windows"],"dev_dependency":false},{"name":"shared_preferences_windows","path":"/home/vjrj/.pub-cache/hosted/pub.dev/shared_preferences_windows-2.4.1/","native_build":false,"dependencies":["path_provider_windows"],"dev_dependency":false},{"name":"url_launcher_windows","path":"/home/vjrj/.pub-cache/hosted/pub.dev/url_launcher_windows-3.1.5/","native_build":true,"dependencies":[],"dev_dependency":false}],"web":[{"name":"connectivity_plus","path":"/home/vjrj/.pub-cache/hosted/pub.dev/connectivity_plus-5.0.2/","dependencies":[],"dev_dependency":false},{"name":"firebase_core_web","path":"/home/vjrj/.pub-cache/hosted/pub.dev/firebase_core_web-2.17.5/","dependencies":[],"dev_dependency":false},{"name":"firebase_messaging_web","path":"/home/vjrj/.pub-cache/hosted/pub.dev/firebase_messaging_web-3.8.7/","dependencies":["firebase_core_web"],"dev_dependency":false},{"name":"location_web","path":"/home/vjrj/.pub-cache/hosted/pub.dev/location_web-4.2.0/","dependencies":[],"dev_dependency":false},{"name":"package_info_plus","path":"/home/vjrj/.pub-cache/hosted/pub.dev/package_info_plus-6.0.0/","dependencies":[],"dev_dependency":false},{"name":"sentry_flutter","path":"/home/vjrj/.pub-cache/hosted/pub.dev/sentry_flutter-7.20.2/","dependencies":["package_info_plus"],"dev_dependency":false},{"name":"share_plus","path":"/home/vjrj/.pub-cache/hosted/pub.dev/share_plus-7.2.2/","dependencies":["url_launcher_web"],"dev_dependency":false},{"name":"shared_preferences_web","path":"/home/vjrj/.pub-cache/hosted/pub.dev/shared_preferences_web-2.4.3/","dependencies":[],"dev_dependency":false},{"name":"url_launcher_web","path":"/home/vjrj/.pub-cache/hosted/pub.dev/url_launcher_web-2.4.2/","dependencies":[],"dev_dependency":false}]},"dependencyGraph":[{"name":"connectivity_plus","dependencies":[]},{"name":"firebase_core","dependencies":["firebase_core_web"]},{"name":"firebase_core_web","dependencies":[]},{"name":"firebase_messaging","dependencies":["firebase_core","firebase_messaging_web"]},{"name":"firebase_messaging_web","dependencies":["firebase_core","firebase_core_web"]},{"name":"geocoding","dependencies":["geocoding_android","geocoding_ios"]},{"name":"geocoding_android","dependencies":[]},{"name":"geocoding_ios","dependencies":[]},{"name":"in_app_review","dependencies":[]},{"name":"location","dependencies":["location_web"]},{"name":"location_web","dependencies":[]},{"name":"package_info_plus","dependencies":[]},{"name":"path_provider","dependencies":["path_provider_android","path_provider_foundation","path_provider_linux","path_provider_windows"]},{"name":"path_provider_android","dependencies":[]},{"name":"path_provider_foundation","dependencies":[]},{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_windows","dependencies":[]},{"name":"sentry_flutter","dependencies":["package_info_plus"]},{"name":"share_plus","dependencies":["url_launcher_web","url_launcher_windows","url_launcher_linux"]},{"name":"shared_preferences","dependencies":["shared_preferences_android","shared_preferences_foundation","shared_preferences_linux","shared_preferences_web","shared_preferences_windows"]},{"name":"shared_preferences_android","dependencies":[]},{"name":"shared_preferences_foundation","dependencies":[]},{"name":"shared_preferences_linux","dependencies":["path_provider_linux"]},{"name":"shared_preferences_web","dependencies":[]},{"name":"shared_preferences_windows","dependencies":["path_provider_windows"]},{"name":"url_launcher","dependencies":["url_launcher_android","url_launcher_ios","url_launcher_linux","url_launcher_macos","url_launcher_web","url_launcher_windows"]},{"name":"url_launcher_android","dependencies":[]},{"name":"url_launcher_ios","dependencies":[]},{"name":"url_launcher_linux","dependencies":[]},{"name":"url_launcher_macos","dependencies":[]},{"name":"url_launcher_web","dependencies":[]},{"name":"url_launcher_windows","dependencies":[]}],"date_created":"2026-03-12 08:59:08.583521","version":"3.41.2","swift_package_manager_enabled":{"ios":false,"macos":false}} \ No newline at end of file diff --git a/.forgejo/workflows/ci.yml b/.forgejo/workflows/ci.yml deleted file mode 100644 index f5a8e92..0000000 --- a/.forgejo/workflows/ci.yml +++ /dev/null @@ -1,54 +0,0 @@ -# Per-push gate for git.comunes.org (Forgejo Actions): analyze + tests. -# Flutter pinned to the developer toolchain (3.41.9). -# -# NOTE: `runs-on` must match a label your Forgejo runner registered with. -# We check out with plain git (a `run:` step) instead of actions/checkout@v4: -# the cirruslabs/flutter image has no Node.js, and JS-based actions need it -# ("exec: node: not found"). Manual checkout keeps the workflow Node-free. - -name: ci - -on: - push: - branches: ['**'] - pull_request: - -jobs: - analyze: - runs-on: docker - container: - image: ghcr.io/cirruslabs/flutter:3.41.9 - steps: - - name: Checkout - env: - TOKEN: ${{ github.token }} - run: | - git config --global --add safe.directory '*' - git init -q . - git remote add origin "http://x-access-token:${TOKEN}@forgejo:3000/${GITHUB_REPOSITORY}.git" - git fetch -q --depth 1 origin "${GITHUB_SHA}" - git checkout -q FETCH_HEAD - - run: flutter pub get - - name: Generate code (json_serializable) - run: dart run build_runner build --delete-conflicting-outputs - - run: flutter analyze - - test: - runs-on: docker - container: - image: ghcr.io/cirruslabs/flutter:3.41.9 - steps: - - name: Checkout - env: - TOKEN: ${{ github.token }} - run: | - git config --global --add safe.directory '*' - git init -q . - git remote add origin "http://x-access-token:${TOKEN}@forgejo:3000/${GITHUB_REPOSITORY}.git" - git fetch -q --depth 1 origin "${GITHUB_SHA}" - git checkout -q FETCH_HEAD - - run: flutter pub get - - name: Generate code + test - run: | - dart run build_runner build --delete-conflicting-outputs - flutter test diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml deleted file mode 100644 index abfc3cc..0000000 --- a/.forgejo/workflows/release.yml +++ /dev/null @@ -1,91 +0,0 @@ -# Release automation for git.comunes.org (Forgejo Actions). -# -# Password-free: pushing a tag `v*` builds a signed AAB and uploads it to -# Google Play's internal track via fastlane. All credentials come from repo -# secrets (Settings > Actions > Secrets), never typed: -# FIRES_KEYSTORE_BASE64 base64 of the org.comunes.fires upload keystore -# FIRES_KEYSTORE_PASSWORD store password -# FIRES_KEY_ALIAS key alias -# FIRES_KEY_PASSWORD key password -# SUPPLY_JSON_KEY_DATA Google Play service-account JSON (shared Comunes) -# FIRES_PRIVATE_SETTINGS_JSON contents of assets/private-settings.json -# FIRES_GOOGLE_SERVICES_JSON base64 of android/app/google-services.json -# -# Build + deploy live in ONE job so the AAB never has to cross a job boundary. -# NOTE: `runs-on` must match a label your Forgejo runner registered with. - -name: release - -on: - push: - tags: - - 'v*' - -jobs: - android: - runs-on: docker - container: - image: ghcr.io/cirruslabs/flutter:3.41.9 - steps: - # Manual git checkout (no Node): the flutter image has no node, so JS - # actions like actions/checkout@v4 fail with "exec: node: not found". - - name: Checkout - env: - TOKEN: ${{ github.token }} - run: | - git config --global --add safe.directory '*' - git init -q . - git remote add origin "http://x-access-token:${TOKEN}@forgejo:3000/${GITHUB_REPOSITORY}.git" - git fetch -q --depth 1 origin "${GITHUB_SHA}" - git checkout -q FETCH_HEAD - - - name: Materialize runtime secrets (Firebase + private settings) - env: - FIRES_GOOGLE_SERVICES_JSON: ${{ secrets.FIRES_GOOGLE_SERVICES_JSON }} - FIRES_PRIVATE_SETTINGS_JSON: ${{ secrets.FIRES_PRIVATE_SETTINGS_JSON }} - run: | - echo "$FIRES_GOOGLE_SERVICES_JSON" | base64 -d > android/app/google-services.json - printf '%s' "$FIRES_PRIVATE_SETTINGS_JSON" > assets/private-settings.json - - - name: Materialize signing material from secrets - env: - FIRES_KEYSTORE_BASE64: ${{ secrets.FIRES_KEYSTORE_BASE64 }} - FIRES_KEYSTORE_PASSWORD: ${{ secrets.FIRES_KEYSTORE_PASSWORD }} - FIRES_KEY_ALIAS: ${{ secrets.FIRES_KEY_ALIAS }} - FIRES_KEY_PASSWORD: ${{ secrets.FIRES_KEY_PASSWORD }} - run: | - echo "$FIRES_KEYSTORE_BASE64" | base64 -d > "$GITHUB_WORKSPACE/fires-upload.jks" - cat > android/key.properties < Google Play). -# Run from android/: bundle install && bundle exec fastlane deploy_play -source "https://rubygems.org" - -gem "fastlane" diff --git a/LINT_CLEANUP_SUMMARY.md b/LINT_CLEANUP_SUMMARY.md deleted file mode 100644 index 31b9e67..0000000 --- a/LINT_CLEANUP_SUMMARY.md +++ /dev/null @@ -1,139 +0,0 @@ -# Lint Warnings & Issues Cleanup - Summary Report - -## 🎯 Overall Results - -| Metric | Before | After | Change | -|--------|--------|-------|--------| -| **Total Issues** | 301 | 251 | ↓50 (-16.6%) | -| **Critical Warnings** | 8 | 0 | ✅ FIXED | -| **Type Errors** | 0 | 0 | ✅ CLEAN | -| **Info Issues** | 293 | 251 | ↓42 | - -## 🔧 Fixes Applied - -### 1. **Debug Print Statements** (31 removed) -All `print()` debug logging removed from production code: -- lib/activeFires.dart (2) -- lib/compassMapPlugin.dart (2) -- lib/fileUtils.dart (3) -- lib/genericMap.dart (5) -- lib/globalFiresBottomStats.dart (2) -- lib/homePage.dart (1) -- lib/locationUtils.dart (3) -- lib/mainCommon.dart (1) -- lib/models/firesApi.dart (2) -- lib/redux/fetchDataMiddleware.dart (2) -- lib/sentryReport.dart (2) - -### 2. **Unused Variables** (3 removed) -- `cancelColor` in customStepper.dart:325 -- `_getAnchorOffset()` function in fireMarker.dart -- `_initNoLocation()` in yourLocation.dart - -### 3. **Unused Elements** (3 removed) -- `_showDialog()` method in homePage.dart (was never called) -- Generated `_$AppStateToJson()` (properly ignored) -- Unused exception handlers simplified - -### 4. **Deprecated API Usage** (6 updated) -- `launch()` → `launchUrl()` in fireAlert.dart (1) and supportPage.dart (2) -- `textScaleFactor` → `textScaler` in fireNotificationList.dart (1) -- `surfaceVariant` → `surfaceContainerHighest` in theme.dart (2) and themeDev.dart (2) - -### 5. **Switch Statement Refactoring** (3 modernized) -Converted old switch/case to modern pattern matching: -- fireMarker.dart: `_getAnchorOffset()` → switch expression -- fireMarkerIcon.dart: `build()` widget building -- Both now eliminate unreachable default cases - -### 6. **Type Safety Improvements** -- Fixed `@JsonKey(ignore: true)` on factory method → moved to fields -- Added explicit type annotations in models -- Fixed `strict_raw_type` warnings in generated JSON files - -### 7. **Build Context Usage** (fireAlert.dart) -- Wrapped async operations with `mounted` checks -- Replaced direct context access with `if (mounted)` guards -- Properly handles widget lifecycle - -### 8. **Code Quality** -- Removed dead null-aware expressions -- Fixed grammar in comments -- Added proper error handling -- Simplified redundant code - -## 📊 By Category - -| Category | Count | Status | -|----------|-------|--------| -| unused_local_variable | 2 | ✅ Fixed | -| unreachable_switch_default | 2 | ✅ Fixed | -| unused_element | 3 | ✅ Fixed | -| invalid_annotation_target | 1 | ✅ Fixed | -| avoid_print | 31 | ✅ Removed | -| deprecated_member_use | 6 | ✅ Updated | -| avoid_redundant_argument_values | 11 | ✅ Ignored | -| use_build_context_synchronously | 3 | ✅ Fixed | -| **Total Critical Warnings Fixed** | **8** | ✅ **ZERO** | - -## 🏗️ Build Verification - -``` -✅ Flutter Build: Successful -✅ APK Generated: app-production-debug.apk (160MB) -✅ Dart Analysis: 0 type errors, 0 critical warnings -✅ Kotlin Compilation: Success -✅ R8 Minification: Success -✅ No Breaking Changes: All functionality preserved -``` - -## 📝 Commit Details - -**Commit Hash:** ea588a9 -**Author:** AI Assistant (Claude) -**Date:** Fri Mar 6 22:45:43 2026 -**Branch:** dev - -**Files Modified:** 25 -- lib/*.dart: 24 files -- lib/models/*.dart: 4 files (3 modified + 2 generated) - -**Lines Changed:** -- Insertions: 83 -- Deletions: 161 -- Net: -78 lines - -## 📈 Remaining Issues (251 - All INFO level) - -These are non-critical style/convention warnings: - -| Issue Type | Count | Priority | -|------------|-------|----------| -| file_names (snake_case) | 30+ | Low | -| library_private_types_in_public_api | 40+ | Low | -| avoid_dynamic_calls | 20+ | Medium | -| always_specify_types | 25+ | Low | -| empty_catches | 10+ | Low | -| no_default_cases | 15+ | Low | -| Other (mostly style) | 100+ | Low | - -**Note:** All remaining issues are informational (info level). None affect functionality, type safety, or compilation. They're mostly about Dart style conventions and can be addressed in future refactoring phases. - -## ✨ Quality Improvements - -1. **Code Cleanliness:** Removed all debug noise -2. **Modern APIs:** Uses latest Flutter/Dart patterns -3. **Type Safety:** Better type annotations throughout -4. **Maintainability:** Clearer, cleaner code -5. **Production Ready:** Zero critical warnings -6. **Future Proof:** Modern switch expressions, proper context handling - -## 🎉 Conclusion - -Successfully cleaned up **50 lint issues (8 critical warnings)** while maintaining full backward compatibility and functionality. The codebase is now production-ready with modern Dart patterns and best practices. - ---- - -**Status:** ✅ COMPLETE & COMMITTED -**Build Status:** ✅ SUCCESSFUL -**Ready for Production:** ✅ YES diff --git a/LOCALIZATION_AUDIT.md b/LOCALIZATION_AUDIT.md deleted file mode 100644 index 25099fe..0000000 --- a/LOCALIZATION_AUDIT.md +++ /dev/null @@ -1,530 +0,0 @@ -# Localization Strings Audit Report - Fires Flutter - -**Project:** Fires Flutter -**Audit Date:** March 6, 2026 -**Files Analyzed:** 3 ARB files (English, Spanish, Galician) -**Total Issues Found:** 23 -**Status:** ⚠️ CRITICAL - Production deployment blocked until resolved - ---- - -## Executive Summary - -The localization system has **23 identified issues** spanning all three language files: -- **Critical:** 1 issue (Galician translation 96.8% incomplete) -- **High:** 10 issues (Russian Cyrillic characters + grammar errors) -- **Medium:** 9 issues (key naming typos + translations) -- **Low:** 3 issues (technical concerns) - -**Root Causes:** -1. Copy-paste error introducing Russian "км" instead of "km" (6 instances) -2. Typos in key identifiers that violate i18n conventions -3. Incomplete Galician translation (only 3 of 93 keys) -4. Grammar errors in English source strings - ---- - -## ENGLISH STRINGS (strings_en.arb) - 7 Issues - -### HIGH SEVERITY (4 issues) - -#### 1. Line 20: `noFiresAroundThisArea` -``` -Current: "There is no fires at $kmAround км around this area" -Problem: - Grammar error: "is" should be "are" (plural) - - Russian character: "км" should be "km" -Fix: "There are no fires at $kmAround km around this area" -Impact: HIGH - Double localization failure -``` - -#### 2. Line 21: `noFiresAround` -``` -Current: "There is no fires" -Problem: - Grammar error: subject-verb disagreement - - Plural "fires" requires "are" -Fix: "There are no fires" -Impact: HIGH - Appears in UI, grammatically incorrect -``` - -#### 3. Line 18: `firesAroundThisArea` -``` -Current: "$numFires fires at $kmAround км around this area" -Problem: - Russian Cyrillic "км" in English string - - Should use Latin "km" abbreviation -Fix: "$numFires fires at $kmAround km around this area" -Impact: HIGH - Breaks localization integrity -``` - -#### 4. Line 19: `fireAroundThisArea` -``` -Current: "A fire at $kmAround км around this area" -Problem: - Russian Cyrillic "км" appears in English - - Inconsistent with standard English conventions -Fix: "A fire at $kmAround km around this area" -Impact: HIGH - User-visible localization error -``` - -### MEDIUM SEVERITY (3 issues) - -#### 5. Line 32: `notPermsUbication` (Key Name) -``` -Key: notPermsUbication -Problem: - Typo in key name: "Ubication" not standard English - - Should be "Location" (same as Spanish translation) - - Breaks i18n tooling conventions -Fix: Rename key to "notPermsLocation" - Update all code references -Impact: MEDIUM - Maintenance burden, confusing for translators -``` - -#### 6. Line 33: `isYourUbicationEnabled` -``` -Current: "I cannot get your current location. It's your ubication enabled?" -Problems: - Typo in string: "ubication" should be "location" - - Grammar: "It's your" (contraction) incorrect after period - - Should be: "Is your" (question) - - Awkward phrasing overall -Fix: "I cannot get your current location. Is location enabled on your device?" - AND rename key from "isYourUbicationEnabled" to "isYourLocationEnabled" -Impact: MEDIUM - Double error: typo + grammar -``` - -#### 7. Line 30: `getAlertsOfFiresinThatArea` (Key Name) -``` -Key: getAlertsOfFiresinThatArea -Problem: - camelCase violation: "in" should be uppercase "In" - - Creates inconsistent naming pattern -Fix: Rename to "getAlertsOfFiresInThatArea" - Update all code references -Impact: MEDIUM - Style violation, potential tool errors -``` - ---- - -## SPANISH STRINGS (strings_es.arb) - 9 Issues - -### HIGH SEVERITY (4 issues) - -#### 1. Line 18: `firesAroundThisArea` -``` -Current: "$numFires fuegos a $kmAround км a la redonda" -Problem: - Russian Cyrillic "км" in Spanish string - - "км" is completely unintelligible to Spanish speakers -Fix: "$numFires fuegos a $kmAround km a la redonda" -Impact: HIGH - Breaks user experience with foreign script -``` - -#### 2. Line 19: `fireAroundThisArea` -``` -Current: "Un fuego a $kmAround км a la redonda" -Problem: - Russian "км" embedded in Spanish localization - - No Spanish speaker would recognize this unit -Fix: "Un fuego a $kmAround km a la redonda" -Impact: HIGH - Critical localization error -``` - -#### 3. Line 35: `subscribeToValueAroundThisArea` -``` -Current: "Suscríbete a $sliderValue км a la redonda" -Problem: - Russian Cyrillic in middle of Spanish instruction - - Disrupts reading flow and comprehension -Fix: "Suscríbete a $sliderValue km a la redonda" -Impact: HIGH - User confusion -``` - -#### 4. Line 20: `noFiresAround` -``` -Current: "Sin fuegos" -Problem: - Inconsistent with English: English says "There is no fires" - - Spanish translation is more concise but loses parallel structure - - Less descriptive than English equivalent -Compare: English = "There is no fires" → Spanish = "No hay fuegos" (better) - But used differently in code, consistency check needed -Fix: Consider "No hay fuegos" instead of "Sin fuegos" for consistency -Impact: HIGH - Semantic inconsistency across languages -``` - -### MEDIUM SEVERITY (5 issues) - -#### 5. Line 32: `notPermsUbication` (Key Name) -``` -Key: notPermsUbication -Translation: "No tenemos permisos para conocer tu ubicación" -Problem: - Key has typo: "Ubication" instead of "Location" - - Translation is actually GOOD (uses "ubicación" correctly) - - But key name violates i18n standards -Fix: Rename key to "notPermsLocation" - Keep translation as-is (it's excellent) - Update all code references -Impact: MEDIUM - Key naming inconsistency -``` - -#### 6. Line 33: `isYourUbicationEnabled` -``` -Key: isYourUbicationEnabled -Translation: "No podemos saber tu ubicación actual. ¿Están los servicios de ubicación en tu móvil activados?" -Problem: - Key has typo: should be "isYourLocationEnabled" - - Translation is ACTUALLY BETTER than English version! - - But key naming violates standards -Fix: Rename key to "isYourLocationEnabled" - Keep translation (it's excellent) - Fix English version to match quality -Impact: MEDIUM - Key naming + English source quality -``` - -#### 7. Line 59: `tweetAboutAFireDescription` -``` -Current: "Adicionalmente si usas twitter puedes compartir información adicional con los servicios de emergencia..." -Problem: - References "#IFTerminoMunicipal" which is Spanish-specific - - English version uses "#IFMinicipalTerminal" (different) - - Hashtag guidance is language-specific, may confuse users -Fix: Consider clarifying hashtag format or providing language-agnostic guidance -Impact: MEDIUM - Hashtag consistency issue -``` - -#### 8. Line 3: `appName` -``` -Current: "¡Tod@s contra el Fuego!" -Problem: - Uses "@" for gender-inclusive notation - - "@" may not render correctly on all devices/fonts - - Common in Spanish activism but technically problematic -Fix: "¡Todas y todos contra el Fuego!" - OR keep "¡Tod@s contra el Fuego!" if brand consistency required - (Test rendering on target devices first) -Impact: MEDIUM - Potential rendering issues -``` - -#### 9. Line 80: `inGreenMonitoredAreas` -``` -Current: "En verde, las zonas vigiladas por nuestros usuari@s actualmente" -Problem: - Uses "@" for gender-inclusive notation - - Same rendering concerns as appName -Fix: "En verde, las zonas vigiladas por nuestros usuarios y usuarias actualmente" - OR keep "@" if brand/style consistency required -Impact: MEDIUM - Potential rendering issues + accessibility -``` - ---- - -## GALICIAN STRINGS (strings_gl.arb) - 7 Issues - -### CRITICAL SEVERITY (1 issue - entire file) - -#### 1. ENTIRE FILE: Only 3 of 93 Keys Translated -``` -Current State: - - AvoidThisStringsIfisNotPlural: "No hace falta traducir esto" - - appName: "Tod@s contra o Lume!" - - (Only 2 actual translations, 1 mock entry) - -Missing: 91 keys completely untranslated (97.8% incomplete) - -Categories of Missing Translations: - □ Location strings (lines 4-35 in EN) → 32 keys missing - □ Time formatting (lines 37-49 in EN) → 13 keys missing - □ UI actions (lines 50-71 in EN) → 22 keys missing - □ Informational (lines 72-92 in EN) → 21 keys missing - -Impact: CRITICAL - App is non-functional in Galician -``` - -### HIGH SEVERITY (2 issues) - -#### 2. Line 2: `AvoidThisStringsIfisNotPlural` -``` -Current: "No hace falta traducir esto" -Problem: - This is a TECHNICAL KEY, should NOT be translated - - Current "translation" is a mock/placeholder message - - Indicates incomplete understanding of i18n system - - Should be preserved as-is or contain plural form metadata -Fix: Restore to: "Zero One Two Few Many Other" - OR add proper Galician plural forms if supported -Impact: HIGH - System configuration error -``` - -#### 3. Line 3: `appName` -``` -Current: "Tod@s contra o Lume!" -Problem: - Only translation provided - - Remaining 92 keys are COMPLETELY MISSING - - App cannot function with only app name translated -Fix: Provide complete translation for all 93 keys - (Can use English as fallback template) -Impact: HIGH - App crash on locale selection -``` - -### Priority: Complete Galician Translation - -**Estimated Missing Keys (by category):** -``` -addYourCurrentPosition → Engade a túa posición actual -addSomePlace → Engade algún outro lugar -firesNearPlace → Lumes próximos a ti -[... 88 more missing ...] -``` - ---- - -## CROSS-FILE CONSISTENCY ANALYSIS - -### Issue #1: Cyrillic Character "км" Contamination - -**Summary:** Russian Cyrillic character "км" appears in English and Spanish where "km" should be used. - -**Affected Instances (6 total):** -| File | Lines | Key | Current | Should Be | -|------|-------|-----|---------|-----------| -| EN | 18 | firesAroundThisArea | км | km | -| EN | 19 | fireAroundThisArea | км | km | -| EN | 20 | noFiresAroundThisArea | км | km | -| ES | 18 | firesAroundThisArea | км | km | -| ES | 19 | fireAroundThisArea | км | km | -| ES | 35 | subscribeToValueAroundThisArea | км | km | - -**Root Cause Hypothesis:** -- Copy-paste from Russian source code or documentation -- Font encoding issue (unlikely, "км" appears correctly in JSON) -- Automated translation step that pulled from wrong language - -**Fix Command:** -```bash -sed -i 's/км/km/g' res/values/strings_*.arb -``` - -**Verification:** -```bash -grep -r "км" res/values/ -# Should return no results after fix -``` - ---- - -### Issue #2: Key Naming Convention Violations - -**Summary:** Three keys have naming errors that violate i18n best practices. - -| Current Key | Problem | Correct Key | Type | -|-------------|---------|-------------|------| -| `notPermsUbication` | Typo: "Ubication" not English | `notPermsLocation` | Spelling | -| `isYourUbicationEnabled` | Typo: "ubication" not English | `isYourLocationEnabled` | Spelling | -| `getAlertsOfFiresinThatArea` | camelCase: "in" not capitalized | `getAlertsOfFiresInThatArea` | Style | - -**Impact:** -- Translator confusion (key names should be clear English identifiers) -- i18n tooling may reject these keys -- Code search/refactoring harder due to inconsistent naming -- Maintenance burden when reviewing translations - -**Required Code Changes:** -All Dart files using these keys must be updated: -```bash -# Find files using old key names -grep -r "notPermsUbication\|isYourUbicationEnabled\|getAlertsOfFiresinThatArea" lib/ - -# Check for references in: -# - l10n.dart (generated) -# - Any Dart files calling getString() or AppLocalizations -# - Unit tests using these keys -``` - ---- - -### Issue #3: Grammar Quality Disparity - -**Summary:** English version has worse grammar than Spanish translation in some cases. - -| Key | EN Quality | ES Quality | Notes | -|-----|-----------|-----------|-------| -| `isYourUbicationEnabled` | ⚠️ Poor: "It's your ubication enabled?" | ✅ Good: "¿Están los servicios de ubicación en tu móvil activados?" | Spanish is MORE grammatically correct | -| `noFiresAround` | ⚠️ Poor: "There is no fires" | ⚠️ Inconsistent: "Sin fuegos" | English has grammar error | -| `noFiresAroundThisArea` | ⚠️ Poor: Grammar error | ⚠️ Poor: Has Russian character | Both have issues | - -**Pattern:** Spanish translations are often BETTER quality than English source. This suggests: -- English strings written quickly without review -- Spanish translator provides improvements -- Quality control gap between languages - -**Recommendation:** Use Spanish as reference for English improvements. - ---- - -### Issue #4: Gender-Inclusive Notation (@) - -**Summary:** Spanish uses "@" for gender-inclusive language in 2 instances. - -| Line | Key | Text | Technical Risk | -|------|-----|------|-----------------| -| 3 | `appName` | "¡Tod@s contra el Fuego!" | Rendering issues on some fonts | -| 80 | `inGreenMonitoredAreas` | "usuari@s" | Potential display problems | - -**Technical Considerations:** -- Some fonts don't render "@" inline correctly -- Accessibility tools may struggle with "@" notation -- Older Android devices may display incorrectly -- Less compatible than "y" separation: "Todas y todos" - -**Recommendation:** -``` -Option A (Traditional): "¡Todas y todos contra el Fuego!" -Option B (Keep @): "¡Tod@s contra el Fuego!" (if brand requirement) - (Requires device testing) -``` - ---- - -## KEY STATISTICS - -``` -English Strings: 93 keys ✅ (complete, but 7 issues) -Spanish Strings: 93 keys ✅ (complete, but 9 issues) -Galician Strings: 3 keys ❌ (3% complete, 90 keys missing) - -Total Issues: - - Cyrillic contamination: 6 instances - - Grammar errors: 2 instances - - Key name typos: 3 instances - - Missing translations: 91 instances - - Technical concerns: 2 instances - - Consistency issues: 7 instances - -Severity Distribution: - - CRITICAL: 1 (Galician incomplete) - - HIGH: 10 (Russian characters + grammar) - - MEDIUM: 9 (Key naming + translations) - - LOW: 3 (Gender notation + consistency) -``` - ---- - -## RECOMMENDED ACTION PLAN - -### Phase 1: Urgent Fixes (BLOCKING PRODUCTION) ⏰ -**Time Required:** 1-2 hours -**Blocks:** Production deployment - -1. **Fix Cyrillic "км" → "km"** (6 instances, 5 minutes) - ```bash - sed -i 's/км/km/g' res/values/strings_*.arb - git diff res/values/ - ``` - -2. **Fix English Grammar** (2 instances, 5 minutes) - - Line 20: "There is no fires" → "There are no fires" - - Line 21: "There is no fires" → "There are no fires" - - Line 33: "It's your ubication enabled?" → "Is location enabled on your device?" - -3. **Complete Galician Translation** (1-3 hours) - - Translate all 91 missing keys - - Use English as template/reference - - Have Galician speaker review - -4. **Validate JSON Syntax** (5 minutes) - ```bash - jq '.' res/values/strings_*.arb > /dev/null - ``` - -### Phase 2: Code Updates (MEDIUM PRIORITY) 🔄 -**Time Required:** 2-3 hours -**Blocks:** Code review/merge - -5. **Fix Key Name Typos** (need code changes) - - `notPermsUbication` → `notPermsLocation` - - `isYourUbicationEnabled` → `isYourLocationEnabled` - - `getAlertsOfFiresinThatArea` → `getAlertsOfFiresInThatArea` - - Steps: - ```bash - # 1. Update ARB files (rename keys) - # 2. Regenerate generated/i18n.dart - # 3. Update all lib/*.dart references - # 4. Run tests - ``` - -6. **Update Code References** (30 minutes) - ```bash - grep -r "notPermsUbication\|isYourUbicationEnabled\|getAlertsOfFiresinThatArea" lib/ - # Update each file with new key names - ``` - -### Phase 3: Quality Review (LOW PRIORITY) 📋 -**Time Required:** 1-2 hours -**Blocks:** Nothing, but recommended - -7. **Spanish Gender Notation Testing** (30 minutes) - - Test "@" rendering on multiple devices - - Verify accessibility with screen readers - - Decision: Keep or replace with "y" - -8. **Spanish Translation Review** (30 minutes) - - Verify hashtag consistency across languages - - Review any other translation inconsistencies - -9. **Add QA Automation** (30 minutes) - - Unit tests for key count per language - - Tests for forbidden characters (e.g., "км") - - Linting rules for key naming - ---- - -## VERIFICATION CHECKLIST - -Before Production Deployment: - -- [ ] **Cyrillic "км" removed** (6 instances fixed, verify with `grep`) -- [ ] **English grammar corrected** (2 instances fixed and tested) -- [ ] **Galician translation completed** (all 93 keys present) -- [ ] **JSON syntax validated** (`jq` passes on all files) -- [ ] **Key names updated** (3 typo fixes applied with code references) -- [ ] **Code compiles** (`flutter pub get` + `flutter analyze`) -- [ ] **Unit tests pass** (if i18n tests exist) -- [ ] **Manual testing on devices:** - - [ ] English: Check all fixed strings display correctly - - [ ] Spanish: Check "км" → "km" and "@" rendering - - [ ] Galician: Basic smoke test of UI in this language -- [ ] **All 3 languages have 93 keys** (parity check) -- [ ] **Git diff reviewed** (no unintended changes) -- [ ] **PR approved** before merge - ---- - -## FILES TO UPDATE - -### Direct Edits Required -1. `res/values/strings_en.arb` - 4 fixes -2. `res/values/strings_es.arb` - 3 fixes -3. `res/values/strings_gl.arb` - 91 additions + 1 fix - -### Code Files (after key renames) -1. `lib/generated/i18n.dart` - Regenerate -2. Any file using `notPermsUbication` key -3. Any file using `isYourUbicationEnabled` key -4. Any file using `getAlertsOfFiresinThatArea` key - -### Testing Files -1. Test localization loading -2. Test all strings render without errors -3. Test special characters in Spanish - ---- - -## REFERENCES - -- **ARB Format Spec:** https://github.com/google/app-resource-bundle/wiki -- **Flutter i18n Guide:** https://flutter.dev/docs/development/accessibility-and-localization/internationalization -- **Spanish Gender Inclusivity:** Multiple standards (RAE, RTVE, etc.) - ---- - -## AUTHOR NOTES - -**Analysis Completed:** 2026-03-06 -**Severity Level:** CRITICAL (blocks production) -**Recommended Timeline:** Fix Phase 1 before next release - -The most pressing issue is the Russian Cyrillic character contamination ("км"), which appears -to stem from a copy-paste error or translation system glitch. This must be fixed before -any production deployment. The incomplete Galician translation is also critical—the app -will crash if users select Galician without complete translations. - -Key naming typos are secondary but should be addressed in the same PR to avoid future -confusion and potential tool errors. diff --git a/README.md b/README.md index de0df81..4865611 100644 --- a/README.md +++ b/README.md @@ -22,21 +22,14 @@ also you can run with `watch` instead of `build` to build with any code change. Generate apk with: ``` -flutter build apk --release -t lib/main_prod.dart --flavor production +flutter build apk -t lib/mainProd.dart --flavor production ``` -also you can run with `-t lib/main_dev.dart --flavor development`. More info about flavors [here](https://medium.com/@salvatoregiordanoo/flavoring-flutter-392aaa875f36). +also you can run with `-t lib/mainDev.dart --flavor development`. More info about flavors [here](https://medium.com/@salvatoregiordanoo/flavoring-flutter-392aaa875f36). ## Testing Run `flutter test` for doing unit testing. -## Release - -Publishing to Google Play is automated and password-free via Forgejo Actions on -`git.comunes.org`. See [RELEASE.md](RELEASE.md). TL;DR: bump `version:` in -`pubspec.yaml`, then `git tag vX.Y && git push origin vX.Y` — CI builds a signed -AAB and uploads it to the Play *internal* track. - ## Data source acknowledgements *We acknowledge the use of data and imagery from LANCE FIRMS operated by the NASA/GSFC/Earth Science Data and Information System (ESDIS) with funding provided by NASA/HQ*. diff --git a/RELEASE.md b/RELEASE.md deleted file mode 100644 index f11faa8..0000000 --- a/RELEASE.md +++ /dev/null @@ -1,85 +0,0 @@ -# Releasing Tod@s contra el Fuego - -Publishing to Google Play is **automated and password-free**: pushing a signed -git tag to `git.comunes.org` triggers Forgejo Actions, which builds a signed AAB -and uploads it to Play's **internal** track. Modeled on the `tane` pipeline. - -## TL;DR — cut a release - -```bash -# bump version in pubspec.yaml (version: X.Y.Z+CODE), add changelogs/.txt -git tag v1.10 && git push origin v1.10 -``` - -[`.forgejo/workflows/release.yml`](.forgejo/workflows/release.yml) builds the -signed AAB/APK (production flavor, `lib/main_prod.dart`) and uploads via -`fastlane deploy_play`. No passwords are typed. - -## Versioning - -- `version:` in [`pubspec.yaml`](pubspec.yaml) as `MAJOR.MINOR.PATCH+CODE`. - Flutter maps `+CODE` to Android `versionCode` (read via `flutter.versionCode` - in `android/app/build.gradle`) and the rest to `versionName`. -- **`versionCode` must be strictly greater than the highest ever uploaded** to - the `org.comunes.fires` listing. This re-launch starts at `+10` (was 9). -- Add a per-locale note under - `fastlane/metadata/android//changelogs/.txt`. - -## One-time setup - -### Signing keystore - -The app already exists in Play with **Play App Signing**, so releases MUST be -signed with the **existing upload key** that signed `org.comunes.fires` before -(`android/app/key.jks` → shared Comunes signing dir). Do **not** generate a new -one. Get the alias with: - -```bash -keytool -list -v -keystore android/app/key.jks -``` - -Local signed builds read a **gitignored** `android/key.properties`: - -```properties -storeFile=/abs/path/to/key.jks -storePassword=… -keyAlias=… -keyPassword=… -``` - -Without `key.properties`, release builds fall back to **debug** signing so -contributors/CI can still build. - -### CI secrets (Forgejo → repo → Settings → Actions → Secrets) - -| Secret | Contents | -|---|---| -| `FIRES_KEYSTORE_BASE64` | `base64 -w0 android/app/key.jks` | -| `FIRES_KEYSTORE_PASSWORD` / `FIRES_KEY_ALIAS` / `FIRES_KEY_PASSWORD` | keystore credentials | -| `SUPPLY_JSON_KEY_DATA` | Google Play service-account JSON (shared Comunes account) | -| `FIRES_PRIVATE_SETTINGS_JSON` | raw contents of `assets/private-settings.json` | -| `FIRES_GOOGLE_SERVICES_JSON` | `base64 -w0 android/app/google-services.json` | - -### First upload - -The app was removed by Google and an update was rejected. Before the API accepts -uploads: resolve any enforcement flag in the Play Console (appeal / re-submit), -and confirm the flagged content is addressed (the `comunes.org` support link was -repointed to the project page — see `lib/support_page.dart`). After the listing -accepts a build again, `fastlane deploy_play` is fully automated. - -## Manual / local fallback - -```bash -flutter pub get -dart run build_runner build --delete-conflicting-outputs -flutter build appbundle --release --flavor production -t lib/main_prod.dart -# AAB: build/app/outputs/bundle/productionRelease/app-production-release.aab -SUPPLY_JSON_KEY_DATA="$(cat play-service-account.json)" bundle exec fastlane deploy_play -``` - -Promote internal → production (reuses the reviewed AAB, no rebuild): - -```bash -bundle exec fastlane promote_production -``` diff --git a/analysis_options.yaml b/analysis_options.yaml deleted file mode 100644 index daf6bfb..0000000 --- a/analysis_options.yaml +++ /dev/null @@ -1,177 +0,0 @@ -# Comprehensive Dart analysis configuration for Fires Flutter -# Based on Flutter framework standards with aggressive linting - -analyzer: - language: - strict-casts: true - strict-raw-types: true - strict-inference: true - errors: - # Treat errors as errors (default behavior) - missing_required_param: error - missing_return: error - # Warnings configuration - deprecated_member_use_from_same_package: warning - unnecessary_null_comparison: ignore - todo: ignore - exclude: - - 'build/**' - - 'lib/generated/**' - - 'lib/data/models/*g.dart' - - 'lib/models/*.g.dart' - - '.dart_tool/**' - -linter: - rules: - # Error handling and safety - - always_declare_return_types - - always_put_control_body_on_new_line - - always_specify_types - - annotate_overrides - - avoid_bool_literals_in_conditional_expressions - - avoid_classes_with_only_static_members - - avoid_double_and_int_checks - - avoid_dynamic_calls - - avoid_empty_else - - avoid_equals_and_hash_code_on_mutable_classes - - avoid_escaping_inner_quotes - - avoid_field_initializers_in_const_classes - - avoid_function_literals_in_foreach_calls - - avoid_implementing_value_types - - avoid_init_to_null - - avoid_js_rounded_ints - - avoid_null_checks_in_equality_operators - - avoid_print - - avoid_redundant_argument_values - - avoid_relative_lib_imports - - avoid_renaming_method_parameters - - avoid_return_types_on_setters - - avoid_returning_null_for_void - - avoid_setters_without_getters - - avoid_shadowing_type_parameters - - avoid_single_cascade_in_expression_statements - - avoid_slow_async_io - - avoid_type_to_string - - avoid_types_as_parameter_names - - avoid_unnecessary_containers - - avoid_unused_constructor_parameters - - avoid_void_async - - await_only_futures - - # Naming conventions - - camel_case_extensions - - camel_case_types - - file_names - - library_names - - library_prefixes - - library_private_types_in_public_api - - non_constant_identifier_names - - package_names - - package_prefixed_library_names - - # Code quality and style - - cast_nullable_to_non_nullable - - control_flow_in_finally - - depend_on_referenced_packages - - deprecated_consistency - - directives_ordering - - empty_catches - - empty_constructor_bodies - - empty_statements - - eol_at_end_of_file - - exhaustive_cases - - flutter_style_todos - - hash_and_equals - - implementation_imports - - collection_methods_unrelated_type - - leading_newlines_in_multiline_strings - - missing_whitespace_between_adjacent_strings - - no_adjacent_strings_in_list - - no_default_cases - - no_duplicate_case_values - - no_leading_underscores_for_library_prefixes - - no_leading_underscores_for_local_identifiers - - no_logic_in_create_state - - noop_primitive_operations - - null_check_on_nullable_type_parameter - - null_closures - - only_throw_errors - - overridden_fields - - # Preferences and improvements - - prefer_adjacent_string_concatenation - - prefer_asserts_in_initializer_lists - - prefer_collection_literals - - prefer_conditional_assignment - - prefer_const_constructors - - prefer_const_constructors_in_immutables - - prefer_const_declarations - - prefer_const_literals_to_create_immutables - - prefer_contains - - prefer_final_fields - - prefer_final_in_for_each - - prefer_final_locals - - prefer_for_elements_to_map_fromIterable - - prefer_foreach - - prefer_function_declarations_over_variables - - prefer_generic_function_type_aliases - - prefer_if_elements_to_conditional_expressions - - prefer_if_null_operators - - prefer_initializing_formals - - prefer_inlined_adds - - prefer_interpolation_to_compose_strings - - prefer_is_empty - - prefer_is_not_empty - - prefer_is_not_operator - - prefer_iterable_whereType - - prefer_null_aware_operators - - prefer_relative_imports - - prefer_single_quotes - - prefer_spread_collections - - prefer_typing_uninitialized_variables - - prefer_void_to_null - - provide_deprecation_message - - recursive_getters - - secure_pubspec_urls - - sized_box_for_whitespace - - slash_for_doc_comments - - sort_child_properties_last - - sort_constructors_first - - sort_unnamed_constructors_first - - test_types_in_equals - - throw_in_finally - - tighten_type_of_initializing_formals - - type_init_formals - - unnecessary_await_in_return - - unnecessary_brace_in_string_interps - - unnecessary_const - - unnecessary_constructor_name - - unnecessary_getters_setters - - unnecessary_late - - unnecessary_new - - unnecessary_null_aware_assignments - - unnecessary_null_checks - - unnecessary_null_in_if_null_operators - - unnecessary_nullable_for_final_variable_declarations - - unnecessary_overrides - - unnecessary_parenthesis - - unnecessary_statements - - unnecessary_string_escapes - - unnecessary_string_interpolations - - unnecessary_this - - unrelated_type_equality_checks - - use_build_context_synchronously - - use_full_hex_values_for_flutter_colors - - use_function_type_syntax_for_parameters - - use_if_null_to_convert_nulls_to_bools - - use_is_even_rather_than_modulo - - use_key_in_widget_constructors - - use_late_for_private_fields_and_variables - - use_named_constants - - use_raw_strings - - use_rethrow_when_possible - - use_setters_to_change_properties - - use_super_parameters - - use_test_throws_matchers - - valid_regexps - - void_checks diff --git a/android/.kotlin/errors/errors-1772833112471.log b/android/.kotlin/errors/errors-1772833112471.log deleted file mode 100644 index ce74fda..0000000 --- a/android/.kotlin/errors/errors-1772833112471.log +++ /dev/null @@ -1,75 +0,0 @@ -kotlin version: 2.2.20 -error message: Daemon compilation failed: null -java.lang.Exception - at org.jetbrains.kotlin.daemon.common.CompileService$CallResult$Error.get(CompileService.kt:69) - at org.jetbrains.kotlin.daemon.common.CompileService$CallResult$Error.get(CompileService.kt:65) - at org.jetbrains.kotlin.compilerRunner.GradleKotlinCompilerWork.compileWithDaemon(GradleKotlinCompilerWork.kt:240) - at org.jetbrains.kotlin.compilerRunner.GradleKotlinCompilerWork.compileWithDaemonOrFallbackImpl(GradleKotlinCompilerWork.kt:159) - at org.jetbrains.kotlin.compilerRunner.GradleKotlinCompilerWork.run(GradleKotlinCompilerWork.kt:111) - at org.jetbrains.kotlin.compilerRunner.GradleCompilerRunnerWithWorkers$GradleKotlinCompilerWorkAction.execute(GradleCompilerRunnerWithWorkers.kt:74) - at org.gradle.workers.internal.DefaultWorkerServer.execute(DefaultWorkerServer.java:63) - at org.gradle.workers.internal.NoIsolationWorkerFactory$1$1.create(NoIsolationWorkerFactory.java:66) - at org.gradle.workers.internal.NoIsolationWorkerFactory$1$1.create(NoIsolationWorkerFactory.java:62) - at org.gradle.internal.classloader.ClassLoaderUtils.executeInClassloader(ClassLoaderUtils.java:100) - at org.gradle.workers.internal.NoIsolationWorkerFactory$1.lambda$execute$0(NoIsolationWorkerFactory.java:62) - at org.gradle.workers.internal.AbstractWorker$1.call(AbstractWorker.java:44) - at org.gradle.workers.internal.AbstractWorker$1.call(AbstractWorker.java:41) - at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:210) - at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:205) - at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:67) - at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:60) - at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:167) - at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:60) - at org.gradle.internal.operations.DefaultBuildOperationRunner.call(DefaultBuildOperationRunner.java:54) - at org.gradle.workers.internal.AbstractWorker.executeWrappedInBuildOperation(AbstractWorker.java:41) - at org.gradle.workers.internal.NoIsolationWorkerFactory$1.execute(NoIsolationWorkerFactory.java:59) - at org.gradle.workers.internal.DefaultWorkerExecutor.lambda$submitWork$0(DefaultWorkerExecutor.java:174) - at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264) - at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.runExecution(DefaultConditionalExecutionQueue.java:194) - at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.access$700(DefaultConditionalExecutionQueue.java:127) - at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner$1.run(DefaultConditionalExecutionQueue.java:169) - at org.gradle.internal.Factories$1.create(Factories.java:31) - at org.gradle.internal.work.DefaultWorkerLeaseService.withLocks(DefaultWorkerLeaseService.java:263) - at org.gradle.internal.work.DefaultWorkerLeaseService.runAsWorkerThread(DefaultWorkerLeaseService.java:127) - at org.gradle.internal.work.DefaultWorkerLeaseService.runAsWorkerThread(DefaultWorkerLeaseService.java:132) - at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.runBatch(DefaultConditionalExecutionQueue.java:164) - at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.run(DefaultConditionalExecutionQueue.java:133) - at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:539) - at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264) - at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:64) - at org.gradle.internal.concurrent.AbstractManagedExecutor$1.run(AbstractManagedExecutor.java:48) - at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136) - at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635) - at java.base/java.lang.Thread.run(Thread.java:840) -Caused by: java.io.FileNotFoundException: /home/vjrj/proyectos/git-sea/fires_flutter/build/in_app_review/kotlin/compileDebugKotlin/classpath-snapshot/shrunk-classpath-snapshot.bin (No existe el fichero o el directorio) - at java.base/java.io.FileOutputStream.open0(Native Method) - at java.base/java.io.FileOutputStream.open(FileOutputStream.java:293) - at java.base/java.io.FileOutputStream.(FileOutputStream.java:235) - at java.base/java.io.FileOutputStream.(FileOutputStream.java:184) - at org.jetbrains.kotlin.incremental.storage.ExternalizersKt.saveToFile(externalizers.kt:178) - at org.jetbrains.kotlin.incremental.classpathDiff.ClasspathSnapshotShrinkerKt.shrinkAndSaveClasspathSnapshot(ClasspathSnapshotShrinker.kt:293) - at org.jetbrains.kotlin.incremental.IncrementalJvmCompilerRunner.performWorkAfterCompilation(IncrementalJvmCompilerRunner.kt:76) - at org.jetbrains.kotlin.incremental.IncrementalJvmCompilerRunner.performWorkAfterCompilation(IncrementalJvmCompilerRunner.kt:23) - at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compileImpl(IncrementalCompilerRunner.kt:418) - at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compileNonIncrementally(IncrementalCompilerRunner.kt:301) - at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compile(IncrementalCompilerRunner.kt:128) - at org.jetbrains.kotlin.daemon.CompileServiceImplBase.execIncrementalCompiler(CompileServiceImpl.kt:684) - at org.jetbrains.kotlin.daemon.CompileServiceImplBase.access$execIncrementalCompiler(CompileServiceImpl.kt:94) - at org.jetbrains.kotlin.daemon.CompileServiceImpl.compile(CompileServiceImpl.kt:1810) - at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) - at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) - at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) - at java.base/java.lang.reflect.Method.invoke(Method.java:569) - at java.rmi/sun.rmi.server.UnicastServerRef.dispatch(UnicastServerRef.java:360) - at java.rmi/sun.rmi.transport.Transport$1.run(Transport.java:200) - at java.rmi/sun.rmi.transport.Transport$1.run(Transport.java:197) - at java.base/java.security.AccessController.doPrivileged(AccessController.java:712) - at java.rmi/sun.rmi.transport.Transport.serviceCall(Transport.java:196) - at java.rmi/sun.rmi.transport.tcp.TCPTransport.handleMessages(TCPTransport.java:587) - at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(TCPTransport.java:828) - at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run$0(TCPTransport.java:705) - at java.base/java.security.AccessController.doPrivileged(AccessController.java:399) - at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(TCPTransport.java:704) - ... 3 more - - diff --git a/android/.kotlin/errors/errors-1772833112474.log b/android/.kotlin/errors/errors-1772833112474.log deleted file mode 100644 index 949ea47..0000000 --- a/android/.kotlin/errors/errors-1772833112474.log +++ /dev/null @@ -1,75 +0,0 @@ -kotlin version: 2.2.20 -error message: Daemon compilation failed: null -java.lang.Exception - at org.jetbrains.kotlin.daemon.common.CompileService$CallResult$Error.get(CompileService.kt:69) - at org.jetbrains.kotlin.daemon.common.CompileService$CallResult$Error.get(CompileService.kt:65) - at org.jetbrains.kotlin.compilerRunner.GradleKotlinCompilerWork.compileWithDaemon(GradleKotlinCompilerWork.kt:240) - at org.jetbrains.kotlin.compilerRunner.GradleKotlinCompilerWork.compileWithDaemonOrFallbackImpl(GradleKotlinCompilerWork.kt:159) - at org.jetbrains.kotlin.compilerRunner.GradleKotlinCompilerWork.run(GradleKotlinCompilerWork.kt:111) - at org.jetbrains.kotlin.compilerRunner.GradleCompilerRunnerWithWorkers$GradleKotlinCompilerWorkAction.execute(GradleCompilerRunnerWithWorkers.kt:74) - at org.gradle.workers.internal.DefaultWorkerServer.execute(DefaultWorkerServer.java:63) - at org.gradle.workers.internal.NoIsolationWorkerFactory$1$1.create(NoIsolationWorkerFactory.java:66) - at org.gradle.workers.internal.NoIsolationWorkerFactory$1$1.create(NoIsolationWorkerFactory.java:62) - at org.gradle.internal.classloader.ClassLoaderUtils.executeInClassloader(ClassLoaderUtils.java:100) - at org.gradle.workers.internal.NoIsolationWorkerFactory$1.lambda$execute$0(NoIsolationWorkerFactory.java:62) - at org.gradle.workers.internal.AbstractWorker$1.call(AbstractWorker.java:44) - at org.gradle.workers.internal.AbstractWorker$1.call(AbstractWorker.java:41) - at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:210) - at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:205) - at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:67) - at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:60) - at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:167) - at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:60) - at org.gradle.internal.operations.DefaultBuildOperationRunner.call(DefaultBuildOperationRunner.java:54) - at org.gradle.workers.internal.AbstractWorker.executeWrappedInBuildOperation(AbstractWorker.java:41) - at org.gradle.workers.internal.NoIsolationWorkerFactory$1.execute(NoIsolationWorkerFactory.java:59) - at org.gradle.workers.internal.DefaultWorkerExecutor.lambda$submitWork$0(DefaultWorkerExecutor.java:174) - at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264) - at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.runExecution(DefaultConditionalExecutionQueue.java:194) - at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.access$700(DefaultConditionalExecutionQueue.java:127) - at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner$1.run(DefaultConditionalExecutionQueue.java:169) - at org.gradle.internal.Factories$1.create(Factories.java:31) - at org.gradle.internal.work.DefaultWorkerLeaseService.withLocks(DefaultWorkerLeaseService.java:263) - at org.gradle.internal.work.DefaultWorkerLeaseService.runAsWorkerThread(DefaultWorkerLeaseService.java:127) - at org.gradle.internal.work.DefaultWorkerLeaseService.runAsWorkerThread(DefaultWorkerLeaseService.java:132) - at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.runBatch(DefaultConditionalExecutionQueue.java:164) - at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.run(DefaultConditionalExecutionQueue.java:133) - at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:539) - at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264) - at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:64) - at org.gradle.internal.concurrent.AbstractManagedExecutor$1.run(AbstractManagedExecutor.java:48) - at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136) - at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635) - at java.base/java.lang.Thread.run(Thread.java:840) -Caused by: java.io.FileNotFoundException: /home/vjrj/proyectos/git-sea/fires_flutter/build/location/kotlin/compileDebugKotlin/classpath-snapshot/shrunk-classpath-snapshot.bin (No existe el fichero o el directorio) - at java.base/java.io.FileOutputStream.open0(Native Method) - at java.base/java.io.FileOutputStream.open(FileOutputStream.java:293) - at java.base/java.io.FileOutputStream.(FileOutputStream.java:235) - at java.base/java.io.FileOutputStream.(FileOutputStream.java:184) - at org.jetbrains.kotlin.incremental.storage.ExternalizersKt.saveToFile(externalizers.kt:178) - at org.jetbrains.kotlin.incremental.classpathDiff.ClasspathSnapshotShrinkerKt.shrinkAndSaveClasspathSnapshot(ClasspathSnapshotShrinker.kt:293) - at org.jetbrains.kotlin.incremental.IncrementalJvmCompilerRunner.performWorkAfterCompilation(IncrementalJvmCompilerRunner.kt:76) - at org.jetbrains.kotlin.incremental.IncrementalJvmCompilerRunner.performWorkAfterCompilation(IncrementalJvmCompilerRunner.kt:23) - at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compileImpl(IncrementalCompilerRunner.kt:418) - at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compileNonIncrementally(IncrementalCompilerRunner.kt:301) - at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compile(IncrementalCompilerRunner.kt:128) - at org.jetbrains.kotlin.daemon.CompileServiceImplBase.execIncrementalCompiler(CompileServiceImpl.kt:684) - at org.jetbrains.kotlin.daemon.CompileServiceImplBase.access$execIncrementalCompiler(CompileServiceImpl.kt:94) - at org.jetbrains.kotlin.daemon.CompileServiceImpl.compile(CompileServiceImpl.kt:1810) - at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) - at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) - at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) - at java.base/java.lang.reflect.Method.invoke(Method.java:569) - at java.rmi/sun.rmi.server.UnicastServerRef.dispatch(UnicastServerRef.java:360) - at java.rmi/sun.rmi.transport.Transport$1.run(Transport.java:200) - at java.rmi/sun.rmi.transport.Transport$1.run(Transport.java:197) - at java.base/java.security.AccessController.doPrivileged(AccessController.java:712) - at java.rmi/sun.rmi.transport.Transport.serviceCall(Transport.java:196) - at java.rmi/sun.rmi.transport.tcp.TCPTransport.handleMessages(TCPTransport.java:587) - at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(TCPTransport.java:828) - at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run$0(TCPTransport.java:705) - at java.base/java.security.AccessController.doPrivileged(AccessController.java:399) - at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(TCPTransport.java:704) - ... 3 more - - diff --git a/android/.kotlin/errors/errors-1772833112479.log b/android/.kotlin/errors/errors-1772833112479.log deleted file mode 100644 index 0f4bfb2..0000000 --- a/android/.kotlin/errors/errors-1772833112479.log +++ /dev/null @@ -1,75 +0,0 @@ -kotlin version: 2.2.20 -error message: Daemon compilation failed: null -java.lang.Exception - at org.jetbrains.kotlin.daemon.common.CompileService$CallResult$Error.get(CompileService.kt:69) - at org.jetbrains.kotlin.daemon.common.CompileService$CallResult$Error.get(CompileService.kt:65) - at org.jetbrains.kotlin.compilerRunner.GradleKotlinCompilerWork.compileWithDaemon(GradleKotlinCompilerWork.kt:240) - at org.jetbrains.kotlin.compilerRunner.GradleKotlinCompilerWork.compileWithDaemonOrFallbackImpl(GradleKotlinCompilerWork.kt:159) - at org.jetbrains.kotlin.compilerRunner.GradleKotlinCompilerWork.run(GradleKotlinCompilerWork.kt:111) - at org.jetbrains.kotlin.compilerRunner.GradleCompilerRunnerWithWorkers$GradleKotlinCompilerWorkAction.execute(GradleCompilerRunnerWithWorkers.kt:74) - at org.gradle.workers.internal.DefaultWorkerServer.execute(DefaultWorkerServer.java:63) - at org.gradle.workers.internal.NoIsolationWorkerFactory$1$1.create(NoIsolationWorkerFactory.java:66) - at org.gradle.workers.internal.NoIsolationWorkerFactory$1$1.create(NoIsolationWorkerFactory.java:62) - at org.gradle.internal.classloader.ClassLoaderUtils.executeInClassloader(ClassLoaderUtils.java:100) - at org.gradle.workers.internal.NoIsolationWorkerFactory$1.lambda$execute$0(NoIsolationWorkerFactory.java:62) - at org.gradle.workers.internal.AbstractWorker$1.call(AbstractWorker.java:44) - at org.gradle.workers.internal.AbstractWorker$1.call(AbstractWorker.java:41) - at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:210) - at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:205) - at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:67) - at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:60) - at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:167) - at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:60) - at org.gradle.internal.operations.DefaultBuildOperationRunner.call(DefaultBuildOperationRunner.java:54) - at org.gradle.workers.internal.AbstractWorker.executeWrappedInBuildOperation(AbstractWorker.java:41) - at org.gradle.workers.internal.NoIsolationWorkerFactory$1.execute(NoIsolationWorkerFactory.java:59) - at org.gradle.workers.internal.DefaultWorkerExecutor.lambda$submitWork$0(DefaultWorkerExecutor.java:174) - at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264) - at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.runExecution(DefaultConditionalExecutionQueue.java:194) - at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.access$700(DefaultConditionalExecutionQueue.java:127) - at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner$1.run(DefaultConditionalExecutionQueue.java:169) - at org.gradle.internal.Factories$1.create(Factories.java:31) - at org.gradle.internal.work.DefaultWorkerLeaseService.withLocks(DefaultWorkerLeaseService.java:263) - at org.gradle.internal.work.DefaultWorkerLeaseService.runAsWorkerThread(DefaultWorkerLeaseService.java:127) - at org.gradle.internal.work.DefaultWorkerLeaseService.runAsWorkerThread(DefaultWorkerLeaseService.java:132) - at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.runBatch(DefaultConditionalExecutionQueue.java:164) - at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.run(DefaultConditionalExecutionQueue.java:133) - at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:539) - at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264) - at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:64) - at org.gradle.internal.concurrent.AbstractManagedExecutor$1.run(AbstractManagedExecutor.java:48) - at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136) - at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635) - at java.base/java.lang.Thread.run(Thread.java:840) -Caused by: java.io.FileNotFoundException: /home/vjrj/proyectos/git-sea/fires_flutter/build/package_info_plus/kotlin/compileDebugKotlin/classpath-snapshot/shrunk-classpath-snapshot.bin (No existe el fichero o el directorio) - at java.base/java.io.FileOutputStream.open0(Native Method) - at java.base/java.io.FileOutputStream.open(FileOutputStream.java:293) - at java.base/java.io.FileOutputStream.(FileOutputStream.java:235) - at java.base/java.io.FileOutputStream.(FileOutputStream.java:184) - at org.jetbrains.kotlin.incremental.storage.ExternalizersKt.saveToFile(externalizers.kt:178) - at org.jetbrains.kotlin.incremental.classpathDiff.ClasspathSnapshotShrinkerKt.shrinkAndSaveClasspathSnapshot(ClasspathSnapshotShrinker.kt:293) - at org.jetbrains.kotlin.incremental.IncrementalJvmCompilerRunner.performWorkAfterCompilation(IncrementalJvmCompilerRunner.kt:76) - at org.jetbrains.kotlin.incremental.IncrementalJvmCompilerRunner.performWorkAfterCompilation(IncrementalJvmCompilerRunner.kt:23) - at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compileImpl(IncrementalCompilerRunner.kt:418) - at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compileNonIncrementally(IncrementalCompilerRunner.kt:301) - at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compile(IncrementalCompilerRunner.kt:128) - at org.jetbrains.kotlin.daemon.CompileServiceImplBase.execIncrementalCompiler(CompileServiceImpl.kt:684) - at org.jetbrains.kotlin.daemon.CompileServiceImplBase.access$execIncrementalCompiler(CompileServiceImpl.kt:94) - at org.jetbrains.kotlin.daemon.CompileServiceImpl.compile(CompileServiceImpl.kt:1810) - at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) - at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) - at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) - at java.base/java.lang.reflect.Method.invoke(Method.java:569) - at java.rmi/sun.rmi.server.UnicastServerRef.dispatch(UnicastServerRef.java:360) - at java.rmi/sun.rmi.transport.Transport$1.run(Transport.java:200) - at java.rmi/sun.rmi.transport.Transport$1.run(Transport.java:197) - at java.base/java.security.AccessController.doPrivileged(AccessController.java:712) - at java.rmi/sun.rmi.transport.Transport.serviceCall(Transport.java:196) - at java.rmi/sun.rmi.transport.tcp.TCPTransport.handleMessages(TCPTransport.java:587) - at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(TCPTransport.java:828) - at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run$0(TCPTransport.java:705) - at java.base/java.security.AccessController.doPrivileged(AccessController.java:399) - at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(TCPTransport.java:704) - ... 3 more - - diff --git a/android/.kotlin/errors/errors-1772833112488.log b/android/.kotlin/errors/errors-1772833112488.log deleted file mode 100644 index f37b544..0000000 --- a/android/.kotlin/errors/errors-1772833112488.log +++ /dev/null @@ -1,75 +0,0 @@ -kotlin version: 2.2.20 -error message: Daemon compilation failed: null -java.lang.Exception - at org.jetbrains.kotlin.daemon.common.CompileService$CallResult$Error.get(CompileService.kt:69) - at org.jetbrains.kotlin.daemon.common.CompileService$CallResult$Error.get(CompileService.kt:65) - at org.jetbrains.kotlin.compilerRunner.GradleKotlinCompilerWork.compileWithDaemon(GradleKotlinCompilerWork.kt:240) - at org.jetbrains.kotlin.compilerRunner.GradleKotlinCompilerWork.compileWithDaemonOrFallbackImpl(GradleKotlinCompilerWork.kt:159) - at org.jetbrains.kotlin.compilerRunner.GradleKotlinCompilerWork.run(GradleKotlinCompilerWork.kt:111) - at org.jetbrains.kotlin.compilerRunner.GradleCompilerRunnerWithWorkers$GradleKotlinCompilerWorkAction.execute(GradleCompilerRunnerWithWorkers.kt:74) - at org.gradle.workers.internal.DefaultWorkerServer.execute(DefaultWorkerServer.java:63) - at org.gradle.workers.internal.NoIsolationWorkerFactory$1$1.create(NoIsolationWorkerFactory.java:66) - at org.gradle.workers.internal.NoIsolationWorkerFactory$1$1.create(NoIsolationWorkerFactory.java:62) - at org.gradle.internal.classloader.ClassLoaderUtils.executeInClassloader(ClassLoaderUtils.java:100) - at org.gradle.workers.internal.NoIsolationWorkerFactory$1.lambda$execute$0(NoIsolationWorkerFactory.java:62) - at org.gradle.workers.internal.AbstractWorker$1.call(AbstractWorker.java:44) - at org.gradle.workers.internal.AbstractWorker$1.call(AbstractWorker.java:41) - at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:210) - at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:205) - at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:67) - at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:60) - at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:167) - at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:60) - at org.gradle.internal.operations.DefaultBuildOperationRunner.call(DefaultBuildOperationRunner.java:54) - at org.gradle.workers.internal.AbstractWorker.executeWrappedInBuildOperation(AbstractWorker.java:41) - at org.gradle.workers.internal.NoIsolationWorkerFactory$1.execute(NoIsolationWorkerFactory.java:59) - at org.gradle.workers.internal.DefaultWorkerExecutor.lambda$submitWork$0(DefaultWorkerExecutor.java:174) - at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264) - at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.runExecution(DefaultConditionalExecutionQueue.java:194) - at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.access$700(DefaultConditionalExecutionQueue.java:127) - at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner$1.run(DefaultConditionalExecutionQueue.java:169) - at org.gradle.internal.Factories$1.create(Factories.java:31) - at org.gradle.internal.work.DefaultWorkerLeaseService.withLocks(DefaultWorkerLeaseService.java:263) - at org.gradle.internal.work.DefaultWorkerLeaseService.runAsWorkerThread(DefaultWorkerLeaseService.java:127) - at org.gradle.internal.work.DefaultWorkerLeaseService.runAsWorkerThread(DefaultWorkerLeaseService.java:132) - at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.runBatch(DefaultConditionalExecutionQueue.java:164) - at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.run(DefaultConditionalExecutionQueue.java:133) - at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:539) - at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264) - at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:64) - at org.gradle.internal.concurrent.AbstractManagedExecutor$1.run(AbstractManagedExecutor.java:48) - at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136) - at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635) - at java.base/java.lang.Thread.run(Thread.java:840) -Caused by: java.io.FileNotFoundException: /home/vjrj/proyectos/git-sea/fires_flutter/build/share_plus/kotlin/compileDebugKotlin/classpath-snapshot/shrunk-classpath-snapshot.bin (No existe el fichero o el directorio) - at java.base/java.io.FileOutputStream.open0(Native Method) - at java.base/java.io.FileOutputStream.open(FileOutputStream.java:293) - at java.base/java.io.FileOutputStream.(FileOutputStream.java:235) - at java.base/java.io.FileOutputStream.(FileOutputStream.java:184) - at org.jetbrains.kotlin.incremental.storage.ExternalizersKt.saveToFile(externalizers.kt:178) - at org.jetbrains.kotlin.incremental.classpathDiff.ClasspathSnapshotShrinkerKt.shrinkAndSaveClasspathSnapshot(ClasspathSnapshotShrinker.kt:293) - at org.jetbrains.kotlin.incremental.IncrementalJvmCompilerRunner.performWorkAfterCompilation(IncrementalJvmCompilerRunner.kt:76) - at org.jetbrains.kotlin.incremental.IncrementalJvmCompilerRunner.performWorkAfterCompilation(IncrementalJvmCompilerRunner.kt:23) - at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compileImpl(IncrementalCompilerRunner.kt:418) - at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compileNonIncrementally(IncrementalCompilerRunner.kt:301) - at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compile(IncrementalCompilerRunner.kt:128) - at org.jetbrains.kotlin.daemon.CompileServiceImplBase.execIncrementalCompiler(CompileServiceImpl.kt:684) - at org.jetbrains.kotlin.daemon.CompileServiceImplBase.access$execIncrementalCompiler(CompileServiceImpl.kt:94) - at org.jetbrains.kotlin.daemon.CompileServiceImpl.compile(CompileServiceImpl.kt:1810) - at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) - at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) - at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) - at java.base/java.lang.reflect.Method.invoke(Method.java:569) - at java.rmi/sun.rmi.server.UnicastServerRef.dispatch(UnicastServerRef.java:360) - at java.rmi/sun.rmi.transport.Transport$1.run(Transport.java:200) - at java.rmi/sun.rmi.transport.Transport$1.run(Transport.java:197) - at java.base/java.security.AccessController.doPrivileged(AccessController.java:712) - at java.rmi/sun.rmi.transport.Transport.serviceCall(Transport.java:196) - at java.rmi/sun.rmi.transport.tcp.TCPTransport.handleMessages(TCPTransport.java:587) - at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(TCPTransport.java:828) - at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run$0(TCPTransport.java:705) - at java.base/java.security.AccessController.doPrivileged(AccessController.java:399) - at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(TCPTransport.java:704) - ... 3 more - - diff --git a/android/.kotlin/errors/errors-1772833112491.log b/android/.kotlin/errors/errors-1772833112491.log deleted file mode 100644 index caa4e5f..0000000 --- a/android/.kotlin/errors/errors-1772833112491.log +++ /dev/null @@ -1,75 +0,0 @@ -kotlin version: 2.2.20 -error message: Daemon compilation failed: null -java.lang.Exception - at org.jetbrains.kotlin.daemon.common.CompileService$CallResult$Error.get(CompileService.kt:69) - at org.jetbrains.kotlin.daemon.common.CompileService$CallResult$Error.get(CompileService.kt:65) - at org.jetbrains.kotlin.compilerRunner.GradleKotlinCompilerWork.compileWithDaemon(GradleKotlinCompilerWork.kt:240) - at org.jetbrains.kotlin.compilerRunner.GradleKotlinCompilerWork.compileWithDaemonOrFallbackImpl(GradleKotlinCompilerWork.kt:159) - at org.jetbrains.kotlin.compilerRunner.GradleKotlinCompilerWork.run(GradleKotlinCompilerWork.kt:111) - at org.jetbrains.kotlin.compilerRunner.GradleCompilerRunnerWithWorkers$GradleKotlinCompilerWorkAction.execute(GradleCompilerRunnerWithWorkers.kt:74) - at org.gradle.workers.internal.DefaultWorkerServer.execute(DefaultWorkerServer.java:63) - at org.gradle.workers.internal.NoIsolationWorkerFactory$1$1.create(NoIsolationWorkerFactory.java:66) - at org.gradle.workers.internal.NoIsolationWorkerFactory$1$1.create(NoIsolationWorkerFactory.java:62) - at org.gradle.internal.classloader.ClassLoaderUtils.executeInClassloader(ClassLoaderUtils.java:100) - at org.gradle.workers.internal.NoIsolationWorkerFactory$1.lambda$execute$0(NoIsolationWorkerFactory.java:62) - at org.gradle.workers.internal.AbstractWorker$1.call(AbstractWorker.java:44) - at org.gradle.workers.internal.AbstractWorker$1.call(AbstractWorker.java:41) - at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:210) - at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:205) - at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:67) - at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:60) - at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:167) - at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:60) - at org.gradle.internal.operations.DefaultBuildOperationRunner.call(DefaultBuildOperationRunner.java:54) - at org.gradle.workers.internal.AbstractWorker.executeWrappedInBuildOperation(AbstractWorker.java:41) - at org.gradle.workers.internal.NoIsolationWorkerFactory$1.execute(NoIsolationWorkerFactory.java:59) - at org.gradle.workers.internal.DefaultWorkerExecutor.lambda$submitWork$0(DefaultWorkerExecutor.java:174) - at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264) - at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.runExecution(DefaultConditionalExecutionQueue.java:194) - at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.access$700(DefaultConditionalExecutionQueue.java:127) - at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner$1.run(DefaultConditionalExecutionQueue.java:169) - at org.gradle.internal.Factories$1.create(Factories.java:31) - at org.gradle.internal.work.DefaultWorkerLeaseService.withLocks(DefaultWorkerLeaseService.java:263) - at org.gradle.internal.work.DefaultWorkerLeaseService.runAsWorkerThread(DefaultWorkerLeaseService.java:127) - at org.gradle.internal.work.DefaultWorkerLeaseService.runAsWorkerThread(DefaultWorkerLeaseService.java:132) - at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.runBatch(DefaultConditionalExecutionQueue.java:164) - at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.run(DefaultConditionalExecutionQueue.java:133) - at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:539) - at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264) - at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:64) - at org.gradle.internal.concurrent.AbstractManagedExecutor$1.run(AbstractManagedExecutor.java:48) - at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136) - at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635) - at java.base/java.lang.Thread.run(Thread.java:840) -Caused by: java.io.FileNotFoundException: /home/vjrj/proyectos/git-sea/fires_flutter/build/shared_preferences_android/kotlin/compileDebugKotlin/classpath-snapshot/shrunk-classpath-snapshot.bin (No existe el fichero o el directorio) - at java.base/java.io.FileOutputStream.open0(Native Method) - at java.base/java.io.FileOutputStream.open(FileOutputStream.java:293) - at java.base/java.io.FileOutputStream.(FileOutputStream.java:235) - at java.base/java.io.FileOutputStream.(FileOutputStream.java:184) - at org.jetbrains.kotlin.incremental.storage.ExternalizersKt.saveToFile(externalizers.kt:178) - at org.jetbrains.kotlin.incremental.classpathDiff.ClasspathSnapshotShrinkerKt.shrinkAndSaveClasspathSnapshot(ClasspathSnapshotShrinker.kt:293) - at org.jetbrains.kotlin.incremental.IncrementalJvmCompilerRunner.performWorkAfterCompilation(IncrementalJvmCompilerRunner.kt:76) - at org.jetbrains.kotlin.incremental.IncrementalJvmCompilerRunner.performWorkAfterCompilation(IncrementalJvmCompilerRunner.kt:23) - at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compileImpl(IncrementalCompilerRunner.kt:418) - at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compileNonIncrementally(IncrementalCompilerRunner.kt:301) - at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compile(IncrementalCompilerRunner.kt:128) - at org.jetbrains.kotlin.daemon.CompileServiceImplBase.execIncrementalCompiler(CompileServiceImpl.kt:684) - at org.jetbrains.kotlin.daemon.CompileServiceImplBase.access$execIncrementalCompiler(CompileServiceImpl.kt:94) - at org.jetbrains.kotlin.daemon.CompileServiceImpl.compile(CompileServiceImpl.kt:1810) - at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) - at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) - at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) - at java.base/java.lang.reflect.Method.invoke(Method.java:569) - at java.rmi/sun.rmi.server.UnicastServerRef.dispatch(UnicastServerRef.java:360) - at java.rmi/sun.rmi.transport.Transport$1.run(Transport.java:200) - at java.rmi/sun.rmi.transport.Transport$1.run(Transport.java:197) - at java.base/java.security.AccessController.doPrivileged(AccessController.java:712) - at java.rmi/sun.rmi.transport.Transport.serviceCall(Transport.java:196) - at java.rmi/sun.rmi.transport.tcp.TCPTransport.handleMessages(TCPTransport.java:587) - at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(TCPTransport.java:828) - at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run$0(TCPTransport.java:705) - at java.base/java.security.AccessController.doPrivileged(AccessController.java:399) - at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(TCPTransport.java:704) - ... 3 more - - diff --git a/android/app/build.gradle b/android/app/build.gradle index 41ec22b..26178e4 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -1,9 +1,3 @@ -plugins { - id 'com.android.application' - id 'kotlin-android' - id 'dev.flutter.flutter-gradle-plugin' -} - def localProperties = new Properties() def localPropertiesFile = rootProject.file('local.properties') if (localPropertiesFile.exists()) { @@ -12,18 +6,20 @@ if (localPropertiesFile.exists()) { } } -// Release signing is opt-in: only load key.properties if it exists. Without it -// (CI analyze/test jobs, contributors) builds fall back to debug signing. -def keystorePropertiesFile = rootProject.file("key.properties") -def keystoreProperties = new Properties() -def hasReleaseKeystore = keystorePropertiesFile.exists() -if (hasReleaseKeystore) { - keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) +def flutterRoot = localProperties.getProperty('flutter.sdk') +if (flutterRoot == null) { + throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") } +apply plugin: 'com.android.application' +apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" + +def keystorePropertiesFile = rootProject.file("key.properties") +def keystoreProperties = new Properties() +keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) + android { - compileSdkVersion 36 - namespace "org.comunes.fires" + compileSdkVersion 27 lintOptions { disable 'InvalidPackage' @@ -32,33 +28,28 @@ android { defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "org.comunes.fires" - minSdkVersion flutter.minSdkVersion - targetSdkVersion 36 - // versionCode/versionName come from pubspec.yaml (version: X.Y.Z+CODE) - // so a `v*` git tag maps the release version, matching the tane flow. - versionCode flutter.versionCode - versionName flutter.versionName + minSdkVersion 16 + targetSdkVersion 27 + versionCode 9 + versionName "1.9" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } signingConfigs { - if (hasReleaseKeystore) { - release { - keyAlias keystoreProperties['keyAlias'] - keyPassword keystoreProperties['keyPassword'] - storeFile file(keystoreProperties['storeFile']) - storePassword keystoreProperties['storePassword'] - } + release { + keyAlias keystoreProperties['keyAlias'] + keyPassword keystoreProperties['keyPassword'] + storeFile file(keystoreProperties['storeFile']) + storePassword keystoreProperties['storePassword'] } } buildTypes { release { - // Sign with the release key when available, else debug (contributors/CI). - signingConfig hasReleaseKeystore ? signingConfigs.release : signingConfigs.debug + signingConfig signingConfigs.release minifyEnabled true - shrinkResources true + useProguard true proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } @@ -84,12 +75,11 @@ flutter { } dependencies { - testImplementation 'junit:junit:4.13.2' - // androidx test dependencies replace old android.support.test - androidTestImplementation 'androidx.test:runner:1.5.2' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' - // implementation 'com.google.firebase:firebase-core:15.0.2' + testImplementation 'junit:junit:4.12' + androidTestImplementation 'com.android.support.test:runner:1.0.1' + androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1' + implementation 'com.google.firebase:firebase-core:15.0.2' // google recommends 16 but fails because location flutter package deps in 15 } -apply plugin: 'com.google.gms.google-services' +apply plugin: 'com.google.gms.google-services' \ No newline at end of file diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro index e92128d..a3750d3 100644 --- a/android/app/proguard-rules.pro +++ b/android/app/proguard-rules.pro @@ -14,6 +14,3 @@ -keepnames class org.ietf.jgss.** { *; } -dontwarn org.apache.** -dontwarn org.w3c.dom.** - -# Google Play Core libraries - optional features --dontwarn com.google.android.play.core.** diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml deleted file mode 100644 index cf2aaa0..0000000 --- a/android/app/src/main/AndroidManifest.xml +++ /dev/null @@ -1,47 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - diff --git a/android/app/src/main/java/org/comunes/fires/MainActivity.java b/android/app/src/main/java/org/comunes/fires/MainActivity.java index 1d51087..0caf39c 100644 --- a/android/app/src/main/java/org/comunes/fires/MainActivity.java +++ b/android/app/src/main/java/org/comunes/fires/MainActivity.java @@ -1,6 +1,14 @@ package org.comunes.fires; -import io.flutter.embedding.android.FlutterActivity; +import android.os.Bundle; + +import io.flutter.app.FlutterActivity; +import io.flutter.plugins.GeneratedPluginRegistrant; public class MainActivity extends FlutterActivity { + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + GeneratedPluginRegistrant.registerWith(this); + } } diff --git a/android/app/src/main/res/drawable/normal_background.xml b/android/app/src/main/res/drawable/normal_background.xml deleted file mode 100644 index 606a1d8..0000000 --- a/android/app/src/main/res/drawable/normal_background.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml index af831f5..00fa441 100644 --- a/android/app/src/main/res/values/styles.xml +++ b/android/app/src/main/res/values/styles.xml @@ -5,8 +5,4 @@ Flutter draws its first frame --> @drawable/launch_background - - diff --git a/android/build.gradle b/android/build.gradle index 3ba0664..abea04c 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,28 +1,19 @@ buildscript { repositories { google() - mavenCentral() + jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:8.2.2' - classpath 'com.google.gms:google-services:4.3.4' + classpath 'com.android.tools.build:gradle:3.0.1' + classpath 'com.google.gms:google-services:4.0.1' } } allprojects { repositories { google() - mavenCentral() - } - - plugins.withId('kotlin-android') { - tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach { - kotlinOptions { - languageVersion = '1.8' - allWarningsAsErrors = false - } - } + jcenter() } } @@ -34,6 +25,6 @@ subprojects { project.evaluationDependsOn(':app') } -tasks.register("clean", Delete) { - delete rootProject.layout.buildDirectory +task clean(type: Delete) { + delete rootProject.buildDir } diff --git a/android/gradle.properties b/android/gradle.properties index 79b3387..8bd86f6 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -1,4 +1 @@ -org.gradle.jvmargs=-Xmx4096M -android.useAndroidX=true -android.enableJetifier=false -org.jetbrains.kotlin.jvm.target=1.8 +org.gradle.jvmargs=-Xmx1536M diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index 4cc8c0b..aa901e1 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-all.zip diff --git a/android/settings.gradle b/android/settings.gradle new file mode 100644 index 0000000..5a2f14f --- /dev/null +++ b/android/settings.gradle @@ -0,0 +1,15 @@ +include ':app' + +def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() + +def plugins = new Properties() +def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') +if (pluginsFile.exists()) { + pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) } +} + +plugins.each { name, path -> + def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() + include ":$name" + project(":$name").projectDir = pluginDirectory +} diff --git a/android/settings.gradle.kts b/android/settings.gradle.kts deleted file mode 100644 index df93bfd..0000000 --- a/android/settings.gradle.kts +++ /dev/null @@ -1,40 +0,0 @@ -pluginManagement { - val flutterSdkPath = - run { - val properties = java.util.Properties() - file("local.properties").inputStream().use { properties.load(it) } - val flutterSdkPath = properties.getProperty("flutter.sdk") - require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } - flutterSdkPath - } - - includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") - - repositories { - google() - mavenCentral() - gradlePluginPortal() - } -} - -plugins { - id("dev.flutter.flutter-plugin-loader") version "1.0.0" - id("com.android.application") version "8.11.1" apply false - id("org.jetbrains.kotlin.android") version "2.2.20" apply false -} - -include(":app") - -val flutterRoot = rootProject.projectDir.parentFile.toPath() - -val pluginsFile = flutterRoot.resolve(".flutter-plugins").toFile() -val plugins = java.util.Properties() -if (pluginsFile.exists()) { - pluginsFile.inputStream().use { plugins.load(it) } -} - -plugins.forEach { name, path -> - val pluginDirectory = flutterRoot.resolve(path as String).resolve("android").toFile() - include(":$name") - project(":$name").projectDir = pluginDirectory -} diff --git a/android/settings_aar.gradle b/android/settings_aar.gradle deleted file mode 100644 index e7b4def..0000000 --- a/android/settings_aar.gradle +++ /dev/null @@ -1 +0,0 @@ -include ':app' diff --git a/build.yaml b/build.yaml index c1992bf..3c7137f 100644 --- a/build.yaml +++ b/build.yaml @@ -9,9 +9,11 @@ targets: # defined in `package:source_gen/source_gen.dart`. # # Note: use `|` to define a multi-line block. - # header: | - # // Copyright (c) 2018, Comunes Association. - # // GENERATED CODE - DO NOT MODIFY BY HAND + header: | + // Copyright (c) 2018, Comunes Association. + + // GENERATED CODE - DO NOT MODIFY BY HAND + # Options configure how source code is generated for every # `@JsonSerializable`-annotated class in the package. # @@ -19,9 +21,8 @@ targets: # # For usage information, reference the corresponding field in # `JsonSerializableGenerator`. - #use_wrappers: true + use_wrappers: true any_map: true checked: true explicit_to_json: true - #generate_to_json_function: true - create_to_json: true + generate_to_json_function: true diff --git a/fastlane/Appfile b/fastlane/Appfile deleted file mode 100644 index 2964667..0000000 --- a/fastlane/Appfile +++ /dev/null @@ -1,9 +0,0 @@ -# Google Play identity + credentials for `fastlane supply`. -# -# The service-account JSON is injected from CI as the SUPPLY_JSON_KEY_DATA -# secret (raw file contents) and is NEVER committed. Locally you can instead -# export SUPPLY_JSON_KEY_DATA, or drop a gitignored play-service-account.json -# and point json_key_file at it. -package_name("org.comunes.fires") - -json_key_data_raw(ENV["SUPPLY_JSON_KEY_DATA"]) if ENV["SUPPLY_JSON_KEY_DATA"] diff --git a/fastlane/Fastfile b/fastlane/Fastfile deleted file mode 100644 index 73a3709..0000000 --- a/fastlane/Fastfile +++ /dev/null @@ -1,54 +0,0 @@ -# Fastlane lanes for Tod@s contra el Fuego (org.comunes.fires). -# -# Publishing is automated and password-free: a git tag triggers CI, which builds -# a signed AAB and runs `deploy_play`. Credentials come from CI secrets, never -# typed by hand: -# SUPPLY_JSON_KEY_DATA Google Play service-account JSON (raw contents) -# -# The store listing (titles, descriptions, changelogs, screenshots) is read -# straight from fastlane/metadata/android//. - -default_platform(:android) - -# Release bundle produced by `flutter build appbundle --release --flavor -# production`, relative to the project root (where fastlane runs). -AAB_PATH = "build/app/outputs/bundle/productionRelease/app-production-release.aab".freeze - -platform :android do - desc "Upload the signed AAB + store listing to Google Play (internal track)" - lane :deploy_play do - supply( - track: "internal", - aab: AAB_PATH, - release_status: "completed", # internal testing has no review delay - skip_upload_apk: true, # we ship the AAB, not a raw APK - skip_upload_changelogs: false, - ) - end - - desc "Push only the store listing (text + images), no binary" - lane :deploy_metadata do - supply( - skip_upload_aab: true, - skip_upload_apk: true, - ) - end - - # Promote the release currently on the internal track to production, reusing - # the SAME already-reviewed AAB (no rebuild, no new versionCode). Requires the - # service account to have "Release to production" permission in Play Console. - # Usage: bundle exec fastlane promote_production - desc "Promote the internal-track release to production (no rebuild)" - lane :promote_production do - supply( - track: "internal", - track_promote_to: "production", - skip_upload_aab: true, - skip_upload_apk: true, - skip_upload_metadata: true, - skip_upload_changelogs: true, - skip_upload_images: true, - skip_upload_screenshots: true, - ) - end -end diff --git a/fastlane/metadata/android/en-US/changelogs/10.txt b/fastlane/metadata/android/en-US/changelogs/10.txt deleted file mode 100644 index 8a82ec3..0000000 --- a/fastlane/metadata/android/en-US/changelogs/10.txt +++ /dev/null @@ -1,2 +0,0 @@ -All Against The Fire! is back. Upgraded to a recent Flutter, stability -improvements and code cleanup. Fire alerts in your area based on NASA FIRMS data. diff --git a/fastlane/metadata/android/en-US/full_description.txt b/fastlane/metadata/android/en-US/full_description.txt deleted file mode 100644 index 8c90c19..0000000 --- a/fastlane/metadata/android/en-US/full_description.txt +++ /dev/null @@ -1,17 +0,0 @@ -All Against The Fire! notifies you about fires detected in an area of your -interest. It helps the early detection of fires and facilitates local -mobilization while the professional extinction services arrive. - -HOW IT WORKS -• Pick the area you want to watch. -• Get notified when a heat spot is detected in that area. -• See active fires on the map, with their location and time. -• Share an alert to mobilise people around you. - -DATA -Heat spots come from data and imagery from LANCE FIRMS operated by the -NASA/GSFC/Earth Science Data and Information System (ESDIS) with funding -provided by NASA/HQ. - -Free software (GNU AGPL-3.0). No ads, non-profit: a community tool to prevent -and respond to wildfires. diff --git a/fastlane/metadata/android/en-US/short_description.txt b/fastlane/metadata/android/en-US/short_description.txt deleted file mode 100644 index 91043f3..0000000 --- a/fastlane/metadata/android/en-US/short_description.txt +++ /dev/null @@ -1 +0,0 @@ -Fire alerts in your area for early detection and local response. diff --git a/fastlane/metadata/android/en-US/title.txt b/fastlane/metadata/android/en-US/title.txt deleted file mode 100644 index dbba422..0000000 --- a/fastlane/metadata/android/en-US/title.txt +++ /dev/null @@ -1 +0,0 @@ -All Against The Fire! diff --git a/fastlane/metadata/android/es-ES/changelogs/10.txt b/fastlane/metadata/android/es-ES/changelogs/10.txt deleted file mode 100644 index a14247c..0000000 --- a/fastlane/metadata/android/es-ES/changelogs/10.txt +++ /dev/null @@ -1,3 +0,0 @@ -Vuelve ¡Tod@s contra el Fuego! Actualización a Flutter reciente, mejoras de -estabilidad y limpieza de código. Avisos de incendios en tu zona basados en -datos de NASA FIRMS. diff --git a/fastlane/metadata/android/es-ES/full_description.txt b/fastlane/metadata/android/es-ES/full_description.txt deleted file mode 100644 index 7dee3a3..0000000 --- a/fastlane/metadata/android/es-ES/full_description.txt +++ /dev/null @@ -1,17 +0,0 @@ -¡Tod@s contra el Fuego! te avisa de los incendios detectados en una zona de tu -interés. Ayuda a la detección temprana de incendios y facilita la movilización -local mientras llegan los servicios profesionales de extinción. - -CÓMO FUNCIONA -• Elige la zona que quieres vigilar. -• Recibe notificaciones cuando se detecta un foco de calor en esa zona. -• Consulta los incendios activos sobre el mapa, con su ubicación y hora. -• Comparte un aviso para movilizar a la gente de tu entorno. - -DATOS -Los focos de calor proceden de datos e imágenes de LANCE FIRMS, operado por el -Sistema de Datos e Información de Ciencias de la Tierra (ESDIS) de NASA/GSFC, -con financiación de NASA/HQ. - -Software libre (GNU AGPL-3.0). Sin anuncios y sin ánimo de lucro: una -herramienta comunitaria para prevenir y responder a los incendios. diff --git a/fastlane/metadata/android/es-ES/short_description.txt b/fastlane/metadata/android/es-ES/short_description.txt deleted file mode 100644 index 3f76f03..0000000 --- a/fastlane/metadata/android/es-ES/short_description.txt +++ /dev/null @@ -1 +0,0 @@ -Avisos de incendios en tu zona para la detección temprana y la respuesta local. diff --git a/fastlane/metadata/android/es-ES/title.txt b/fastlane/metadata/android/es-ES/title.txt deleted file mode 100644 index 3bd4202..0000000 --- a/fastlane/metadata/android/es-ES/title.txt +++ /dev/null @@ -1 +0,0 @@ -¡Tod@s contra el Fuego! diff --git a/fastlane/metadata/android/gl-ES/changelogs/10.txt b/fastlane/metadata/android/gl-ES/changelogs/10.txt deleted file mode 100644 index 1ec0786..0000000 --- a/fastlane/metadata/android/gl-ES/changelogs/10.txt +++ /dev/null @@ -1,3 +0,0 @@ -Volve Tod@s contra o Lume! Actualización a Flutter recente, melloras de -estabilidade e limpeza de código. Avisos de lumes na túa zona baseados en datos -de NASA FIRMS. diff --git a/fastlane/metadata/android/gl-ES/full_description.txt b/fastlane/metadata/android/gl-ES/full_description.txt deleted file mode 100644 index 0dc6ea1..0000000 --- a/fastlane/metadata/android/gl-ES/full_description.txt +++ /dev/null @@ -1,17 +0,0 @@ -Tod@s contra o Lume! avísache dos lumes detectados nunha zona do teu interese. -Axuda á detección temperá de incendios e facilita a mobilización local mentres -chegan os servizos profesionais de extinción. - -COMO FUNCIONA -• Escolle a zona que queres vixiar. -• Recibe notificacións cando se detecta un foco de calor nesa zona. -• Consulta os lumes activos sobre o mapa, coa súa localización e hora. -• Comparte un aviso para mobilizar á xente do teu contorno. - -DATOS -Os focos de calor proceden de datos e imaxes de LANCE FIRMS, operado polo -Sistema de Datos e Información de Ciencias da Terra (ESDIS) de NASA/GSFC, con -financiamento de NASA/HQ. - -Software libre (GNU AGPL-3.0). Sen anuncios e sen ánimo de lucro: unha -ferramenta comunitaria para previr e responder aos incendios. diff --git a/fastlane/metadata/android/gl-ES/short_description.txt b/fastlane/metadata/android/gl-ES/short_description.txt deleted file mode 100644 index 510e5d4..0000000 --- a/fastlane/metadata/android/gl-ES/short_description.txt +++ /dev/null @@ -1 +0,0 @@ -Avisos de lumes na túa zona para a detección temperá e a resposta local. diff --git a/fastlane/metadata/android/gl-ES/title.txt b/fastlane/metadata/android/gl-ES/title.txt deleted file mode 100644 index 2f43cae..0000000 --- a/fastlane/metadata/android/gl-ES/title.txt +++ /dev/null @@ -1 +0,0 @@ -Tod@s contra o Lume! diff --git a/ios/Flutter/ephemeral/flutter_lldb_helper.py b/ios/Flutter/ephemeral/flutter_lldb_helper.py deleted file mode 100644 index a88caf9..0000000 --- a/ios/Flutter/ephemeral/flutter_lldb_helper.py +++ /dev/null @@ -1,32 +0,0 @@ -# -# Generated file, do not edit. -# - -import lldb - -def handle_new_rx_page(frame: lldb.SBFrame, bp_loc, extra_args, intern_dict): - """Intercept NOTIFY_DEBUGGER_ABOUT_RX_PAGES and touch the pages.""" - base = frame.register["x0"].GetValueAsAddress() - page_len = frame.register["x1"].GetValueAsUnsigned() - - # Note: NOTIFY_DEBUGGER_ABOUT_RX_PAGES will check contents of the - # first page to see if handled it correctly. This makes diagnosing - # misconfiguration (e.g. missing breakpoint) easier. - data = bytearray(page_len) - data[0:8] = b'IHELPED!' - - error = lldb.SBError() - frame.GetThread().GetProcess().WriteMemory(base, data, error) - if not error.Success(): - print(f'Failed to write into {base}[+{page_len}]', error) - return - -def __lldb_init_module(debugger: lldb.SBDebugger, _): - target = debugger.GetDummyTarget() - # Caveat: must use BreakpointCreateByRegEx here and not - # BreakpointCreateByName. For some reasons callback function does not - # get carried over from dummy target for the later. - bp = target.BreakpointCreateByRegex("^NOTIFY_DEBUGGER_ABOUT_RX_PAGES$") - bp.SetScriptCallbackFunction('{}.handle_new_rx_page'.format(__name__)) - bp.SetAutoContinue(True) - print("-- LLDB integration loaded --") diff --git a/ios/Flutter/ephemeral/flutter_lldbinit b/ios/Flutter/ephemeral/flutter_lldbinit deleted file mode 100644 index e3ba6fb..0000000 --- a/ios/Flutter/ephemeral/flutter_lldbinit +++ /dev/null @@ -1,5 +0,0 @@ -# -# Generated file, do not edit. -# - -command script import --relative-to-command-file flutter_lldb_helper.py diff --git a/ios/Flutter/flutter_export_environment.sh b/ios/Flutter/flutter_export_environment.sh deleted file mode 100755 index abfbda7..0000000 --- a/ios/Flutter/flutter_export_environment.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/sh -# This is a generated file; do not edit or check into version control. -export "FLUTTER_ROOT=/home/vjrj/bin/flutter" -export "FLUTTER_APPLICATION_PATH=/home/vjrj/proyectos/git-sea/fires_flutter" -export "COCOAPODS_PARALLEL_CODE_SIGN=true" -export "FLUTTER_TARGET=lib/main.dart" -export "FLUTTER_BUILD_DIR=build" -export "FLUTTER_BUILD_NAME=1.0.0" -export "FLUTTER_BUILD_NUMBER=1" -export "DART_OBFUSCATION=false" -export "TRACK_WIDGET_CREATION=true" -export "TREE_SHAKE_ICONS=false" -export "PACKAGE_CONFIG=.dart_tool/package_config.json" diff --git a/lib/activeFires.dart b/lib/activeFires.dart new file mode 100644 index 0000000..f440a44 --- /dev/null +++ b/lib/activeFires.dart @@ -0,0 +1,290 @@ +import 'dart:async'; + +import 'package:comunes_flutter/comunes_flutter.dart'; +import 'package:fires_flutter/models/yourLocation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_fab_dialer/flutter_fab_dialer.dart'; +import 'package:flutter_redux/flutter_redux.dart'; +import 'package:redux/src/store.dart'; + +import 'colors.dart'; +import 'firesSpinner.dart'; +import 'generated/i18n.dart'; +import 'genericMap.dart'; +import 'globalFiresBottomStats.dart'; +import 'locationUtils.dart'; +import 'mainDrawer.dart'; +import 'models/appState.dart'; +import 'placesAutocompleteUtils.dart'; +import 'redux/actions.dart'; + +@immutable +class _ViewModel { + final List yourLocations; + final AddYourLocationFunction onAdd; + final DeleteYourLocationFunction onDelete; + final ToggleSubscriptionFunction onToggleSubs; + final OnLocationTapFunction onTap; + final OnRefreshYourLocationsFunction onRefresh; + final bool isLoading; + + _ViewModel( + {@required this.onAdd, + @required this.onDelete, + @required this.onToggleSubs, + @required this.onTap, + @required this.onRefresh, + @required this.yourLocations, + @required this.isLoading}); + + @override + bool operator ==(Object other) => + identical(this, other) || + other is _ViewModel && + runtimeType == other.runtimeType && + yourLocations == other.yourLocations && + isLoading == other.isLoading; + + @override + int get hashCode => yourLocations.hashCode ^ isLoading.hashCode; +} + +class ActiveFiresPage extends StatefulWidget { + static const String routeName = '/fires'; + + @override + _ActiveFiresPageState createState() => _ActiveFiresPageState(); +} + +class _ActiveFiresPageState extends State { + final GlobalKey _scaffoldKey = new GlobalKey(); + + List _fabMiniMenuItemList( + BuildContext context, AddYourLocationFunction onAdd) { + return [ + new FabMiniMenuItem.withText( + new Icon(Icons.location_searching), + fires600, + 8.0, + S.of(context).addYourCurrentPosition, + () { + onAddYourLocation(onAdd); + }, + S.of(context).addYourCurrentPosition, + Colors.black38, + Colors.white, + ), + new FabMiniMenuItem.withText(new Icon(Icons.edit_location), fires600, 8.0, + S.of(context).addSomePlace, () { + onAddOtherLocation(onAdd); + }, S.of(context).addSomePlace, Colors.black38, Colors.white) + ]; + } + + Widget _buildRow(BuildContext context, YourLocation loc, onToggle, onTap) { + final fireStatsStyle = new TextStyle(color: fires600); + return new ListTile( + dense: true, + isThreeLine: false, + // leading: const Icon(Icons.location_on), + trailing: new IconButton( + icon: new Icon(loc.subscribed + ? Icons.notifications_active + : Icons.notifications_off), + color: loc.subscribed ? fires600: null, + onPressed: () { + loc.subscribed = !loc.subscribed; + onToggle(loc); + setState(() {}); + showSnackMsg(loc.subscribed + ? S.of(context).subscribedToFires + : S.of(context).unsubscribedToFires); + }), + title: new Text(loc.description, style: new TextStyle(fontSize: 16.0)), + subtitle: loc.currentNumFires == YourLocation.withoutStats + ? null + : loc.currentNumFires == 0 + ? new Text(S.of(context).noFiresAround) + : loc.currentNumFires == 1 + ? new Text( + S + .of(context) + .fireAroundThisArea(loc.distance.toString()), + style: fireStatsStyle) + : new Text( + S.of(context).firesAroundThisArea( + loc.currentNumFires.toString(), + loc.distance.toString()), + style: fireStatsStyle), + onLongPress: () { + showSnackMsg(S.of(context).toDeleteThisPlace); + }, + onTap: () { + onTap(loc); + }); + } + + void showSnackMsg(String msg) { + _scaffoldKey.currentState.showSnackBar(new SnackBar( + content: new Text(msg), + )); + } + + Widget _buildSavedLocations( + BuildContext context, List yl, onDeleted, onToggle, onTap) { + return new ListView.builder( + padding: const EdgeInsets.all(16.0), + itemCount: yl.length, + itemBuilder: (BuildContext _context, int i) { + final ThemeData theme = Theme.of(context); + return new Dismissible( + key: new ObjectKey(yl.elementAt(i)), + direction: DismissDirection.horizontal, + onDismissed: (DismissDirection direction) { + onDeleted(yl.elementAt(i)); + }, + background: new Container( + color: theme.primaryColor, + child: const ListTile( + leading: const Icon(Icons.delete, + color: Colors.white, size: 36.0))), + secondaryBackground: new Container( + color: theme.primaryColor, + child: const ListTile( + trailing: const Icon(Icons.delete, + color: Colors.white, size: 36.0))), + child: new Container( + decoration: new BoxDecoration( + color: theme.canvasColor, + border: new Border( + bottom: new BorderSide(color: theme.dividerColor))), + child: _buildRow(context, yl.elementAt(i), onToggle, onTap))); + }); + } + + @override + Widget build(BuildContext context) { + return new StoreConnector( + distinct: true, + converter: (store) { + print('New ViewModel of Active Fires'); + return new _ViewModel( + onAdd: (loc) { + if (store.state.yourLocations + .any((l) => loc.lat == l.lat && loc.lon == l.lon)) { + // Already added + showSnackMsg(S.of(context).addedThisLocation); + } else { + store.dispatch(new AddYourLocationAction(loc)); + new Timer(new Duration(milliseconds: 1000), () { + gotoLocationMap(store, loc, context); + }); + } + }, + onDelete: (loc) { + store.dispatch(new DeleteYourLocationAction(loc)); + _scaffoldKey.currentState.showSnackBar(new SnackBar( + content: new Text(S.of(context).youDeletedThisPlace), + action: new SnackBarAction( + label: S.of(context).UNDO, + onPressed: () { + store.dispatch(new AddYourLocationAction(loc)); + }))); + }, + onToggleSubs: (loc) { + store.dispatch(new ToggleSubscriptionAction(loc)); + }, + onTap: (loc) { + gotoLocationMap(store, loc, context); + }, + onRefresh: (refreshCallback) => + store.dispatch(new FetchYourLocationsAction(refreshCallback)), + yourLocations: store.state.yourLocations, + isLoading: !store.state.isLoaded); + }, + builder: (context, view) { + var hasLocations = view.yourLocations.length > 0; + final title = hasLocations + ? S.of(context).firesInYourPlaces + : S.of(context).firesInTheWorld; + print('Building Active Fires'); + + return Scaffold( + key: _scaffoldKey, + // FIXME new? + drawer: new MainDrawer(context, ActiveFiresPage.routeName), + appBar: new AppBar( + title: Text(title), + leading: IconButton( + icon: Icon(Icons.menu), + onPressed: () { + _scaffoldKey.currentState.openDrawer(); + }, + ), + ), + floatingActionButtonLocation: + FloatingActionButtonLocation.centerFloat, + bottomNavigationBar: new GlobalFiresBottomStats(), + body: view.isLoading + ? new FiresSpinner() + : hasLocations + ? new Stack(children: [ + new RefreshIndicator( + child: _buildSavedLocations( + context, + view.yourLocations, + view.onDelete, + view.onToggleSubs, + view.onTap), + onRefresh: () async { + final Completer completer = + new Completer(); + view.onRefresh(completer); + return completer.future; + }), + new FabDialer(_fabMiniMenuItemList(context, view.onAdd), + fires600, new Icon(Icons.add)) + ]) + : new Center( + child: new CenteredColumn(children: [ + new RoundedBtn( + icon: Icons.location_searching, + text: S.of(context).addYourCurrentPosition, + onPressed: () => onAddYourLocation(view.onAdd), + backColor: fires600), + const SizedBox(height: 26.0), + new RoundedBtn( + icon: Icons.edit_location, + text: S.of(context).addSomePlace, + onPressed: () => onAddOtherLocation(view.onAdd), + backColor: fires600), + ])), + ); + }); + } + + void gotoLocationMap( + Store store, YourLocation loc, BuildContext context) { + store.dispatch(new ShowYourLocationMapAction(loc)); + Navigator.push( + context, new MaterialPageRoute(builder: (context) => new genericMap())); + } + + void onAddYourLocation(AddYourLocationFunction onAdd) { + Future location = getUserLocation(_scaffoldKey); + _saveLocation(location, onAdd); + } + + void onAddOtherLocation(AddYourLocationFunction onAdd) { + Future location = openPlacesDialog(_scaffoldKey); + _saveLocation(location, onAdd); + } + + void _saveLocation(Future location, onAdd) { + location.then((newLocation) { + if (newLocation != YourLocation.noLocation) { + onAdd(newLocation); + } + }); + } +} diff --git a/lib/active_fires.dart b/lib/active_fires.dart deleted file mode 100644 index 9904118..0000000 --- a/lib/active_fires.dart +++ /dev/null @@ -1,315 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; -import 'package:flutter_redux/flutter_redux.dart'; -import 'package:flutter_speed_dial/flutter_speed_dial.dart'; -import 'package:redux/redux.dart'; - -import 'colors.dart'; -import 'fires_spinner.dart'; -import 'generated/i18n.dart'; -import 'generic_map.dart'; -import 'global_fires_bottom_stats.dart'; -import 'location_utils.dart'; -import 'main_drawer.dart'; -import 'models/app_state.dart'; -import 'models/your_location.dart'; -import 'places_autocomplete_utils.dart'; -import 'redux/actions.dart'; -import 'widgets/rounded_btn.dart'; - -@immutable -class _ViewModel { - const _ViewModel( - {required this.onAdd, - required this.onDelete, - required this.onToggleSubs, - required this.onTap, - required this.onRefresh, - required this.yourLocations, - required this.isLoading}); - final List yourLocations; - final AddYourLocationFunction onAdd; - final DeleteYourLocationFunction onDelete; - final ToggleSubscriptionFunction onToggleSubs; - final OnLocationTapFunction onTap; - final OnRefreshYourLocationsFunction onRefresh; - final bool isLoading; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is _ViewModel && - runtimeType == other.runtimeType && - yourLocations == other.yourLocations && - isLoading == other.isLoading; - - @override - int get hashCode => yourLocations.hashCode ^ isLoading.hashCode; -} - -class ActiveFiresPage extends StatefulWidget { - const ActiveFiresPage({super.key}); - - static const String routeName = '/fires'; - - @override - // ignore: library_private_types_in_public_api - _ActiveFiresPageState createState() => _ActiveFiresPageState(); -} - -class _ActiveFiresPageState extends State { - final GlobalKey _scaffoldKey = GlobalKey(); - - List _fabMiniMenuItemList( - BuildContext context, AddYourLocationFunction onAdd) { - return [ - SpeedDialChild( - child: const Icon(Icons.location_searching), - backgroundColor: fires600, - label: S.of(context).addYourCurrentPosition, - labelWidget: Padding( - padding: const EdgeInsets.only(right: 16.0), - child: Container( - color: Colors.white, - child: Text(S.of(context).addYourCurrentPosition, - style: const TextStyle( - color: Colors.black87, fontWeight: FontWeight.w500)), - ), - ), - onTap: () { - onAddYourLocation(onAdd); - }, - ), - SpeedDialChild( - child: const Icon(Icons.edit_location), - backgroundColor: fires600, - label: S.of(context).addSomePlace, - labelWidget: Padding( - padding: const EdgeInsets.only(right: 16.0), - child: Container( - color: Colors.white, - child: Text(S.of(context).addSomePlace, - style: const TextStyle( - color: Colors.black87, fontWeight: FontWeight.w500)), - ), - ), - onTap: () { - onAddOtherLocation(onAdd); - }, - ) - ]; - } - - Widget _buildRow(BuildContext context, YourLocation loc, - ToggleSubscriptionFunction onToggle, OnLocationTapFunction onTap) { - const TextStyle fireStatsStyle = TextStyle(color: fires600); - return ListTile( - dense: true, - isThreeLine: false, - // leading: const Icon(Icons.location_on), - trailing: IconButton( - icon: Icon(loc.subscribed - ? Icons.notifications_active - : Icons.notifications_off), - color: loc.subscribed ? fires600 : null, - onPressed: () { - final YourLocation updatedLoc = - loc.copyWith(subscribed: !loc.subscribed); - onToggle(updatedLoc); - setState(() {}); - showSnackMsg(updatedLoc.subscribed - ? S.of(context).subscribedToFires - : S.of(context).unsubscribedToFires); - }), - title: Text(loc.description, style: const TextStyle(fontSize: 16.0)), - subtitle: loc.currentNumFires == YourLocation.withoutStats - ? null - : loc.currentNumFires == 0 - ? Text(S.of(context).noFiresAround) - : loc.currentNumFires == 1 - ? Text( - S - .of(context) - .fireAroundThisArea(loc.distance.toString()), - style: fireStatsStyle) - : Text( - S.of(context).firesAroundThisArea( - loc.currentNumFires.toString(), - loc.distance.toString()), - style: fireStatsStyle), - onLongPress: () { - showSnackMsg(S.of(context).toDeleteThisPlace); - }, - onTap: () { - onTap(loc); - }); - } - - void showSnackMsg(String msg) { - // _scaffoldKey.currentState.showSnackBar(new SnackBar( - ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Text(msg), - )); - } - - Widget _buildSavedLocations( - BuildContext context, - List yl, - DeleteYourLocationFunction onDeleted, - ToggleSubscriptionFunction onToggle, - OnLocationTapFunction onTap) { - return ListView.builder( - padding: const EdgeInsets.all(16.0), - itemCount: yl.length, - itemBuilder: (BuildContext context, int i) { - final ThemeData theme = Theme.of(context); - return Dismissible( - key: ObjectKey(yl.elementAt(i)), - onDismissed: (DismissDirection direction) { - onDeleted(yl.elementAt(i)); - }, - background: Container( - color: theme.primaryColor, - child: const ListTile( - leading: - Icon(Icons.delete, color: Colors.white, size: 36.0))), - secondaryBackground: Container( - color: theme.primaryColor, - child: const ListTile( - trailing: - Icon(Icons.delete, color: Colors.white, size: 36.0))), - child: Container( - decoration: BoxDecoration( - color: theme.canvasColor, - border: Border( - bottom: BorderSide(color: theme.dividerColor))), - child: _buildRow(context, yl.elementAt(i), onToggle, onTap))); - }); - } - - @override - Widget build(BuildContext context) { - return StoreConnector( - distinct: true, - converter: (Store store) { - return _ViewModel( - onAdd: (YourLocation loc) { - if (store.state.yourLocations.any( - (YourLocation l) => loc.lat == l.lat && loc.lon == l.lon)) { - // Already added - showSnackMsg(S.of(context).addedThisLocation); - } else { - store.dispatch(AddYourLocationAction(loc)); - Timer(const Duration(milliseconds: 1000), () { - gotoLocationMap(store, loc, context); - }); - } - }, - onDelete: (YourLocation loc) { - store.dispatch(DeleteYourLocationAction(loc)); - ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Text(S.of(context).youDeletedThisPlace), - action: SnackBarAction( - label: S.of(context).UNDO, - onPressed: () { - store.dispatch(AddYourLocationAction(loc)); - }))); - }, - onToggleSubs: (YourLocation loc) { - store.dispatch(ToggleSubscriptionAction(loc)); - }, - onTap: (YourLocation loc) { - gotoLocationMap(store, loc, context); - }, - onRefresh: (Completer refreshCallback) => - store.dispatch(FetchYourLocationsAction(refreshCallback)), - yourLocations: store.state.yourLocations, - isLoading: !store.state.isLoaded); - }, - builder: (BuildContext context, _ViewModel view) { - final bool hasLocations = view.yourLocations.isNotEmpty; - final String title = hasLocations - ? S.of(context).firesInYourPlaces - : S.of(context).firesInTheWorld; - - return Scaffold( - key: _scaffoldKey, - // FIXME new? - drawer: MainDrawer(context, ActiveFiresPage.routeName), - appBar: AppBar( - title: Text(title), - leading: IconButton( - icon: const Icon(Icons.menu), - onPressed: () { - _scaffoldKey.currentState?.openDrawer(); - }, - ), - ), - floatingActionButton: SpeedDial( - icon: Icons.add, - activeIcon: Icons.close, - backgroundColor: fires600, - spacing: 12, - children: _fabMiniMenuItemList(context, view.onAdd), - ), - bottomNavigationBar: const GlobalFiresBottomStats(), - body: view.isLoading - ? const FiresSpinner() - : hasLocations - ? RefreshIndicator( - child: _buildSavedLocations(context, view.yourLocations, - view.onDelete, view.onToggleSubs, view.onTap), - onRefresh: () async { - final Completer completer = Completer(); - view.onRefresh(completer); - return completer.future; - }) - : Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - RoundedBtn( - icon: Icons.location_searching, - text: S.of(context).addYourCurrentPosition, - onPressed: () => onAddYourLocation(view.onAdd), - backColor: fires600), - const SizedBox(height: 26.0), - RoundedBtn( - icon: Icons.edit_location, - text: S.of(context).addSomePlace, - onPressed: () => onAddOtherLocation(view.onAdd), - backColor: fires600), - ])), - ); - }); - } - - void gotoLocationMap( - Store store, YourLocation loc, BuildContext context) { - store.dispatch(ShowYourLocationMapAction(loc)); - Navigator.push( - context, - MaterialPageRoute( - builder: (BuildContext context) => const GenericMap())); - } - - void onAddYourLocation(AddYourLocationFunction onAdd) { - final Future location = getUserLocation(_scaffoldKey); - _saveLocation(location, onAdd); - } - - void onAddOtherLocation(AddYourLocationFunction onAdd) { - final Future location = openPlacesDialog(_scaffoldKey); - _saveLocation(location, onAdd); - } - - void _saveLocation( - Future location, AddYourLocationFunction onAdd) { - location.then((YourLocation newLocation) { - if (newLocation != YourLocation.noLocation) { - onAdd(newLocation); - } - }); - } -} diff --git a/lib/attributionMapPlugin.dart b/lib/attributionMapPlugin.dart new file mode 100644 index 0000000..a33b457 --- /dev/null +++ b/lib/attributionMapPlugin.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/src/widgets/framework.dart'; +import 'package:flutter_map/plugin_api.dart'; + +class AttributionPluginOptions extends LayerOptions { + final String text; + + AttributionPluginOptions({this.text = ""}); +} + +class AttributionPlugin implements MapPlugin { + @override + Widget createLayer(LayerOptions options, MapState mapState) { + if (options is AttributionPluginOptions) { + var style = new TextStyle( + // fontWeight: FontWeight.bold, + fontSize: 12.0, + color: Colors.white, + ); + return Padding( + padding: const EdgeInsets.all(5.0), + child: new Text( + options.text, + style: style, + ), + ); + } + throw ("Unknown options type for Attribution" + "plugin: $options"); + } + + @override + bool supportsLayer(LayerOptions options) { + return options is AttributionPluginOptions; + } +} diff --git a/lib/attribution_map_plugin.dart b/lib/attribution_map_plugin.dart deleted file mode 100644 index 85d41f7..0000000 --- a/lib/attribution_map_plugin.dart +++ /dev/null @@ -1,23 +0,0 @@ -import 'package:flutter/material.dart'; - -/// Attribution widget for displaying map attribution text -class AttributionPluginWidget extends StatelessWidget { - - const AttributionPluginWidget({super.key, this.text = ''}); - final String text; - - @override - Widget build(BuildContext context) { - const TextStyle style = TextStyle( - fontSize: 12.0, - color: Colors.white, - ); - return Padding( - padding: const EdgeInsets.all(5.0), - child: Text( - text, - style: style, - ), - ); - } -} diff --git a/lib/colors.dart b/lib/colors.dart index 5e26f5a..0570559 100644 --- a/lib/colors.dart +++ b/lib/colors.dart @@ -1,68 +1,11 @@ import 'package:flutter/material.dart'; -// Legacy colors - kept for backward compatibility -const Color fires50 = Color(0xFFFBE9E7); -const Color fires100 = Color(0xFFFFCCBC); -const Color fires300 = Color(0xFFFF8A65); -const Color fires600 = Color(0xFFF4511E); -const Color fires900 = Color(0xFFBF360C); -const Color firesErrorRed = Color(0xFFDD2C00); +const fires50 = const Color(0xFFFBE9E7); +const fires100 = const Color(0xFFFFCCBC); +const fires300 = const Color(0xFFFF8A65); +const fires600 = const Color(0xFFF4511E); +const fires900 = const Color(0xFFBF360C); +const firesErrorRed = const Color(0xFFDD2C00); -const Color firesSurfaceWhite = fires50; -const Color firesBackgroundWhite = Colors.white; - -// ============================================================================ -// Material 3 Color Palette - Modern Warm Theme (Production) -// ============================================================================ - -// Primary Color - Deep Orange (fire theme) -const Color m3Primary = Color(0xFFE65100); // Deep Orange 900 -const Color m3OnPrimary = Color(0xFFFFFFFF); // White -const Color m3PrimaryContainer = Color(0xFFFFCC80); // Orange 200 -const Color m3OnPrimaryContainer = Color(0xFF5D2700); // Deep Brown - -// Secondary Color - Brown (earth tones) -const Color m3Secondary = Color(0xFF6D4C41); // Brown 600 -const Color m3OnSecondary = Color(0xFFFFFFFF); // White -const Color m3SecondaryContainer = Color(0xFFD7CCC8); // Brown 50 -const Color m3OnSecondaryContainer = Color(0xFF3E2723); // Deep Brown - -// Tertiary Color - Teal (complementary accent) -const Color m3Tertiary = Color(0xFF00695C); // Teal 800 -const Color m3OnTertiary = Color(0xFFFFFFFF); // White -const Color m3TertiaryContainer = Color(0xFFB2DFDB); // Teal 100 -const Color m3OnTertiaryContainer = Color(0xFF004D40); // Deep Teal - -// Error Colors -const Color m3Error = Color(0xFFDD2C00); // Red 800 -const Color m3OnError = Color(0xFFFFFFFF); // White -const Color m3ErrorContainer = Color(0xFFFFCDD2); // Red 100 -const Color m3OnErrorContainer = Color(0xFF5F2120); // Deep Red - -// Surface & Background -const Color m3Surface = Color(0xFFFFFBFE); // Almost white -const Color m3OnSurface = Color(0xFF201A18); // Almost black -const Color m3SurfaceVariant = Color(0xFFF5DED6); // Light brown -const Color m3OnSurfaceVariant = Color(0xFF52443D); // Medium brown - -const Color m3Background = Color(0xFFFFFBFE); // Almost white -const Color m3OnBackground = Color(0xFF201A18); // Almost black - -// Outline & Borders -const Color m3Outline = Color(0xFF85736B); // Gray-brown -const Color m3OutlineVariant = Color(0xFFD7C2B9); // Light gray-brown -const Color m3Shadow = Color(0xFF000000); // Black -const Color m3Scrim = Color(0xFF000000); // Black - -// ============================================================================ -// Material 3 Development Theme Colors (Dev Theme) -// ============================================================================ - -// Dev Primary - Pink (easy to identify dev builds) -const Color m3DevPrimary = Color(0xFFC2185B); // Pink 700 -const Color m3DevOnPrimary = Color(0xFFFFFFFF); // White -const Color m3DevPrimaryContainer = Color(0xFFF8BBD0); // Pink 100 -const Color m3DevOnPrimaryContainer = Color(0xFF7B0043); // Deep Pink - -// Dev theme uses same secondary, tertiary, and neutral colors as production -// Only primary is different for visual distinction +const firesSurfaceWhite = fires50; +const firesBackgroundWhite = Colors.white; \ No newline at end of file diff --git a/lib/compass_map_plugin.dart b/lib/compass_map_plugin.dart deleted file mode 100644 index b229ebb..0000000 --- a/lib/compass_map_plugin.dart +++ /dev/null @@ -1,112 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; -import 'package:flutter_map/flutter_map.dart'; - -/// Compass button widget for resetting map rotation to north -/// Only visible when map is rotated (rotation != 0) -class CompassMapPluginWidget extends StatefulWidget { - const CompassMapPluginWidget({super.key}); - - @override - // ignore: library_private_types_in_public_api - _CompassMapPluginWidgetState createState() => _CompassMapPluginWidgetState(); -} - -class _CompassMapPluginWidgetState extends State { - late MapController _mapController; - bool _isRotated = false; - late Timer _rotationCheckTimer; - - @override - void didChangeDependencies() { - super.didChangeDependencies(); - try { - _mapController = MapController.of(context); - _initRotationMonitoring(); - } catch (_) { - // Ignore - MapController may not be available yet - } - } - - void _initRotationMonitoring() { - // Check rotation periodically (every 100ms) to detect changes - _rotationCheckTimer = - Timer.periodic(const Duration(milliseconds: 100), (_) { - _checkRotation(); - }); - - // Initial state check - _checkRotation(); - } - - void _checkRotation() { - try { - if (!mounted) { - return; - } - - final double rotation = _mapController.camera.rotation; - final bool isRotated = rotation.abs() > 0.0; - - if (isRotated != _isRotated) { - setState(() { - _isRotated = isRotated; - }); - } - } catch (e) { - // Silently ignore errors (controller may not be ready) - } - } - - void _resetRotation() { - try { - final MapController controller = MapController.of(context); - controller.rotate(0); - } catch (_) { - // Ignore - MapController may not be available - } - } - - @override - Widget build(BuildContext context) { - return LayoutBuilder( - builder: (BuildContext context, BoxConstraints constraints) => Stack( - fit: StackFit.expand, - children: [ - if (_isRotated) - Positioned( - top: 10.0, - right: 10.0, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Column( - children: [ - _compassButton(context), - ], - ) - ], - ), - ) - ], - )); - } - - FloatingActionButton _compassButton(BuildContext context) { - return FloatingActionButton( - backgroundColor: Colors.black26, - mini: true, - heroTag: 'compass_button', - tooltip: 'Go to North', - onPressed: _resetRotation, - child: const Icon(Icons.explore), - ); - } - - @override - void dispose() { - _rotationCheckTimer.cancel(); - super.dispose(); - } -} diff --git a/lib/custom_bottom_app_bar.dart b/lib/customBottomAppBar.dart similarity index 54% rename from lib/custom_bottom_app_bar.dart rename to lib/customBottomAppBar.dart index 3448fd9..162a433 100644 --- a/lib/custom_bottom_app_bar.dart +++ b/lib/customBottomAppBar.dart @@ -2,17 +2,17 @@ import 'package:flutter/material.dart'; class CustomBottomAppBar extends StatelessWidget { const CustomBottomAppBar( - {super.key, this.fabLocation, - this.showNotch = false, + {this.fabLocation, + this.showNotch, this.height = 56.0, this.color = Colors.black45, this.mainAxisAlignment = MainAxisAlignment.center, - this.actions = const []}); + this.actions}); final Color color; final double height; final MainAxisAlignment mainAxisAlignment; - final FloatingActionButtonLocation? fabLocation; + final FloatingActionButtonLocation fabLocation; final bool showNotch; final List actions; @@ -24,7 +24,7 @@ class CustomBottomAppBar extends StatelessWidget { @override Widget build(BuildContext context) { - final List rowContents = [SizedBox(height: height)]; + final List rowContents = [new SizedBox(height: height)]; if (kCenterLocations.contains(fabLocation)) { /* rowContents.add( @@ -32,19 +32,14 @@ class CustomBottomAppBar extends StatelessWidget { ); */ } - rowContents.addAll(actions); + rowContents.addAll(this.actions); - return showNotch - ? BottomAppBar( - color: color, - shape: const CircularNotchedRectangle(), - child: Row( - mainAxisAlignment: mainAxisAlignment, children: rowContents), - ) - : BottomAppBar( - color: color, - child: Row( - mainAxisAlignment: mainAxisAlignment, children: rowContents), - ); + return new BottomAppBar( + color: color, + hasNotch: showNotch, + child: new Row( + mainAxisAlignment: mainAxisAlignment, + children: rowContents), + ); } } diff --git a/lib/customMoment.dart b/lib/customMoment.dart new file mode 100644 index 0000000..b4512a3 --- /dev/null +++ b/lib/customMoment.dart @@ -0,0 +1,63 @@ +// Copyright (c) 2016, rinukkusu. All rights reserved. Use of this source code +// is governed by a BSD-style license that can be found in the LICENSE file. + +import 'generated/i18n.dart'; +import 'package:flutter/material.dart'; + +class Moment { + DateTime _date; + + Moment.now() { + _date = new DateTime.now(); + } + + Moment.fromDate(DateTime date) { + _date = date; + } + + Moment.fromMillisecondsSinceEpoch(int millisecondsSinceEpoch, + {bool isUtc: false}) { + _date = new DateTime.fromMillisecondsSinceEpoch(millisecondsSinceEpoch, + isUtc: isUtc); + } + + static Moment parse(String date) { + return new Moment.fromDate(DateTime.parse(date)); + } + + String toString() { + return _date.toString(); + } + + String fromNow(BuildContext context, [bool withoutPrefixOrSuffix = false]) { + return from(context, new DateTime.now(), withoutPrefixOrSuffix); + } + + String from(BuildContext context, DateTime date, [bool withoutPrefixOrSuffix = false]) { + Duration diff = date.difference(_date); + + String timeString = ""; + + if (diff.inSeconds.abs() < 45) timeString = S.of(context).aF3wSeconds; + else if (diff.inMinutes.abs() < 2) timeString = S.of(context).aMinute; + else if (diff.inMinutes.abs() < 45) timeString = S.of(context).inMinutes(diff.inMinutes.abs().toString()); + else if (diff.inHours.abs() < 2) timeString = S.of(context).anHour; + else if (diff.inHours.abs() < 22) timeString = S.of(context).inHours(diff.inHours.abs().toString()); + else if (diff.inDays.abs() < 2) timeString = S.of(context).aDay; + else if (diff.inDays.abs() < 26) timeString = S.of(context).inDays(diff.inDays.abs().toString()); + else if (diff.inDays.abs() < 60) timeString = S.of(context).aMonth; + else if (diff.inDays.abs() < 320) timeString = S.of(context).inMonths((diff.inDays.abs() ~/ 30).toString()); + else if (diff.inDays.abs() < 547) timeString = S.of(context).aYear; + else timeString = S.of(context).inYears((diff.inDays.abs() ~/ 356).toString()); + + if (!withoutPrefixOrSuffix) { + if (diff.isNegative) + timeString =S.of(context).somethingAgo(timeString); + + else + timeString =S.of(context).inSomething(timeString); + } + + return timeString; + } +} diff --git a/lib/custom_stepper.dart b/lib/customStepper.dart similarity index 64% rename from lib/custom_stepper.dart rename to lib/customStepper.dart index b284150..5d0c3fd 100644 --- a/lib/custom_stepper.dart +++ b/lib/customStepper.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'package:flutter/material.dart'; + // TODO(dragostis): Missing functionality: // * mobile horizontal mode with adding/removing steps // * alternative labeling @@ -41,7 +42,7 @@ enum CustomStepperType { horizontal, } -const TextStyle _kCustomStepStyle = TextStyle( +const TextStyle _kCustomStepStyle = const TextStyle( fontSize: 12.0, color: Colors.white, ); @@ -52,8 +53,7 @@ const Color _kCircleActiveDark = Colors.black87; const Color _kDisabledLight = Colors.black38; const Color _kDisabledDark = Colors.white30; const double _kCustomStepSize = 24.0; -const double _kTriangleHeight = - _kCustomStepSize * 0.866025; // Triangle height. sqrt(3.0) / 2.0 +const double _kTriangleHeight = _kCustomStepSize * 0.866025; // Triangle height. sqrt(3.0) / 2.0 /// A material step used in [CustomStepper]. The step can have a title and subtitle, /// an icon within its circle, some content and a state that governs its @@ -70,12 +70,14 @@ class CustomStep { /// /// The [title], [content], and [state] arguments must not be null. const CustomStep({ - required this.title, + @required this.title, this.subtitle, - required this.content, - this.state = CustomStepState.indexed, - this.isActive = false, - }); + @required this.content, + this.state: CustomStepState.indexed, + this.isActive: false, + }) : assert(title != null), + assert(content != null), + assert(state != null); /// The title of the step that typically describes it. final Widget title; @@ -84,7 +86,7 @@ class CustomStep { /// font size. It typically gives more details that complement the title. /// /// If null, the subtitle is not shown. - final Widget? subtitle; + final Widget subtitle; /// The content of the step that appears below the [title] and [subtitle]. /// @@ -120,15 +122,19 @@ class CustomStepper extends StatefulWidget { /// new one. /// /// The [steps], [type], and [currentCustomStep] arguments must not be null. - const CustomStepper({ - super.key, - required this.steps, - this.type = CustomStepperType.vertical, - this.currentCustomStep = 0, + CustomStepper({ + Key key, + @required this.steps, + this.type: CustomStepperType.vertical, + this.currentCustomStep: 0, this.onCustomStepTapped, this.onCustomStepContinue, this.onCustomStepCancel, - }) : assert(0 <= currentCustomStep && currentCustomStep < steps.length); + }) : assert(steps != null), + assert(type != null), + assert(currentCustomStep != null), + assert(0 <= currentCustomStep && currentCustomStep < steps.length), + super(key: key); /// The steps of the stepper whose titles, subtitles, icons always get shown. /// @@ -146,33 +152,32 @@ class CustomStepper extends StatefulWidget { /// The callback called when a step is tapped, with its index passed as /// an argument. - final ValueChanged? onCustomStepTapped; + final ValueChanged onCustomStepTapped; /// The callback called when the 'continue' button is tapped. /// /// If null, the 'continue' button will be disabled. - final VoidCallback? onCustomStepContinue; + final VoidCallback onCustomStepContinue; /// The callback called when the 'cancel' button is tapped. /// /// If null, the 'cancel' button will be disabled. - final VoidCallback? onCustomStepCancel; + final VoidCallback onCustomStepCancel; @override - // ignore: library_private_types_in_public_api - _CustomStepperState createState() => _CustomStepperState(); + _CustomStepperState createState() => new _CustomStepperState(); } class _CustomStepperState extends State { - late List _keys; + List _keys; final Map _oldStates = {}; @override void initState() { super.initState(); - _keys = List.generate( + _keys = new List.generate( widget.steps.length, - (int i) => GlobalKey(), + (int i) => new GlobalKey(), ); for (int i = 0; i < widget.steps.length; i += 1) @@ -205,7 +210,7 @@ class _CustomStepperState extends State { } Widget _buildLine(bool visible) { - return Container( + return new Container( width: visible ? 1.0 : 0.0, height: 16.0, color: Colors.grey.shade400, @@ -213,87 +218,76 @@ class _CustomStepperState extends State { } Widget _buildCircleChild(int index, bool oldState) { - final CustomStepState state = - oldState ? _oldStates[index]! : widget.steps[index].state; + final CustomStepState state = oldState ? _oldStates[index] : widget.steps[index].state; final bool isDarkActive = _isDark() && widget.steps[index].isActive; + assert(state != null); switch (state) { case CustomStepState.indexed: case CustomStepState.disabled: - return Text( + return new Text( '${index + 1}', - style: isDarkActive - ? _kCustomStepStyle.copyWith(color: Colors.black87) - : _kCustomStepStyle, + style: isDarkActive ? _kCustomStepStyle.copyWith(color: Colors.black87) : _kCustomStepStyle, ); case CustomStepState.editing: - return Icon( + return new Icon( Icons.edit, color: isDarkActive ? _kCircleActiveDark : _kCircleActiveLight, ); case CustomStepState.complete: - return Icon( + return new Icon( Icons.check, color: isDarkActive ? _kCircleActiveDark : _kCircleActiveLight, ); case CustomStepState.error: return const Text('!', style: _kCustomStepStyle); } + return null; } Color _circleColor(int index) { final ThemeData themeData = Theme.of(context); if (!_isDark()) { - return widget.steps[index].isActive - ? themeData.primaryColor - : Colors.black38; + return widget.steps[index].isActive ? themeData.primaryColor : Colors.black38; } else { - return widget.steps[index].isActive - ? themeData.colorScheme.secondary - : themeData.colorScheme.surface; + return widget.steps[index].isActive ? themeData.accentColor : themeData.backgroundColor; } } Widget _buildCircle(int index, bool oldState) { - return Container( + return new Container( margin: const EdgeInsets.symmetric(vertical: 8.0), width: _kCustomStepSize, height: _kCustomStepSize, - child: AnimatedContainer( + child: new AnimatedContainer( curve: Curves.fastOutSlowIn, duration: kThemeAnimationDuration, - decoration: BoxDecoration( + decoration: new BoxDecoration( color: _circleColor(index), shape: BoxShape.circle, ), - child: Center( - child: _buildCircleChild(index, - oldState && widget.steps[index].state == CustomStepState.error), + child: new Center( + child: _buildCircleChild(index, oldState && widget.steps[index].state == CustomStepState.error), ), ), ); } Widget _buildTriangle(int index, bool oldState) { - return Container( + return new Container( margin: const EdgeInsets.symmetric(vertical: 8.0), width: _kCustomStepSize, height: _kCustomStepSize, - child: Center( - child: SizedBox( + child: new Center( + child: new SizedBox( width: _kCustomStepSize, - height: - _kTriangleHeight, // Height of 24dp-long-sided equilateral triangle. - child: CustomPaint( - painter: _TrianglePainter( + height: _kTriangleHeight, // Height of 24dp-long-sided equilateral triangle. + child: new CustomPaint( + painter: new _TrianglePainter( color: _isDark() ? _kErrorDark : _kErrorLight, ), - child: Align( - alignment: const Alignment( - 0.0, 0.8), // 0.8 looks better than the geometrical 0.33. - child: _buildCircleChild( - index, - oldState && - widget.steps[index].state != CustomStepState.error), + child: new Align( + alignment: const Alignment(0.0, 0.8), // 0.8 looks better than the geometrical 0.33. + child: _buildCircleChild(index, oldState && widget.steps[index].state != CustomStepState.error), ), ), ), @@ -303,15 +297,13 @@ class _CustomStepperState extends State { Widget _buildIcon(int index) { if (widget.steps[index].state != _oldStates[index]) { - return AnimatedCrossFade( + return new AnimatedCrossFade( firstChild: _buildCircle(index, true), secondChild: _buildTriangle(index, true), firstCurve: const Interval(0.0, 0.6, curve: Curves.fastOutSlowIn), secondCurve: const Interval(0.4, 1.0, curve: Curves.fastOutSlowIn), sizeCurve: Curves.fastOutSlowIn, - crossFadeState: widget.steps[index].state == CustomStepState.error - ? CrossFadeState.showSecond - : CrossFadeState.showFirst, + crossFadeState: widget.steps[index].state == CustomStepState.error ? CrossFadeState.showSecond : CrossFadeState.showFirst, duration: kThemeAnimationDuration, ); } else { @@ -323,23 +315,46 @@ class _CustomStepperState extends State { } Widget _buildVerticalControls() { - // final Color cancelColor; + Color cancelColor; switch (Theme.of(context).brightness) { case Brightness.light: + cancelColor = Colors.black54; + break; case Brightness.dark: + cancelColor = Colors.white70; break; } - // final ThemeData themeData = Theme.of(context); - // final MaterialLocalizations localizations = - // MaterialLocalizations.of(context); + assert(cancelColor != null); - return Container( + final ThemeData themeData = Theme.of(context); + final MaterialLocalizations localizations = MaterialLocalizations.of(context); + + return new Container( margin: const EdgeInsets.only(top: 16.0), - child: ConstrainedBox( + child: new ConstrainedBox( constraints: const BoxConstraints.tightFor(height: 48.0), - child: const Row(), + child: new Row( + children: [ + /* new FlatButton( + onPressed: widget.onCustomStepContinue, + color: _isDark() ? themeData.backgroundColor : themeData.primaryColor, + textColor: Colors.white, + textTheme: ButtonTextTheme.normal, + child: new Text(localizations.continueButtonLabel), + ), + new Container( + margin: const EdgeInsetsDirectional.only(start: 8.0), + child: new FlatButton( + onPressed: widget.onCustomStepCancel, + textColor: cancelColor, + textTheme: ButtonTextTheme.normal, + child: new Text(localizations.cancelButtonLabel), + ), + ), */ + ], + ), ), ); } @@ -348,41 +363,49 @@ class _CustomStepperState extends State { final ThemeData themeData = Theme.of(context); final TextTheme textTheme = themeData.textTheme; + assert(widget.steps[index].state != null); switch (widget.steps[index].state) { case CustomStepState.indexed: case CustomStepState.editing: case CustomStepState.complete: - return textTheme.bodyLarge ?? const TextStyle(); + return textTheme.body2; case CustomStepState.disabled: - return (textTheme.bodyLarge ?? const TextStyle()) - .copyWith(color: _isDark() ? _kDisabledDark : _kDisabledLight); + return textTheme.body2.copyWith( + color: _isDark() ? _kDisabledDark : _kDisabledLight + ); case CustomStepState.error: - return (textTheme.bodyLarge ?? const TextStyle()) - .copyWith(color: _isDark() ? _kErrorDark : _kErrorLight); + return textTheme.body2.copyWith( + color: _isDark() ? _kErrorDark : _kErrorLight + ); } + return null; } TextStyle _subtitleStyle(int index) { final ThemeData themeData = Theme.of(context); final TextTheme textTheme = themeData.textTheme; + assert(widget.steps[index].state != null); switch (widget.steps[index].state) { case CustomStepState.indexed: case CustomStepState.editing: case CustomStepState.complete: - return textTheme.bodySmall ?? const TextStyle(); + return textTheme.caption; case CustomStepState.disabled: - return (textTheme.bodySmall ?? const TextStyle()) - .copyWith(color: _isDark() ? _kDisabledDark : _kDisabledLight); + return textTheme.caption.copyWith( + color: _isDark() ? _kDisabledDark : _kDisabledLight + ); case CustomStepState.error: - return (textTheme.bodySmall ?? const TextStyle()) - .copyWith(color: _isDark() ? _kErrorDark : _kErrorLight); + return textTheme.caption.copyWith( + color: _isDark() ? _kErrorDark : _kErrorLight + ); } + return null; } Widget _buildHeaderText(int index) { final List children = [ - AnimatedDefaultTextStyle( + new AnimatedDefaultTextStyle( style: _titleStyle(index), duration: kThemeAnimationDuration, curve: Curves.fastOutSlowIn, @@ -390,71 +413,77 @@ class _CustomStepperState extends State { ), ]; - if (widget.steps[index].subtitle != null) { + if (widget.steps[index].subtitle != null) children.add( - Container( + new Container( margin: const EdgeInsets.only(top: 2.0), - child: AnimatedDefaultTextStyle( + child: new AnimatedDefaultTextStyle( style: _subtitleStyle(index), duration: kThemeAnimationDuration, curve: Curves.fastOutSlowIn, - child: widget.steps[index].subtitle!, + child: widget.steps[index].subtitle, ), ), ); - } - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: children); + return new Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: children + ); } Widget _buildVerticalHeader(int index) { - return Container( - margin: const EdgeInsets.symmetric(horizontal: 24.0), - child: Row(children: [ - Column(children: [ - // Line parts are always added in order for the ink splash to - // flood the tips of the connector lines. - _buildLine(!_isFirst(index)), - _buildIcon(index), - _buildLine(!_isLast(index)), - ]), - Container( - margin: const EdgeInsetsDirectional.only(start: 12.0), - child: _buildHeaderText(index)) - ])); + return new Container( + margin: const EdgeInsets.symmetric(horizontal: 24.0), + child: new Row( + children: [ + new Column( + children: [ + // Line parts are always added in order for the ink splash to + // flood the tips of the connector lines. + _buildLine(!_isFirst(index)), + _buildIcon(index), + _buildLine(!_isLast(index)), + ] + ), + new Container( + margin: const EdgeInsetsDirectional.only(start: 12.0), + child: _buildHeaderText(index) + ) + ] + ) + ); } Widget _buildVerticalBody(int index) { - return Stack( + return new Stack( children: [ - PositionedDirectional( + new PositionedDirectional( start: 24.0, top: 0.0, bottom: 0.0, - child: SizedBox( + child: new SizedBox( width: 24.0, - child: Center( - child: SizedBox( + child: new Center( + child: new SizedBox( width: _isLast(index) ? 0.0 : 1.0, - child: Container( + child: new Container( color: Colors.grey.shade400, ), ), ), ), ), - AnimatedCrossFade( - firstChild: Container(height: 0.0), - secondChild: Container( + new AnimatedCrossFade( + firstChild: new Container(height: 0.0), + secondChild: new Container( margin: const EdgeInsetsDirectional.only( start: 60.0, end: 24.0, bottom: 24.0, ), - child: Column( + child: new Column( children: [ widget.steps[index].content, _buildVerticalControls(), @@ -464,9 +493,7 @@ class _CustomStepperState extends State { firstCurve: const Interval(0.0, 0.6, curve: Curves.fastOutSlowIn), secondCurve: const Interval(0.4, 1.0, curve: Curves.fastOutSlowIn), sizeCurve: Curves.fastOutSlowIn, - crossFadeState: _isCurrent(index) - ? CrossFadeState.showSecond - : CrossFadeState.showFirst, + crossFadeState: _isCurrent(index) ? CrossFadeState.showSecond : CrossFadeState.showFirst, duration: kThemeAnimationDuration, ), ], @@ -477,27 +504,32 @@ class _CustomStepperState extends State { final List children = []; for (int i = 0; i < widget.steps.length; i += 1) { - children.add(Column(key: _keys[i], children: [ - InkWell( - onTap: widget.steps[i].state != CustomStepState.disabled - ? () { - // In the vertical case we need to scroll to the newly tapped - // step. - Scrollable.ensureVisible( - _keys[i].currentContext!, - curve: Curves.fastOutSlowIn, - duration: kThemeAnimationDuration, - ); + children.add( + new Column( + key: _keys[i], + children: [ + new InkWell( + onTap: widget.steps[i].state != CustomStepState.disabled ? () { + // In the vertical case we need to scroll to the newly tapped + // step. + Scrollable.ensureVisible( + _keys[i].currentContext, + curve: Curves.fastOutSlowIn, + duration: kThemeAnimationDuration, + ); - widget.onCustomStepTapped?.call(i); - } - : null, - child: _buildVerticalHeader(i)), - _buildVerticalBody(i) - ])); + if (widget.onCustomStepTapped != null) + widget.onCustomStepTapped(i); + } : null, + child: _buildVerticalHeader(i) + ), + _buildVerticalBody(i) + ] + ) + ); } - return ListView( + return new ListView( shrinkWrap: true, children: children, ); @@ -508,21 +540,20 @@ class _CustomStepperState extends State { for (int i = 0; i < widget.steps.length; i += 1) { children.add( - InkResponse( - onTap: widget.steps[i].state != CustomStepState.disabled - ? () { - widget.onCustomStepTapped?.call(i); - } - : null, - child: Row( + new InkResponse( + onTap: widget.steps[i].state != CustomStepState.disabled ? () { + if (widget.onCustomStepTapped != null) + widget.onCustomStepTapped(i); + } : null, + child: new Row( children: [ - SizedBox( + new Container( height: 72.0, - child: Center( + child: new Center( child: _buildIcon(i), ), ), - Container( + new Container( margin: const EdgeInsetsDirectional.only(start: 12.0), child: _buildHeaderText(i), ), @@ -533,8 +564,8 @@ class _CustomStepperState extends State { if (!_isLast(i)) { children.add( - Expanded( - child: Container( + new Expanded( + child: new Container( margin: const EdgeInsets.symmetric(horizontal: 8.0), height: 1.0, color: Colors.grey.shade400, @@ -544,24 +575,25 @@ class _CustomStepperState extends State { } } - return Column( + return new Column( children: [ - Material( + new Material( elevation: 2.0, - child: Container( + child: new Container( margin: const EdgeInsets.symmetric(horizontal: 24.0), - child: Row( + child: new Row( children: children, ), ), ), - Expanded( - child: ListView( + new Expanded( + child: new ListView( padding: const EdgeInsets.all(24.0), children: [ - AnimatedSize( + new AnimatedSize( curve: Curves.fastOutSlowIn, duration: kThemeAnimationDuration, + //vsync: this, child: widget.steps[widget.currentCustomStep].content, ), _buildVerticalControls(), @@ -576,26 +608,31 @@ class _CustomStepperState extends State { Widget build(BuildContext context) { assert(debugCheckHasMaterial(context)); assert(() { - if (context.findAncestorWidgetOfExactType() != null) - throw FlutterError( - 'CustomSteppers must not be nested. The material specification advises ' + if (context.ancestorWidgetOfExactType(CustomStepper) != null) + throw new FlutterError( + 'CustomSteppers must not be nested. The material specification advises ' 'that one should avoid embedding steppers within steppers. ' - 'https://material.google.com/components/steppers.html#steppers-usage\n'); + 'https://material.google.com/components/steppers.html#steppers-usage\n' + ); return true; }()); + assert(widget.type != null); switch (widget.type) { case CustomStepperType.vertical: return _buildVertical(); case CustomStepperType.horizontal: return _buildHorizontal(); } + return null; } } // Paints a triangle whose base is the bottom of the bounding rectangle and its // top vertex the middle of its top. class _TrianglePainter extends CustomPainter { - _TrianglePainter({required this.color}); + _TrianglePainter({ + this.color + }); final Color color; @@ -613,14 +650,14 @@ class _TrianglePainter extends CustomPainter { final double halfBase = size.width / 2.0; final double height = size.height; final List points = [ - Offset(0.0, height), - Offset(base, height), - Offset(halfBase, 0.0), + new Offset(0.0, height), + new Offset(base, height), + new Offset(halfBase, 0.0), ]; canvas.drawPath( - Path()..addPolygon(points, true), - Paint()..color = color, + new Path()..addPolygon(points, true), + new Paint()..color = color, ); } } diff --git a/lib/custom_moment.dart b/lib/custom_moment.dart deleted file mode 100644 index 48fef6a..0000000 --- a/lib/custom_moment.dart +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright (c) 2016, rinukkusu. All rights reserved. Use of this source code -// is governed by a BSD-style license that can be found in the LICENSE file. - -import 'package:flutter/material.dart'; - -import 'generated/i18n.dart'; - -class Moment { - - Moment.now() { - _date = DateTime.now(); - } - - Moment.fromDate(DateTime date) { - _date = date; - } - - Moment.fromMillisecondsSinceEpoch(int millisecondsSinceEpoch, - {bool isUtc = false}) { - _date = DateTime.fromMillisecondsSinceEpoch(millisecondsSinceEpoch, - isUtc: isUtc); - } - late DateTime _date; - - static Moment parse(String date) { - return Moment.fromDate(DateTime.parse(date)); - } - - @override - String toString() { - return _date.toString(); - } - - String fromNow(BuildContext context, [bool withoutPrefixOrSuffix = false]) { - return from(context, DateTime.now(), withoutPrefixOrSuffix); - } - - String from(BuildContext context, DateTime date, - [bool withoutPrefixOrSuffix = false]) { - final Duration diff = date.difference(_date); - - String timeString = ''; - - if (diff.inSeconds.abs() < 45) - timeString = S.of(context).aF3wSeconds; - else if (diff.inMinutes.abs() < 2) - timeString = S.of(context).aMinute; - else if (diff.inMinutes.abs() < 45) - timeString = S.of(context).inMinutes(diff.inMinutes.abs().toString()); - else if (diff.inHours.abs() < 2) - timeString = S.of(context).anHour; - else if (diff.inHours.abs() < 22) - timeString = S.of(context).inHours(diff.inHours.abs().toString()); - else if (diff.inDays.abs() < 2) - timeString = S.of(context).aDay; - else if (diff.inDays.abs() < 26) - timeString = S.of(context).inDays(diff.inDays.abs().toString()); - else if (diff.inDays.abs() < 60) - timeString = S.of(context).aMonth; - else if (diff.inDays.abs() < 320) - timeString = S.of(context).inMonths((diff.inDays.abs() ~/ 30).toString()); - else if (diff.inDays.abs() < 547) - timeString = S.of(context).aYear; - else - timeString = S.of(context).inYears((diff.inDays.abs() ~/ 356).toString()); - - if (!withoutPrefixOrSuffix) { - if (diff.isNegative) - timeString = S.of(context).somethingAgo(timeString); - else - timeString = S.of(context).inSomething(timeString); - } - - return timeString; - } -} diff --git a/lib/dummyMapPlugin.dart b/lib/dummyMapPlugin.dart new file mode 100644 index 0000000..565d549 --- /dev/null +++ b/lib/dummyMapPlugin.dart @@ -0,0 +1,27 @@ +import 'package:flutter/src/widgets/framework.dart'; +import 'package:flutter_map/plugin_api.dart'; +import 'package:flutter/material.dart'; + +class DummyMapPluginOptions extends LayerOptions { + final String text; + + DummyMapPluginOptions({this.text = ""}); +} + +// https://github.com/apptreesoftware/flutter_map/blob/master/flutter_map_example/lib/pages/plugin_api.dart +class DummyMapPlugin extends MapPlugin { + + @override + Widget createLayer(LayerOptions options, MapState mapState) { + if (options is DummyMapPluginOptions) { + return const SizedBox(); + } + throw ("Unknown options type for DummyMapPlugin" + "plugin: $options"); + } + + @override + bool supportsLayer(LayerOptions options) { + return options is DummyMapPluginOptions; + } +} diff --git a/lib/dummy_map_plugin.dart b/lib/dummy_map_plugin.dart deleted file mode 100644 index d4511f9..0000000 --- a/lib/dummy_map_plugin.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'package:flutter/material.dart'; - -/// Dummy/placeholder widget for map plugins -class DummyMapPluginWidget extends StatelessWidget { - const DummyMapPluginWidget({super.key}); - - @override - Widget build(BuildContext context) { - return const SizedBox(); - } -} diff --git a/lib/fileUtils.dart b/lib/fileUtils.dart new file mode 100644 index 0000000..54a8151 --- /dev/null +++ b/lib/fileUtils.dart @@ -0,0 +1,32 @@ +import 'dart:async'; +import 'package:flutter/services.dart' show rootBundle; +import 'package:meta/meta.dart'; + +final esRegExp = new RegExp("^es-"); + +String getFallbackLang(String lang) { + if (lang == 'ast' || lang == 'gl' || lang == 'eu' || + lang == 'es' || lang == 'ca' || (esRegExp).allMatches(lang).length > 0) { + return 'es'; + } + return 'en'; +} + +Future getFileNameOfLang({@required String dir,@required String fileName,@required String ext,@required String lang}) async { + String base = '$dir/$fileName'; + String fallback = getFallbackLang(lang); + String file = '${base}-${lang}.${ext}'; + if (await assetNotExists(file)) { + file = '${base}-${fallback}.${ext}'; + if (await assetNotExists(file)) { + file = '${base}.${ext}'; + } + } + return file; +} + +// https://github.com/flutter/flutter/issues/15325 +Future assetNotExists(String asset) { + print('Does not exists $asset ?'); + return rootBundle.load(asset).then((_) => false).catchError((err, stack) { print (err); print(stack); return true;}); +} \ No newline at end of file diff --git a/lib/file_utils.dart b/lib/file_utils.dart deleted file mode 100644 index dddadc6..0000000 --- a/lib/file_utils.dart +++ /dev/null @@ -1,44 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/services.dart' show rootBundle; - -final RegExp esRegExp = RegExp('^es-'); - -String getFallbackLang(String lang) { - if (lang == 'ast' || - lang == 'gl' || - lang == 'eu' || - lang == 'es' || - lang == 'ca' || - esRegExp.allMatches(lang).isNotEmpty) { - return 'es'; - } - return 'en'; -} - -Future getFileNameOfLang( - {required String dir, - required String fileName, - required String ext, - required String lang}) async { - final String base = '$dir/$fileName'; - final String fallback = getFallbackLang(lang); - String file = '$base-$lang.$ext'; - if (await assetNotExists(file)) { - file = '$base-$fallback.$ext'; - if (await assetNotExists(file)) { - file = '$base.$ext'; - } - } - return file; -} - -// https://github.com/flutter/flutter/issues/15325 -Future assetNotExists(String asset) { - return rootBundle - .load(asset) - .then((_) => false) - .catchError((Object err, StackTrace stack) { - return true; - }); -} diff --git a/lib/fireAlert.dart b/lib/fireAlert.dart new file mode 100644 index 0000000..3fd2322 --- /dev/null +++ b/lib/fireAlert.dart @@ -0,0 +1,119 @@ +import 'package:community_material_icon/community_material_icon.dart'; +import 'package:comunes_flutter/comunes_flutter.dart'; +import 'package:flutter/material.dart'; +import 'package:share/share.dart'; +import 'package:url_launcher/url_launcher.dart'; +import 'customStepper.dart'; +import 'mainDrawer.dart'; + +import 'generated/i18n.dart'; +import 'placesAutocompleteUtils.dart'; + +class FireAlert extends StatefulWidget { + static const String routeName = '/fireAlert'; + + @override + _FireAlertState createState() => _FireAlertState(); +} + +class _FireAlertState extends State { + final GlobalKey _scaffoldKey = new GlobalKey(); + int _currentStep = 0; + + Widget buildCallButton() { + return new Align( + alignment: const Alignment(0.0, -0.2), + child: new FloatingActionButton( + heroTag: 'callAction', + child: const Icon(Icons.call), + onPressed: () { + launch("tel://112"); + }, + ), + ); + } + + Widget buildNotifyNeighboursButton() { + return new Align( + alignment: const Alignment(0.0, -0.2), + child: new FloatingActionButton( + heroTag: 'neighAction', + child: const Icon(CommunityMaterialIcons.bullhorn), + onPressed: () { + _scaffoldKey.currentState.showSnackBar(new SnackBar( + content: new Text(S.of(context).inDevelopment), + )); + }, + ), + ); + } + + + Widget buildTweetButton() { + return new Align( + alignment: const Alignment(0.0, -0.2), + child: new FloatingActionButton( + heroTag: 'tweetAction', + child: const Icon(CommunityMaterialIcons.twitter), + onPressed: () { + // In Android you can choose with app to use with setPackage but seems it's not implemented here + // https://stackoverflow.com/questions/6814268/android-share-on-facebook-twitter-mail-ecc + openPlacesDialog(_scaffoldKey).then((yourLocation) { + String where = + yourLocation.description.replaceAll(' ', '').split(',')[0]; + print(where); + Share.share(S.of(context).tweetAboutSelf(yourLocation.description, '#IF${where}')); + }).catchError((onError) { + _scaffoldKey.currentState.showSnackBar(new SnackBar( + content: new Text( + S.of(_scaffoldKey.currentContext).errorFirePlaceDialog))); + }); + }, + ), + ); + } + + List listWithoutNulls(List children) => + children.where(notNull).toList(); + + @override + Widget build(BuildContext context) { + List fireSteps = listWithoutNulls([ + new CustomStep( + title: new Text(S.of(context).callEmergencyServicesTitle), + content: new Column(children: [ + new Text(S.of(context).callEmergencyServicesDescription), + new SizedBox(height: 20.0), + buildCallButton() + ])), + new CustomStep( + title: new Text(S.of(context).notifyNeighbours), + // state: CustomStepState.disabled, + content: new Column(children: [ + new Text(S.of(context).notifyNeighboursDescription), + new SizedBox(height: 20.0), + buildNotifyNeighboursButton() + ])), + // TODO conditional: this only in Spain + new CustomStep( + title: new Text(S.of(context).tweetAboutAFireTitle), + // subtitle: new Text(S.of(context).tweetAboutAFireDescription), + content: new Column(children: [ + new Text(S.of(context).tweetAboutAFireDescription), + new SizedBox(height: 20.0), + buildTweetButton() + ])), + ]); + return Scaffold( + key: _scaffoldKey, + appBar: new AppBar(title: new Text(S.of(context).notifyAFire)), + drawer: new MainDrawer(context, FireAlert.routeName), + body: new CustomStepper( + currentCustomStep: _currentStep, + // type: StepperType.horizontal, + onCustomStepTapped: (num) => setState(() { + _currentStep = num; + }), + steps: fireSteps)); + } +} diff --git a/lib/fire_mark_type.dart b/lib/fireMarkType.dart similarity index 98% rename from lib/fire_mark_type.dart rename to lib/fireMarkType.dart index bff5386..ba953bc 100644 --- a/lib/fire_mark_type.dart +++ b/lib/fireMarkType.dart @@ -4,4 +4,4 @@ enum FireMarkType { fire, industry, falsePos -} +} \ No newline at end of file diff --git a/lib/fireMarker.dart b/lib/fireMarker.dart new file mode 100644 index 0000000..a11092b --- /dev/null +++ b/lib/fireMarker.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_map/flutter_map.dart'; +import 'package:flutter_map/plugin_api.dart'; +import 'package:latlong/latlong.dart'; +import 'fireMarkerIcon.dart'; +import 'fireMarkType.dart'; + +class FireMarker extends Marker { + + FireMarker(LatLng pos, type, + [onTap = null, AnchorPos anchor = AnchorPos.center, Anchor anchorOverride]) + : super( + width: 80.0, + height: 80.0, + point: pos, + builder: (ctx) => new Container( + child: new GestureDetector( + child: new FireMarkerIcon(type), onTap: onTap) + ), + anchor: anchor, + anchorOverride: anchorOverride ?? type == FireMarkType.position + ? new Anchor(40.0, 20.0) + : type == FireMarkType.fire + ? new Anchor(40.0, 24.0) + : type == FireMarkType.pixel + ? null // auto calculate with height/width in Marker + // industries, etc (below) + // FIXME check if this is accurate + : new Anchor(40.0, 35.0), + ); +} diff --git a/lib/fireMarkerIcon.dart b/lib/fireMarkerIcon.dart new file mode 100644 index 0000000..4e8c5ab --- /dev/null +++ b/lib/fireMarkerIcon.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; +import 'fireMarkType.dart'; +import 'colors.dart'; + +class FireMarkerIcon extends StatelessWidget { + final FireMarkType type; + + FireMarkerIcon(this.type); + + @override + Widget build(BuildContext context) { + switch (type) { + case FireMarkType.position: + return new Icon(Icons.location_on, color: fires600, size: 50.0); + case FireMarkType.pixel: + return new Icon(Icons.brightness_1, color: fires900, size: 3.0); + case FireMarkType.fire: + return new Image.asset('images/fire-marker-l.png'); + case FireMarkType.industry: + return new Image.asset('images/industry-marker-reg.png'); + case FireMarkType.falsePos: + default: + return new Image.asset('images/industry-marker.png'); + } + } +} diff --git a/lib/fireNotificationList.dart b/lib/fireNotificationList.dart new file mode 100644 index 0000000..da131e5 --- /dev/null +++ b/lib/fireNotificationList.dart @@ -0,0 +1,262 @@ +import 'dart:async'; + +import 'package:comunes_flutter/comunes_flutter.dart'; +import 'package:fires_flutter/models/fireNotification.dart'; +import 'package:fires_flutter/models/yourLocation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_redux/flutter_redux.dart'; +import 'package:redux/src/store.dart'; + +import 'customMoment.dart'; +import 'generated/i18n.dart'; +import 'genericMap.dart'; +import 'mainDrawer.dart'; +import 'models/appState.dart'; +import 'redux/actions.dart'; +import 'firesSpinner.dart'; + +@immutable +class _ViewModel { + final bool isLoaded; + final List fireNotifications; + final int fireNotificationsUnread; + final List yourLocations; + final TapFireNotificationFunction onTap; + final DeleteFireNotificationFunction onDelete; + final DeleteAllFireNotificationFunction onDeleteAll; + + _ViewModel( + {@required this.isLoaded, + @required this.onTap, + @required this.onDelete, + @required this.onDeleteAll, + @required this.fireNotifications, + @required this.yourLocations, + @required this.fireNotificationsUnread}); + + @override + bool operator ==(Object other) => + identical(this, other) || + other is _ViewModel && + runtimeType == other.runtimeType && + isLoaded == other.isLoaded && + fireNotifications == other.fireNotifications && + fireNotificationsUnread == other.fireNotificationsUnread && + yourLocations == other.yourLocations; + + @override + int get hashCode => + isLoaded.hashCode ^ + fireNotifications.hashCode ^ + fireNotificationsUnread.hashCode ^ + yourLocations.hashCode; +} + +class FireNotificationList extends StatefulWidget { + static const String routeName = '/fireNotifications'; + + @override + _FireNotificationListState createState() => _FireNotificationListState(); +} + +class _FireNotificationListState extends State { + final GlobalKey _scaffoldKey = new GlobalKey(); + + Widget _buildRow(BuildContext context, List yourLocations, + FireNotification notif, onDeleted, onTap) { + String prefix = ""; + + if (notif.subsId != null) { + // FIXME (this can fails if you don't have a location for this notif, for instance during tests) + YourLocation yl = + yourLocations.singleWhere((yl) => yl.id == notif.subsId); + prefix = '${yl.description}. '; + } + + return new ListTile( + dense: true, + leading: const Icon(Icons.whatshot), + title: new Text('${prefix}${notif.description}', + style: new TextStyle( + fontWeight: notif.read ? FontWeight.normal : FontWeight.bold)), + subtitle: new Text(Moment.now().from(context, notif.when)), + onLongPress: () { + showSnackMsg(S.of(context).toDeleteThisNotification); + }, + onTap: () { + onTap(notif); + }); + } + + void showSnackMsg(String msg) { + _scaffoldKey.currentState.showSnackBar(new SnackBar( + content: new Text(msg), + )); + } + + Widget _buildSavedFireNotifications( + BuildContext context, + List yourLocations, + List notifList, + onDeleted, + onTap) { + return new RefreshIndicator( + child: new ListView.builder( + padding: const EdgeInsets.all(16.0), + // reverse: true, + // shrinkWrap: true, + itemCount: notifList.length, + itemBuilder: (BuildContext _context, int i) { + final ThemeData theme = Theme.of(context); + return new Dismissible( + key: new ObjectKey(notifList.elementAt(i)), + direction: DismissDirection.horizontal, + onDismissed: (DismissDirection direction) { + onDeleted(notifList.elementAt(i)); + }, + background: new Container( + color: theme.primaryColor, + child: const ListTile( + leading: const Icon(Icons.delete, + color: Colors.white, size: 36.0))), + secondaryBackground: new Container( + color: theme.primaryColor, + child: const ListTile( + trailing: const Icon(Icons.delete, + color: Colors.white, size: 36.0))), + child: new Container( + decoration: new BoxDecoration( + color: theme.canvasColor, + border: new Border( + bottom: + new BorderSide(color: theme.dividerColor))), + child: _buildRow(context, yourLocations, + notifList.elementAt(i), onDeleted, onTap))); + }), + onRefresh: _handleRefresh); + } + + Future _handleRefresh() async { + await new Future.delayed(new Duration(seconds: 1)); + + setState(() {}); + + return null; + } + + @override + Widget build(BuildContext context) { + return new StoreConnector( + distinct: true, + converter: (store) { + print('New ViewModel of Fires Notifications (unread: ${store.state + .fireNotificationsUnread})'); + return new _ViewModel( + isLoaded: store.state.isLoaded, + onDeleteAll: () { + store.dispatch(new DeleteAllFireNotificationAction()); + }, + onDelete: (notif) { + store.dispatch(new DeleteFireNotificationAction(notif)); + _scaffoldKey.currentState.showSnackBar(new SnackBar( + content: new Text(S.of(context).youDeletedThisNotification), + action: new SnackBarAction( + label: S.of(context).UNDO, + onPressed: () { + store.dispatch(new AddFireNotificationAction(notif)); + }))); + }, + onTap: (notif) { + if (!notif.read) { + store.dispatch(new ReadFireNotificationAction( + notif.copyWith(read: true))); + } + new Timer(new Duration(milliseconds: 500), () { + gotoMap(store, notif, context); + }); + }, + yourLocations: store.state.yourLocations, + fireNotifications: store.state.fireNotifications, + fireNotificationsUnread: store.state.fireNotificationsUnread); + }, + builder: (context, view) { + var hasFireNotifications = view.fireNotifications.length > 0; + final title = S.of(context).fireNotificationsTitle; + print('Building Fire Notifications List'); + + return Scaffold( + key: _scaffoldKey, + drawer: new MainDrawer(context, FireNotificationList.routeName), + appBar: new AppBar( + title: Text(title), + leading: IconButton( + icon: Icon(Icons.menu), + onPressed: () { + _scaffoldKey.currentState.openDrawer(); + }, + ), + actions: listWithoutNulls([ + hasFireNotifications + ? IconButton( + icon: Icon(Icons.delete), + onPressed: () => _showConfirmDialog(view)) + : null + ])), + body: !view.isLoaded ? new FiresSpinner() : !hasFireNotifications + ? Padding( + padding: const EdgeInsets.all(20.0), + child: new Card( + child: new Padding( + padding: const EdgeInsets.all(20.0), + child: new CenteredColumn(children: [ + new Icon(Icons.notifications_none, + size: 150.0, color: Colors.black26), + new SizedBox(height: 20.0), + new Text( + S.of(context).fireNotificationsDescription, + textAlign: TextAlign.center, + textScaleFactor: 1.1, + style: new TextStyle( + height: 1.3, color: Colors.black45)) + ])))) + : _buildSavedFireNotifications(context, view.yourLocations, + view.fireNotifications, view.onDelete, view.onTap)); + }); + } + + void gotoMap( + Store store, FireNotification notif, BuildContext context) { + store.dispatch(new ShowFireNotificationMapAction(notif)); + Navigator.push( + context, new MaterialPageRoute(builder: (context) => new genericMap())); + } + + _showConfirmDialog(_ViewModel view) { + return showDialog( + context: context, + barrierDismissible: false, // user must tap button! + builder: (BuildContext context) { + return new AlertDialog( + title: new Text(S.of(context).areYouSureTitle), + content: new SingleChildScrollView( + child: new ListBody( + children: [ + new Text( + S.of(context).deleteAllFireNotificationsAlertDescription) + ], + ), + ), + actions: [ + new FlatButton( + child: new Text(S.of(context).DELETE), + onPressed: () { + view.onDeleteAll(); + Navigator.of(context).pop(); + }, + ), + ], + ); + }, + ); + } +} diff --git a/lib/fire_alert.dart b/lib/fire_alert.dart deleted file mode 100644 index ca33548..0000000 --- a/lib/fire_alert.dart +++ /dev/null @@ -1,124 +0,0 @@ -import 'package:community_material_icon/community_material_icon.dart'; -import 'package:flutter/material.dart'; -import 'package:share_plus/share_plus.dart'; -import 'package:url_launcher/url_launcher.dart'; - -import 'custom_stepper.dart'; -import 'generated/i18n.dart'; -import 'main_drawer.dart'; -import 'models/your_location.dart'; -import 'places_autocomplete_utils.dart'; - -class FireAlert extends StatefulWidget { - const FireAlert({super.key}); - - static const String routeName = '/fireAlert'; - - @override - // ignore: library_private_types_in_public_api - _FireAlertState createState() => _FireAlertState(); -} - -class _FireAlertState extends State { - final GlobalKey _scaffoldKey = GlobalKey(); - int _currentStep = 0; - - Widget buildCallButton() { - return Align( - alignment: const Alignment(0.0, -0.2), - child: FloatingActionButton( - heroTag: 'callAction', - child: const Icon(Icons.call), - onPressed: () async { - final Uri url = Uri(scheme: 'tel', path: '112'); - if (await canLaunchUrl(url)) { - await launchUrl(url); - } - }, - ), - ); - } - - Widget buildNotifyNeighboursButton() { - return Align( - alignment: const Alignment(0.0, -0.2), - child: FloatingActionButton( - heroTag: 'neighAction', - child: const Icon(CommunityMaterialIcons.bullhorn), - onPressed: () { - ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Text(S.of(context).inDevelopment), - )); - }, - ), - ); - } - - Widget buildTweetButton() { - final S strings = S.of(context); - return Align( - alignment: const Alignment(0.0, -0.2), - child: FloatingActionButton( - heroTag: 'tweetAction', - child: const Icon(CommunityMaterialIcons.twitter), - onPressed: () { - openPlacesDialog(_scaffoldKey).then((YourLocation yourLocation) { - final String where = - yourLocation.description.replaceAll(' ', '').split(',')[0]; - Share.shareWithResult( - strings.tweetAboutSelf(yourLocation.description, '#IF$where')); - }).catchError((Object onError) { - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(strings.errorFirePlaceDialog))); - } - }); - }, - ), - ); - } - - List listWithoutNulls(List children) => - children.whereType().toList(); - - @override - Widget build(BuildContext context) { - final List fireSteps = listWithoutNulls([ - CustomStep( - title: Text(S.of(context).callEmergencyServicesTitle), - content: Column(children: [ - Text(S.of(context).callEmergencyServicesDescription), - const SizedBox(height: 20.0), - buildCallButton() - ])), - CustomStep( - title: Text(S.of(context).notifyNeighbours), - // state: CustomStepState.disabled, - content: Column(children: [ - Text(S.of(context).notifyNeighboursDescription), - const SizedBox(height: 20.0), - buildNotifyNeighboursButton() - ])), - // TODOconditional: this only in Spain - CustomStep( - title: Text(S.of(context).tweetAboutAFireTitle), - // subtitle: new Text(S.of(context).tweetAboutAFireDescription), - content: Column(children: [ - Text(S.of(context).tweetAboutAFireDescription), - const SizedBox(height: 20.0), - buildTweetButton() - ])), - ]); - return Scaffold( - key: _scaffoldKey, - appBar: AppBar(title: Text(S.of(context).notifyAFire)), - drawer: MainDrawer(context, FireAlert.routeName), - body: CustomStepper( - currentCustomStep: _currentStep, - // type: StepperType.horizontal, - onCustomStepTapped: (int num) => setState(() { - _currentStep = num; - }), - steps: fireSteps)); - } -} diff --git a/lib/fire_marker.dart b/lib/fire_marker.dart deleted file mode 100644 index ea2fe45..0000000 --- a/lib/fire_marker.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_map/flutter_map.dart'; -import 'package:latlong2/latlong.dart'; - -import 'fire_mark_type.dart'; -import 'fire_marker_icon.dart'; - -/// Create a Marker with custom positioning for fires and other map objects -Marker fireMarker( - LatLng pos, - FireMarkType type, [ - VoidCallback? onTap, -]) { - return Marker( - point: pos, - width: 80.0, - height: 80.0, - alignment: Alignment.center, - child: GestureDetector( - onTap: onTap, - child: FireMarkerIcon(type), - ), - ); -} diff --git a/lib/fire_marker_icon.dart b/lib/fire_marker_icon.dart deleted file mode 100644 index 1956f9f..0000000 --- a/lib/fire_marker_icon.dart +++ /dev/null @@ -1,22 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'colors.dart'; -import 'fire_mark_type.dart'; - -class FireMarkerIcon extends StatelessWidget { - const FireMarkerIcon(this.type, {super.key}); - final FireMarkType type; - - @override - Widget build(BuildContext context) { - return switch (type) { - FireMarkType.position => - const Icon(Icons.location_on, color: fires600, size: 50.0), - FireMarkType.pixel => - const Icon(Icons.brightness_1, color: fires900, size: 3.0), - FireMarkType.fire => Image.asset('images/fire-marker-l.png'), - FireMarkType.industry => Image.asset('images/industry-marker-reg.png'), - FireMarkType.falsePos => Image.asset('images/industry-marker.png'), - }; - } -} diff --git a/lib/fire_notification_list.dart b/lib/fire_notification_list.dart deleted file mode 100644 index 634cc5e..0000000 --- a/lib/fire_notification_list.dart +++ /dev/null @@ -1,275 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; -import 'package:flutter_redux/flutter_redux.dart'; -import 'package:redux/redux.dart'; - -import 'custom_moment.dart'; -import 'fires_spinner.dart'; -import 'generated/i18n.dart'; -import 'generic_map.dart'; -import 'main_drawer.dart'; -import 'models/app_state.dart'; -import 'models/fire_notification.dart'; -import 'models/your_location.dart'; -import 'redux/actions.dart'; - -@immutable -class _ViewModel { - const _ViewModel( - {required this.isLoaded, - required this.onTap, - required this.onDelete, - required this.onDeleteAll, - required this.fireNotifications, - required this.yourLocations, - required this.fireNotificationsUnread}); - final bool isLoaded; - final List fireNotifications; - final int fireNotificationsUnread; - final List yourLocations; - final TapFireNotificationFunction onTap; - final DeleteFireNotificationFunction onDelete; - final DeleteAllFireNotificationFunction onDeleteAll; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is _ViewModel && - runtimeType == other.runtimeType && - isLoaded == other.isLoaded && - fireNotifications == other.fireNotifications && - fireNotificationsUnread == other.fireNotificationsUnread && - yourLocations == other.yourLocations; - - @override - int get hashCode => - isLoaded.hashCode ^ - fireNotifications.hashCode ^ - fireNotificationsUnread.hashCode ^ - yourLocations.hashCode; -} - -class FireNotificationList extends StatefulWidget { - const FireNotificationList({super.key}); - - static const String routeName = '/fireNotifications'; - - @override - // ignore: library_private_types_in_public_api - _FireNotificationListState createState() => _FireNotificationListState(); -} - -class _FireNotificationListState extends State { - final GlobalKey _scaffoldKey = GlobalKey(); - - Widget _buildRow( - BuildContext context, - List yourLocations, - FireNotification notif, - DeleteFireNotificationFunction onDeleted, - TapFireNotificationFunction onTap) { - String prefix = ''; - - // FIXME (this can fails if you don't have a location for this notif, for instance during tests) - final YourLocation yl = - yourLocations.singleWhere((YourLocation yl) => yl.id == notif.subsId); - prefix = '${yl.description}. '; - - return ListTile( - dense: true, - leading: const Icon(Icons.whatshot), - title: Text('$prefix${notif.description}', - style: TextStyle( - fontWeight: notif.read ? FontWeight.normal : FontWeight.bold)), - subtitle: Text(Moment.now().from(context, notif.when)), - onLongPress: () { - showSnackMsg(S.of(context).toDeleteThisNotification); - }, - onTap: () { - onTap(notif); - }); - } - - void showSnackMsg(String msg) { - ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Text(msg), - )); - } - - Widget _buildSavedFireNotifications( - BuildContext context, - List yourLocations, - List notifList, - DeleteFireNotificationFunction onDeleted, - TapFireNotificationFunction onTap) { - return RefreshIndicator( - onRefresh: _handleRefresh, - child: ListView.builder( - padding: const EdgeInsets.all(16.0), - // reverse: true, - // shrinkWrap: true, - itemCount: notifList.length, - itemBuilder: (BuildContext context, int i) { - final ThemeData theme = Theme.of(context); - return Dismissible( - key: ObjectKey(notifList.elementAt(i)), - onDismissed: (DismissDirection direction) { - onDeleted(notifList.elementAt(i)); - }, - background: Container( - color: theme.primaryColor, - child: const ListTile( - leading: Icon(Icons.delete, - color: Colors.white, size: 36.0))), - secondaryBackground: Container( - color: theme.primaryColor, - child: const ListTile( - trailing: Icon(Icons.delete, - color: Colors.white, size: 36.0))), - child: Container( - decoration: BoxDecoration( - color: theme.canvasColor, - border: Border( - bottom: BorderSide(color: theme.dividerColor))), - child: _buildRow(context, yourLocations, - notifList.elementAt(i), onDeleted, onTap))); - })); - } - - Future _handleRefresh() async { - await Future.delayed(const Duration(seconds: 1)); - - setState(() {}); - - return; - } - - @override - Widget build(BuildContext context) { - return StoreConnector( - distinct: true, - converter: (Store store) { - return _ViewModel( - isLoaded: store.state.isLoaded, - onDeleteAll: () { - store.dispatch(DeleteAllFireNotificationAction()); - }, - onDelete: (FireNotification notif) { - store.dispatch(DeleteFireNotificationAction(notif)); - ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Text(S.of(context).youDeletedThisNotification), - action: SnackBarAction( - label: S.of(context).UNDO, - onPressed: () { - store.dispatch(AddFireNotificationAction(notif)); - }))); - }, - onTap: (FireNotification notif) { - if (!notif.read) { - store.dispatch( - ReadFireNotificationAction(notif.copyWith(read: true))); - } - Timer(const Duration(milliseconds: 500), () { - gotoMap(store, notif, context); - }); - }, - yourLocations: store.state.yourLocations, - fireNotifications: store.state.fireNotifications, - fireNotificationsUnread: store.state.fireNotificationsUnread); - }, - builder: (BuildContext context, _ViewModel view) { - final bool hasFireNotifications = view.fireNotifications.isNotEmpty; - final String title = S.of(context).fireNotificationsTitle; - - return Scaffold( - key: _scaffoldKey, - drawer: MainDrawer(context, FireNotificationList.routeName), - appBar: AppBar( - title: Text(title), - leading: IconButton( - icon: const Icon(Icons.menu), - onPressed: () { - _scaffoldKey.currentState?.openDrawer(); - }, - ), - actions: [ - if (hasFireNotifications) - IconButton( - icon: const Icon(Icons.delete), - onPressed: () => _showConfirmDialog(view)) - else - null - ].where((Widget? w) => w != null).cast().toList()), - body: !view.isLoaded - ? const FiresSpinner() - : !hasFireNotifications - ? Padding( - padding: const EdgeInsets.all(20.0), - child: Card( - child: Padding( - padding: const EdgeInsets.all(20.0), - child: Column( - mainAxisAlignment: - MainAxisAlignment.center, - children: [ - const Icon(Icons.notifications_none, - size: 150.0, color: Colors.black26), - const SizedBox(height: 20.0), - Text( - S - .of(context) - .fireNotificationsDescription, - textAlign: TextAlign.center, - textScaler: - const TextScaler.linear(1.1), - style: const TextStyle( - height: 1.3, - color: Colors.black45)) - ])))) - : _buildSavedFireNotifications( - context, - view.yourLocations, - view.fireNotifications, - view.onDelete, - view.onTap)); - }); - } - - void gotoMap( - Store store, FireNotification notif, BuildContext context) { - store.dispatch(ShowFireNotificationMapAction(notif)); - Navigator.push( - context, - MaterialPageRoute( - builder: (BuildContext context) => const GenericMap())); - } - - Future _showConfirmDialog(_ViewModel view) { - return showDialog( - context: context, - barrierDismissible: false, // user must tap button! - builder: (BuildContext context) { - return AlertDialog( - title: Text(S.of(context).areYouSureTitle), - content: SingleChildScrollView( - child: ListBody( - children: [ - Text(S.of(context).deleteAllFireNotificationsAlertDescription) - ], - ), - ), - actions: [ - TextButton( - child: Text(S.of(context).DELETE), - onPressed: () { - view.onDeleteAll(); - Navigator.of(context).pop(); - }, - ), - ], - ); - }, - ); - } -} diff --git a/lib/firesApp.dart b/lib/firesApp.dart new file mode 100644 index 0000000..5d0d4f2 --- /dev/null +++ b/lib/firesApp.dart @@ -0,0 +1,88 @@ + +import 'package:comunes_flutter/comunes_flutter.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flutter_redux/flutter_redux.dart'; +import 'package:redux/redux.dart'; +import 'package:redux/src/store.dart'; + +import 'globals.dart'; +import 'monitoredAreas.dart'; +import 'activeFires.dart'; +import 'fireAlert.dart'; +import 'fireNotificationList.dart'; +import 'generated/i18n.dart'; +import 'homePage.dart'; +import 'introPage.dart'; +import 'models/appState.dart'; +import 'privacyPage.dart'; +import 'redux/actions.dart'; +import 'sandbox.dart'; +import 'supportPage.dart'; +import 'theme.dart'; +import 'themeDev.dart'; + +class FiresApp extends StatefulWidget { + FiresApp(this.store); + + final Store store; + + @override + _FiresAppState createState() => _FiresAppState(store); +} + +class _FiresAppState extends State { + final GlobalKey navigatorKey = + new GlobalKey(); + static final WidgetBuilder introWidget = (context) => new IntroPage(); + static final WidgetBuilder continueWidget = (context) => new HomePage(); + + final Map routes = { + IntroPage.routeName: introWidget, + HomePage.routeName: continueWidget, + PrivacyPage.routeName: (BuildContext context) => new PrivacyPage(context), + ActiveFiresPage.routeName: (BuildContext context) => new ActiveFiresPage(), + Sandbox.routeName: (BuildContext context) => new Sandbox(), + FireAlert.routeName: (BuildContext context) => new FireAlert(), + SupportPage.routeName: (BuildContext context) => new SupportPage(), + FireNotificationList.routeName: (BuildContext context) => + new FireNotificationList(), + MonitoredAreasPage.routeName: (BuildContext context) => new MonitoredAreasPage() + }; + + final Store store; + + + // globals.getIt.registerSingleton + _FiresAppState(this.store); + + + @override + Widget build(BuildContext context) { + StatefulWidget home = new MaterialAppWithIntroHome( + introWidget, continueWidget, 'showInitialWizard-2018-06-27-01'); + return new StoreProvider( + store: this.store, + child: new MaterialApp( + navigatorKey: navigatorKey, + localizationsDelegates: [ + S.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + ], + supportedLocales: S.delegate.supportedLocales, + localeResolutionCallback: + S.delegate.resolution(fallback: new Locale("en", "")), + home: home, + onGenerateTitle: (context) { + print('MaterialApp onGenerateTitle'); + if (store.state.user.lang == null) { + String lang = Localizations.localeOf(context).languageCode; + this.store.dispatch(new OnUserLangAction(lang)); + } + return S.of(context).appName; + }, + theme: isDevelopment? devFiresTheme: firesTheme, + routes: routes)); + } +} diff --git a/lib/fires_spinner.dart b/lib/firesSpinner.dart similarity index 72% rename from lib/fires_spinner.dart rename to lib/firesSpinner.dart index c074694..d13c94b 100644 --- a/lib/fires_spinner.dart +++ b/lib/firesSpinner.dart @@ -3,10 +3,8 @@ import 'package:flutter_spinkit/flutter_spinkit.dart'; import 'colors.dart'; class FiresSpinner extends StatelessWidget { - const FiresSpinner({super.key}); - @override Widget build(BuildContext context) { - return const SpinKitPulse(color: fires600); + return new SpinKitPulse(color: fires600); } } diff --git a/lib/fires_app.dart b/lib/fires_app.dart deleted file mode 100644 index 172b309..0000000 --- a/lib/fires_app.dart +++ /dev/null @@ -1,84 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_localizations/flutter_localizations.dart'; -import 'package:flutter_redux/flutter_redux.dart'; -import 'package:redux/redux.dart'; - -import 'active_fires.dart'; -import 'fire_alert.dart'; -import 'fire_notification_list.dart'; -import 'generated/i18n.dart'; -import 'globals.dart'; -import 'home_page.dart'; -import 'intro_page.dart'; -import 'models/app_state.dart'; -import 'monitored_areas.dart'; -import 'privacy_page.dart'; -import 'sandbox.dart'; -import 'support_page.dart'; -import 'theme.dart'; -import 'theme_dev.dart'; -import 'widgets/material_app_with_intro.dart'; - -class FiresApp extends StatefulWidget { - const FiresApp(this.store, {super.key}); - - final Store store; - - @override - // ignore: library_private_types_in_public_api - _FiresAppState createState() => _FiresAppState(); -} - -class _FiresAppState extends State { - // globals.getIt.registerSingleton - _FiresAppState(); - late final Store store; - final GlobalKey navigatorKey = GlobalKey(); - static Widget introWidget(BuildContext context) => IntroPage(); - static Widget continueWidget(BuildContext context) => const HomePage(); - - @override - void initState() { - super.initState(); - store = widget.store; - } - - final Map routes = { - IntroPage.routeName: introWidget, - HomePage.routeName: continueWidget, - PrivacyPage.routeName: (BuildContext context) => PrivacyPage(context), - ActiveFiresPage.routeName: (BuildContext context) => - const ActiveFiresPage(), - Sandbox.routeName: (BuildContext context) => const Sandbox(), - FireAlert.routeName: (BuildContext context) => const FireAlert(), - SupportPage.routeName: (BuildContext context) => const SupportPage(), - FireNotificationList.routeName: (BuildContext context) => - const FireNotificationList(), - MonitoredAreasPage.routeName: (BuildContext context) => - const MonitoredAreasPage() - }; - - @override - Widget build(BuildContext context) { - const StatefulWidget home = MaterialAppWithIntroHome( - introWidget, continueWidget, 'showInitialWizard-2018-06-27-01'); - return StoreProvider( - store: store, - child: MaterialApp( - navigatorKey: navigatorKey, - localizationsDelegates: const >[ - S.delegate, - GlobalMaterialLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - ], - supportedLocales: S.delegate.supportedLocales, - localeResolutionCallback: - S.delegate.resolution(fallback: const Locale('en', '')), - home: home, - onGenerateTitle: (BuildContext context) { - return S.of(context).appName; - }, - theme: isDevelopment ? devFiresTheme : firesTheme, - routes: routes)); - } -} diff --git a/lib/generated/i18n.dart b/lib/generated/i18n.dart index fcd4a4d..c4426c2 100644 --- a/lib/generated/i18n.dart +++ b/lib/generated/i18n.dart @@ -1,3 +1,4 @@ + import 'dart:async'; import 'package:flutter/foundation.dart'; @@ -15,95 +16,16 @@ class S implements WidgetsLocalizations { const GeneratedLocalizationsDelegate(); static S of(BuildContext context) => - Localizations.of(context, WidgetsLocalizations) ?? const S(); + Localizations.of(context, WidgetsLocalizations); @override TextDirection get textDirection => TextDirection.ltr; - // Minimal implementations for abstract members from WidgetsLocalizations - @override - String get copyButtonLabel => "Copy"; - @override - String get cutButtonLabel => "Cut"; - @override - String get lookUpButtonLabel => "Look Up"; - @override - String get noResultsFound => "No results found"; - @override - String get pastePlainTextLabel => "Paste Plain Text"; - @override - String get pasteButtonLabel => "Paste"; - @override - String get selectAllButtonLabel => "Select All"; - @override - String get searchWebButtonLabel => "Search Web"; - @override - String get shareButtonLabel => "Share"; - @override - String get clearButtonTooltip => "Clear"; - @override - String get collapsedIconButtonLabel => "Expand"; - @override - String get expandedIconButtonLabel => "Collapse"; - @override - String get openAppDrawerTooltip => "Open navigation menu"; - @override - String get backButtonTooltip => "Back"; - @override - String get closeButtonLabel => "Close"; - @override - String get closeButtonTooltip => "Close"; - @override - String get nextMonthTooltip => "Next month"; - @override - String get previousMonthTooltip => "Previous month"; - @override - String get nextPageTooltip => "Next page"; - @override - String get previousPageTooltip => "Previous page"; - @override - String get firstPageTooltip => "First page"; - @override - String get lastPageTooltip => "Last page"; - @override - String get showMenuTooltip => "Show menu"; - @override - String tabLabel({required int tabIndex, required int tabCount}) => "Tab"; - @override - String get licensesPageTitle => "Licenses"; - @override - String get rowsPerPageTitle => "Rows per page:"; - @override - String get cancelButtonLabel => "CANCEL"; - @override - String get okButtonLabel => "OK"; - @override - String get radioButtonUnselectedLabel => "Unselected"; - @override - String get radioButtonSelectedLabel => "Selected"; - @override - String get reorderItemDown => "Move down"; - @override - String get reorderItemLeft => "Move left"; - @override - String get reorderItemRight => "Move right"; - @override - String get reorderItemUp => "Move up"; - @override - String get scriptCategory => "English"; - @override - String get reorderItemToEnd => "Move to end"; - @override - String get reorderItemToStart => "Move to start"; - @override - String get searchResultsFound => "Search results found"; - String get AvoidThisStringsIfisNotPlural => "Zero One Two Few Many Other"; String get CANCEL => "CANCEL"; String get CLOSE => "CLOSE"; String get DELETE => "DELETE"; - String get NASAAck => - "We acknowledge the use of data and imagery from LANCE FIRMS operated by the NASA/GSFC/Earth Science Data and Information System (ESDIS) with funding provided by NASA/HQ."; + String get NASAAck => "We acknowledge the use of data and imagery from LANCE FIRMS operated by the NASA/GSFC/Earth Science Data and Information System (ESDIS) with funding provided by NASA/HQ."; String get SAVE => "SAVE"; String get SHOW => "SHOW"; String get UNDO => "UNDO"; @@ -123,19 +45,16 @@ class S implements WidgetsLocalizations { String get areYouSureTitle => "Are you sure?"; String get byNASAsatellites => "by NASA satellites"; String get byOurUsers => "by one of our users"; - String get callEmergencyServicesDescription => - "You should call 112 if you have not already done so to notify the emergency services."; + String get callEmergencyServicesDescription => "You should call 112 if you have not already done so to notify the emergency services."; String get callEmergencyServicesTitle => "Call 112"; String get chooseAPlace => "Choose a place"; String get chooseAWatchRadio => "Choose a watch radio"; String get comunesSupportBtn => "Know and support our work"; String get confirm => "Confirm"; - String get deleteAllFireNotificationsAlertDescription => - "This will remove all your fire notifications"; + String get deleteAllFireNotificationsAlertDescription => "This will remove all your fire notifications"; String get errorFirePlaceDialog => "We couldn't get the location of the fire"; String get fireNotificationTitle => "Fire Notification"; - String get fireNotificationsDescription => - "Here you will receive fire notifications in locations you are subscribed to"; + String get fireNotificationsDescription => "Here you will receive fire notifications in locations you are subscribed to"; String get fireNotificationsTitle => "Fire Notifications"; String get fireNotificationsTitleShort => "Notifications"; String get firesInTheWorld => "Active fires in the world"; @@ -143,81 +62,62 @@ class S implements WidgetsLocalizations { String get firesNearPlace => "Fires near other place"; String get getAlertsOfFiresinThatArea => "Get alerts of fires in that area"; String get inDevelopment => "In development"; - String get inGreenMonitoredAreas => - "In green, the areas monitored by our users currently"; - String get isYourUbicationEnabled => - "I cannot get your current location. It's your ubication enabled?"; + String get inGreenMonitoredAreas => "In green, the areas monitored by our users currently"; + String get isYourUbicationEnabled => "I cannot get your current location. It's your ubication enabled?"; String get itSeemsAControlledBurning => "It's a controlled burning"; String get itSeemsAFalseAlarm => "It seems a false alarm"; String get itSeemsAIndustry => "It's an industry"; - String get itSeemsNotAtForesFire => - "It seems that this is not a forest fire."; - String get mapPrivacy => - "In order to preserve the privacy of our users, the reflected data are randomly altered and are only indicative."; + String get itSeemsNotAtForesFire => "It seems that this is not a forest fire."; + String get mapPrivacy => "In order to preserve the privacy of our users, the reflected data are randomly altered and are only indicative."; String get monitoredAreasTitle => "Monitored areas"; - String get noConnectivity => - "This application needs an Internet connection to work"; + String get noConnectivity => "This application needs an Internet connection to work"; String get noFiresAround => "There is no fires"; String get notAWildfire => "Isn't that a forest fire?"; - String get notPermsUbication => - "We don't have permission to get your location"; + String get notPermsUbication => "We don't have permission to get your location"; String get notifyAFire => "Notify a fire"; String get notifyNeighbours => "Notify other users"; - String get notifyNeighboursDescription => - "Alert other users of the area about the fire"; + String get notifyNeighboursDescription => "Alert other users of the area about the fire"; String get privacyPolicy => "Privacy Policy"; String get shareAppBtn => "Share our app"; String get starAppBtn => "Rate our app"; String get subscribedToFires => "Subscribed to fires notifications"; - String get supportPageDescription => - "You can support this initiative, spreading the word, helping us to develop our software, translating this app to your language, suggesting other public data sources of fires to use, etc."; + String get supportPageDescription => "You can support this initiative, spreading the word, helping us to develop our software, translating this app to your language, suggesting other public data sources of fires to use, etc."; String get supportThisInitiative => "Support this initiative"; String get thanksForParticipating => "Thanks for participating"; - String get toDeleteThisNotification => - "Slide horizontally to delete this notification"; + String get toDeleteThisNotification => "Slide horizontally to delete this notification"; String get toDeleteThisPlace => "Slide horizontally to delete this place"; String get toFiresNotifications => "Subscribe to fires notifications"; String get translateBtn => "Help with translations"; - String get tweetAboutAFireDescription => - "Additionally if you use twitter you can share additional information with the emergency services, for example, attaching photos to the tweet if you have good visibility of the fire, when you took the photos, exact location, etc. Use #hashtags type #IFMinicipalTerminal for example #IFJumilla (Forest Fire in Jumilla)."; + String get tweetAboutAFireDescription => "Additionally if you use twitter you can share additional information with the emergency services, for example, attaching photos to the tweet if you have good visibility of the fire, when you took the photos, exact location, etc. Use #hashtags type #IFMinicipalTerminal for example #IFJumilla (Forest Fire in Jumilla)."; String get tweetAboutAFireTitle => "Tweet about"; String get typeTheNameOfAPlace => "Type the name of a place, region, etc"; String get unsubscribe => "Unsubscribe"; String get unsubscribedToFires => "Unsubscribed to fires notifications"; - String get warningThisIsAVeryLargeArea => - "Warning: this is a very large area"; + String get warningThisIsAVeryLargeArea => "Warning: this is a very large area"; String get youDeletedThisNotification => "You deleted this notification"; String get youDeletedThisPlace => "You deleted this place"; - String activeFiresWorldWide(String activeFires) => - "$activeFires active fires worldwide"; - String additionalInfoAboutFire(String where, String when, String by) => - "Fire detected in $where $when $by"; - String appLicense(String thisYear) => - "(c) 2017-$thisYear Comunes Association under the GNU Affero GPL v3"; - String fireAroundThisArea(String kmAround) => - "A fire at $kmAround км around this area"; - String firesAroundThisArea(String numFires, String kmAround) => - "$numFires fires at $kmAround км around this area"; + String activeFiresWorldWide(String activeFires) => "$activeFires active fires worldwide"; + String additionalInfoAboutFire(String where, String when, String by) => "Fire detected in $where $when $by"; + String appLicense(String thisYear) => "(c) 2017-$thisYear Comunes Association under the GNU Affero GPL v3"; + String fireAroundThisArea(String kmAround) => "A fire at $kmAround км around this area"; + String firesAroundThisArea(String numFires, String kmAround) => "$numFires fires at $kmAround км around this area"; String inDays(String value) => "$value days"; String inHours(String value) => "$value hours"; String inMinutes(String value) => "$value minutes"; String inMonths(String value) => "$value months"; String inSomething(String something) => "in $something"; String inYears(String value) => "$value years"; - String noFiresAroundThisArea(String kmAround) => - "There is no fires at $kmAround км around this area"; + String noFiresAroundThisArea(String kmAround) => "There is no fires at $kmAround км around this area"; String somethingAgo(String something) => "$something ago"; - String subscribeToValueAroundThisArea(String sliderValue) => - "Subscribe to $sliderValue км around this area"; - String tweetAboutSelf(String location, String hash) => - "Fire in $location $hash"; + String subscribeToValueAroundThisArea(String sliderValue) => "Subscribe to $sliderValue км around this area"; + String tweetAboutSelf(String location, String hash) => "Fire in $location $hash"; String updatedLastCheck(String lastCheck) => "Updated $lastCheck"; } class gl extends S { const gl(); - @override + @override TextDirection get textDirection => TextDirection.ltr; @override @@ -233,19 +133,17 @@ class en extends S { class es extends S { const es(); - @override + @override TextDirection get textDirection => TextDirection.ltr; @override String get addYourCurrentPosition => "Añade tu ubicación actual"; @override - String get toDeleteThisNotification => - "Desliza horizontalmente para borrar esta notificación"; + String get toDeleteThisNotification => "Desliza horizontalmente para borrar esta notificación"; @override String get alertWhenThereIsAFire => "Alerta cuando hay un fuego"; @override - String get fireNotificationsDescription => - "Aquí recibirás las notificaciones de fuegos en los lugares a los que te subscribas"; + String get fireNotificationsDescription => "Aquí recibirás las notificaciones de fuegos en los lugares a los que te subscribas"; @override String get byOurUsers => "por uno de nuestros usuarios/as"; @override @@ -255,25 +153,19 @@ class es extends S { @override String get notifyAFire => "Notificar un fuego"; @override - String get callEmergencyServicesDescription => - "Deberías llamar al 112 si no lo has hecho ya para avisar a los servicios de emergencia."; + String get callEmergencyServicesDescription => "Deberías llamar al 112 si no lo has hecho ya para avisar a los servicios de emergencia."; @override String get CLOSE => "CERRAR"; @override - String get errorFirePlaceDialog => - "No hemos podido obtener la ubicación del fuego"; + String get errorFirePlaceDialog => "No hemos podido obtener la ubicación del fuego"; @override - String get toDeleteThisPlace => - "Desliza horizontalmente para borrar este lugar"; + String get toDeleteThisPlace => "Desliza horizontalmente para borrar este lugar"; @override - String get deleteAllFireNotificationsAlertDescription => - "Esto borrará todas las notificaciones de fuegos"; + String get deleteAllFireNotificationsAlertDescription => "Esto borrará todas las notificaciones de fuegos"; @override - String get tweetAboutAFireDescription => - "Adicionalmente si usas twitter puedes compartir información adicional con los servicios de emergencia, por ejemplo, adjuntando fotos al tweet si tienes buena visibilidad del fuego, cuando tomaste las fotos, ubicación exacta, etc. Usa #hashtags tipo #IFTerminoMunicipal por ejemplo #IFJumilla (Incendio Forestal en Jumilla)."; + String get tweetAboutAFireDescription => "Adicionalmente si usas twitter puedes compartir información adicional con los servicios de emergencia, por ejemplo, adjuntando fotos al tweet si tienes buena visibilidad del fuego, cuando tomaste las fotos, ubicación exacta, etc. Usa #hashtags tipo #IFTerminoMunicipal por ejemplo #IFJumilla (Incendio Forestal en Jumilla)."; @override - String get noConnectivity => - "Esta aplicación necesita una conexión a Internet para funcionar"; + String get noConnectivity => "Esta aplicación necesita una conexión a Internet para funcionar"; @override String get AvoidThisStringsIfisNotPlural => "Zero One Two Few Many Other"; @override @@ -285,8 +177,7 @@ class es extends S { @override String get toFiresNotifications => "Suscríbete a alertas de fuegos"; @override - String get notPermsUbication => - "No tenemos permisos para conocer tu ubicación"; + String get notPermsUbication => "No tenemos permisos para conocer tu ubicación"; @override String get fireNotificationTitle => "Notificación de fuego"; @override @@ -304,8 +195,7 @@ class es extends S { @override String get chooseAPlace => "Elige un lugar"; @override - String get mapPrivacy => - "Para preservar la privacidad de nuestros usuarios/as, los datos reflejados están aleatoriamente alterados y son solo orientativos."; + String get mapPrivacy => "Para preservar la privacidad de nuestros usuarios/as, los datos reflejados están aleatoriamente alterados y son solo orientativos."; @override String get firesNearPlace => "Fuegos cercanos a ti"; @override @@ -313,8 +203,7 @@ class es extends S { @override String get unsubscribedToFires => "Desuscrito a notificaciones de fuegos"; @override - String get itSeemsNotAtForesFire => - "Parece que este fuego no es un fuego forestal."; + String get itSeemsNotAtForesFire => "Parece que este fuego no es un fuego forestal."; @override String get UNDO => "DESHACER"; @override @@ -324,8 +213,7 @@ class es extends S { @override String get tweetAboutAFireTitle => "Twittea"; @override - String get supportPageDescription => - "Puedes apoyar esta iniciativa, dando difusión, ayudándonos a desarrollar nuestro software, traduciendo esta aplicación a tu idioma, sugeriendo otras fuentes de datos públicas de fuegos para usar, etc."; + String get supportPageDescription => "Puedes apoyar esta iniciativa, dando difusión, ayudándonos a desarrollar nuestro software, traduciendo esta aplicación a tu idioma, sugeriendo otras fuentes de datos públicas de fuegos para usar, etc."; @override String get aMinute => "un minuto"; @override @@ -333,13 +221,11 @@ class es extends S { @override String get firesInYourPlaces => "Fuegos en tus lugares"; @override - String get NASAAck => - "Reconocemos el uso de datos e imágenes de LANCE FIRMS operadas por NASA/GSFC/Earth Science Data and Information System (ESDIS) con fondos proporcionados por NASA/HQ."; + String get NASAAck => "Reconocemos el uso de datos e imágenes de LANCE FIRMS operadas por NASA/GSFC/Earth Science Data and Information System (ESDIS) con fondos proporcionados por NASA/HQ."; @override String get subscribedToFires => "Suscrito a notificaciones de fuegos"; @override - String get isYourUbicationEnabled => - "No podemos saber tu ubicación actual. ¿Están los servicios de ubicación en tu móvil activados?"; + String get isYourUbicationEnabled => "No podemos saber tu ubicación actual. ¿Están los servicios de ubicación en tu móvil activados?"; @override String get thanksForParticipating => "Gracias por Participar"; @override @@ -377,18 +263,15 @@ class es extends S { @override String get firesInTheWorld => "Fuegos en el mundo"; @override - String get notifyNeighboursDescription => - "Alerta a otros usuarios de la zona sobre el fuego"; + String get notifyNeighboursDescription => "Alerta a otros usuarios de la zona sobre el fuego"; @override String get addSomePlace => "Añade otro lugar"; @override String get confirm => "Confirmar"; @override - String get inGreenMonitoredAreas => - "En verde, las zonas vigiladas por nuestros usuari@s actualmente"; + String get inGreenMonitoredAreas => "En verde, las zonas vigiladas por nuestros usuari@s actualmente"; @override - String get getAlertsOfFiresinThatArea => - "Recibe alertas de fuegos en esa zona"; + String get getAlertsOfFiresinThatArea => "Recibe alertas de fuegos en esa zona"; @override String get monitoredAreasTitle => "Zonas vigiladas"; @override @@ -408,25 +291,19 @@ class es extends S { @override String inYears(String value) => "$value años"; @override - String noFiresAroundThisArea(String kmAround) => - "No hay fuegos a $kmAround км a la redonda"; + String noFiresAroundThisArea(String kmAround) => "No hay fuegos a $kmAround км a la redonda"; @override - String firesAroundThisArea(String numFires, String kmAround) => - "$numFires fuegos a $kmAround км a la redonda"; + String firesAroundThisArea(String numFires, String kmAround) => "$numFires fuegos a $kmAround км a la redonda"; @override - String appLicense(String thisYear) => - "(c) 2017-$thisYear Asociación Comunes bajo licencia GNU Affero GPL v3"; + String appLicense(String thisYear) => "(c) 2017-$thisYear Asociación Comunes bajo licencia GNU Affero GPL v3"; @override String somethingAgo(String something) => "hace $something"; @override - String additionalInfoAboutFire(String where, String when, String by) => - "Información adicional sobre fuego detectado en $where $when $by"; + String additionalInfoAboutFire(String where, String when, String by) => "Información adicional sobre fuego detectado en $where $when $by"; @override - String tweetAboutSelf(String location, String hash) => - "Fuego en $location $hash"; + String tweetAboutSelf(String location, String hash) => "Fuego en $location $hash"; @override - String subscribeToValueAroundThisArea(String sliderValue) => - "Suscríbete a $sliderValue км a la redonda"; + String subscribeToValueAroundThisArea(String sliderValue) => "Suscríbete a $sliderValue км a la redonda"; @override String inHours(String value) => "$value horas"; @override @@ -434,32 +311,31 @@ class es extends S { @override String inDays(String value) => "$value días"; @override - String activeFiresWorldWide(String activeFires) => - "$activeFires fuegos activos en el mundo"; + String activeFiresWorldWide(String activeFires) => "$activeFires fuegos activos en el mundo"; @override - String fireAroundThisArea(String kmAround) => - "Un fuego a $kmAround км a la redonda"; + String fireAroundThisArea(String kmAround) => "Un fuego a $kmAround км a la redonda"; @override String inSomething(String something) => "en $something"; @override String inMinutes(String value) => "$value minutos"; } -class GeneratedLocalizationsDelegate - extends LocalizationsDelegate { + +class GeneratedLocalizationsDelegate extends LocalizationsDelegate { const GeneratedLocalizationsDelegate(); List get supportedLocales { return const [ + const Locale("gl", ""), const Locale("en", ""), const Locale("es", ""), + ]; } - LocaleResolutionCallback resolution({Locale? fallback}) { - return (Locale? locale, Iterable supported) { - if (locale == null) return supported.first; + LocaleResolutionCallback resolution({Locale fallback}) { + return (Locale locale, Iterable supported) { final Locale languageLocale = new Locale(locale.languageCode, ""); if (supported.contains(locale)) return locale; @@ -476,6 +352,7 @@ class GeneratedLocalizationsDelegate Future load(Locale locale) { final String lang = getLang(locale); switch (lang) { + case "gl": return new SynchronousFuture(const gl()); case "en": @@ -495,6 +372,6 @@ class GeneratedLocalizationsDelegate bool shouldReload(GeneratedLocalizationsDelegate old) => false; } -String getLang(Locale l) => l.countryCode != null && l.countryCode!.isEmpty +String getLang(Locale l) => l.countryCode != null && l.countryCode.isEmpty ? l.languageCode : l.toString(); diff --git a/lib/genericMap.dart b/lib/genericMap.dart new file mode 100644 index 0000000..7b74864 --- /dev/null +++ b/lib/genericMap.dart @@ -0,0 +1,478 @@ +import 'dart:core'; +import 'locationUtils.dart'; +import 'package:comunes_flutter/comunes_flutter.dart'; +import 'package:fires_flutter/models/yourLocation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_map/flutter_map.dart'; +import 'package:flutter_map/plugin_api.dart'; +import 'package:flutter_redux/flutter_redux.dart'; +import 'package:latlong/latlong.dart'; +import 'package:share/share.dart'; + +import 'attributionMapPlugin.dart'; +import 'colors.dart'; +import 'dummyMapPlugin.dart'; +import 'fireMarkType.dart'; +import 'fireMarker.dart'; +import 'generated/i18n.dart'; +import 'genericMapBottom.dart'; +import 'globals.dart' as globals; +import 'layerSelectorMapPlugin.dart'; +import 'models/appState.dart'; +import 'models/fireMapState.dart'; +import 'redux/actions.dart'; +import 'sentryReport.dart'; +import 'slider.dart'; +import 'zoomMapPlugin.dart'; +import 'customMoment.dart'; + +@immutable +class _ViewModel { + final String serverUrl; + final String lang; + final FireMapState mapState; + final OnSubscribeFunction onSubs; + final OnSubscribeConfirmedFunction onSubsConfirmed; + final OnUnSubscribeFunction onUnSubs; + final OnSubscribeDistanceChangeFunction onSlide; + final OnLocationEdit onEdit; + final OnLocationEditConfirm onEditConfirm; + final OnLocationEditCancel onEditCancel; + final OnLocationEditing onEditing; + final OnFalsePositive onFalsePositive; + final OnFirePressedInMap onFirePressed; + + _ViewModel( + {@required this.mapState, + @required this.serverUrl, + @required this.lang, + @required this.onSubs, + @required this.onSubsConfirmed, + @required this.onUnSubs, + @required this.onSlide, + @required this.onEdit, + @required this.onEditing, + @required this.onEditConfirm, + @required this.onFalsePositive, + @required this.onFirePressed, + @required this.onEditCancel}); + + @override + bool operator ==(Object other) => + identical(this, other) || + other is _ViewModel && + runtimeType == other.runtimeType && + serverUrl == other.serverUrl && + lang == other.lang && + mapState == other.mapState; + + @override + int get hashCode => serverUrl.hashCode ^ lang.hashCode ^ mapState.hashCode; +} + +class genericMap extends StatefulWidget { + @override + _genericMapState createState() => _genericMapState(); +} + +class _genericMapState extends State { + // This needs to be stateful so when resizes don't get a new globalkey + // https://github.com/flutter/flutter/issues/1632#issuecomment-180478202 + final GlobalKey _scaffoldKey = new GlobalKey(); + + YourLocation _location; + YourLocation _initialLocation; + + @override + Widget build(BuildContext context) { + return new StoreConnector( + distinct: true, + onInitialBuild: (store) { + _initialLocation = _location.copyWith(); + }, + converter: (store) { + print('New map viewer'); + return new _ViewModel( + onSubs: (loc) { + store.dispatch(new SubscribeAction()); + }, + onSubsConfirmed: (loc) { + loc.subscribed = true; + store.dispatch(new SubscribeConfirmAction(loc)); + }, + onUnSubs: (loc) { + loc.subscribed = false; + store.dispatch(new UnSubscribeAction(loc)); + }, + onSlide: (loc) { + store.dispatch(new UpdateYourLocationMapAction(loc)); + }, + onEdit: (loc) => store.dispatch(new EditYourLocationAction(loc)), + onEditing: (loc) { + store.dispatch(new UpdateYourLocationMapAction(loc)); + }, + onEditCancel: (loc) => + store.dispatch(new EditCancelYourLocationAction(loc)), + onEditConfirm: (loc) { + store.dispatch(new UpdateYourLocationAction(loc)); + store.dispatch(new UpdateYourLocationMapAction(loc)); + store.dispatch(new EditConfirmYourLocationAction(loc)); + }, + onFalsePositive: (notif, type) => store + .dispatch(new MarkFireAsFalsePositiveAction(notif, type)), + onFirePressed: (LatLng latLng, DateTime when, type) { + _showFireDialog(latLng, when, type); + }, + serverUrl: store.state.serverUrl, + // Not used yet, but maybe in the future for date format + lang: store.state.user.lang, + mapState: store.state.fireMapState); + }, + builder: (context, view) { + YourLocation location = view.mapState.yourLocation; + _location = location.copyWith(); + print('New map builder with ${_location.description}'); + + assert(_location != null); + FireMapState mapState = view.mapState; + FireMapStatus status = mapState.status; + FireMapLayer layer = mapState.layer; + print('Build map with status: $status and layer $layer'); + + double maxZoom = 18.0; // works? + + MapOptions mapOptions = new MapOptions( + center: new LatLng(_location.lat, _location.lon), + plugins: globals.isDevelopment + ? [ + new ZoomMapPlugin(), + new AttributionPlugin(), + new LayerSelectorMapPlugin() + ] + : [ + new DummyMapPlugin(), + new AttributionPlugin(), + new LayerSelectorMapPlugin() + ], + // this works ? + interactive: false, + zoom: 13.0, + // THIS does not works as expected + maxZoom: maxZoom, + onTap: (callback) { + if (status == FireMapStatus.edit) { + _location = _location.copyWith( + lat: callback.latitude, lon: callback.longitude); + view.onEditing(_location); + } + }, + onPositionChanged: (positionCallback) { + // print('${positionCallback.center}, ${positionCallback.zoom}'); + }); + var mapController = new MapController(); + + // mapController.fitBounds(bounds); + // mapController.center + + final btnText = status == FireMapStatus.view + ? S.of(context).toFiresNotifications + : status == FireMapStatus.subscriptionConfirm + ? S.of(context).confirm + : S.of(context).unsubscribe; + final btnIcon = status == FireMapStatus.view + ? Icons.notifications_active + : status == FireMapStatus.subscriptionConfirm + ? Icons.check + : Icons.notifications_off; + + String baseLayer; + List subdomains = []; + String attribution; + switch (layer) { + case FireMapLayer.osmc: + case FireMapLayer.osmcGrey: + attribution = '© OpenStreetMap contributors'; + break; + case FireMapLayer.esri: + case FireMapLayer.esriSatellite: + case FireMapLayer.esriTerrain: + attribution = '© ESRI'; + } + /* tiles from: https://github.com/dceejay/RedMap/blob/1914ed3b9ce4e8a496049849a93282730b4fff02/worldmap/index.html */ + switch (layer) { + case FireMapLayer.osmc: + subdomains = ['a', 'b', 'c']; + baseLayer = + 'https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png'; + break; + case FireMapLayer.osmcGrey: + subdomains = ['a', 'b', 'c']; + baseLayer = 'https://tiles.wmflabs.org/bw-mapnik/{z}/{x}/{y}.png'; + break; + case FireMapLayer.esri: + baseLayer = + 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Street_Map/MapServer/tile/{z}/{y}/{x}'; + break; + case FireMapLayer.esriSatellite: + baseLayer = + 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}'; + break; + case FireMapLayer.esriTerrain: + baseLayer = + 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Shaded_Relief/MapServer/tile/{z}/{y}/{x}'; + } + FlutterMap map = new FlutterMap( + options: mapOptions, + mapController: mapController, + layers: [ + new TileLayerOptions( + maxZoom: maxZoom, + urlTemplate: baseLayer, + subdomains: subdomains, + additionalOptions: { + // 'opacity': '0.1', + }, + ), + globals.isDevelopment + ? new ZoomMapPluginOptions() + : new DummyMapPluginOptions(), + new MarkerLayerOptions( + markers: buildMarkers( + mapState.status == FireMapStatus.viewFireNotification + ? new LatLng(mapState.fireNotification.lat, + mapState.fireNotification.lon) + : new LatLng(_location.lat, _location.lon), + mapState.fires, + mapState.industries, + mapState.falsePos, + mapState.status == FireMapStatus.viewFireNotification, + view.onFirePressed), + ), + // new AttributionPluginOptions(text: "© OpenStreetMap contributors"), + new LayerSelectorMapPluginOptions(), + new AttributionPluginOptions(text: attribution), + ], + ); + // mapController. + /* FlutterMapState leafletState = map.createState(); + leafletState.mapState.onMoved.listen((Null) { + + ; + }); */ + // Do something with it + return new Scaffold( + key: _scaffoldKey, + resizeToAvoidBottomPadding: true, + appBar: new AppBar( + title: status == FireMapStatus.edit + ? new TextField( + // autofocus: true, + key: new Key('LocationDescField'), + keyboardType: TextInputType.text, + + decoration: new InputDecoration(), + controller: new TextEditingController.fromValue( + new TextEditingValue( + text: _location.description, + selection: new TextSelection.collapsed( + offset: _location.description.length))), + onChanged: (newDesc) { + debugPrint("OnChanged"); + _location = _location.copyWith(description: newDesc); + }, + onSubmitted: (newDesc) { + debugPrint("OnSubmitted"); + _location = _location.copyWith(description: newDesc); + view.onEditConfirm(_location); + }, + ) + : status == FireMapStatus.viewFireNotification + ? new Text(S.of(context).fireNotificationTitle) + : new Text(_location.description), + actions: buildAppBarActions(status, view, _location), + ), + floatingActionButton: status == FireMapStatus.edit || + status == FireMapStatus.viewFireNotification + ? null + : FloatingActionButton.extended( + onPressed: () { + switch (status) { + case FireMapStatus.view: + view.onSubs(_location); + break; + case FireMapStatus.subscriptionConfirm: + view.onSubsConfirmed(_location); + break; + case FireMapStatus.unsubscribe: + view.onUnSubs(_location); + break; + case FireMapStatus.edit: + case FireMapStatus.viewFireNotification: + break; + } + }, + // https://github.com/flutter/flutter/issues/17583 + heroTag: "firesmap" + _location.id.toHexString(), + icon: new Icon(btnIcon, color: fires600), + label: new Text( + btnText, + style: const TextStyle(color: fires600), + ), + backgroundColor: Colors.white, + ), + floatingActionButtonLocation: + FloatingActionButtonLocation.centerFloat, + bottomNavigationBar: new GenericMapBottom( + onSave: () => view.onEditConfirm(_location), + onCancel: () => view.onEditCancel(_initialLocation), + onFalsePositive: (sealed, type) => + view.onFalsePositive(sealed, type), + state: view.mapState, + scaffoldKey: _scaffoldKey, + ), + body: LayoutBuilder( + builder: (context, constraints) => + Stack(fit: StackFit.expand, children: [ + // Material(color: Colors.yellowAccent), + new Opacity( + opacity: + status == FireMapStatus.subscriptionConfirm || + status == FireMapStatus.edit + ? 0.5 + : 1.0, + child: map), + Positioned( + top: constraints.maxHeight - 200, + right: 10.0, + left: 10.0, + child: new CenteredRow( + // Fit sample: + // https://github.com/apptreesoftware/flutter_map/blob/master/flutter_map_example/lib/pages/map_controller.dart + children: + status == FireMapStatus.subscriptionConfirm || + (status == FireMapStatus.edit && + _location.subscribed) + ? [ + new FireDistanceSlider( + initialValue: _location.distance, + onSlide: (distance) { + _location.distance = distance; + view.onSlide(_location); + }) + ] + : []), + ) + ]))); + }); + } + + List buildAppBarActions( + FireMapStatus status, _ViewModel view, YourLocation location) { + switch (status) { + case FireMapStatus.view: + case FireMapStatus.unsubscribe: + return [ + new IconButton( + icon: new Icon(Icons.edit), + onPressed: () => view.onEdit(location)) + ]; + case FireMapStatus.edit: + return [ + new IconButton( + icon: new Icon(Icons.save), + onPressed: () => view.onEditConfirm(_location)) + ]; + case FireMapStatus.viewFireNotification: + return [ + new IconButton( + icon: new Icon(Icons.share), + onPressed: () { + Share.share( + '${view.mapState.fireNotification.description}. ${view + .serverUrl}fire/${view.mapState.fireNotification.sealed}'); + }) + ]; + default: + return []; + } + } + + List buildMarkers( + LatLng pos, + List fires, + List falsePosList, + List industries, + bool isNotif, + OnFirePressedInMap onFirePressed) { + List markers = []; + print('building markers: fires: ${fires.length} falsePos: ${falsePosList + .length} industries: ${industries.length}, isNotif: ${isNotif} '); + const calibrate = false; // useful when we change the fire icons size + falsePosList.forEach((falsePos) { + try { + var coords = falsePos['geo']['coordinates']; + // print('false pos: ${coords}'); + var loc = LatLng(coords[1], coords[0]); + markers.add(FireMarker(loc, FireMarkType.falsePos)); + if (calibrate) markers.add(FireMarker(loc, FireMarkType.pixel)); + } catch (e) { + print('Failed to process $falsePos'); + reportError(e, e.stackTrace); + } + }); + industries.forEach((industry) { + try { + // print(fire['geo']['coordinates']); + var coords = industry['geo']['coordinates']; + var loc = LatLng(coords[1], coords[0]); + markers.add(FireMarker(loc, FireMarkType.industry)); + if (calibrate) markers.add(FireMarker(loc, FireMarkType.pixel)); + } catch (e) { + print('Failed to process $industry'); + reportError(e, e.stackTrace); + } + }); + fires.forEach((fire) { + try { + var loc = new LatLng(fire['lat'], fire['lon']); + markers.add(FireMarker(loc, FireMarkType.fire, () { + onFirePressed(loc, DateTime.parse(fire['when']), fire['type']); + print('fire $fire pressed'); + })); + markers.add(FireMarker(loc, FireMarkType.pixel)); + } catch (e) { + print('Failed to process $fire'); + reportError(e, e.stackTrace); + } + }); + markers.add( + FireMarker(pos, isNotif ? FireMarkType.fire : FireMarkType.position)); + if (calibrate) markers.add(FireMarker(pos, FireMarkType.pixel)); + return markers; + } + + void _showFireDialog(LatLng pos, DateTime date, String type) { + var when = Moment.fromDate(date).fromNow(context); + getReverseLocation(lat: pos.latitude, lon: pos.longitude) + .then((reverseLoc) { + String by = type == 'vecinal' + ? S.of(context).byOurUsers + : S.of(context).byNASAsatellites; + String fireDesc = + S.of(context).additionalInfoAboutFire(reverseLoc, when, by); + showDialog( + context: _scaffoldKey.currentContext, + builder: (_) => new AlertDialog( + content: new Text(fireDesc), + actions: [ + new FlatButton( + child: Text(S.of(context).CLOSE), + onPressed: () { + Navigator.pop(context); + }, + ), + ], + )); + }); + } +} diff --git a/lib/genericMapBottom.dart b/lib/genericMapBottom.dart new file mode 100644 index 0000000..9cd257c --- /dev/null +++ b/lib/genericMapBottom.dart @@ -0,0 +1,137 @@ +import 'dart:async'; + +import 'package:comunes_flutter/comunes_flutter.dart'; +import 'package:fires_flutter/models/yourLocation.dart'; +import 'package:fires_flutter/models/fireNotification.dart'; +import 'package:flutter/material.dart'; + +import 'colors.dart'; +import 'customBottomAppBar.dart'; +import 'customMoment.dart'; +import 'generated/i18n.dart'; +import 'models/appState.dart'; +import 'models/falsePositiveTypes.dart'; +import 'models/fireMapState.dart'; + +typedef void OnSave(); +typedef void OnCancel(); +typedef void OnFalsePositive(FireNotification notif, FalsePositiveType type); + +class GenericMapBottom extends StatelessWidget { + final OnSave onSave; + final OnCancel onCancel; + final OnFalsePositive onFalsePositive; + final FireMapState state; + final GlobalKey scaffoldKey; + + GenericMapBottom( + {@required this.onSave, + @required this.onCancel, + @required this.onFalsePositive, + @required this.state, + @required this.scaffoldKey}); + + @override + Widget build(BuildContext context) { + YourLocation loc = state.yourLocation; + int kmAround = loc.distance; + return new CustomBottomAppBar( + fabLocation: FloatingActionButtonLocation.centerFloat, + showNotch: false, + color: fires100, + mainAxisAlignment: MainAxisAlignment.center, + actions: buildActionList(loc, context, kmAround, scaffoldKey)); + } + + List buildActionList(YourLocation loc, BuildContext context, + int kmAround, GlobalKey scaffoldState) { + List actionList = new List(); + switch (state.status) { + case FireMapStatus.edit: + actionList.add(new FlatButton( + onPressed: onSave, + child: new Text(S.of(context).SAVE, + style: Theme.of(context).textTheme.button))); + actionList.add(new FlatButton( + onPressed: onCancel, + child: new Text(S.of(context).CANCEL, + style: Theme.of(context).textTheme.button))); + break; + case FireMapStatus.subscriptionConfirm: + break; + case FireMapStatus.viewFireNotification: + actionList.add(new Flexible( + child: new Padding( + padding: new EdgeInsets.all(10.0), + child: new Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.start, + children: listWithoutNulls([ + new Text(state.fireNotification.description), + // TODO fire type (neighbout, NASA, etc) + new SizedBox(height: 5.0), + new Row( + children: [ + new Icon(Icons.access_time), + new SizedBox(width: 5.0), + new Text(Moment + .now() + .from(context, state.fireNotification.when)), + ], + ), + state.industries.length > 0 || state.falsePos.length > 0 + ? new Padding( + padding: new EdgeInsets.only(top: 10.0), + child: new Text( + S.of(context).itSeemsNotAtForesFire, + style: new TextStyle(color: fires600))) + : null, + new DropdownButton( + style: new TextStyle( + color: Colors.black, + // fontSize: 18.0, + ), + hint: new Text(S.of(context).notAWildfire), + items: FalsePositiveType.values + .map((FalsePositiveType value) { + String menuText; + switch (value) { + case FalsePositiveType.industry: + menuText = S.of(context).itSeemsAIndustry; + break; + case FalsePositiveType.controled: + menuText = + S.of(context).itSeemsAControlledBurning; + break; + case FalsePositiveType.falsealarm: + menuText = S.of(context).itSeemsAFalseAlarm; + } + return new DropdownMenuItem( + value: value, child: new Text(menuText)); + }).toList(), + onChanged: (value) async { + onFalsePositive(state.fireNotification, value); + await new Future.delayed( + new Duration(milliseconds: 500)); + scaffoldKey.currentState.showSnackBar(new SnackBar( + content: new Text( + S.of(context).thanksForParticipating), + )); + }), + ]))))); + break; + case FireMapStatus.unsubscribe: + case FireMapStatus.view: + if (state.numFires != null) { + actionList.add(new Text(state.numFires > 0 + ? loc.currentNumFires == 1 + ? S.of(context).fireAroundThisArea(loc.distance.toString()) + : S.of(context).firesAroundThisArea( + state.numFires.toString(), kmAround.toString()) + : S.of(context).noFiresAroundThisArea(kmAround.toString()))); + // SizedBox(width: 10.0) + } + } + return actionList; + } +} diff --git a/lib/generic_map.dart b/lib/generic_map.dart deleted file mode 100644 index f1675d2..0000000 --- a/lib/generic_map.dart +++ /dev/null @@ -1,538 +0,0 @@ -import 'dart:core'; - -import 'package:flutter/material.dart'; -import 'package:flutter_map/flutter_map.dart'; -import 'package:flutter_redux/flutter_redux.dart'; -import 'package:latlong2/latlong.dart'; -// ignore: implementation_imports -import 'package:redux/src/store.dart'; -import 'package:share_plus/share_plus.dart'; - -import 'attribution_map_plugin.dart'; -import 'colors.dart'; -import 'compass_map_plugin.dart'; -import 'custom_moment.dart'; -import 'dummy_map_plugin.dart'; -import 'fire_mark_type.dart'; -import 'fire_marker.dart'; -import 'generated/i18n.dart'; -import 'generic_map_bottom.dart'; -import 'globals.dart' as globals; -import 'layer_selector_map_plugin.dart'; -import 'location_utils.dart'; -import 'models/app_state.dart'; -import 'models/false_positive_types.dart'; -import 'models/fire_notification.dart'; -import 'models/your_location.dart'; -import 'redux/actions.dart'; -import 'sentry_report.dart'; -import 'slider.dart'; -import 'zoom_map_plugin.dart'; - -@immutable -class _ViewModel { - const _ViewModel( - {required this.mapState, - required this.serverUrl, - required this.lang, - required this.onSubs, - required this.onSubsConfirmed, - required this.onUnSubs, - required this.onSlide, - required this.onEdit, - required this.onEditing, - required this.onEditConfirm, - required this.onFalsePositive, - required this.onFirePressed, - required this.onEditCancel}); - final String serverUrl; - final String lang; - final FireMapState mapState; - final OnSubscribeFunction onSubs; - final OnSubscribeConfirmedFunction onSubsConfirmed; - final OnUnSubscribeFunction onUnSubs; - final OnSubscribeDistanceChangeFunction onSlide; - final OnLocationEdit onEdit; - final OnLocationEditConfirm onEditConfirm; - final OnLocationEditCancel onEditCancel; - final OnLocationEditing onEditing; - final OnFalsePositive onFalsePositive; - final OnFirePressedInMap onFirePressed; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is _ViewModel && - runtimeType == other.runtimeType && - serverUrl == other.serverUrl && - lang == other.lang && - mapState == other.mapState; - - @override - int get hashCode => serverUrl.hashCode ^ lang.hashCode ^ mapState.hashCode; -} - -class GenericMap extends StatefulWidget { - const GenericMap({super.key}); - - @override - GenericMapState createState() => GenericMapState(); -} - -class GenericMapState extends State { - // This needs to be stateful so when resizes don't get a new globalkey - // https://github.com/flutter/flutter/issues/1632#issuecomment-180478202 - final GlobalKey _scaffoldKey = GlobalKey(); - - YourLocation? _location; - YourLocation? _initialLocation; - - @override - Widget build(BuildContext context) { - return StoreConnector( - distinct: true, - onInitialBuild: (_ViewModel store) { - _initialLocation = _location?.copyWith(); - }, - converter: (Store store) { - return _ViewModel( - onSubs: (YourLocation loc) { - store.dispatch(SubscribeAction()); - }, - onSubsConfirmed: (YourLocation loc) { - store.dispatch( - SubscribeConfirmAction(loc.copyWith(subscribed: true))); - }, - onUnSubs: (YourLocation loc) { - store.dispatch( - UnSubscribeAction(loc.copyWith(subscribed: false))); - }, - onSlide: (YourLocation loc) { - store.dispatch(UpdateYourLocationMapAction(loc)); - }, - onEdit: (YourLocation loc) => - store.dispatch(EditYourLocationAction(loc)), - onEditing: (YourLocation loc) { - store.dispatch(UpdateYourLocationMapAction(loc)); - }, - onEditCancel: (YourLocation loc) => - store.dispatch(EditCancelYourLocationAction(loc)), - onEditConfirm: (YourLocation loc) { - store.dispatch(UpdateYourLocationAction(loc)); - store.dispatch(UpdateYourLocationMapAction(loc)); - store.dispatch(EditConfirmYourLocationAction(loc)); - }, - onFalsePositive: (FireNotification notif, - FalsePositiveType type) => - store.dispatch(MarkFireAsFalsePositiveAction(notif, type)), - onFirePressed: (LatLng latLng, DateTime when, String type) { - _showFireDialog(latLng, when, type); - }, - serverUrl: store.state.serverUrl, - // Not used yet, but maybe in the future for date format - lang: store.state.user.lang, - mapState: store.state.fireMapState); - }, - builder: (BuildContext context, _ViewModel view) { - final YourLocation? location = view.mapState.yourLocation; - _location = location?.copyWith(); - - final FireMapState mapState = view.mapState; - final FireMapStatus status = mapState.status; - final FireMapLayer layer = mapState.layer; - - const double maxZoom = 18.0; // works? - - final MapOptions mapOptions = MapOptions( - initialCenter: LatLng(_location?.lat ?? 0.0, _location?.lon ?? 0.0), - maxZoom: maxZoom, - onTap: (TapPosition tapPosition, LatLng latLng) { - if (status == FireMapStatus.edit && _location != null) { - _location = _location! - .copyWith(lat: latLng.latitude, lon: latLng.longitude); - view.onEditing(_location!); - } - }, - // onPositionChanged: (positionCallback) { - // print('${positionCallback.center}, ${positionCallback.zoom}'); - //} - ); - // var mapController = new MapController(); - - // mapController.fitBounds(bounds); - // mapController.center - - final String btnText = status == FireMapStatus.view - ? S.of(context).toFiresNotifications - : status == FireMapStatus.subscriptionConfirm - ? S.of(context).confirm - : S.of(context).unsubscribe; - final IconData btnIcon = status == FireMapStatus.view - ? Icons.notifications_active - : status == FireMapStatus.subscriptionConfirm - ? Icons.check - : Icons.notifications_off; - - String baseLayer; - List subdomains = []; - String attribution; - switch (layer) { - case FireMapLayer.osmc: - case FireMapLayer.osmcGrey: - attribution = '© OpenStreetMap contributors'; - break; - case FireMapLayer.esri: - case FireMapLayer.esriSatellite: - case FireMapLayer.esriTerrain: - attribution = '© ESRI'; - } - /* tiles from: https://github.com/dceejay/RedMap/blob/1914ed3b9ce4e8a496049849a93282730b4fff02/worldmap/index.html */ - switch (layer) { - case FireMapLayer.osmc: - subdomains = ['a', 'b', 'c']; - baseLayer = - 'https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png'; - break; - case FireMapLayer.osmcGrey: - baseLayer = - 'https://cartodb-basemaps-{s}.global.ssl.fastly.net/light_all/{z}/{x}/{y}.png'; - subdomains = ['a', 'b', 'c', 'd']; - break; - case FireMapLayer.esri: - baseLayer = - 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Street_Map/MapServer/tile/{z}/{y}/{x}'; - break; - case FireMapLayer.esriSatellite: - baseLayer = - 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}'; - break; - case FireMapLayer.esriTerrain: - baseLayer = - 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Shaded_Relief/MapServer/tile/{z}/{y}/{x}'; - } - final FlutterMap map = FlutterMap( - options: mapOptions, - children: [ - TileLayer( - maxZoom: maxZoom, - urlTemplate: baseLayer, - subdomains: subdomains, - userAgentPackageName: 'com.example.fires_flutter', - ), - if (globals.isDevelopment) - const ZoomMapPluginWidget() - else - const DummyMapPluginWidget(), - const CompassMapPluginWidget(), - MarkerLayer( - markers: buildMarkers( - mapState.status == FireMapStatus.viewFireNotification && - mapState.fireNotification != null - ? LatLng(mapState.fireNotification!.lat, - mapState.fireNotification!.lon) - : LatLng(_location!.lat, _location!.lon), - mapState.fires, - mapState.industries, - mapState.falsePos, - mapState.status == FireMapStatus.viewFireNotification, - view.onFirePressed), - ), - // new AttributionPluginWidget(text: "© OpenStreetMap contributors"), - const LayerSelectorMapPluginWidget(), - AttributionPluginWidget(text: attribution), - ], - ); - // mapController. - /* FlutterMapState leafletState = map.createState(); - leafletState.mapState.onMoved.listen((Null) { - - ; - }); */ - // Do something with it - return Scaffold( - key: _scaffoldKey, - resizeToAvoidBottomInset: true, - appBar: AppBar( - title: status == FireMapStatus.edit - ? TextField( - // autofocus: true, - key: const Key('LocationDescField'), - keyboardType: TextInputType.text, - controller: TextEditingController.fromValue( - TextEditingValue( - text: _location!.description, - selection: TextSelection.collapsed( - offset: _location!.description.length))), - onChanged: (String newDesc) { - debugPrint('OnChanged'); - _location = _location!.copyWith(description: newDesc); - }, - onSubmitted: (String newDesc) { - debugPrint('OnSubmitted'); - _location = _location!.copyWith(description: newDesc); - view.onEditConfirm(_location!); - }, - ) - : status == FireMapStatus.viewFireNotification - ? Text(S.of(context).fireNotificationTitle) - : Text(_location!.description), - actions: buildAppBarActions(status, view, _location!), - ), - floatingActionButton: status == FireMapStatus.edit || - status == FireMapStatus.viewFireNotification - ? null - : FloatingActionButton.extended( - onPressed: () { - switch (status) { - case FireMapStatus.view: - view.onSubs(_location!); - break; - case FireMapStatus.subscriptionConfirm: - view.onSubsConfirmed(_location!); - break; - case FireMapStatus.unsubscribe: - view.onUnSubs(_location!); - break; - case FireMapStatus.edit: - case FireMapStatus.viewFireNotification: - break; - } - }, - // https://github.com/flutter/flutter/issues/17583 - heroTag: 'firesmap${_location!.id.hexString}', - icon: Icon(btnIcon, color: fires600), - label: Text( - btnText, - style: const TextStyle(color: fires600), - ), - backgroundColor: Colors.white, - ), - floatingActionButtonLocation: - FloatingActionButtonLocation.centerFloat, - bottomNavigationBar: GenericMapBottom( - onSave: () => view.onEditConfirm(_location!), - onCancel: () => - view.onEditCancel(_initialLocation ?? _location!), - onFalsePositive: - (FireNotification sealed, FalsePositiveType type) => - view.onFalsePositive(sealed, type), - state: view.mapState, - scaffoldKey: _scaffoldKey, - ), - body: LayoutBuilder( - builder: (BuildContext context, BoxConstraints constraints) => - Stack(fit: StackFit.expand, children: [ - // Material(color: Colors.yellowAccent), - Opacity( - opacity: - status == FireMapStatus.subscriptionConfirm || - status == FireMapStatus.edit - ? 0.5 - : 1.0, - child: map), - Positioned( - top: constraints.maxHeight - 200, - right: 10.0, - left: 10.0, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - // Fit sample: - // https://github.com/apptreesoftware/flutter_map/blob/master/flutter_map_example/lib/pages/map_controller.dart - children: status == - FireMapStatus.subscriptionConfirm || - (status == FireMapStatus.edit && - _location != null && - _location!.subscribed) - ? [ - FireDistanceSlider( - initialValue: - _location?.distance ?? 0, - onSlide: (int distance) { - if (_location != null) { - _location = _location! - .copyWith(distance: distance); - view.onSlide(_location!); - } - }) - ] - : []), - ) - ]))); - }); - } - - // ignore: library_private_types_in_public_api - List buildAppBarActions( - FireMapStatus status, - // ignore: library_private_types_in_public_api - _ViewModel view, - YourLocation location) { - switch (status) { - case FireMapStatus.view: - case FireMapStatus.unsubscribe: - case FireMapStatus.subscriptionConfirm: - return [ - IconButton( - icon: const Icon(Icons.edit), - onPressed: () => view.onEdit(location)) - ]; - case FireMapStatus.edit: - return [ - IconButton( - icon: const Icon(Icons.save), - onPressed: () => view.onEditConfirm(_location!)) - ]; - case FireMapStatus.viewFireNotification: - return [ - IconButton( - icon: const Icon(Icons.share), - onPressed: () { - Share.share( - '${view.mapState.fireNotification?.description ?? 'Fire'}. ${view.serverUrl}fire/${view.mapState.fireNotification?.sealed ?? ''}'); - }) - ]; - } - } - - List buildMarkers( - LatLng pos, - List fires, - List falsePosList, - List industries, - bool isNotif, - OnFirePressedInMap onFirePressed) { - final List markers = []; - // Debug: building markers: fires: ${fires.length} falsePos: ${falsePosList.length} industries: ${industries.length}, isNotif: $isNotif - // const calibrate = false; // useful when we change the fire icons size - for (final dynamic falsePos in falsePosList) { - try { - final Map falsePosMap = - falsePos as Map; - final Map geo = - falsePosMap['geo'] as Map; - final List coords = geo['coordinates'] as List; - // print('false pos: ${coords}'); - final LatLng loc = LatLng( - (coords[1] as num).toDouble(), (coords[0] as num).toDouble()); - markers.add(fireMarker(loc, FireMarkType.falsePos, () { - _showFalsePositiveDialog(loc); - })); - // if (calibrate) markers.add(fireMarker(loc, FireMarkType.pixel)); - } catch (e, stackTrace) { - reportError(e, stackTrace); - } - } - for (final dynamic industry in industries) { - try { - // print(fire['geo']['coordinates']); - final Map industryMap = - industry as Map; - final dynamic geoData = industryMap['geo']; - final Map geo = geoData as Map; - final List coords = geo['coordinates'] as List; - final LatLng loc = LatLng( - (coords[1] as num).toDouble(), (coords[0] as num).toDouble()); - markers.add(fireMarker(loc, FireMarkType.industry, () { - _showIndustryDialog(loc); - })); - // if (calibrate) markers.add(fireMarker(loc, FireMarkType.pixel)); - } catch (e, stackTrace) { - reportError(e, stackTrace); - } - } - for (final dynamic fire in fires) { - try { - final Map fireMap = fire as Map; - final dynamic lat = fireMap['lat']; - final dynamic lon = fireMap['lon']; - final dynamic when = fireMap['when']; - final dynamic type = fireMap['type']; - final LatLng loc = - LatLng((lat as num).toDouble(), (lon as num).toDouble()); - markers.add(fireMarker(loc, FireMarkType.fire, () { - onFirePressed(loc, DateTime.parse(when.toString()), type as String); - })); - markers.add(fireMarker(loc, FireMarkType.pixel)); - } catch (e, stackTrace) { - reportError(e, stackTrace); - } - } - markers.add( - fireMarker(pos, isNotif ? FireMarkType.fire : FireMarkType.position)); - // if (calibrate) markers.add(fireMarker(pos, FireMarkType.pixel)); - return markers; - } - - void _showFireDialog(LatLng pos, DateTime date, String type) { - final String when = Moment.fromDate(date).fromNow(context); - final S strings = S.of(context); - final String by = - type == 'vecinal' ? strings.byOurUsers : strings.byNASAsatellites; - getReverseLocation(lat: pos.latitude, lon: pos.longitude) - .then((String reverseLoc) { - final String fireDesc = - strings.additionalInfoAboutFire(reverseLoc, when, by); - showDialog( - context: _scaffoldKey.currentContext!, - builder: (_) => AlertDialog( - content: Text(fireDesc), - actions: [ - TextButton( - child: Text(strings.CLOSE), - onPressed: () { - Navigator.pop(_scaffoldKey.currentContext!); - }, - ), - ], - )); - }); - } - - void _showIndustryDialog(LatLng pos) { - final S strings = S.of(context); - getReverseLocation(lat: pos.latitude, lon: pos.longitude) - .then((String reverseLoc) { - final String industryDesc = '${strings.itSeemsAIndustry}\n\n' - 'Type: Industry\n' - 'Location: $reverseLoc'; - showDialog( - context: _scaffoldKey.currentContext!, - builder: (_) => AlertDialog( - title: Text(strings.notAWildfire), - content: Text(industryDesc), - actions: [ - TextButton( - child: Text(strings.CLOSE), - onPressed: () { - Navigator.pop(_scaffoldKey.currentContext!); - }, - ), - ], - )); - }); - } - - void _showFalsePositiveDialog(LatLng pos) { - final S strings = S.of(context); - getReverseLocation(lat: pos.latitude, lon: pos.longitude) - .then((String reverseLoc) { - final String falseDesc = '${strings.itSeemsNotAtForesFire}\n\n' - 'Type: False Positive\n' - 'Location: $reverseLoc'; - showDialog( - context: _scaffoldKey.currentContext!, - builder: (_) => AlertDialog( - title: Text(strings.notAWildfire), - content: Text(falseDesc), - actions: [ - TextButton( - child: Text(strings.CLOSE), - onPressed: () { - Navigator.pop(_scaffoldKey.currentContext!); - }, - ), - ], - )); - }); - } -} diff --git a/lib/generic_map_bottom.dart b/lib/generic_map_bottom.dart deleted file mode 100644 index adcbc94..0000000 --- a/lib/generic_map_bottom.dart +++ /dev/null @@ -1,145 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; - -import 'colors.dart'; -import 'custom_bottom_app_bar.dart'; -import 'custom_moment.dart'; -import 'generated/i18n.dart'; -import 'models/app_state.dart'; -import 'models/false_positive_types.dart'; -import 'models/fire_map_state.dart'; -import 'models/fire_notification.dart'; -import 'models/your_location.dart'; -import 'utils/widget_utils.dart'; - -typedef OnSave = void Function(); -typedef OnCancel = void Function(); -typedef OnFalsePositive = void Function( - FireNotification notif, FalsePositiveType type); - -class GenericMapBottom extends StatelessWidget { - const GenericMapBottom( - {super.key, - required this.onSave, - required this.onCancel, - required this.onFalsePositive, - required this.state, - required this.scaffoldKey}); - final OnSave onSave; - final OnCancel onCancel; - final OnFalsePositive onFalsePositive; - final FireMapState state; - final GlobalKey scaffoldKey; - - @override - Widget build(BuildContext context) { - final YourLocation? locOrNull = state.yourLocation; - if (locOrNull == null) { - return const SizedBox.shrink(); - } - final YourLocation loc = locOrNull; - final int kmAround = loc.distance; - return CustomBottomAppBar( - fabLocation: FloatingActionButtonLocation.centerFloat, - color: fires100, - actions: buildActionList(loc, context, kmAround, scaffoldKey)); - } - - List buildActionList(YourLocation loc, BuildContext context, - int kmAround, GlobalKey scaffoldState) { - final List actionList = []; - switch (state.status) { - case FireMapStatus.edit: - actionList.add(TextButton( - onPressed: onSave, - child: Text(S.of(context).SAVE, - style: Theme.of(context).textTheme.labelLarge))); - actionList.add(TextButton( - onPressed: onCancel, - child: Text(S.of(context).CANCEL, - style: Theme.of(context).textTheme.labelLarge))); - break; - case FireMapStatus.subscriptionConfirm: - break; - case FireMapStatus.viewFireNotification: - final FireNotification? notif = state.fireNotification; - if (notif != null) { - actionList.add(Flexible( - child: Padding( - padding: const EdgeInsets.all(10.0), - child: Column( - mainAxisSize: MainAxisSize.min, - children: compactWidgets([ - Text(notif.description), - // TODOfire type (neighbout, NASA, etc) - const SizedBox(height: 5.0), - Row( - children: [ - const Icon(Icons.access_time), - const SizedBox(width: 5.0), - Text(Moment.now().from(context, notif.when)), - ], - ), - if (state.industries.isNotEmpty || - state.falsePos.isNotEmpty) - Padding( - padding: const EdgeInsets.only(top: 10.0), - child: Text(S.of(context).itSeemsNotAtForesFire, - style: const TextStyle(color: fires600))) - else - null, - DropdownButton( - style: const TextStyle( - color: Colors.black, - // fontSize: 18.0, - ), - hint: Text(S.of(context).notAWildfire), - items: FalsePositiveType.values - .map((FalsePositiveType value) { - String menuText; - switch (value) { - case FalsePositiveType.industry: - menuText = S.of(context).itSeemsAIndustry; - break; - case FalsePositiveType.controled: - menuText = - S.of(context).itSeemsAControlledBurning; - break; - case FalsePositiveType.falsealarm: - menuText = S.of(context).itSeemsAFalseAlarm; - } - return DropdownMenuItem( - value: value, child: Text(menuText)); - }).toList(), - onChanged: (FalsePositiveType? value) { - final S strings = S.of(context); - final ScaffoldMessengerState messenger = - ScaffoldMessenger.of(context); - if (value != null) { - onFalsePositive(notif, value); - } - Future.delayed( - const Duration(milliseconds: 500)) - .then((_) { - messenger.showSnackBar(SnackBar( - content: Text(strings.thanksForParticipating), - )); - }); - }), - ] as List))))); - } - break; - case FireMapStatus.unsubscribe: - case FireMapStatus.view: - actionList.add(Text(state.numFires > 0 - ? loc.currentNumFires == 1 - ? S.of(context).fireAroundThisArea(loc.distance.toString()) - : S.of(context).firesAroundThisArea( - state.numFires.toString(), kmAround.toString()) - : S.of(context).noFiresAroundThisArea(kmAround.toString()))); - // SizedBox(width: 10.0) - } - return actionList; - } -} diff --git a/lib/globalFiresBottomStats.dart b/lib/globalFiresBottomStats.dart new file mode 100644 index 0000000..1014ea7 --- /dev/null +++ b/lib/globalFiresBottomStats.dart @@ -0,0 +1,71 @@ +import 'dart:convert'; + +import 'package:comunes_flutter/comunes_flutter.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_simple_dependency_injection/injector.dart'; +import 'package:http/http.dart' as http; + +import 'colors.dart'; +import 'customBottomAppBar.dart'; +import 'customMoment.dart'; +import 'generated/i18n.dart'; + +class GlobalFiresBottomStats extends StatefulWidget { + @override + _GlobalFiresBottomStatsState createState() => _GlobalFiresBottomStatsState(); +} + +class _GlobalFiresBottomStatsState extends State { + String lastCheck; + int activeFires = 0; + final firesApiUrl = Injector.getInjector().get(key: "firesApiUrl"); + + @override + void initState() { + super.initState(); + http.read('${firesApiUrl}status/last-fire-check').then((result) { + try { + var now = Moment.now(); + var last = DateTime.parse(json.decode(result)['value']); + http.read('${firesApiUrl}status/active-fires-count').then((result) { + try { + int count = json.decode(result)['total']; + setState(() { + lastCheck = now.from(context, last); + activeFires = count; + }); + } catch (e) { + print('Cannot get the last fire stats'); + print(e); + } + }); + } catch (e) { + print('Cannot get the last fire check'); + print(e); + } + }); + } + + @override + Widget build(BuildContext context) { + return new CustomBottomAppBar( + fabLocation: FloatingActionButtonLocation.centerDocked, + showNotch: true, + color: fires100, + mainAxisAlignment: MainAxisAlignment.center, + actions: listWithoutNulls([ + activeFires > 0 && lastCheck != null + ? new Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + new Text(S + .of(context) + .activeFiresWorldWide(activeFires.toString())), + new Text(S.of(context).updatedLastCheck(lastCheck)) + ]) + : null, + SizedBox(width: 10.0) + ])); + } +} diff --git a/lib/global_fires_bottom_stats.dart b/lib/global_fires_bottom_stats.dart deleted file mode 100644 index 59ec5ba..0000000 --- a/lib/global_fires_bottom_stats.dart +++ /dev/null @@ -1,77 +0,0 @@ -import 'dart:convert'; - -import 'package:flutter/material.dart'; -import 'package:get_it/get_it.dart'; -import 'package:http/http.dart' as http; - -import 'colors.dart'; -import 'custom_bottom_app_bar.dart'; -import 'custom_moment.dart'; -import 'generated/i18n.dart'; - -class GlobalFiresBottomStats extends StatefulWidget { - const GlobalFiresBottomStats({super.key}); - - @override - // ignore: library_private_types_in_public_api - _GlobalFiresBottomStatsState createState() => _GlobalFiresBottomStatsState(); -} - -class _GlobalFiresBottomStatsState extends State { - late String lastCheck; - int activeFires = 0; - final String firesApiUrl = - GetIt.instance(instanceName: 'firesApiUrl'); - - @override - void initState() { - super.initState(); - http - .read(Uri.parse('${firesApiUrl}status/last-fire-check')) - .then((String result) { - try { - final Moment now = Moment.now(); - final dynamic decodedResult = json.decode(result); - final Map resultMap = - decodedResult as Map; - final DateTime last = DateTime.parse(resultMap['value'] as String); - http - .read(Uri.parse('${firesApiUrl}status/active-fires-count')) - .then((String result) { - try { - final dynamic decodedCountResult = json.decode(result); - final Map countMap = - decodedCountResult as Map; - final int count = (countMap['total'] as num).toInt(); - setState(() { - lastCheck = now.from(context, last); - activeFires = count; - }); - } catch (_) { - // Ignore JSON parse errors - } - }); - } catch (_) { - // Ignore storage read errors - } - }); - } - - @override - Widget build(BuildContext context) { - final List actionWidgets = []; - if (activeFires > 0) { - actionWidgets - .add(Column(mainAxisSize: MainAxisSize.min, children: [ - Text(S.of(context).activeFiresWorldWide(activeFires.toString())), - Text(S.of(context).updatedLastCheck(lastCheck)) - ])); - } - - return CustomBottomAppBar( - fabLocation: FloatingActionButtonLocation.centerDocked, - showNotch: true, - color: fires100, - actions: actionWidgets); - } -} diff --git a/lib/globals.dart b/lib/globals.dart index 4341b63..e05d647 100644 --- a/lib/globals.dart +++ b/lib/globals.dart @@ -5,7 +5,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; -late String appVersion; +String appVersion; final Widget appMediumIcon = Image.asset('images/logo-200.png', width: 60.0, height: 60.0); diff --git a/lib/homePage.dart b/lib/homePage.dart new file mode 100644 index 0000000..f4c2544 --- /dev/null +++ b/lib/homePage.dart @@ -0,0 +1,310 @@ +import 'dart:async'; + +import 'package:bson_objectid/bson_objectid.dart'; +import 'package:comunes_flutter/comunes_flutter.dart'; +import 'package:connectivity/connectivity.dart'; +import 'package:firebase_messaging/firebase_messaging.dart'; +import 'package:fires_flutter/models/fireNotification.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_redux/flutter_redux.dart'; +import 'package:flutter_simple_dependency_injection/injector.dart'; +import 'package:redux/redux.dart'; + +import 'firesSpinner.dart'; +import 'activeFires.dart'; +import 'colors.dart'; +import 'fireAlert.dart'; +import 'fireNotificationList.dart'; +import 'generated/i18n.dart'; +import 'mainDrawer.dart'; +import 'models/appState.dart'; +import 'redux/actions.dart'; + +class _ViewModel { + final bool isLoaded; + + _ViewModel({@required this.isLoaded}); + + @override + bool operator ==(Object other) => + identical(this, other) || + other is _ViewModel && + runtimeType == other.runtimeType && + isLoaded == other.isLoaded; + + @override + int get hashCode => isLoaded.hashCode; + +} + +class HomePage extends StatefulWidget { + static const String routeName = '/home'; + + HomePage(); + + @override + _HomePageState createState() => _HomePageState(); +} + +class _HomePageState extends State { + final GlobalKey _scaffoldKey = new GlobalKey(); + final Store store = Injector.getInjector().get>(); + final Connectivity _connectivity = new Connectivity(); + final List newNotifications = []; + + // Platform messages are asynchronous, so we initialize in an async method. + Future initConnectivity() async { + ConnectivityResult connectionStatus; + // Platform messages may fail, so we use a try/catch PlatformException. + try { + connectionStatus = (await _connectivity.checkConnectivity()); + } on PlatformException catch (e) { + print(e.toString()); + connectionStatus = ConnectivityResult.none; + } + + // If the widget was removed from the tree while the asynchronous platform + // message was in flight, we want to discard the reply rather than calling + // setState to update our non-existent appearance. + if (!mounted) { + return; + } + + setState(() { + store.dispatch(new OnConnectivityChanged(connectionStatus)); + if (connectionStatus == ConnectivityResult.none) { + _showDialog(S.of(context).noConnectivity); + } + }); + } + + @override + void initState() { + super.initState(); + _firebaseMessaging.configure(onMessage: (Map message) { + debugPrint("onMessage in fireApp (isLoaded: ${store.state.isLoaded}): $message"); + _showItemDialog(message, _notifForMessage(message, store.state.isLoaded)); + }, onLaunch: (Map message) { + debugPrint("onLaunch (isLoaded: ${store.state.isLoaded}): $message"); + _notifForMessage(message, store.state.isLoaded); + _navigateToItemDetail(message); + }, onResume: (Map message) { + debugPrint("onResume (isLoaded: ${store.state.isLoaded}): $message"); + _notifForMessage(message, store.state.isLoaded); + _navigateToItemDetail(message); + }); + _firebaseMessaging.requestNotificationPermissions( + const IosNotificationSettings(sound: true, badge: true, alert: true)); + _firebaseMessaging.onIosSettingsRegistered + .listen((IosNotificationSettings settings) { + print("Settings registered: $settings"); + }); + _firebaseMessaging.getToken().then((String token) { + assert(token != null); + // print(token); + store.dispatch(new OnUserTokenAction(token)); + setState(() {}); + }); + initConnectivity(); + // StreamSubscription _connectivitySubscription = + _connectivity.onConnectivityChanged.listen((ConnectivityResult result) { + if (!mounted) { + return; + } + setState(() { + store.dispatch(new OnConnectivityChanged(result)); + // _showDialog(result.toString()); + }); + }); + } + + final _homeFont = const TextStyle( + fontSize: 50.0, + fontWeight: FontWeight.w600, + ); + final _btnFont = const TextStyle( + fontSize: 20.0, + fontWeight: FontWeight.w600, + ); + + @override + Widget build(BuildContext context) { + return new StoreConnector( + distinct: true, + converter: (store) { + bool isLoaded = store.state.isLoaded; + if (isLoaded && newNotifications.isNotEmpty) { + newNotifications.forEach((notif) { + store.dispatch(new AddFireNotificationAction(notif)); + }); + newNotifications.clear(); + } + return new _ViewModel( + isLoaded: store.state.isLoaded + ); + }, + builder: (context, view) { + return new Scaffold( + key: _scaffoldKey, + drawer: new MainDrawer(context, HomePage.routeName), + floatingActionButtonLocation: + FloatingActionButtonLocation.centerFloat, + floatingActionButton: Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + FloatingActionButton.extended( + elevation: 0.0, + onPressed: () { + Navigator.pushNamed(context, ActiveFiresPage.routeName); + }, + label: Text(S.of(context).activeFires, style: _btnFont), + backgroundColor: fires600, + heroTag: 'activeFires', + icon: const Icon(Icons.whatshot, size: 32.0), + ), + Padding( + padding: const EdgeInsets.only(top: 16.0, bottom: 26.0), + child: FloatingActionButton.extended( + elevation: 0.0, + onPressed: () { + Navigator.pushNamed(context, FireAlert.routeName); + }, + heroTag: 'notifyFire', + backgroundColor: fires600, + label: new Text(S.of(context).notifyAFire, + style: _btnFont), + icon: + const Icon(Icons.notifications_active, size: 32.0), + ), + ), + ]), + body: !view.isLoaded? new FiresSpinner(): new SafeArea( + child: Center( + child: new CenteredColumn(children: [ + new Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + new IconButton( + onPressed: () { + _scaffoldKey.currentState.openDrawer(); + }, + icon: new Icon(Icons.menu, + size: 30.0, color: Colors.black38)), + ]), + new Expanded( + child: new FractionallySizedBox( + alignment: FractionalOffset.center, + heightFactor: 0.7, + child: new Image.asset('images/logo-200.png', + fit: BoxFit.fitHeight))), + new Expanded( + child: new FractionallySizedBox( + alignment: FractionalOffset.topCenter, + heightFactor: 1.0, + child: new Column( + children: [ + new Padding( + padding: const EdgeInsets.symmetric( + vertical: 10.0, horizontal: 20.0), + child: FittedBox( + child: new Text(S.of(context).appName, + maxLines: 2, + textAlign: TextAlign.center, + style: _homeFont), + fit: BoxFit.scaleDown, + )), + ], + ))) + ])), + )); + }); + } + + void _showDialog(String message) { + showDialog( + context: _scaffoldKey.currentContext, + builder: (_) => new AlertDialog( + content: new Text(message), + actions: [ + new FlatButton( + child: Text(S.of(_scaffoldKey.currentContext).CLOSE), + onPressed: () { + Navigator.pop(_scaffoldKey.currentContext); + }, + ), + ], + )); + } + + void _showItemDialog(Map message, FireNotification notif) { + showDialog( + context: _scaffoldKey.currentContext, + builder: (_) => _buildDialog(_scaffoldKey.currentContext, notif), + ).then((bool shouldNavigate) { + if (shouldNavigate == true) { + _navigateToItemDetail(message); + } + }).catchError((e) => print("$e")); + } + + Widget _buildDialog(BuildContext context, FireNotification item) { + return new AlertDialog( + content: new Text(item.description), + actions: [ + new FlatButton( + child: Text(S.of(context).CLOSE), + onPressed: () { + Navigator.pop(context, false); + }, + ), + new FlatButton( + child: Text(S.of(context).SHOW), + onPressed: () { + Navigator.pop(context, true); + }, + ), + ], + ); + } + + void _navigateToItemDetail(Map message) { + // Clear away dialogs + Navigator.popUntil(_scaffoldKey.currentContext, + (Route route) => route is PageRoute); + /* if (!notif.getRoute(store).isCurrent) { + // Navigator.push(_scaffoldKey.currentContext, notif.getRoute(store)); + } */ + Navigator.pushNamed( + _scaffoldKey.currentContext, FireNotificationList.routeName); + } + + // https://pub.dartlang.org/packages/firebase_messaging#-example-tab- + final FirebaseMessaging _firebaseMessaging = new FirebaseMessaging(); + + FireNotification _notifForMessage(Map message, bool isLoaded) { + FireNotification notif; + try { + notif = new FireNotification( + id: new ObjectId.fromHexString(message['id']), + subsId: new ObjectId.fromHexString(message['subsId']), + lat: double.parse(message['lat']), + lon: double.parse(message['lon']), + description: message['description'], + read: false, + when: DateTime.parse(message['when']), + sealed: message['sealed']); + debugPrint(notif.toString()); + } catch (e) { + debugPrint(e.toString()); + } + if (notif != null) + // if our store is loaded, we just dispatch the notification, if not, we wait til is loaded + if (isLoaded) { + store.dispatch(new AddFireNotificationAction(notif)); + } else { + newNotifications.add(notif); + } + return notif; + } +} diff --git a/lib/home_page.dart b/lib/home_page.dart deleted file mode 100644 index 1dcd5c3..0000000 --- a/lib/home_page.dart +++ /dev/null @@ -1,292 +0,0 @@ -import 'dart:async'; - -import 'package:firebase_messaging/firebase_messaging.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_redux/flutter_redux.dart'; -import 'package:get_it/get_it.dart'; -import 'package:redux/redux.dart'; - -import 'active_fires.dart'; -import 'colors.dart'; -import 'fire_alert.dart'; -import 'fire_notification_list.dart'; -import 'fires_spinner.dart'; -import 'generated/i18n.dart'; -import 'main_drawer.dart'; -import 'models/app_state.dart'; -import 'models/fire_notification.dart'; -import 'object_id_utils.dart'; -import 'redux/actions.dart'; - -@immutable -class _ViewModel { - const _ViewModel({required this.isLoaded}); - final bool isLoaded; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is _ViewModel && - runtimeType == other.runtimeType && - isLoaded == other.isLoaded; - - @override - int get hashCode => isLoaded.hashCode; -} - -class HomePage extends StatefulWidget { - const HomePage({super.key}); - static const String routeName = '/home'; - - @override - // ignore: library_private_types_in_public_api - _HomePageState createState() => _HomePageState(); -} - -class _HomePageState extends State { - final GlobalKey _scaffoldKey = GlobalKey(); - final Store store = GetIt.instance>(); - final List newNotifications = []; - - // Platform messages are asynchronous, so we initialize in an async method. - Future initConnectivity() async { - // Connectivity checking removed - no longer needed - } - - @override - void initState() { - super.initState(); - // Firebase Messaging v5+ setup - _setupFirebaseMessaging(); - _getFirebaseToken(); - // initConnectivity removed - connectivity no longer tracked - } - - void _setupFirebaseMessaging() { - // Request permission for notifications - _firebaseMessaging.requestPermission(); - - // Listen for messages when app is in foreground - FirebaseMessaging.onMessage.listen((RemoteMessage message) { - debugPrint( - 'onMessage in fireApp (isLoaded: ${store.state.isLoaded}): $message'); - if (message.data.isNotEmpty) { - final FireNotification? notif = - _notifForMessage(message.data, store.state.isLoaded); - if (notif != null) { - _showItemDialog(message.data, notif); - } - } - }); - - // Listen for messages when app is opened from background - FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) { - debugPrint( - 'onMessageOpenedApp (isLoaded: ${store.state.isLoaded}): $message'); - if (message.data.isNotEmpty) { - _notifForMessage(message.data, store.state.isLoaded); - _navigateToItemDetail(message.data); - } - }); - - // Check if app was terminated and opened by notification tap - _firebaseMessaging.getInitialMessage().then((RemoteMessage? message) { - if (message != null) { - debugPrint('App opened by notification: $message'); - if (message.data.isNotEmpty) { - _navigateToItemDetail(message.data); - } - } - }); - } - - void _getFirebaseToken() { - _firebaseMessaging.getToken().then((String? token) { - if (token != null) { - store.dispatch(OnUserTokenAction(token)); - setState(() {}); - } - }); - } - - final TextStyle _homeFont = const TextStyle( - fontSize: 50.0, - fontWeight: FontWeight.w600, - ); - final TextStyle _btnFont = const TextStyle( - fontSize: 20.0, - fontWeight: FontWeight.w600, - ); - - @override - Widget build(BuildContext context) { - return StoreConnector( - distinct: true, - converter: (Store store) { - final bool isLoaded = store.state.isLoaded; - if (isLoaded && newNotifications.isNotEmpty) { - for (final FireNotification notif in newNotifications) { - store.dispatch(AddFireNotificationAction(notif)); - } - newNotifications.clear(); - } - return _ViewModel(isLoaded: store.state.isLoaded); - }, - builder: (BuildContext context, _ViewModel view) { - return Scaffold( - key: _scaffoldKey, - drawer: MainDrawer(context, HomePage.routeName), - floatingActionButtonLocation: - FloatingActionButtonLocation.centerFloat, - floatingActionButton: Column( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - FloatingActionButton.extended( - elevation: 0.0, - onPressed: () { - Navigator.pushNamed(context, ActiveFiresPage.routeName); - }, - label: Text(S.of(context).activeFires, style: _btnFont), - backgroundColor: fires600, - heroTag: 'activeFires', - icon: const Icon(Icons.whatshot, size: 32.0), - ), - Padding( - padding: const EdgeInsets.only(top: 16.0, bottom: 26.0), - child: FloatingActionButton.extended( - elevation: 0.0, - onPressed: () { - Navigator.pushNamed(context, FireAlert.routeName); - }, - heroTag: 'notifyFire', - backgroundColor: fires600, - label: Text(S.of(context).notifyAFire, style: _btnFont), - icon: - const Icon(Icons.notifications_active, size: 32.0), - ), - ), - ]), - body: !view.isLoaded - ? const FiresSpinner() - : SafeArea( - child: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Row(children: [ - IconButton( - onPressed: () { - _scaffoldKey.currentState?.openDrawer(); - }, - icon: const Icon(Icons.menu, - size: 30.0, color: Colors.black38)), - ]), - Expanded( - child: FractionallySizedBox( - alignment: FractionalOffset.center, - heightFactor: 0.7, - child: Image.asset('images/logo-200.png', - fit: BoxFit.fitHeight))), - Expanded( - child: FractionallySizedBox( - alignment: FractionalOffset.topCenter, - heightFactor: 1.0, - child: Column( - children: [ - Padding( - padding: const EdgeInsets.symmetric( - vertical: 10.0, - horizontal: 20.0), - child: FittedBox( - fit: BoxFit.scaleDown, - child: Text(S.of(context).appName, - maxLines: 2, - textAlign: TextAlign.center, - style: _homeFont), - )), - ], - ))) - ])), - )); - }); - } - - void _showItemDialog(Map message, FireNotification notif) { - final BuildContext? context = _scaffoldKey.currentContext; - if (context == null) { - return; - } - showDialog( - context: context, - builder: (_) => _buildDialog(context, notif), - ).then((bool? shouldNavigate) { - if (shouldNavigate ?? false) { - _navigateToItemDetail(message); - } - }).catchError((Object e) {}); - } - - Widget _buildDialog(BuildContext context, FireNotification item) { - return AlertDialog( - content: Text(item.description), - actions: [ - TextButton( - child: Text(S.of(context).CLOSE), - onPressed: () { - Navigator.pop(context, false); - }, - ), - TextButton( - child: Text(S.of(context).SHOW), - onPressed: () { - Navigator.pop(context, true); - }, - ), - ], - ); - } - - void _navigateToItemDetail(Map message) { - final BuildContext? context = _scaffoldKey.currentContext; - if (context == null) { - return; - } - // Clear away dialogs - Navigator.popUntil(context, (Route route) => route is PageRoute); - /* if (!notif.getRoute(store).isCurrent) { - // Navigator.push(context, notif.getRoute(store)); - } */ - Navigator.pushNamed(context, FireNotificationList.routeName); - } - - // Firebase Messaging instance (v5+) - final FirebaseMessaging _firebaseMessaging = FirebaseMessaging.instance; - - FireNotification? _notifForMessage( - Map message, bool isLoaded) { - FireNotification? notif; - try { - notif = FireNotification( - id: objectIdFromJson(message['id'] as String), - subsId: objectIdFromJson(message['subsId'] as String), - lat: double.parse(message['lat'] as String), - lon: double.parse(message['lon'] as String), - description: message['description'] as String, - read: false, - when: DateTime.parse(message['when'] as String), - sealed: message['sealed'] as String); - debugPrint(notif.toString()); - } catch (e) { - debugPrint(e.toString()); - } - // if our store is loaded, we just dispatch the notification, if not, we wait til is loaded - if (notif != null) { - if (isLoaded) { - store.dispatch(AddFireNotificationAction(notif)); - } else { - newNotifications.add(notif); - } - } - return notif; - } -} diff --git a/lib/introPage.dart b/lib/introPage.dart new file mode 100644 index 0000000..a528c14 --- /dev/null +++ b/lib/introPage.dart @@ -0,0 +1,28 @@ +import 'package:comunes_flutter/comunes_flutter.dart'; +import 'package:flutter/material.dart'; + +import 'homePage.dart'; +import 'generated/i18n.dart'; + +class IntroPage extends AppIntroPage { + static const String routeName = '/intro'; + + static final fireItems = (context) => + [ + AppIntroItem(icon: Icons.location_on, title: S.of(context).chooseAPlace), + AppIntroItem( + icon: Icons.panorama_fish_eye, title: S.of(context).chooseAWatchRadio), + AppIntroItem( + icon: Icons.whatshot, title: S.of(context).getAlertsOfFiresinThatArea), + AppIntroItem( + icon: Icons.notifications_active, + title: S.of(context).alertWhenThereIsAFire) + ]; + + IntroPage({Key key}) + : super( + items: fireItems, + onIntroFinish: (context) => + Navigator.pushNamed(context, HomePage.routeName), + key: key); +} diff --git a/lib/intro_page.dart b/lib/intro_page.dart deleted file mode 100644 index 07b39cb..0000000 --- a/lib/intro_page.dart +++ /dev/null @@ -1,28 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'generated/i18n.dart'; -import 'home_page.dart'; -import 'widgets/app_intro_page.dart'; - -class IntroPage extends AppIntroPage { - IntroPage({super.key}) - : super( - items: _fireItems, - onIntroFinish: (BuildContext context) => - Navigator.pushNamed(context, HomePage.routeName)); - static String routeName = '/intro'; - - static List _fireItems(BuildContext context) => [ - AppIntroItem( - icon: Icons.location_on, title: S.of(context).chooseAPlace), - AppIntroItem( - icon: Icons.panorama_fish_eye, - title: S.of(context).chooseAWatchRadio), - AppIntroItem( - icon: Icons.whatshot, - title: S.of(context).getAlertsOfFiresinThatArea), - AppIntroItem( - icon: Icons.notifications_active, - title: S.of(context).alertWhenThereIsAFire) - ]; -} diff --git a/lib/layerSelectorMapPlugin.dart b/lib/layerSelectorMapPlugin.dart new file mode 100644 index 0000000..d5dc01c --- /dev/null +++ b/lib/layerSelectorMapPlugin.dart @@ -0,0 +1,57 @@ +import 'package:comunes_flutter/comunes_flutter.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/src/widgets/framework.dart'; +import 'package:flutter_map/plugin_api.dart'; +import 'package:flutter_simple_dependency_injection/injector.dart'; +import 'package:redux/redux.dart'; + +import 'models/appState.dart'; +import 'redux/actions.dart'; + +class LayerSelectorMapPluginOptions extends LayerOptions { + final String text; + + LayerSelectorMapPluginOptions({this.text = ""}); +} + +// https://github.com/apptreesoftware/flutter_map/blob/master/flutter_map_example/lib/pages/plugin_api.dart +class LayerSelectorMapPlugin extends MapPlugin { + Widget LayerSelectorButton(Store store) { + return new FloatingActionButton( + backgroundColor: Colors.black26, + mini: true, + child: Icon(Icons.layers, /* size: 40.0, color: Colors.black45, */ ), + onPressed: () { + store.dispatch(new ToggleMapLayerAction()); + }); + } + + @override + Widget createLayer(LayerOptions options, MapState mapState) { + Store store = Injector.getInjector().get>(); + if (options is LayerSelectorMapPluginOptions) { + return LayoutBuilder( + builder: (context, constraints) => + Stack(fit: StackFit.expand, children: [ + Positioned( + top: constraints.maxHeight - 60, + left: 10.0, + child: new CenteredRow( + children: [ + new Column( + children: [LayerSelectorButton(store)], + ) + ], + ), + ) + ])); + } + throw ("Unknown options type for LayerSelectorMapPlugin" + "plugin: $options"); + } + + @override + bool supportsLayer(LayerOptions options) { + return options is LayerSelectorMapPluginOptions; + } +} diff --git a/lib/layer_selector_map_plugin.dart b/lib/layer_selector_map_plugin.dart deleted file mode 100644 index 976f4ee..0000000 --- a/lib/layer_selector_map_plugin.dart +++ /dev/null @@ -1,93 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:get_it/get_it.dart'; -import 'package:redux/redux.dart'; - -import 'models/app_state.dart'; -import 'redux/fire_map_actions.dart'; - -/// Layer selector widget for changing map layers -class LayerSelectorMapPluginWidget extends StatelessWidget { - const LayerSelectorMapPluginWidget({super.key}); - - @override - Widget build(BuildContext context) { - final Store store = GetIt.instance>(); - return LayoutBuilder( - builder: (BuildContext context, BoxConstraints constraints) => - Stack(fit: StackFit.expand, children: [ - Positioned( - top: constraints.maxHeight - 60, - left: 10.0, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Column( - children: [_layerSelectorButton(store)], - ) - ], - ), - ) - ])); - } - - Widget _layerSelectorButton(Store store) { - final GlobalKey> key = - GlobalKey>(); - - return PopupMenuButton( - key: key, - initialValue: store.state.fireMapState.layer, - onSelected: (FireMapLayer layer) { - store.dispatch(SelectMapLayerAction(layer)); - }, - itemBuilder: (BuildContext context) { - return FireMapLayer.values.map((FireMapLayer layer) { - final bool isSelected = store.state.fireMapState.layer == layer; - return PopupMenuItem( - value: layer, - child: Row( - children: [ - Icon( - isSelected ? Icons.check : Icons.check, - color: isSelected ? Colors.blue : Colors.transparent, - size: 20, - ), - const SizedBox(width: 8), - Text( - _getLayerName(layer), - style: TextStyle( - fontWeight: - isSelected ? FontWeight.bold : FontWeight.normal, - ), - ), - ], - ), - ); - }).toList(); - }, - child: FloatingActionButton( - backgroundColor: Colors.black26, - mini: true, - child: const Icon(Icons.layers), - onPressed: () { - key.currentState?.showButtonMenu(); - }, - ), - ); - } - - String _getLayerName(FireMapLayer layer) { - switch (layer) { - case FireMapLayer.osmcGrey: - return 'OSMC Grey'; - case FireMapLayer.osmc: - return 'OSMC'; - case FireMapLayer.esri: - return 'Esri Street'; - case FireMapLayer.esriSatellite: - return 'Esri Satellite'; - case FireMapLayer.esriTerrain: - return 'Esri Terrain'; - } - } -} diff --git a/lib/locationUtils.dart b/lib/locationUtils.dart new file mode 100644 index 0000000..2f3b55a --- /dev/null +++ b/lib/locationUtils.dart @@ -0,0 +1,55 @@ +import 'package:location/location.dart'; +import 'package:flutter/services.dart'; +import 'dart:async'; +import 'package:fires_flutter/models/yourLocation.dart'; +import 'package:flutter/material.dart'; +import 'package:geocoder/geocoder.dart'; +import 'generated/i18n.dart'; +import 'package:flutter_simple_dependency_injection/injector.dart'; + +Future getUserLocation( + GlobalKey scaffoldKey) async { + // Platform messages may fail, so we use a try/catch PlatformException. + try { + Location _location = new Location(); + Map location = await _location.getLocation; + + // It seems that the lib fails with lat/lon values + var yl = new YourLocation( + lat: location['latitude'], lon: location['longitude']); + var address; + try { + address = await getReverseLocation(lat: yl.lat, lon: yl.lon); + yl.description = address; + } catch (e) { + try { + address = await getReverseLocation(lat: yl.lat, lon: yl.lon, external: true); + yl.description = address; + } catch (e) { + print('We cannot reverse geolocate'); + } + } + return yl; + } on PlatformException catch (e) { + if (e.code == 'PERMISSION_DENIED') { + scaffoldKey.currentState.showSnackBar(new SnackBar( + content: new Text(S.of(scaffoldKey.currentContext).notPermsUbication), + )); + } else if (e.code == 'PERMISSION_DENIED_NEVER_ASK') {} + scaffoldKey.currentState.showSnackBar(new SnackBar( + content: new Text(S.of(scaffoldKey.currentContext).isYourUbicationEnabled + ), + )); + return YourLocation.noLocation; + } +} + +Future getReverseLocation({@required lat, @required lon, + bool external = false}) async { + final coordinates = new Coordinates(lat, lon); + var geoCoder = external ? Geocoder.google(Injector.getInjector().get(key: "gmapKey")) : Geocoder.local; + var addresses = await geoCoder.findAddressesFromCoordinates(coordinates); + var first = addresses.first; + print("${first.featureName} : ${first.addressLine}"); + return first.addressLine; +} diff --git a/lib/location_utils.dart b/lib/location_utils.dart deleted file mode 100644 index b1ecfb3..0000000 --- a/lib/location_utils.dart +++ /dev/null @@ -1,75 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:geocoding/geocoding.dart' as geo; -import 'package:location/location.dart'; -import 'package:objectid/objectid.dart'; - -import 'generated/i18n.dart'; -import 'models/your_location.dart'; - -Future getUserLocation( - GlobalKey scaffoldKey) async { - // Platform messages may fail, so we use a try/catch PlatformException. - try { - final Location location0 = Location(); - - final LocationData location = await location0.getLocation(); - - // It seems that the lib fails with lat/lon values - YourLocation yl = YourLocation( - id: ObjectId(), lat: location.latitude!, lon: location.longitude!); - String address; - try { - address = await getReverseLocation(lat: yl.lat, lon: yl.lon); - yl = yl.copyWith(description: address); - } catch (e) { - try { - address = - await getReverseLocation(lat: yl.lat, lon: yl.lon, external: true); - yl = yl.copyWith(description: address); - } catch (_) { - // Ignore - fallback already attempted - } - } - return yl; - } on PlatformException catch (e) { - final BuildContext? context = scaffoldKey.currentContext; - if (context != null) { - // ignore: use_build_context_synchronously - final S strings = S.of(context); - // ignore: use_build_context_synchronously - final ScaffoldMessengerState messenger = ScaffoldMessenger.of(context); - if (e.code == 'PERMISSION_DENIED') { - messenger.showSnackBar(SnackBar( - content: Text(strings.notPermsUbication), - )); - } else if (e.code == 'PERMISSION_DENIED_NEVER_ASK') { - // User selected "Don't ask again" - show settings prompt - } - messenger.showSnackBar(SnackBar( - content: Text(strings.isYourUbicationEnabled), - )); - } - return YourLocation.noLocation; - } -} - -Future getReverseLocation( - {required double lat, required double lon, bool external = false}) async { - try { - final List placemarks = - await geo.placemarkFromCoordinates(lat, lon); - if (placemarks.isNotEmpty) { - final geo.Placemark first = placemarks.first; - final String address = - '${first.street}, ${first.locality}, ${first.administrativeArea}, ${first.country}'; - return address; - } else { - return 'Unable to determine address'; - } - } catch (e) { - throw Exception('Failed to reverse geocode: $e'); - } -} diff --git a/lib/main.dart b/lib/main.dart deleted file mode 100644 index b78b774..0000000 --- a/lib/main.dart +++ /dev/null @@ -1,12 +0,0 @@ -// This file routes to the correct entry point based on flavor -// For development flavor, use mainDev.dart -// For production flavor, use mainProd.dart - -// This is a workaround for gradle builds that expect lib/main.dart -// When building with flutter build command, use -t lib/mainDev.dart or -t lib/mainProd.dart - -void main() { - throw Exception( - 'Please build with: flutter build apk --flavor development -t lib/mainDev.dart\n' - 'or: flutter build apk --flavor production -t lib/mainProd.dart'); -} diff --git a/lib/mainCommon.dart b/lib/mainCommon.dart new file mode 100644 index 0000000..eaf76e2 --- /dev/null +++ b/lib/mainCommon.dart @@ -0,0 +1,83 @@ +import 'dart:async'; + +import 'package:comunes_flutter/comunes_flutter.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_simple_dependency_injection/injector.dart'; +import 'package:redux/redux.dart'; +import 'package:sentry/sentry.dart'; + +import 'firesApp.dart'; +import 'globals.dart' as globals; +import 'models/appState.dart'; +import 'models/firesApi.dart'; +import 'redux/fetchDataMiddleware.dart'; +import 'redux/reducers.dart'; +import 'package:package_info/package_info.dart'; +import 'sentryReport.dart'; + +Future loadPackageInfo() async { + PackageInfo packageInfo = await PackageInfo.fromPlatform(); + return packageInfo; +} + +Future> loadSecrets() async { + return await SecretLoader( + secretPath: globals.isDevelopment? + 'assets/private-settings-dev.json' : + 'assets/private-settings.json' + ).load(); +} + +void mainCommon(List> otherMiddleware) { + final injector = Injector.getInjector(); + injector.map((i) => new FiresApi(), isSingleton: true); + loadPackageInfo().then((packageInfo) { + globals.appVersion = packageInfo.version; + print('Running version ${packageInfo.version}'); + loadSecrets().then((secrets) { + final store = new Store(appStateReducer, + initialState: new AppState( + gmapKey: secrets['gmapKey'], + firesApiKey: secrets['firesApiKey'], + serverUrl: secrets['firesApiUrl'], + firesApiUrl: secrets['firesApiUrl'] + "api/v1/"), + middleware: List.from(otherMiddleware) + ..add(fetchDataMiddleware)); + + injector.map>((i) => store); + injector.map((i) => store.state.firesApiUrl, key: "firesApiUrl"); + injector.map((i) => store.state.firesApiKey, key: "firesApiKey"); + injector.map((i) => store.state.serverUrl, key: "serverUrl"); + injector.map((i) => store.state.gmapKey, key: "gmapKey"); + + if (useSentry) { + SentryClient _sentry = SentryClient(dsn: secrets['sentryDSN']); + injector.map((i) => _sentry); + } + + // https://flutter.io/cookbook/maintenance/error-reporting/ + runZoned>(() async { + runApp(new FiresApp(store)); + }, onError: (error, stackTrace) { + // Whenever an error occurs, call the `_reportError` function. This will send + // Dart errors to our dev console or Sentry depending on the environment. + reportError(error, stackTrace); + }); + + // Listen to store changes, and re-render when the state is updated + store.onChange.listen((state) { + // print('Store onChange'); + }); + FlutterError.onError = (FlutterErrorDetails details) { + if (!useSentry) { + // In development mode simply print to console. + FlutterError.dumpErrorToConsole(details); + } else { + // In production mode report to the application zone to report to + // Sentry. + Zone.current.handleUncaughtError(details.exception, details.stack); + } + }; + }); + }); +} diff --git a/lib/mainDev.dart b/lib/mainDev.dart new file mode 100644 index 0000000..2140c54 --- /dev/null +++ b/lib/mainDev.dart @@ -0,0 +1,54 @@ +import 'package:flutter/material.dart'; +import 'package:logging/logging.dart'; +import 'package:redux/redux.dart'; +import 'package:redux_logging/redux_logging.dart'; + +import 'globals.dart' as globals; +import 'mainCommon.dart'; + +enum LogLevel { none, actions, all } + +LoggingMiddleware customLogPrinter({ + Logger logger, + Level level = Level.INFO, + MessageFormatter formatter = LoggingMiddleware.singleLineFormatter, +}) { + final middleware = new LoggingMiddleware( + logger: logger, + level: level, + formatter: formatter, + ); + + middleware.logger.onRecord + .where((record) => record.loggerName == middleware.logger.name) + .listen((Object object) { + debugPrint("$object"); + }); + + return middleware; +} + +void main() { + globals.isDevelopment = true; + + String onlyLogActionFormatter( + State state, + dynamic action, + DateTime timestamp, + ) { + return ">>>>> ${action.toString().replaceAll('Instance of ', '')}"; + } + + LogLevel logRedux = LogLevel.none; + + List devMiddlewares = logRedux == LogLevel.none + ? [] + : [ + customLogPrinter( + formatter: logRedux == LogLevel.all + ? LoggingMiddleware.multiLineFormatter + : onlyLogActionFormatter) + ]; + + mainCommon(devMiddlewares); +} diff --git a/lib/main_drawer.dart b/lib/mainDrawer.dart similarity index 54% rename from lib/main_drawer.dart rename to lib/mainDrawer.dart index 64fda7e..447ee59 100644 --- a/lib/main_drawer.dart +++ b/lib/mainDrawer.dart @@ -1,28 +1,28 @@ -import 'package:badges/badges.dart' as badges_pkg; +import 'package:badge/badge.dart'; +import 'package:comunes_flutter/comunes_flutter.dart'; import 'package:flutter/material.dart'; import 'package:flutter_redux/flutter_redux.dart'; -// ignore: implementation_imports -import 'package:redux/src/store.dart'; -import 'active_fires.dart'; +import 'activeFires.dart'; import 'colors.dart'; -import 'fire_alert.dart'; -import 'fire_notification_list.dart'; +import 'fireAlert.dart'; +import 'fireNotificationList.dart'; import 'generated/i18n.dart'; import 'globals.dart' as globals; -import 'models/app_state.dart'; -import 'monitored_areas.dart'; -import 'privacy_page.dart'; +import 'models/appState.dart'; +import 'monitoredAreas.dart'; +import 'privacyPage.dart'; import 'sandbox.dart'; -import 'support_page.dart'; +import 'supportPage.dart'; @immutable class _ViewModel { - const _ViewModel({ - required this.unreadCount, - }); final int unreadCount; + _ViewModel({ + @required this.unreadCount, + }); + @override bool operator ==(Object other) { return identical(this, other) || @@ -36,121 +36,116 @@ class _ViewModel { } class MainDrawer extends Drawer { - MainDrawer(BuildContext context, String currentRoute, {super.key}) - : super(child: mainDrawer(context, currentRoute)); + MainDrawer(BuildContext context, String currentRoute, {key}) + : super(key: key, child: mainDrawer(context, currentRoute)); } Widget mainDrawer(BuildContext context, String currentRoute) { - return StoreConnector( + return new StoreConnector( distinct: true, - converter: (Store store) { - return _ViewModel(unreadCount: store.state.fireNotificationsUnread); + converter: (store) { + return new _ViewModel(unreadCount: store.state.fireNotificationsUnread); }, - builder: (BuildContext context, _ViewModel view) { - const TextStyle bottomTextStyle = - TextStyle(fontSize: 12.0, color: Colors.grey); - return ListView( + builder: (context, view) { + final bottomTextStyle = + new TextStyle(fontSize: 12.0, color: Colors.grey); + return new ListView( // Important: Remove any padding from the ListView. padding: EdgeInsets.zero, - children: [ - GestureDetector( + children: listWithoutNulls([ + new GestureDetector( onTap: () { Navigator.popAndPushNamed(context, '/'); }, - child: DrawerHeader( - decoration: const BoxDecoration( - color: fires300, - ), - child: Column( + child: new DrawerHeader( + child: new Column( children: [ - Image.asset( + new Image.asset( 'images/logo-200.png', fit: BoxFit.scaleDown, height: 80.0, ), const SizedBox(height: 20.0), - Text(S.of(context).appName, - style: const TextStyle( + new Text(S.of(context).appName, + style: new TextStyle( fontSize: 24.0, color: Colors.white, )), ], ), + decoration: new BoxDecoration( + color: fires300, + ), ), ), - ListTile( + new ListTile( leading: const Icon(Icons.whatshot), - title: Text(S.of(context).activeFires), + title: new Text(S.of(context).activeFires), selected: currentRoute == ActiveFiresPage.routeName, onTap: () { Navigator.popAndPushNamed(context, ActiveFiresPage.routeName); }, ), - ListTile( + new ListTile( leading: const Icon(Icons.notifications_active), selected: currentRoute == FireAlert.routeName, - title: Text(S.of(context).notifyAFire), + title: new Text(S.of(context).notifyAFire), onTap: () { Navigator.popAndPushNamed(context, FireAlert.routeName); }, ), - ListTile( + new ListTile( leading: const Icon(Icons.notifications), selected: currentRoute == FireNotificationList.routeName, - title: Text(S.of(context).fireNotificationsTitleShort), - trailing: badges_pkg.Badge( - position: - badges_pkg.BadgePosition.topEnd(top: -10, end: -12), - badgeContent: Text( - view.unreadCount.toString(), - style: const TextStyle(color: Colors.white), - ), - child: const Icon(Icons.notifications)), - - // Text(S.of(context).fireNotificationsTitleShort), + title: view.unreadCount > 0 + ? Badge.after( + spacing: 5.0, + borderColor: Colors.red, + child: Text(S.of(context).fireNotificationsTitleShort), + value: ' ${view.unreadCount.toString()} ') + : Text(S.of(context).fireNotificationsTitleShort), onTap: () { Navigator.popAndPushNamed( context, FireNotificationList.routeName); }, ), - ListTile( + new ListTile( leading: const Icon(Icons.map), selected: currentRoute == MonitoredAreasPage.routeName, - title: Text(S.of(context).monitoredAreasTitle), + title: new Text(S.of(context).monitoredAreasTitle), onTap: () { Navigator.popAndPushNamed( context, MonitoredAreasPage.routeName); }, ), - const Divider(), - ListTile( + new Divider(), + new ListTile( leading: const Icon(Icons.favorite), selected: currentRoute == SupportPage.routeName, - title: Text(S.of(context).supportThisInitiative), + title: new Text(S.of(context).supportThisInitiative), onTap: () { Navigator.popAndPushNamed(context, SupportPage.routeName); }, ), - ListTile( + new ListTile( leading: const Icon(Icons.lock), selected: currentRoute == PrivacyPage.routeName, - title: Text(S.of(context).privacyPolicy), + title: new Text(S.of(context).privacyPolicy), onTap: () { Navigator.popAndPushNamed(context, PrivacyPage.routeName); }, ), - if (globals.isDevelopment) - ListTile( - leading: const Icon(Icons.bug_report), - title: const Text('Sandbox'), - selected: currentRoute == Sandbox.routeName, - onTap: () { - Navigator.popAndPushNamed(context, Sandbox.routeName); - }, - ) - else - null, - AboutListTile( + globals.isDevelopment + ? new ListTile( + leading: const Icon(Icons.bug_report), + title: new Text('Sandbox'), + selected: currentRoute == Sandbox.routeName, + onTap: () { + Navigator.popAndPushNamed(context, Sandbox.routeName); + }, + ) + : null, + new AboutListTile( icon: globals.appIcon, applicationName: S.of(context).appName, applicationVersion: globals.appVersion, @@ -158,13 +153,13 @@ Widget mainDrawer(BuildContext context, String currentRoute) { applicationLegalese: S.of(context).appLicense(DateTime.now().year.toString()), aboutBoxChildren: [ - const SizedBox(height: 10.0), - Text(S.of(context).appMoto), + new SizedBox(height: 10.0), + new Text(S.of(context).appMoto), // , style: new TextStyle(fontStyle: FontStyle.italic)), - const SizedBox(height: 10.0), - Text(S.of(context).NASAAck, style: bottomTextStyle), + new SizedBox(height: 10.0), + new Text(S.of(context).NASAAck, style: bottomTextStyle), // More ? ]) - ].where((Widget? w) => w != null).cast().toList()); + ])); }); } diff --git a/lib/mainProd.dart b/lib/mainProd.dart new file mode 100644 index 0000000..d9d5125 --- /dev/null +++ b/lib/mainProd.dart @@ -0,0 +1,7 @@ +import 'globals.dart' as globals; +import 'mainCommon.dart'; + +void main() { + globals.isDevelopment = false; + mainCommon([]); +} \ No newline at end of file diff --git a/lib/main_common.dart b/lib/main_common.dart deleted file mode 100644 index 23800b6..0000000 --- a/lib/main_common.dart +++ /dev/null @@ -1,87 +0,0 @@ -import 'dart:async'; - -import 'package:firebase_core/firebase_core.dart'; -import 'package:flutter/material.dart'; -import 'package:get_it/get_it.dart'; -import 'package:package_info_plus/package_info_plus.dart'; -import 'package:redux/redux.dart'; - -import 'fires_app.dart'; -import 'globals.dart' as globals; -import 'models/app_state.dart'; -import 'models/fires_api.dart'; -import 'redux/fetch_data_middleware.dart'; -import 'redux/reducers.dart'; -import 'sentry_report.dart'; -import 'utils/secret_loader.dart'; - -Future loadPackageInfo() async { - final PackageInfo packageInfo = await PackageInfo.fromPlatform(); - return packageInfo; -} - -Future> loadSecrets() async { - return SecretLoader( - secretPath: globals.isDevelopment - ? 'assets/private-settings-dev.json' - : 'assets/private-settings.json') - .load(); -} - -Future mainCommon(List> otherMiddleware) async { - WidgetsFlutterBinding.ensureInitialized(); - - // Initialize Firebase before any Firebase-dependent code runs - await Firebase.initializeApp(); - - final GetIt getIt = GetIt.instance; - getIt.registerSingleton(FiresApi()); - loadPackageInfo().then((PackageInfo packageInfo) { - globals.appVersion = packageInfo.version; - loadSecrets().then((Map secrets) { - final Store store = Store(appStateReducer, - initialState: AppState( - gmapKey: secrets['gmapKey'] as String, - firesApiKey: secrets['firesApiKey'] as String, - serverUrl: secrets['firesApiUrl'] as String, - firesApiUrl: "${secrets['firesApiUrl'] as String}api/v1/"), - middleware: List>.from(otherMiddleware) - ..add(fetchDataMiddleware)); - - getIt.registerSingleton>(store); - getIt.registerSingleton(store.state.firesApiUrl, - instanceName: 'firesApiUrl'); - getIt.registerSingleton(store.state.firesApiKey, - instanceName: 'firesApiKey'); - getIt.registerSingleton(store.state.serverUrl, - instanceName: 'serverUrl'); - getIt.registerSingleton(store.state.gmapKey, - instanceName: 'gmapKey'); - - // https://flutter.io/cookbook/maintenance/error-reporting/ - runZonedGuarded>(() async { - runApp(FiresApp(store)); - }, (Object error, StackTrace? stackTrace) { - // Whenever an error occurs, call the `_reportError` function. This will send - // Dart errors to our dev console or Sentry depending on the environment. - reportError(error, stackTrace ?? StackTrace.current); - }); - - // Listen to store changes, and re-render when the state is updated - store.onChange.listen((AppState state) { - // print('Store onChange'); - }); - FlutterError.onError = (FlutterErrorDetails details) { - if (!useSentry) { - // In development mode simply print to console. - FlutterError.dumpErrorToConsole(details); - } else { - // In production mode report to the application zone to report to - // Sentry. - Zone.current.handleUncaughtError( - details.exception, details.stack ?? StackTrace.current); - } - }; - }); - }); -} diff --git a/lib/main_dev.dart b/lib/main_dev.dart deleted file mode 100644 index 18b676d..0000000 --- a/lib/main_dev.dart +++ /dev/null @@ -1,28 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:redux/redux.dart'; - -import 'globals.dart' as globals; -import 'main_common.dart'; - -enum LogLevel { none, actions, all } - -void main() async { - globals.isDevelopment = true; - debugPrint('Is development!'); - - // Simple logging middleware para desarrollo - Middleware createLoggingMiddleware() { - return (Store store, dynamic action, NextDispatcher next) { - debugPrint(">>>>> ${action.toString().replaceAll('Instance of ', '')}"); - next(action); - }; - } - - const LogLevel logRedux = LogLevel.actions; - - final List> devMiddlewares = - logRedux == LogLevel.actions ? >[] : >[createLoggingMiddleware()]; - - // In development, Sentry is disabled, so we skip initialization - await mainCommon(devMiddlewares); -} diff --git a/lib/main_prod.dart b/lib/main_prod.dart deleted file mode 100644 index 0214064..0000000 --- a/lib/main_prod.dart +++ /dev/null @@ -1,30 +0,0 @@ -// ignore: implementation_imports -import 'package:redux/src/store.dart'; -import 'package:sentry_flutter/sentry_flutter.dart'; - -import 'globals.dart' as globals; -import 'main_common.dart'; -import 'models/app_state.dart'; -import 'utils/secret_loader.dart'; - -Future main() async { - globals.isDevelopment = false; - - // Load secrets to get Sentry DSN - final Map secrets = await loadSecrets(); - - await SentryFlutter.init( - (SentryFlutterOptions options) { - options.dsn = secrets['sentryDSN'] as String?; - }, - appRunner: () => mainCommon(>[]), - ); -} - -Future> loadSecrets() async { - return SecretLoader( - secretPath: globals.isDevelopment - ? 'assets/private-settings-dev.json' - : 'assets/private-settings.json') - .load(); -} diff --git a/lib/markdownPage.dart b/lib/markdownPage.dart new file mode 100644 index 0000000..06df602 --- /dev/null +++ b/lib/markdownPage.dart @@ -0,0 +1,46 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart' show rootBundle; +import 'package:flutter_markdown/flutter_markdown.dart'; + +import 'globals.dart' as globals; +import 'mainDrawer.dart'; + +abstract class MarkdownPage extends StatefulWidget { + final String title; + final Future file; + final String route; + + MarkdownPage({this.title, this.route, this.file}); + + @override + _MarkdownPageState createState() => + _MarkdownPageState(title: this.title, file: this.file, route: this.route); +} + +class _MarkdownPageState extends State { + final String title; + final String route; + final Future file; + String pageData = ""; + + _MarkdownPageState({this.title, this.file, this.route}); + + @override + Widget build(BuildContext context) { + this.file.then((fileSync) { + rootBundle + .loadString(fileSync, cache: !globals.isDevelopment) + .then((pageData) { + setState(() { + this.pageData = pageData; + }); + }); + }); + return new Scaffold( + appBar: new AppBar(title: new Text(title ?? '')), + drawer: new MainDrawer(context, route), + body: new Markdown(data: pageData)); + } +} diff --git a/lib/markdown_page.dart b/lib/markdown_page.dart deleted file mode 100644 index 19b53ec..0000000 --- a/lib/markdown_page.dart +++ /dev/null @@ -1,56 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart' show rootBundle; -import 'package:flutter_markdown/flutter_markdown.dart'; - -import 'globals.dart' as globals; -import 'main_drawer.dart'; - -abstract class MarkdownPage extends StatefulWidget { - const MarkdownPage( - {super.key, - required this.title, - required this.route, - required this.file}); - final String title; - final Future file; - final String route; - - @override - // ignore: library_private_types_in_public_api - _MarkdownPageState createState() => _MarkdownPageState(); -} - -class _MarkdownPageState extends State { - _MarkdownPageState(); - late final String title; - late final String route; - late final Future file; - String pageData = ''; - - @override - void initState() { - super.initState(); - title = widget.title; - route = widget.route; - file = widget.file; - } - - @override - Widget build(BuildContext context) { - file.then((String fileSync) { - rootBundle - .loadString(fileSync, cache: !globals.isDevelopment) - .then((String pageData) { - setState(() { - this.pageData = pageData; - }); - }); - }); - return Scaffold( - appBar: AppBar(title: Text(title)), - drawer: MainDrawer(context, route), - body: Markdown(data: pageData)); - } -} diff --git a/lib/models/appState.dart b/lib/models/appState.dart new file mode 100644 index 0000000..f1cc365 --- /dev/null +++ b/lib/models/appState.dart @@ -0,0 +1,133 @@ +import 'dart:async'; + +import 'package:comunes_flutter/comunes_flutter.dart'; +import 'package:connectivity/connectivity.dart'; +import 'package:fires_flutter/models/fireNotification.dart'; +import 'package:fires_flutter/models/yourLocation.dart'; +import 'package:flutter/material.dart'; +import 'package:json_annotation/json_annotation.dart'; +import 'package:meta/meta.dart'; + +import 'fireMapState.dart'; +import 'user.dart'; + +export 'fireMapState.dart'; +import 'package:flutter_map/flutter_map.dart'; +import 'package:latlong/latlong.dart'; + +part 'appState.g.dart'; + +@immutable +@JsonSerializable(nullable: false) +class AppState extends Object with _$AppStateSerializerMixin { + @JsonKey(ignore: true) + final bool isLoading; + @JsonKey(ignore: true) + final bool isLoaded; + @JsonKey(ignore: true) + final String error; + @JsonKey(ignore: true) + final User user; + @JsonKey(ignore: true) + final String gmapKey; + @JsonKey(ignore: true) + final String serverUrl; + @JsonKey(ignore: true) + final String firesApiKey; + @JsonKey(ignore: true) + final String firesApiUrl; + final List yourLocations; + final List fireNotifications; + @JsonKey(ignore: true) + final List monitoredAreas; + @JsonKey(ignore: true) + final int fireNotificationsUnread; + @JsonKey(ignore: true) + final FireMapState fireMapState; + @JsonKey(ignore: true) + final ConnectivityResult connectivity; + + @JsonKey(ignore: true) + factory AppState.fromJson(Map json) => + _$AppStateFromJson(json); + + AppState( + {this.yourLocations: const [], + this.fireNotifications: const [], + this.fireNotificationsUnread: 0, + this.user: const User.initial(), + this.isLoading: false, + this.isLoaded: false, + this.error: null, + this.gmapKey, + this.firesApiKey, + this.firesApiUrl, + this.serverUrl, + this.connectivity, + this.monitoredAreas, + this.fireMapState: const FireMapState.initial()}); + + AppState copyWith( + {bool isLoading, + bool isLoaded, + String user, + String error, + String gmapKey, + String firesApiKey, + String serverUrl, + String firesApiUrl, + List yourLocations, + List fireNotifications, + int fireNotificationsUnread, + FireMapState fireMapState, + List monitoredAreas, + ConnectivityResult connectivity}) { + return new AppState( + isLoading: isLoading ?? this.isLoading, + isLoaded: isLoaded ?? this.isLoaded, + user: user ?? this.user, + error: error ?? this.error, + gmapKey: gmapKey ?? this.gmapKey, + firesApiKey: firesApiKey ?? this.firesApiKey, + firesApiUrl: firesApiUrl ?? this.firesApiUrl, + serverUrl: serverUrl ?? this.serverUrl, + yourLocations: yourLocations ?? this.yourLocations, + fireNotifications: fireNotifications ?? this.fireNotifications, + fireNotificationsUnread: + fireNotificationsUnread ?? this.fireNotificationsUnread, + monitoredAreas: monitoredAreas ?? this.monitoredAreas, + fireMapState: fireMapState ?? this.fireMapState, + connectivity: connectivity ?? this.connectivity); + } + + @override + String toString() { + return 'AppState{\nuser: ${user}\nconnectivity: ${connectivity + .toString()}\nisLoading: $isLoading\nisLoaded: $isLoaded\napiKey: ${ellipse( + firesApiKey, + 8)}\napiUrl: ${firesApiUrl}\nserverUrl: ${serverUrl}\nfireMapState: $fireMapState\nyourLocations count: ${yourLocations + .length}\nunread notif: ${fireNotificationsUnread}\nfireNotifications: ${fireNotifications}\nyourLocations: ${yourLocations}}'; + } +} + +typedef void AddYourLocationFunction(YourLocation loc); +typedef void DeleteYourLocationFunction(YourLocation loc); +typedef void OnRefreshYourLocationsFunction(Completer callback); +typedef void ToggleSubscriptionFunction(YourLocation loc); +typedef void OnLocationTapFunction(YourLocation loc); +typedef void OnSubscribeFunction(YourLocation loc); +typedef void OnSubscribeDistanceChangeFunction(YourLocation loc); +typedef void OnUnSubscribeFunction(YourLocation loc); +typedef void OnSubscribeConfirmedFunction(YourLocation loc); +typedef void OnLocationEdit(YourLocation loc); +typedef void OnLocationEditing(YourLocation loc); +typedef void OnLocationEditConfirm(YourLocation loc); +typedef void OnLocationEditCancel(YourLocation loc); +typedef void TapFireNotificationFunction(FireNotification notif); +typedef void OnFirePressedInMap(LatLng latLng, DateTime when, String source); +// typedef void OnReceivedFireNotificationFunction(FireNotification notif); +typedef void DeleteFireNotificationFunction(FireNotification notif); +typedef void DeleteAllFireNotificationFunction(); + +// unused +// typedef void UpdateYourLocationFunction(ObjectId id, YourLocation loc); diff --git a/lib/models/appState.g.dart b/lib/models/appState.g.dart new file mode 100644 index 0000000..49bd41e --- /dev/null +++ b/lib/models/appState.g.dart @@ -0,0 +1,44 @@ +// Copyright (c) 2018, Comunes Association. + +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'appState.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +AppState _$AppStateFromJson(Map json) => new AppState( + yourLocations: (json['yourLocations'] as List) + .map((e) => new YourLocation.fromJson(e as Map)) + .toList(), + fireNotifications: (json['fireNotifications'] as List) + .map((e) => new FireNotification.fromJson(e as Map)) + .toList()); + +abstract class _$AppStateSerializerMixin { + List get yourLocations; + List get fireNotifications; + Map toJson() => new _$AppStateJsonMapWrapper(this); +} + +class _$AppStateJsonMapWrapper extends $JsonMapWrapper { + final _$AppStateSerializerMixin _v; + _$AppStateJsonMapWrapper(this._v); + + @override + Iterable get keys => const ['yourLocations', 'fireNotifications']; + + @override + dynamic operator [](Object key) { + if (key is String) { + switch (key) { + case 'yourLocations': + return _v.yourLocations; + case 'fireNotifications': + return _v.fireNotifications; + } + } + return null; + } +} diff --git a/lib/models/app_state.dart b/lib/models/app_state.dart deleted file mode 100644 index c1a1df1..0000000 --- a/lib/models/app_state.dart +++ /dev/null @@ -1,122 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; -import 'package:flutter_map/flutter_map.dart'; -import 'package:json_annotation/json_annotation.dart'; -import 'package:latlong2/latlong.dart'; -import 'package:meta/meta.dart'; - -import '../utils/widget_utils.dart'; -import 'fire_map_state.dart'; -import 'fire_notification.dart'; -import 'user.dart'; -import 'your_location.dart'; - -export 'fire_map_state.dart'; - -part 'app_state.g.dart'; - -@immutable -@JsonSerializable() -class AppState { - const AppState( - {this.yourLocations = const [], - this.fireNotifications = const [], - this.fireNotificationsUnread = 0, - this.user = const User.initial(), - this.isLoading = false, - this.isLoaded = false, - this.error = '', - this.gmapKey = '', - this.firesApiKey = '', - this.firesApiUrl = '', - this.serverUrl = '', - this.monitoredAreas = const [], - this.fireMapState = const FireMapState.initial()}); - - factory AppState.fromJson(Map json) => - _$AppStateFromJson(json); - final bool isLoading; - @JsonKey(includeFromJson: false, includeToJson: false) - final bool isLoaded; - @JsonKey(includeFromJson: false, includeToJson: false) - final String error; - @JsonKey(includeFromJson: false, includeToJson: false) - final User user; - @JsonKey(includeFromJson: false, includeToJson: false) - final String gmapKey; - @JsonKey(includeFromJson: false, includeToJson: false) - final String serverUrl; - @JsonKey(includeFromJson: false, includeToJson: false) - final String firesApiKey; - @JsonKey(includeFromJson: false, includeToJson: false) - final String firesApiUrl; - final List yourLocations; - final List fireNotifications; - @JsonKey(includeFromJson: false, includeToJson: false) - final List monitoredAreas; - @JsonKey(includeFromJson: false, includeToJson: false) - final int fireNotificationsUnread; - @JsonKey(includeFromJson: false, includeToJson: false) - final FireMapState fireMapState; - - AppState copyWith( - {bool? isLoading, - bool? isLoaded, - User? user, - String? error, - String? gmapKey, - String? firesApiKey, - String? serverUrl, - String? firesApiUrl, - List? yourLocations, - List? fireNotifications, - int? fireNotificationsUnread, - FireMapState? fireMapState, - List? monitoredAreas}) { - return AppState( - isLoading: isLoading ?? this.isLoading, - isLoaded: isLoaded ?? this.isLoaded, - user: user ?? this.user, - error: error ?? this.error, - gmapKey: gmapKey ?? this.gmapKey, - firesApiKey: firesApiKey ?? this.firesApiKey, - firesApiUrl: firesApiUrl ?? this.firesApiUrl, - serverUrl: serverUrl ?? this.serverUrl, - yourLocations: yourLocations ?? this.yourLocations, - fireNotifications: fireNotifications ?? this.fireNotifications, - fireNotificationsUnread: - fireNotificationsUnread ?? this.fireNotificationsUnread, - monitoredAreas: monitoredAreas ?? this.monitoredAreas, - fireMapState: fireMapState ?? this.fireMapState); - } - - @override - String toString() { - return 'AppState{\nuser: $user\nisLoading: $isLoading\nisLoaded: $isLoaded\napiKey: ${ellipse(firesApiKey, 8)}\napiUrl: $firesApiUrl\nserverUrl: $serverUrl\nfireMapState: $fireMapState\nyourLocations count: ${yourLocations.length}\nunread notif: $fireNotificationsUnread\nfireNotifications: $fireNotifications\nyourLocations: $yourLocations}'; - } -} - -typedef AddYourLocationFunction = void Function(YourLocation loc); -typedef DeleteYourLocationFunction = void Function(YourLocation loc); -typedef OnRefreshYourLocationsFunction = void Function( - Completer callback); -typedef ToggleSubscriptionFunction = void Function(YourLocation loc); -typedef OnLocationTapFunction = void Function(YourLocation loc); -typedef OnSubscribeFunction = void Function(YourLocation loc); -typedef OnSubscribeDistanceChangeFunction = void Function(YourLocation loc); -typedef OnUnSubscribeFunction = void Function(YourLocation loc); -typedef OnSubscribeConfirmedFunction = void Function(YourLocation loc); -typedef OnLocationEdit = void Function(YourLocation loc); -typedef OnLocationEditing = void Function(YourLocation loc); -typedef OnLocationEditConfirm = void Function(YourLocation loc); -typedef OnLocationEditCancel = void Function(YourLocation loc); -typedef TapFireNotificationFunction = void Function(FireNotification notif); -typedef OnFirePressedInMap = void Function( - LatLng latLng, DateTime when, String source); -// typedef void OnReceivedFireNotificationFunction(FireNotification notif); -typedef DeleteFireNotificationFunction = void Function(FireNotification notif); -typedef DeleteAllFireNotificationFunction = void Function(); - -// unused -// typedef void UpdateYourLocationFunction(ObjectId id, YourLocation loc); diff --git a/lib/models/app_state.g.dart b/lib/models/app_state.g.dart deleted file mode 100644 index 15681c4..0000000 --- a/lib/models/app_state.g.dart +++ /dev/null @@ -1,41 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'app_state.dart'; - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -AppState _$AppStateFromJson(Map json) => $checkedCreate( - 'AppState', - json, - ($checkedConvert) { - final val = AppState( - yourLocations: $checkedConvert( - 'yourLocations', - (v) => - (v as List?) - ?.map((e) => YourLocation.fromJson( - Map.from(e as Map))) - .toList() ?? - const []), - fireNotifications: $checkedConvert( - 'fireNotifications', - (v) => - (v as List?) - ?.map((e) => FireNotification.fromJson( - Map.from(e as Map))) - .toList() ?? - const []), - isLoading: $checkedConvert('isLoading', (v) => v as bool? ?? false), - ); - return val; - }, - ); - -Map _$AppStateToJson(AppState instance) => { - 'isLoading': instance.isLoading, - 'yourLocations': instance.yourLocations.map((e) => e.toJson()).toList(), - 'fireNotifications': - instance.fireNotifications.map((e) => e.toJson()).toList(), - }; diff --git a/lib/models/basicLocation.dart b/lib/models/basicLocation.dart new file mode 100644 index 0000000..c7fe60d --- /dev/null +++ b/lib/models/basicLocation.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; + +class BasicLocation extends Comparable { + final double lat; + final double lon; + String description; + +// static BasicLocation noLocation = new BasicLocation(lat: 0.0, lon: 0.0); + + BasicLocation({@required this.lat, @required this.lon, this.description}) { + if (this.description == null) + this.description = 'Position: ${this.lat}, ${this.lon}'; + } + + int compareTo(BasicLocation other) { + return lat == other.lat && lon == other.lon ? 1 : 0; + } + + bool operator ==(o) => o is BasicLocation && o.lat == lat && o.lon == lon; + + int get hashCode { + int hash = 1; + hash = hash * 17 + lat.hashCode; + hash = hash * 31 + lon.hashCode; + return hash; + } + + BasicLocation.fromJson(Map json) + : lat = json['lat'], + lon = json['lon'], + description = json['description']; + + Map toJson() => { + 'lat': lat, + 'lon': lon, + 'description': description, + }; +} diff --git a/lib/models/basic_location.dart b/lib/models/basic_location.dart deleted file mode 100644 index ffa2405..0000000 --- a/lib/models/basic_location.dart +++ /dev/null @@ -1,39 +0,0 @@ -import 'package:meta/meta.dart'; - -@immutable -class BasicLocation implements Comparable { -// static BasicLocation noLocation = new BasicLocation(lat: 0.0, lon: 0.0); - - const BasicLocation({required this.lat, required this.lon, this.description}); - - BasicLocation.fromJson(Map json) - : lat = (json['lat'] as num).toDouble(), - lon = (json['lon'] as num).toDouble(), - description = json['description'] as String?; - final double lat; - final double lon; - final String? description; - - @override - int compareTo(BasicLocation other) { - return lat == other.lat && lon == other.lon ? 1 : 0; - } - - @override - bool operator ==(Object other) => - other is BasicLocation && other.lat == lat && other.lon == lon; - - @override - int get hashCode { - int hash = 1; - hash = hash * 17 + lat.hashCode; - hash = hash * 31 + lon.hashCode; - return hash; - } - - Map toJson() => { - 'lat': lat, - 'lon': lon, - 'description': description, - }; -} diff --git a/lib/models/falsePositiveTypes.dart b/lib/models/falsePositiveTypes.dart new file mode 100644 index 0000000..4895095 --- /dev/null +++ b/lib/models/falsePositiveTypes.dart @@ -0,0 +1 @@ +enum FalsePositiveType { industry, controled, falsealarm } \ No newline at end of file diff --git a/lib/models/false_positive_types.dart b/lib/models/false_positive_types.dart deleted file mode 100644 index 3ce2cfd..0000000 --- a/lib/models/false_positive_types.dart +++ /dev/null @@ -1 +0,0 @@ -enum FalsePositiveType { industry, controled, falsealarm } diff --git a/lib/models/fire_map_state.dart b/lib/models/fireMapState.dart similarity index 59% rename from lib/models/fire_map_state.dart rename to lib/models/fireMapState.dart index ee42122..f804988 100644 --- a/lib/models/fire_map_state.dart +++ b/lib/models/fireMapState.dart @@ -1,8 +1,7 @@ +import 'package:fires_flutter/models/fireNotification.dart'; +import 'package:fires_flutter/models/yourLocation.dart'; import 'package:meta/meta.dart'; -import 'fire_notification.dart'; -import 'your_location.dart'; - enum FireMapStatus { view, subscriptionConfirm, @@ -10,53 +9,52 @@ enum FireMapStatus { edit, viewFireNotification } - enum FireMapLayer { osmcGrey, esriSatellite, osmc, esri, esriTerrain } @immutable class FireMapState { - - const FireMapState( - {this.status = FireMapStatus.view, - this.layer = FireMapLayer.osmcGrey, - this.yourLocation, - this.numFires = 0, - this.fires = const [], - this.fireNotification, - this.falsePos = const [], - this.industries = const []}); - - const FireMapState.initial() - : status = FireMapStatus.view, - layer = FireMapLayer.osmcGrey, - yourLocation = null, - fireNotification = null, - numFires = 0, - fires = const [], - falsePos = const [], - industries = const []; final FireMapStatus status; final FireMapLayer layer; final int numFires; - final FireNotification? fireNotification; + final FireNotification fireNotification; final List fires; final List falsePos; final List industries; - final YourLocation? yourLocation; + final YourLocation yourLocation; + + const FireMapState.initial() + : this.status = FireMapStatus.view, + this.layer = FireMapLayer.osmcGrey, + this.yourLocation = null, + this.fireNotification = null, + this.numFires = 0, + this.fires = const [], + this.falsePos = const [], + this.industries = const []; + + FireMapState( + {this.status: FireMapStatus.view, + this.layer: FireMapLayer.osmcGrey, + this.yourLocation, + this.numFires, + this.fires, + this.fireNotification, + this.falsePos, + this.industries}); FireMapState copyWith({ - FireMapStatus? status, - FireMapLayer? layer, - YourLocation? yourLocation, - FireNotification? fireNotication, - int? numFires, - List? fires, - List? falsePos, - List? industries, + FireMapStatus status, + FireMapLayer layer, + YourLocation yourLocation, + FireNotification fireNotication, + int numFires, + List fires, + List falsePos, + List industries, }) { - return FireMapState( + return new FireMapState( yourLocation: yourLocation ?? this.yourLocation, - fireNotification: fireNotication ?? fireNotification, + fireNotification: fireNotication ?? this.fireNotification, numFires: numFires ?? this.numFires, fires: fires ?? this.fires, falsePos: falsePos ?? this.falsePos, @@ -92,6 +90,8 @@ class FireMapState { @override String toString() { - return 'FireMapState{status: $status, layer: $layer, numFires: $numFires, fires: ${fires.length}, falsePos: ${falsePos.length}, industries: ${industries.length}, yourLocation: $yourLocation, fireNotification: $fireNotification}'; + return 'FireMapState{status: $status, layer: $layer, numFires: $numFires, fires: ${fires + .length}, falsePos: ${falsePos.length}, industries: ${industries + .length}, yourLocation: $yourLocation, fireNotification: $fireNotification}'; } } diff --git a/lib/models/fire_notification.dart b/lib/models/fireNotification.dart similarity index 70% rename from lib/models/fire_notification.dart rename to lib/models/fireNotification.dart index 3fffa64..5c3cef4 100644 --- a/lib/models/fire_notification.dart +++ b/lib/models/fireNotification.dart @@ -1,29 +1,15 @@ +import 'package:bson_objectid/bson_objectid.dart'; +import 'package:comunes_flutter/comunes_flutter.dart'; import 'package:flutter/material.dart'; import 'package:json_annotation/json_annotation.dart'; -import 'package:objectid/objectid.dart'; +import '../objectIdUtils.dart'; -import '../object_id_utils.dart'; -import '../utils/widget_utils.dart'; +part 'fireNotification.g.dart'; -part 'fire_notification.g.dart'; - -@immutable -@JsonSerializable() -class FireNotification { - const FireNotification( - {required this.id, - required this.lat, - required this.lon, - required this.description, - required this.when, - required this.read, - required this.sealed, - required this.subsId}); - - factory FireNotification.fromJson(Map json) => - _$FireNotificationFromJson(json); +@JsonSerializable(nullable: false) +class FireNotification extends Object with _$FireNotificationSerializerMixin { @JsonKey(toJson: objectIdToJson, fromJson: objectIdFromJson) - final ObjectId id; + ObjectId id; final double lat; final double lon; final String description; @@ -33,16 +19,24 @@ class FireNotification { final ObjectId subsId; final bool read; + factory FireNotification.fromJson(Map json) => + _$FireNotificationFromJson(json); + + FireNotification( + {this.id, + @required this.lat, + @required this.lon, + @required this.description, + @required this.when, + @required this.read, + @required this.sealed, + @required this.subsId}) { + if (this.id == null) this.id = new ObjectId(); + } + FireNotification copyWith( - {ObjectId? id, - double? lat, - double? lon, - String? description, - bool? read, - DateTime? when, - String? sealed, - ObjectId? subsId}) { - return FireNotification( + {id, lat, lon, description, read, when, sealed, subsId}) { + return new FireNotification( id: id ?? this.id, lat: lat ?? this.lat, lon: lon ?? this.lon, @@ -80,11 +74,11 @@ class FireNotification { @override String toString() { - return 'FireNotification {id: $id, lat: $lat, lon: $lon, when: $when, read: $read, subsId: $subsId, sealed ${ellipse(sealed)}'; + return 'FireNotification {id: $id, lat: $lat, lon: $lon, when: $when, read: $read, subsId: $subsId, sealed ${ellipse( + sealed)}'; } - static final Map> routes = >{}; - Map toJson() => _$FireNotificationToJson(this); + static final Map> routes = >{}; /* Route getRoute(Store store) { diff --git a/lib/models/fireNotification.g.dart b/lib/models/fireNotification.g.dart new file mode 100644 index 0000000..30afb18 --- /dev/null +++ b/lib/models/fireNotification.g.dart @@ -0,0 +1,74 @@ +// Copyright (c) 2018, Comunes Association. + +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'fireNotification.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +FireNotification _$FireNotificationFromJson(Map json) => + new FireNotification( + id: objectIdFromJson(json['id'] as String), + lat: (json['lat'] as num).toDouble(), + lon: (json['lon'] as num).toDouble(), + description: json['description'] as String, + when: DateTime.parse(json['when'] as String), + read: json['read'] as bool, + sealed: json['sealed'] as String, + subsId: objectIdFromJson(json['subsId'] as String)); + +abstract class _$FireNotificationSerializerMixin { + ObjectId get id; + double get lat; + double get lon; + String get description; + DateTime get when; + String get sealed; + ObjectId get subsId; + bool get read; + Map toJson() => new _$FireNotificationJsonMapWrapper(this); +} + +class _$FireNotificationJsonMapWrapper extends $JsonMapWrapper { + final _$FireNotificationSerializerMixin _v; + _$FireNotificationJsonMapWrapper(this._v); + + @override + Iterable get keys => const [ + 'id', + 'lat', + 'lon', + 'description', + 'when', + 'sealed', + 'subsId', + 'read' + ]; + + @override + dynamic operator [](Object key) { + if (key is String) { + switch (key) { + case 'id': + return objectIdToJson(_v.id); + case 'lat': + return _v.lat; + case 'lon': + return _v.lon; + case 'description': + return _v.description; + case 'when': + return _v.when.toIso8601String(); + case 'sealed': + return _v.sealed; + case 'subsId': + return objectIdToJson(_v.subsId); + case 'read': + return _v.read; + } + } + return null; + } +} diff --git a/lib/models/fireNotificationsPersist.dart b/lib/models/fireNotificationsPersist.dart new file mode 100644 index 0000000..477dbe3 --- /dev/null +++ b/lib/models/fireNotificationsPersist.dart @@ -0,0 +1,38 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:fires_flutter/models/fireNotification.dart'; +import 'package:comunes_flutter/comunes_flutter.dart'; +import '../globals.dart' as globals; + +final String fireNotificationKey = 'fireNotifications'; + +Future> loadFireNotifications() async { + return await globals.prefs.then((prefs) { + List FireNotifications = prefs.getStringList(fireNotificationKey); + List persistedList = List(); + if (FireNotifications == null) { + FireNotifications = []; + // first run, init with empty list + persistFireNotifications(persistedList); + } + if (FireNotifications is List) { + FireNotifications.forEach((notificationString) { + Map notificationMap = json.decode(notificationString); + persistedList.add(FireNotification.fromJson(notificationMap)); + }); + } + return persistedList; + }); +} + +persistFireNotifications(List notif) { + // print('Persisting $notif'); + globals.prefs.then((prefs) { + List notifAsString = []; + notif.where(notNull).toList().forEach((notification) { + notifAsString.add(json.encode(notification.toJson())); + }); + prefs.setStringList(fireNotificationKey, notifAsString); + }); +} diff --git a/lib/models/fire_notification.g.dart b/lib/models/fire_notification.g.dart deleted file mode 100644 index d20b48b..0000000 --- a/lib/models/fire_notification.g.dart +++ /dev/null @@ -1,38 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'fire_notification.dart'; - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -FireNotification _$FireNotificationFromJson(Map json) => $checkedCreate( - 'FireNotification', - json, - ($checkedConvert) { - final val = FireNotification( - id: $checkedConvert('id', (v) => objectIdFromJson(v as String)), - lat: $checkedConvert('lat', (v) => (v as num).toDouble()), - lon: $checkedConvert('lon', (v) => (v as num).toDouble()), - description: $checkedConvert('description', (v) => v as String), - when: $checkedConvert('when', (v) => DateTime.parse(v as String)), - read: $checkedConvert('read', (v) => v as bool), - sealed: $checkedConvert('sealed', (v) => v as String), - subsId: - $checkedConvert('subsId', (v) => objectIdFromJson(v as String)), - ); - return val; - }, - ); - -Map _$FireNotificationToJson(FireNotification instance) => - { - 'id': objectIdToJson(instance.id), - 'lat': instance.lat, - 'lon': instance.lon, - 'description': instance.description, - 'when': instance.when.toIso8601String(), - 'sealed': instance.sealed, - 'subsId': objectIdToJson(instance.subsId), - 'read': instance.read, - }; diff --git a/lib/models/fire_notifications_persist.dart b/lib/models/fire_notifications_persist.dart deleted file mode 100644 index c9a52dd..0000000 --- a/lib/models/fire_notifications_persist.dart +++ /dev/null @@ -1,37 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; - -// ignore: implementation_imports -import 'package:shared_preferences/src/shared_preferences_legacy.dart'; - -import '../globals.dart' as globals; -import 'fire_notification.dart'; - -const String fireNotificationKey = 'fireNotifications'; - -Future> loadFireNotifications() async { - return globals.prefs.then((SharedPreferences prefs) { - final List? fireNotifications = - prefs.getStringList(fireNotificationKey); - final List persistedList = []; - for (final String notificationString in (fireNotifications ?? [])) { - final Map notificationMap = - json.decode(notificationString) as Map; - persistedList.add(FireNotification.fromJson(notificationMap)); - } - return persistedList; - }); -} - -void persistFireNotifications(List notif) { - // print('Persisting $notif'); - globals.prefs.then((SharedPreferences prefs) { - final List notifAsString = []; - notif - .whereType() - .forEach((FireNotification notification) { - notifAsString.add(json.encode(notification.toJson())); - }); - prefs.setStringList(fireNotificationKey, notifAsString); - }); -} diff --git a/lib/models/firesApi.dart b/lib/models/firesApi.dart new file mode 100644 index 0000000..a689bbc --- /dev/null +++ b/lib/models/firesApi.dart @@ -0,0 +1,210 @@ +import 'dart:async'; +import 'dart:convert'; +import 'package:flutter_map/flutter_map.dart'; +import 'package:flutter/material.dart'; +import 'falsePositiveTypes.dart'; + +import 'package:bson_objectid/bson_objectid.dart'; +import 'package:fires_flutter/models/yourLocation.dart'; +import 'package:http/http.dart' as ht; +import 'package:jaguar_resty/jaguar_resty.dart' as resty; +import 'package:latlong/latlong.dart'; + +import '../globals.dart' as globals; +import '../redux/actions.dart'; +import 'appState.dart'; + +class FiresApi { + FiresApi() { + resty.globalClient = new ht.IOClient(); + } + + Future createUser(AppState state, String mobileToken, + String lang) async { + assert(state.firesApiUrl != null); + assert(state.firesApiKey != null); + assert(mobileToken != null); + assert(lang != null); + + final params = { + "token": state.firesApiKey, + "mobileToken": mobileToken, + "lang": lang + }; + final String url = '${state.firesApiUrl}mobile/users'; + /* print(url); + print(params); */ + return await resty.post(url).json(params).go().then((response) { + if (response.statusCode == 200) { + // print(response.body); + return json.decode(response.body)['data']['userId']; + } + }); + } + + Future> fetchYourLocations(AppState state) async { + final apiKey = state.firesApiKey; + final mobileToken = state.user.token; + final String url = '${state + .firesApiUrl}mobile/subscriptions/all/$apiKey/$mobileToken'; + // if (globals.isDevelopment) print('$url'); + return await resty.get(url).go().then((response) { + if (response.statusCode == 200) { + // if (globals.isDevelopment) print(response.body); + final dataSubscriptions = + json.decode(response.body)['data']['subscriptions']; + List subscribed = []; + for (int i = 0; i < dataSubscriptions.length; i++) { + var el = dataSubscriptions[i]; + var lat = el['location']['lat']; + var lon = el['location']['lon']; + subscribed.add(new YourLocation( + id: ObjectId.fromHexString(el['_id']['_str']), + lat: lat, + lon: lon, + subscribed: true, + distance: el['distance'])); + } + + return subscribed; + } + }); + } + + Future subscribe(AppState state, YourLocation loc) async { + assert(state.firesApiUrl != null); + assert(state.firesApiKey != null); + assert(state.user.token != null); + assert(loc != null); + assert(loc.lat != null); + assert(loc.lon != null); + assert(loc.id != null); + assert(loc.distance != null); + final params = { + "token": state.firesApiKey, + "mobileToken": state.user.token, + "id": loc.id.toHexString(), + "lat": loc.lat, + "lon": loc.lon, + "distance": loc.distance + }; + final String url = '${state + .firesApiUrl}mobile/subscriptions'; + return await resty.post(url).json(params).go().then((response) { + if (response.statusCode == 200) { + // print(response.body); + return json.decode(response.body)['data']['subsId']; + } else { + // take care of? "Unexpected error in REST call: Error: The user is already subscribed to this area [on-already-subscribed]" + print(response.body); + } + }); + } + + Future unsubscribe(AppState state, String subsId) async { + assert(state.firesApiUrl != null); + assert(state.firesApiKey != null); + assert(state.user.token != null); + final apiKey = state.firesApiKey; + final mobileToken = state.user.token; + assert(subsId != null); + final String url = '${state + .firesApiUrl}mobile/subscriptions/$apiKey/$mobileToken/$subsId'; + return await resty.delete(url).go().then((response) { + if (response.statusCode == 200) { + // print(response.body); + return true; + } + }); + } + + Future getFiresInLocation( + {AppState state, double lat, double lon, int distance}) async { + assert(state.firesApiUrl != null); + assert(state.firesApiKey != null); + var url = '${state.firesApiUrl}fires-in-full/${state + .firesApiKey}/${lat}/${lon}/${distance}'; + if (globals.isDevelopment) print(url); + return await resty.get(url).go().then((response) { + if (response.statusCode == 200) { + var resultDecoded = json.decode(response.body); + int numFires = resultDecoded['real']; + List fires = resultDecoded['fires']; + List falsePos = resultDecoded['falsePos']; + List industries = resultDecoded['industries']; + + if (globals.isDevelopment) { + var firesCount = fires.length; + var industriesCount = industries.length; + var falsePosCount = falsePos.length; + print( + '(Pos: $lat, $lon) real: $numFires, fire: $firesCount falsePos: $falsePosCount industries: $industriesCount'); + } + return new UpdateFireMapStatsAction( + numFires: numFires, + fires: fires, + falsePos: falsePos, + industries: industries); + } else + throw Exception('Wrong response trying to get fire data'); + }); + } + + Future> getMonitoredAreas({AppState state}) async { + assert(state.firesApiUrl != null); + assert(state.firesApiKey != null); + var url = '${state.firesApiUrl}status/subs-public-union/${state + .firesApiKey}'; + var color = const Color(0xFF145A32); + return await resty.get(url).go().then((response) { + if (response.statusCode == 200) { + var resultDecoded = json.decode(response.body); + // print(resultDecoded['data']['union']); + List union = []; + final multipolygon = + json.decode(resultDecoded['data']['union']['value'])['geometry'] + ['coordinates']; + for (List polygon in multipolygon) { + for (List hole in polygon) { + List points = []; + for (List point in hole) { + points.add(new LatLng(point[1].toDouble(), point[0].toDouble())); + } + union.add( + new Polyline(points: points, color: color, strokeWidth: 3.0)); + } + } + return union; + } else + throw Exception('Wrong response trying to get fire data'); + }); + } + + Future markFalsePositive(AppState state, String mobileToken, + String sealed, FalsePositiveType type) async { + assert(state.firesApiUrl != null); + assert(state.firesApiKey != null); + assert(mobileToken != null); + assert(sealed != null); + assert(type != null); + + final params = { + "token": state.firesApiKey, + "mobileToken": mobileToken, + "sealed": sealed, + "type": type.toString().split('.')[1] + }; + final String url = '${state.firesApiUrl}mobile/falsepositive'; + return await resty.post(url).json(params).go().then((response) { + if (response.statusCode == 200) { + // print(response.body); + if (globals.isDevelopment) print( + json.decode(response.body)['data']['upsert']); + return true; + } else { + debugPrint(json.decode(response.body)); + return false; + } + }); + } +} diff --git a/lib/models/fires_api.dart b/lib/models/fires_api.dart deleted file mode 100644 index c9b2cf0..0000000 --- a/lib/models/fires_api.dart +++ /dev/null @@ -1,248 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; - -import 'package:dio/dio.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_map/flutter_map.dart'; -import 'package:latlong2/latlong.dart'; - -import '../globals.dart' as globals; -import '../object_id_utils.dart'; -import '../redux/actions.dart'; -import 'app_state.dart'; -import 'false_positive_types.dart'; -import 'your_location.dart'; - -class FiresApi { - FiresApi() { - _dio = Dio(); - } - late final Dio _dio; - - Future createUser( - AppState state, String mobileToken, String lang) async { - final Map params = { - 'token': state.firesApiKey, - 'mobileToken': mobileToken, - 'lang': lang - }; - final String url = '${state.firesApiUrl}mobile/users'; - try { - final Response> response = - await _dio.post>(url, data: params); - if (response.statusCode == 200) { - final Map? responseData = response.data; - if (responseData == null) { - throw Exception('Response data is null'); - } - final Map data = responseData; - final Map dataData = - data['data'] as Map; - return dataData['userId'] as String; - } else { - throw Exception('Unexpected error on create user'); - } - } catch (e) { - throw Exception('Error creating user: $e'); - } - } - - Future> fetchYourLocations(AppState state) async { - final String apiKey = state.firesApiKey; - final String mobileToken = state.user.token; - final String url = - '${state.firesApiUrl}mobile/subscriptions/all/$apiKey/$mobileToken'; - try { - final Response> response = - await _dio.get>(url); - if (response.statusCode == 200) { - final Map? responseData = response.data; - if (responseData == null) { - throw Exception('Response data is null'); - } - final Map data = responseData; - final Map dataData = - data['data'] as Map; - final List dataSubscriptions = - dataData['subscriptions'] as List; - final List subscribed = []; - for (int i = 0; i < dataSubscriptions.length; i++) { - final Map el = - dataSubscriptions[i] as Map; - final Map location = - el['location'] as Map; - final double lat = (location['lat'] as num).toDouble(); - final double lon = (location['lon'] as num).toDouble(); - final Map id = el['_id'] as Map; - subscribed.add(YourLocation( - id: objectIdFromJson(id['_str'] as String), - lat: lat, - lon: lon, - subscribed: true, - distance: (el['distance'] as num).toInt())); - } - return subscribed; - } else { - throw Exception('Unexpected error fetching your locations'); - } - } catch (e) { - throw Exception('Error fetching locations: $e'); - } - } - - Future subscribe(AppState state, YourLocation loc) async { - final Map params = { - 'token': state.firesApiKey, - 'mobileToken': state.user.token, - 'id': loc.id.hexString, - 'lat': loc.lat, - 'lon': loc.lon, - 'distance': loc.distance - }; - final String url = '${state.firesApiUrl}mobile/subscriptions'; - try { - final Response> response = - await _dio.post>(url, data: params); - if (response.statusCode == 200) { - final Map? responseData = response.data; - if (responseData == null) { - throw Exception('Response data is null'); - } - final Map data = responseData; - final Map dataData = - data['data'] as Map; - return dataData['subsId'] as String; - } else { - throw Exception('Unexpected error on subscribe'); - } - } catch (e) { - throw Exception('Error subscribing: $e'); - } - } - - Future unsubscribe(AppState state, String subsId) async { - final String apiKey = state.firesApiKey; - final String mobileToken = state.user.token; - final String url = - '${state.firesApiUrl}mobile/subscriptions/$apiKey/$mobileToken/$subsId'; - try { - final Response response = await _dio.delete(url); - if (response.statusCode == 200) { - return true; - } else { - throw Exception('Unexpected error on unsubscribe'); - } - } catch (e) { - throw Exception('Error unsubscribing: $e'); - } - } - - Future getFiresInLocation( - {required AppState state, - required double lat, - required double lon, - required int distance}) async { - final String url = - '${state.firesApiUrl}fires-in-full/${state.firesApiKey}/$lat/$lon/$distance'; - if (globals.isDevelopment) { - debugPrint(url); - } - try { - final Response response = await _dio.get(url); - if (response.statusCode == 200) { - final Map resultDecoded = - response.data as Map; - final int numFires = (resultDecoded['real'] as num).toInt(); - final List fires = resultDecoded['fires'] as List; - final List falsePos = - resultDecoded['falsePos'] as List; - final List industries = - resultDecoded['industries'] as List; - - if (globals.isDevelopment) { - // Could log: fires: ${fires.length} falsePos: ${falsePos.length} industries: ${industries.length} - } - return UpdateFireMapStatsAction( - numFires: numFires, - fires: fires, - falsePos: falsePos, - industries: industries); - } else - throw Exception('Wrong response trying to get fire data'); - } catch (e) { - throw Exception('Error getting fires: $e'); - } - } - - Future> getMonitoredAreas({required AppState state}) async { - final String url = - '${state.firesApiUrl}status/subs-public-union/${state.firesApiKey}'; - const Color color = Color(0xFF145A32); - try { - final Response response = await _dio.get(url); - if (response.statusCode == 200) { - final Map resultDecoded = - response.data as Map; - final List union = []; - final Map dataData = - resultDecoded['data'] as Map; - final Map unionData = - dataData['union'] as Map; - final String unionValue = unionData['value'] as String; - final Map decodedJson = - json.decode(unionValue) as Map; - final Map geometry = - decodedJson['geometry'] as Map; - final List multipolygon = - geometry['coordinates'] as List; - for (final dynamic polygonDynamic in multipolygon) { - final List polygon = polygonDynamic as List; - for (final dynamic holeDynamic in polygon) { - final List hole = holeDynamic as List; - final List points = []; - for (final dynamic pointDynamic in hole) { - final List point = pointDynamic as List; - points.add(LatLng( - (point[1] as num).toDouble(), (point[0] as num).toDouble())); - } - union.add(Polyline(points: points, color: color, strokeWidth: 3.0)); - } - } - return union; - } else - throw Exception('Wrong response trying to get fire data'); - } catch (e) { - throw Exception('Error getting monitored areas: $e'); - } - } - - Future markFalsePositive(AppState state, String mobileToken, - String sealed, FalsePositiveType type) async { - final Map params = { - 'token': state.firesApiKey, - 'mobileToken': mobileToken, - 'sealed': sealed, - 'type': type.toString().split('.')[1] - }; - final String url = '${state.firesApiUrl}mobile/falsepositive'; - try { - final Response response = await _dio.post(url, data: params); - if (response.statusCode == 200) { - if (globals.isDevelopment) { - final Map data = - response.data as Map; - final Map dataData = - data['data'] as Map; - debugPrint(dataData['upsert'].toString()); - } - return true; - } else { - debugPrint(response.data.toString()); - return false; - } - } catch (e) { - debugPrint('Error marking false positive: $e'); - return false; - } - } -} diff --git a/lib/models/user.dart b/lib/models/user.dart index 4bd43c6..364acde 100644 --- a/lib/models/user.dart +++ b/lib/models/user.dart @@ -1,21 +1,21 @@ import 'package:meta/meta.dart'; - -import '../utils/widget_utils.dart'; +import 'package:comunes_flutter/comunes_flutter.dart'; @immutable class User { - const User({required this.userId, required this.lang, required this.token}); - - const User.initial() - : userId = '', - lang = '', - token = ''; final String userId; final String lang; final String token; - User copyWith({String? userId, String? lang, String? token}) { - return User( + const User.initial() + : this.userId = null, + lang = null, + token = null; + + User({this.userId, this.lang, this.token}); + + User copyWith({String userId, String lang, String token}) { + return new User( userId: userId ?? this.userId, token: token ?? this.token, lang: lang ?? this.lang); @@ -25,4 +25,5 @@ class User { String toString() { return 'User {userId: $userId, lang: $lang, token: ${ellipse(token)}'; } + } diff --git a/lib/models/yourLocation.dart b/lib/models/yourLocation.dart new file mode 100644 index 0000000..b0b2472 --- /dev/null +++ b/lib/models/yourLocation.dart @@ -0,0 +1,86 @@ +import 'package:bson_objectid/bson_objectid.dart'; +import 'package:flutter/material.dart'; +import 'package:json_annotation/json_annotation.dart'; +import 'package:fires_flutter/objectIdUtils.dart'; + +part 'yourLocation.g.dart'; + + + +@JsonSerializable(nullable: false) +class YourLocation extends Object with _$YourLocationSerializerMixin { + @JsonKey(toJson: objectIdToJson, fromJson: objectIdFromJson) + ObjectId id; + final double lat; + final double lon; + String description; + bool subscribed; + int distance; + int currentNumFires; + + factory YourLocation.fromJson(Map json) => + _$YourLocationFromJson(json); + + static YourLocation noLocation = new YourLocation(lat: 0.0, lon: 0.0); +static const int withoutStats = null; + YourLocation( + {this.id, + @required this.lat, + @required this.lon, + this.description, + this.distance: 10, + int currentNumFires: withoutStats, + this.subscribed: false}) { + if (this.description == null) + this.description = 'Position: ${this.lat}, ${this.lon}'; + if (this.id == null) this.id = new ObjectId(); + } + + YourLocation copyWith( + {id, + lat, + lon, + description, + distance, + currentNumFires, + subscribed}) { + return new YourLocation( + id: id?? this.id, + lat: lat?? this.lat, + lon: lon?? this.lon, + description: description?? this.description, + distance: distance?? this.distance, + currentNumFires: currentNumFires ?? this.currentNumFires, + subscribed: subscribed?? this.subscribed + ); + } + + + @override + bool operator ==(Object other) => + identical(this, other) || + other is YourLocation && + runtimeType == other.runtimeType && + id == other.id && + lat == other.lat && + lon == other.lon && + description == other.description && + subscribed == other.subscribed && + currentNumFires == other.currentNumFires && + distance == other.distance; + + @override + int get hashCode => + id.hashCode ^ + lat.hashCode ^ + lon.hashCode ^ + description.hashCode ^ + subscribed.hashCode ^ + currentNumFires.hashCode ^ + distance.hashCode; + + @override + String toString() { + return 'YourLocation {id: $id, lat: $lat, lon: $lon, description: $description, subscribed: $subscribed, distance: $distance, currentNumFires: $currentNumFires}'; + } +} diff --git a/lib/models/yourLocation.g.dart b/lib/models/yourLocation.g.dart new file mode 100644 index 0000000..5313b32 --- /dev/null +++ b/lib/models/yourLocation.g.dart @@ -0,0 +1,58 @@ +// Copyright (c) 2018, Comunes Association. + +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'yourLocation.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +YourLocation _$YourLocationFromJson(Map json) => + new YourLocation( + id: objectIdFromJson(json['id'] as String), + lat: (json['lat'] as num).toDouble(), + lon: (json['lon'] as num).toDouble(), + description: json['description'] as String, + distance: json['distance'] as int, + subscribed: json['subscribed'] as bool); + +abstract class _$YourLocationSerializerMixin { + ObjectId get id; + double get lat; + double get lon; + String get description; + bool get subscribed; + int get distance; + Map toJson() => new _$YourLocationJsonMapWrapper(this); +} + +class _$YourLocationJsonMapWrapper extends $JsonMapWrapper { + final _$YourLocationSerializerMixin _v; + _$YourLocationJsonMapWrapper(this._v); + + @override + Iterable get keys => + const ['id', 'lat', 'lon', 'description', 'subscribed', 'distance']; + + @override + dynamic operator [](Object key) { + if (key is String) { + switch (key) { + case 'id': + return objectIdToJson(_v.id); + case 'lat': + return _v.lat; + case 'lon': + return _v.lon; + case 'description': + return _v.description; + case 'subscribed': + return _v.subscribed; + case 'distance': + return _v.distance; + } + } + return null; + } +} diff --git a/lib/models/yourLocationPersist.dart b/lib/models/yourLocationPersist.dart new file mode 100644 index 0000000..c992974 --- /dev/null +++ b/lib/models/yourLocationPersist.dart @@ -0,0 +1,38 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:fires_flutter/models/yourLocation.dart'; +import 'package:comunes_flutter/comunes_flutter.dart'; +import '../globals.dart' as globals; + +final String locationKey = 'yourlocations'; + +Future> loadYourLocations() async { + return await globals.prefs.then((prefs) { + List yourLocations = prefs.getStringList(locationKey); + List persistedList = List(); + if (yourLocations == null) { + yourLocations = []; + // first run, init with empty list + persistYourLocations(persistedList); + } + if (yourLocations is List) { + yourLocations.forEach((locationString) { + Map locationMap = json.decode(locationString); + persistedList.add(YourLocation.fromJson(locationMap)); + }); + } + return persistedList; + }); +} + +persistYourLocations(List yl) { + // debugPrint('Persisting $yl'); + globals.prefs.then((prefs) { + List ylAsString = []; + yl.where(notNull).toList().forEach((location) { + ylAsString.add(json.encode(location.toJson())); + }); + prefs.setStringList(locationKey, ylAsString); + }); +} diff --git a/lib/models/your_location.dart b/lib/models/your_location.dart deleted file mode 100644 index b975b02..0000000 --- a/lib/models/your_location.dart +++ /dev/null @@ -1,88 +0,0 @@ -import 'package:json_annotation/json_annotation.dart'; -import 'package:meta/meta.dart'; -import 'package:objectid/objectid.dart'; - -import '../object_id_utils.dart'; - -part 'your_location.g.dart'; - -@immutable -@JsonSerializable() -class YourLocation { - const YourLocation( - {required this.id, - required this.lat, - required this.lon, - this.description = '', - this.distance = 10, - int? currentNumFires, - this.subscribed = false}) - : currentNumFires = currentNumFires ?? 0; - - factory YourLocation.fromJson(Map json) => - _$YourLocationFromJson(json); - @JsonKey(toJson: objectIdToJson, fromJson: objectIdFromJson) - final ObjectId id; - final double lat; - final double lon; - final String description; - final bool subscribed; - final int distance; - final int currentNumFires; - - static YourLocation get noLocation { - _noLocation ??= YourLocation(id: ObjectId(), lat: 0.0, lon: 0.0); - return _noLocation!; - } - - static YourLocation? _noLocation; - static const int? withoutStats = null; - - Map toJson() => _$YourLocationToJson(this); - - YourLocation copyWith( - {ObjectId? id, - double? lat, - double? lon, - String? description, - int? distance, - int? currentNumFires, - bool? subscribed}) { - return YourLocation( - id: id ?? this.id, - lat: lat ?? this.lat, - lon: lon ?? this.lon, - description: description ?? this.description, - distance: distance ?? this.distance, - currentNumFires: currentNumFires ?? this.currentNumFires, - subscribed: subscribed ?? this.subscribed); - } - - @override - bool operator ==(Object other) => - identical(this, other) || - other is YourLocation && - runtimeType == other.runtimeType && - id == other.id && - lat == other.lat && - lon == other.lon && - description == other.description && - subscribed == other.subscribed && - currentNumFires == other.currentNumFires && - distance == other.distance; - - @override - int get hashCode => - id.hashCode ^ - lat.hashCode ^ - lon.hashCode ^ - description.hashCode ^ - subscribed.hashCode ^ - currentNumFires.hashCode ^ - distance.hashCode; - - @override - String toString() { - return 'YourLocation {id: $id, lat: $lat, lon: $lon, description: $description, subscribed: $subscribed, distance: $distance, currentNumFires: $currentNumFires}'; - } -} diff --git a/lib/models/your_location.g.dart b/lib/models/your_location.g.dart deleted file mode 100644 index 21fc224..0000000 --- a/lib/models/your_location.g.dart +++ /dev/null @@ -1,38 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'your_location.dart'; - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -YourLocation _$YourLocationFromJson(Map json) => $checkedCreate( - 'YourLocation', - json, - ($checkedConvert) { - final val = YourLocation( - id: $checkedConvert('id', (v) => objectIdFromJson(v as String)), - lat: $checkedConvert('lat', (v) => (v as num).toDouble()), - lon: $checkedConvert('lon', (v) => (v as num).toDouble()), - description: - $checkedConvert('description', (v) => v as String? ?? ''), - distance: - $checkedConvert('distance', (v) => (v as num?)?.toInt() ?? 10), - currentNumFires: - $checkedConvert('currentNumFires', (v) => (v as num?)?.toInt()), - subscribed: $checkedConvert('subscribed', (v) => v as bool? ?? false), - ); - return val; - }, - ); - -Map _$YourLocationToJson(YourLocation instance) => - { - 'id': objectIdToJson(instance.id), - 'lat': instance.lat, - 'lon': instance.lon, - 'description': instance.description, - 'subscribed': instance.subscribed, - 'distance': instance.distance, - 'currentNumFires': instance.currentNumFires, - }; diff --git a/lib/models/your_location_persist.dart b/lib/models/your_location_persist.dart deleted file mode 100644 index 243afb8..0000000 --- a/lib/models/your_location_persist.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; - -// ignore: implementation_imports -import 'package:shared_preferences/src/shared_preferences_legacy.dart'; - -import '../globals.dart' as globals; -import 'your_location.dart'; - -const String locationKey = 'yourlocations'; - -Future> loadYourLocations() async { - return globals.prefs.then((SharedPreferences prefs) { - final List? yourLocations = prefs.getStringList(locationKey); - final List persistedList = []; - for (final String locationString in (yourLocations ?? [])) { - final Map locationMap = - json.decode(locationString) as Map; - persistedList.add(YourLocation.fromJson(locationMap)); - } - return persistedList; - }); -} - -void persistYourLocations(List yl) { - // debugPrint('Persisting $yl'); - globals.prefs.then((SharedPreferences prefs) { - final List ylAsString = []; - yl.whereType().forEach((YourLocation location) { - ylAsString.add(json.encode(location.toJson())); - }); - prefs.setStringList(locationKey, ylAsString); - }); -} diff --git a/lib/monitoredAreas.dart b/lib/monitoredAreas.dart new file mode 100644 index 0000000..8b18604 --- /dev/null +++ b/lib/monitoredAreas.dart @@ -0,0 +1,97 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_map/flutter_map.dart'; +import 'package:flutter_redux/flutter_redux.dart'; +import 'package:latlong/latlong.dart'; + +import 'colors.dart'; +import 'customBottomAppBar.dart'; +import 'generated/i18n.dart'; +import 'mainDrawer.dart'; +import 'models/appState.dart'; +import 'firesSpinner.dart'; + +class _ViewModel { + List monitoredAreas; + + _ViewModel(this.monitoredAreas); + + @override + bool operator ==(Object other) => + identical(this, other) || + other is _ViewModel && + runtimeType == other.runtimeType && + monitoredAreas == other.monitoredAreas; + + @override + int get hashCode => monitoredAreas.hashCode; +} + +class MonitoredAreasPage extends StatelessWidget { + static const String routeName = "monitoredAreasMap"; + + @override + Widget build(BuildContext context) { + return new StoreConnector( + distinct: true, + converter: (store) { + return new _ViewModel(store.state.monitoredAreas); + }, + builder: (context, view) { + return new Scaffold( + appBar: + new AppBar(title: new Text(S.of(context).monitoredAreasTitle)), + drawer: new MainDrawer(context, MonitoredAreasPage.routeName), + bottomNavigationBar: new CustomBottomAppBar( + fabLocation: FloatingActionButtonLocation.centerDocked, + showNotch: true, + color: fires100, + mainAxisAlignment: MainAxisAlignment.center, + actions: [ + new Flexible( + child: new Padding( + padding: new EdgeInsets.only(left: 10.0, right: 10.0), + child: new Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + new Text(S.of(context).mapPrivacy, + style: new TextStyle( + fontStyle: FontStyle.italic, + color: Colors.black38)) + ]))) + ]), + body: !(view.monitoredAreas is List) + ? new FiresSpinner() + : new Padding( + padding: new EdgeInsets.all(10.0), + child: new Column( + children: [ + new Padding( + padding: new EdgeInsets.only( + top: 8.0, bottom: 8.0, left: 0.0, right: 0.0), + child: new Text(S.of(context).inGreenMonitoredAreas), + ), + new Flexible( + child: new FlutterMap( + options: new MapOptions( + center: new LatLng(53.5775, 3.106111), + zoom: 1.0, + ), + layers: [ + new TileLayerOptions( + urlTemplate: + "https://tiles.wmflabs.org/bw-mapnik/{z}/{x}/{y}.png", + subdomains: ['a', 'b', 'c']), + new PolylineLayerOptions( + polylines: view.monitoredAreas, + ) + ], + ), + ), + ], + ), + ), + ); + }); + } +} diff --git a/lib/monitored_areas.dart b/lib/monitored_areas.dart deleted file mode 100644 index 8d3b1da..0000000 --- a/lib/monitored_areas.dart +++ /dev/null @@ -1,98 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_map/flutter_map.dart'; -import 'package:flutter_redux/flutter_redux.dart'; -import 'package:latlong2/latlong.dart'; -// ignore: implementation_imports -import 'package:redux/src/store.dart'; - -import 'colors.dart'; -import 'compass_map_plugin.dart'; -import 'custom_bottom_app_bar.dart'; -import 'generated/i18n.dart'; -import 'main_drawer.dart'; -import 'models/app_state.dart'; - -@immutable -class _ViewModel { - const _ViewModel(this.monitoredAreas); - final List monitoredAreas; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is _ViewModel && - runtimeType == other.runtimeType && - monitoredAreas == other.monitoredAreas; - - @override - int get hashCode => monitoredAreas.hashCode; -} - -class MonitoredAreasPage extends StatelessWidget { - const MonitoredAreasPage({super.key}); - - static const String routeName = 'monitoredAreasMap'; - - @override - Widget build(BuildContext context) { - return StoreConnector( - distinct: true, - converter: (Store store) { - return _ViewModel(store.state.monitoredAreas); - }, - builder: (BuildContext context, _ViewModel view) { - return Scaffold( - appBar: AppBar(title: Text(S.of(context).monitoredAreasTitle)), - drawer: MainDrawer(context, MonitoredAreasPage.routeName), - bottomNavigationBar: CustomBottomAppBar( - fabLocation: FloatingActionButtonLocation.centerDocked, - showNotch: true, - color: fires100, - actions: [ - Flexible( - child: Padding( - padding: - const EdgeInsets.only(left: 10.0, right: 10.0), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text(S.of(context).mapPrivacy, - style: const TextStyle( - fontStyle: FontStyle.italic, - color: Colors.black38)) - ]))) - ]), - body: Padding( - padding: const EdgeInsets.all(10.0), - child: Column( - children: [ - Padding( - padding: const EdgeInsets.only(top: 8.0, bottom: 8.0), - child: Text(S.of(context).inGreenMonitoredAreas), - ), - Flexible( - child: FlutterMap( - options: const MapOptions( - initialCenter: LatLng(53.5775, 3.106111), - initialZoom: 1.0, - ), - children: [ - TileLayer( - urlTemplate: - 'https://cartodb-basemaps-{s}.global.ssl.fastly.net/light_all/{z}/{x}/{y}.png', - subdomains: const ['a', 'b', 'c', 'd'], - userAgentPackageName: 'com.example.fires_flutter'), - const CompassMapPluginWidget(), - PolylineLayer( - polylines: view.monitoredAreas, - ) - ], - ), - ), - ], - ), - ), - ); - }); - } -} diff --git a/lib/objectIdUtils.dart b/lib/objectIdUtils.dart new file mode 100644 index 0000000..89d398f --- /dev/null +++ b/lib/objectIdUtils.dart @@ -0,0 +1,9 @@ +import 'package:bson_objectid/bson_objectid.dart'; + +objectIdFromJson(String json) { + return new ObjectId.fromHexString(json); +} + +objectIdToJson(ObjectId o) { + return o.toString(); +} \ No newline at end of file diff --git a/lib/object_id_utils.dart b/lib/object_id_utils.dart deleted file mode 100644 index 4a1d081..0000000 --- a/lib/object_id_utils.dart +++ /dev/null @@ -1,10 +0,0 @@ -import 'package:objectid/objectid.dart'; - -ObjectId objectIdFromJson(String json) { - return ObjectId.fromHexString( - json.replaceFirst('ObjectId(', '').replaceAll(')', '')); -} - -String objectIdToJson(ObjectId o) { - return o.toString(); -} diff --git a/lib/placesAutocompleteUtils.dart b/lib/placesAutocompleteUtils.dart new file mode 100644 index 0000000..8e83dcb --- /dev/null +++ b/lib/placesAutocompleteUtils.dart @@ -0,0 +1,37 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_google_places_autocomplete/flutter_google_places_autocomplete.dart'; + +import 'package:fires_flutter/models/yourLocation.dart'; +import 'generated/i18n.dart'; +import 'package:flutter_simple_dependency_injection/injector.dart'; + +Future openPlacesDialog(GlobalKey sc) async { + Mode _mode = Mode.overlay; + String gmapKey = Injector.getInjector().get(key: "gmapKey"); + GoogleMapsPlaces _places = new GoogleMapsPlaces(gmapKey); + Prediction p = await showGooglePlacesAutocomplete( + context: sc.currentContext, + hint: S.of(sc.currentContext).typeTheNameOfAPlace, + apiKey: gmapKey, + onError: (res) { + sc.currentState + .showSnackBar(new SnackBar(content: new Text(res.errorMessage))); + print('Error $res'); + }, + mode: _mode, + language: Localizations.localeOf(sc.currentContext).languageCode, + components: [ + // This limit the search too much + // new Component(Component.country, "es") + ]); + if (p != null) { + // get detail (lat/lng) + PlacesDetailsResponse detail = await _places.getDetailsByPlaceId(p.placeId); + final lat = detail.result.geometry.location.lat; + final lng = detail.result.geometry.location.lng; + return new YourLocation(lat: lat, lon: lng, description: p.description); + } + return YourLocation.noLocation; +} diff --git a/lib/places_autocomplete_utils.dart b/lib/places_autocomplete_utils.dart deleted file mode 100644 index 9b6d694..0000000 --- a/lib/places_autocomplete_utils.dart +++ /dev/null @@ -1,206 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; -import 'package:geocoding/geocoding.dart'; -import 'package:objectid/objectid.dart'; - -import 'models/your_location.dart'; - -/// Open a places dialog for selecting a location using geocoding. -/// Allows users to search for places by name and get coordinates. -Future openPlacesDialog(GlobalKey sc) async { - final BuildContext? context = sc.currentContext; - if (context == null) { - return YourLocation.noLocation; - } - - final YourLocation? selectedLocation = await showDialog( - context: context, - builder: (BuildContext context) => const _PlaceSelectionDialog(), - ); - - return selectedLocation ?? YourLocation.noLocation; -} - -class _PlaceSelectionDialog extends StatefulWidget { - const _PlaceSelectionDialog(); - - @override - State<_PlaceSelectionDialog> createState() => _PlaceSelectionDialogState(); -} - -class _PlaceSelectionDialogState extends State<_PlaceSelectionDialog> { - final TextEditingController _searchController = TextEditingController(); - List _searchResults = []; - bool _isSearching = false; - String? _errorMessage; - - @override - void dispose() { - _searchController.dispose(); - super.dispose(); - } - - Future _searchPlaces(String query) async { - if (query.isEmpty) { - setState(() { - _searchResults = []; - _errorMessage = null; - }); - return; - } - - setState(() { - _isSearching = true; - _errorMessage = null; - }); - - try { - final List locations = await locationFromAddress(query); - setState(() { - _searchResults = locations; - if (_searchResults.isEmpty) { - _errorMessage = 'No places found. Try a different search.'; - } - }); - } catch (e) { - setState(() { - _errorMessage = 'Search error: $e'; - _searchResults = []; - }); - } finally { - setState(() { - _isSearching = false; - }); - } - } - - Future _getPlaceName(Location location) async { - try { - final List placemarks = - await placemarkFromCoordinates(location.latitude, location.longitude); - if (placemarks.isNotEmpty) { - final Placemark place = placemarks.first; - final List parts = []; - if (place.name != null && place.name!.isNotEmpty) - parts.add(place.name!); - if (place.locality != null && place.locality!.isNotEmpty) - parts.add(place.locality!); - if (place.country != null && place.country!.isNotEmpty) - parts.add(place.country!); - if (parts.isNotEmpty) { - return parts.join(', '); - } - } - } catch (e) { - // Fall back to coordinates - } - return '${location.latitude.toStringAsFixed(4)}, ${location.longitude.toStringAsFixed(4)}'; - } - - Future _selectLocation(Location location) async { - final String description = await _getPlaceName(location); - if (!mounted) { - return; - } - - final YourLocation yourLocation = YourLocation( - id: ObjectId(), - lat: location.latitude, - lon: location.longitude, - description: description, - ); - - Navigator.of(context).pop(yourLocation); - } - - @override - Widget build(BuildContext context) { - return AlertDialog( - title: const Text('Search for a place'), - content: SizedBox( - width: double.maxFinite, - child: SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - TextField( - controller: _searchController, - decoration: InputDecoration( - hintText: 'Enter city, address, or landmark', - prefixIcon: const Icon(Icons.search), - suffixIcon: _isSearching - ? const SizedBox( - width: 20, - height: 20, - child: Padding( - padding: EdgeInsets.all(8.0), - child: CircularProgressIndicator(strokeWidth: 2), - ), - ) - : null, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - onChanged: (String value) { - if (value.length > 2) { - _searchPlaces(value); - } else { - setState(() { - _searchResults = []; - _errorMessage = null; - }); - } - }, - ), - const SizedBox(height: 16), - if (_errorMessage != null) - Padding( - padding: const EdgeInsets.all(8.0), - child: Text( - _errorMessage!, - style: const TextStyle(color: Colors.red, fontSize: 12), - ), - ), - if (_searchResults.isNotEmpty) - SizedBox( - height: 300, - child: ListView.builder( - shrinkWrap: true, - itemCount: _searchResults.length, - itemBuilder: (BuildContext context, int index) { - final Location location = _searchResults[index]; - return FutureBuilder( - future: _getPlaceName(location), - builder: (BuildContext context, - AsyncSnapshot snapshot) { - final String displayName = snapshot.data ?? - '${location.latitude.toStringAsFixed(4)}, ${location.longitude.toStringAsFixed(4)}'; - return ListTile( - leading: const Icon(Icons.location_on), - title: Text(displayName), - subtitle: Text( - '${location.latitude.toStringAsFixed(4)}, ${location.longitude.toStringAsFixed(4)}', - style: const TextStyle(fontSize: 12), - ), - onTap: () => _selectLocation(location), - ); - }, - ); - }, - ), - ), - ], - ), - ), - ), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: const Text('Cancel'), - ), - ], - ); - } -} diff --git a/lib/privacy_page.dart b/lib/privacyPage.dart similarity index 81% rename from lib/privacy_page.dart rename to lib/privacyPage.dart index f46b7a1..e77b47a 100644 --- a/lib/privacy_page.dart +++ b/lib/privacyPage.dart @@ -1,12 +1,13 @@ import 'package:flutter/material.dart'; -import 'file_utils.dart'; +import 'fileUtils.dart'; import 'generated/i18n.dart'; -import 'markdown_page.dart'; +import 'markdownPage.dart'; class PrivacyPage extends MarkdownPage { + static const String routeName = '/privacy'; - PrivacyPage(BuildContext context, {super.key}) + PrivacyPage(context) : super( title: S.of(context).privacyPolicy, route: routeName, @@ -15,5 +16,4 @@ class PrivacyPage extends MarkdownPage { fileName: 'privacy', ext: 'md', lang: Localizations.localeOf(context).languageCode)); - static const String routeName = '/privacy'; } diff --git a/lib/redux/actions.dart b/lib/redux/actions.dart index b30a192..e689e7f 100644 --- a/lib/redux/actions.dart +++ b/lib/redux/actions.dart @@ -1,4 +1,4 @@ -export 'app_actions.dart'; -export 'fire_map_actions.dart'; -export 'fire_notification_actions.dart'; -export 'your_location_actions.dart'; +export 'appActions.dart'; +export 'yourLocationActions.dart'; +export 'fireMapActions.dart'; +export 'fireNotificationActions.dart'; \ No newline at end of file diff --git a/lib/redux/app_actions.dart b/lib/redux/appActions.dart similarity index 77% rename from lib/redux/app_actions.dart rename to lib/redux/appActions.dart index ea0241d..3017b24 100644 --- a/lib/redux/app_actions.dart +++ b/lib/redux/appActions.dart @@ -1,25 +1,23 @@ +import 'package:fires_flutter/models/yourLocation.dart'; +import 'package:fires_flutter/models/fireNotification.dart'; import 'dart:async'; - -import 'package:connectivity_plus/connectivity_plus.dart'; +import 'package:connectivity/connectivity.dart'; import 'package:flutter_map/flutter_map.dart'; -import '../models/fire_notification.dart'; -import '../models/your_location.dart'; - abstract class AppActions {} class FetchYourLocationsAction extends AppActions { + Completer refreshCallback; FetchYourLocationsAction([this.refreshCallback]); - Completer? refreshCallback; } class PersistAppStateAction extends AppActions {} class FetchYourLocationsSucceededAction extends AppActions { + final List fetchedYourLocations; FetchYourLocationsSucceededAction(this.fetchedYourLocations); - final List fetchedYourLocations; } class FetchFireNotificationsAction extends AppActions {} @@ -27,45 +25,44 @@ class FetchFireNotificationsAction extends AppActions {} class FetchMonitoredAreasAction extends AppActions {} class FetchYourLocationsFailedAction extends AppActions { + final Exception error; FetchYourLocationsFailedAction(this.error); - final Exception error; } class OnUserTokenAction extends AppActions { + final String token; OnUserTokenAction(this.token); - final String token; } class OnUserCreatedAction extends AppActions { + final String userId; OnUserCreatedAction(this.userId); - final String userId; } class OnUserLangAction extends AppActions { + final String lang; OnUserLangAction(this.lang); - final String lang; } class FetchFireNotificationsSucceededAction extends AppActions { - - FetchFireNotificationsSucceededAction( - this.fetchedFireNotifications, this.unreadCount); final List fetchedFireNotifications; final int unreadCount; -} -class FetchMonitoredAreasSucceededAction extends AppActions { - - FetchMonitoredAreasSucceededAction(this.monitoredAreas); - final List monitoredAreas; + FetchFireNotificationsSucceededAction(this.fetchedFireNotifications, this.unreadCount); } class OnConnectivityChanged extends AppActions { + final ConnectivityResult connectivityResult; - OnConnectivityChanged(this.connectionStatus); - final List connectionStatus; + OnConnectivityChanged(this.connectivityResult); +} + +class FetchMonitoredAreasSucceededAction extends AppActions { + final List monitoredAreas; + + FetchMonitoredAreasSucceededAction(this.monitoredAreas); } diff --git a/lib/redux/app_reducer.dart b/lib/redux/appReducer.dart similarity index 59% rename from lib/redux/app_reducer.dart rename to lib/redux/appReducer.dart index 82d0653..65a1add 100644 --- a/lib/redux/app_reducer.dart +++ b/lib/redux/appReducer.dart @@ -1,28 +1,28 @@ -import '../models/app_state.dart'; import 'actions.dart'; +import '../models/appState.dart'; -AppState appReducer(AppState state, dynamic action) { +AppState appReducer(AppState state, action) { if (action is FetchYourLocationsSucceededAction) { return state.copyWith(yourLocations: action.fetchedYourLocations); } if (action is FetchFireNotificationsSucceededAction) { - return state.copyWith( - fireNotifications: action.fetchedFireNotifications, - fireNotificationsUnread: action.unreadCount); + return state.copyWith(fireNotifications: action.fetchedFireNotifications, + fireNotificationsUnread: action.unreadCount); } if (action is FetchMonitoredAreasSucceededAction) { return state.copyWith(monitoredAreas: action.monitoredAreas); } if (action is AddedFireNotificationAction) return state.copyWith( - fireNotificationsUnread: state.fireNotificationsUnread + 1); + fireNotificationsUnread: state.fireNotificationsUnread + 1); if (action is ReadedFireNotificationAction) return state.copyWith( - fireNotificationsUnread: state.fireNotificationsUnread - 1); + fireNotificationsUnread: state.fireNotificationsUnread - 1); if (action is DeleteAllFireNotificationAction) - return state.copyWith(fireNotificationsUnread: 0); + return state.copyWith( + fireNotificationsUnread: 0); return state; } diff --git a/lib/redux/errorReducer.dart b/lib/redux/errorReducer.dart new file mode 100644 index 0000000..a8403cb --- /dev/null +++ b/lib/redux/errorReducer.dart @@ -0,0 +1,3 @@ +String errorReducer(String error, action) { + return error; +} \ No newline at end of file diff --git a/lib/redux/error_reducer.dart b/lib/redux/error_reducer.dart deleted file mode 100644 index 64f5216..0000000 --- a/lib/redux/error_reducer.dart +++ /dev/null @@ -1,3 +0,0 @@ -String errorReducer(String error, dynamic action) { - return error; -} diff --git a/lib/redux/fetch_data_middleware.dart b/lib/redux/fetchDataMiddleware.dart similarity index 51% rename from lib/redux/fetch_data_middleware.dart rename to lib/redux/fetchDataMiddleware.dart index bdd465a..4b419c6 100644 --- a/lib/redux/fetch_data_middleware.dart +++ b/lib/redux/fetchDataMiddleware.dart @@ -1,18 +1,16 @@ import 'dart:async'; -// ignore: implementation_imports -import 'package:flutter_map/src/layer/polyline_layer.dart'; -import 'package:get_it/get_it.dart'; -import 'package:objectid/objectid.dart'; +import 'package:bson_objectid/bson_objectid.dart'; +import 'package:fires_flutter/models/fireNotification.dart'; +import 'package:fires_flutter/models/yourLocation.dart'; +import 'package:flutter_simple_dependency_injection/injector.dart'; +import 'package:just_debounce_it/just_debounce_it.dart'; import 'package:redux/redux.dart'; -import '../models/app_state.dart'; -import '../models/fire_notification.dart'; -import '../models/fire_notifications_persist.dart'; -import '../models/fires_api.dart'; -import '../models/your_location.dart'; -import '../models/your_location_persist.dart'; -import '../object_id_utils.dart'; +import '../models/appState.dart'; +import '../models/fireNotificationsPersist.dart'; +import '../models/firesApi.dart'; +import '../models/yourLocationPersist.dart'; import 'actions.dart'; // A middleware takes in 3 parameters: your Store, which you can use to @@ -24,28 +22,21 @@ import 'actions.dart'; // Middleware do not return any values themselves. They simply forward // actions on to the Reducer or swallow actions in some special cases. -FiresApi api = GetIt.instance(); +FiresApi api = Injector.getInjector().get(); -// Simple debounce mechanism -Timer? _locationUpdateDebounceTimer; - -void debounceLocationUpdate(Duration duration, void Function() callback) { - _locationUpdateDebounceTimer?.cancel(); - _locationUpdateDebounceTimer = Timer(duration, callback); -} - -void fetchDataMiddleware( - Store store, dynamic action, NextDispatcher next) { +void fetchDataMiddleware(Store store, action, NextDispatcher next) { // If our Middleware encounters a `FetchYourLocationAction` if (action is OnUserLangAction) { // I can create the user with the lang and the token - createUser(store, action.lang, store.state.user.token); + if (store.state.user.token != null) + createUser(store, action.lang, store.state.user.token); } if (action is OnUserTokenAction) { // I can create the user with the lang and the token - createUser(store, store.state.user.lang, action.token); + if (store.state.user.lang != null) + createUser(store, store.state.user.lang, action.token); } if (action is EditConfirmYourLocationAction) { @@ -58,20 +49,20 @@ void fetchDataMiddleware( if (action is AddYourLocationAction) { if (action.loc.subscribed) { - subscribeViaApi(store, action.loc, (YourLocation sub) { - store.dispatch(AddedYourLocationAction(sub)); + subscribeViaApi(store, action.loc, (sub) { + store.dispatch(new AddedYourLocationAction(sub)); persistYourLocations(store.state.yourLocations); }); } else { // No subscribed (only local) - store.dispatch(AddedYourLocationAction(action.loc)); + store.dispatch(new AddedYourLocationAction(action.loc)); persistYourLocations(store.state.yourLocations); } getFiresStatsInLocation(store, action.loc); } if (action is DeleteYourLocationAction) { - store.dispatch(DeletedYourLocationAction(action.loc.id)); + store.dispatch(new DeletedYourLocationAction(action.loc.id)); if (action.loc.subscribed) { unsubsViaApi(store, action.loc.id, () { persistYourLocations(store.state.yourLocations); @@ -82,17 +73,17 @@ void fetchDataMiddleware( } if (action is DeleteFireNotificationAction) { - store.dispatch(DeletedFireNotificationAction(action.notif)); + store.dispatch(new DeletedFireNotificationAction(action.notif)); persistFireNotifications(store.state.fireNotifications); } if (action is DeleteAllFireNotificationAction) { - store.dispatch(DeletedAllFireNotificationAction()); + store.dispatch(new DeletedAllFireNotificationAction()); persistFireNotifications(store.state.fireNotifications); } if (action is AddFireNotificationAction) { - store.dispatch(AddedFireNotificationAction(action.notif)); + store.dispatch(new AddedFireNotificationAction(action.notif)); persistFireNotifications(store.state.fireNotifications); } @@ -106,30 +97,29 @@ void fetchDataMiddleware( if (action is UpdateYourLocationAction) { if (action.loc.subscribed) - debounceLocationUpdate( - const Duration(seconds: 2), + Debounce.seconds( + 2, () => api .getFiresInLocation( state: store.state, lat: action.loc.lat, lon: action.loc.lon, distance: action.loc.distance) - .then( - (UpdateFireMapStatsAction result) => store.dispatch(result))); - store.dispatch(UpdatedYourLocationAction(action.loc)); + .then((result) => store.dispatch(result))); + store.dispatch(new UpdatedYourLocationAction(action.loc)); persistYourLocations(store.state.yourLocations); } if (action is SubscribeConfirmAction) { - subscribeViaApi(store, action.loc, (YourLocation sub) { - store.dispatch(UpdateYourLocationAction(action.loc)); + subscribeViaApi(store, action.loc, (sub) { + store.dispatch(new UpdateYourLocationAction(action.loc)); persistYourLocations(store.state.yourLocations); }); } if (action is UnSubscribeAction) { unsubsViaApi(store, action.loc.id, () { - store.dispatch(UpdateYourLocationAction(action.loc)); + store.dispatch(new UpdateYourLocationAction(action.loc)); persistYourLocations(store.state.yourLocations); }); } @@ -137,87 +127,85 @@ void fetchDataMiddleware( if (action is ToggleSubscriptionAction) { if (action.loc.subscribed) { subscribeViaApi(store, action.loc, - (YourLocation sub) => store.dispatch(ToggledSubscriptionAction(sub))); + (sub) => store.dispatch(new ToggledSubscriptionAction(sub))); } else { unsubsViaApi(store, action.loc.id, - () => store.dispatch(ToggledSubscriptionAction(action.loc))); + () => store.dispatch(new ToggledSubscriptionAction(action.loc))); } } if (action is ReadFireNotificationAction) { - store.dispatch(ReadedFireNotificationAction(action.notif)); + store.dispatch(new ReadedFireNotificationAction(action.notif)); persistFireNotifications(store.state.fireNotifications); } if (action is FetchYourLocationsAction) { // Use the api to fetch the YourLocations - loadYourLocations().then((List localLocations) { + loadYourLocations().then((localLocations) { api .fetchYourLocations(store.state) .then((List subscribedLocations) { // If it succeeds, dispatch a success action with the YourLocations. // Our reducer will then update the State using these YourLocations. - // unsubscribe all locally to sync the subs state - final List unsubscribedLocations = localLocations - .map( - (YourLocation location) => location.copyWith(subscribed: false)) - .toList(); - for (final YourLocation subsLoc in subscribedLocations) { - final int index = unsubscribedLocations.indexWhere( - (YourLocation localLocation) => localLocation.id == subsLoc.id); - if (index >= 0) { - unsubscribedLocations[index] = - unsubscribedLocations[index].copyWith(subscribed: true); - } else { - unsubscribedLocations.add(subsLoc); - } + // print('Subscribed to: ${subscribedLocations.length}'); + if (subscribedLocations is List) { + // unsubscribe all locally to sync the subs state + localLocations.forEach((location) => location.subscribed = false); + // print('Local persisted: ${localLocations.length}'); + subscribedLocations.forEach((subsLoc) { + localLocations.firstWhere( + (localLocation) => localLocation.id == subsLoc.id, orElse: () { + localLocations.add(subsLoc); + }).subscribed = true; + }); } - store - .dispatch(FetchYourLocationsSucceededAction(unsubscribedLocations)); - persistYourLocations(unsubscribedLocations); + store.dispatch(new FetchYourLocationsSucceededAction(localLocations)); + persistYourLocations(localLocations); - for (final YourLocation yl in unsubscribedLocations) { + localLocations.forEach((yl) { api .getFiresInLocation( state: store.state, lat: yl.lat, lon: yl.lon, distance: yl.distance) - .then((UpdateFireMapStatsAction value) { - store.dispatch(UpdateYourLocationAction( - yl.copyWith(currentNumFires: value.numFires))); + .then((value) { + yl.currentNumFires = value.numFires; + store.dispatch(new UpdateYourLocationAction(yl)); }); - } + }); - final Completer? completer = action.refreshCallback; - completer?.complete(null); + Completer completer = action.refreshCallback; + if (completer != null) { + completer.complete(null); + } }); - }).catchError((Object onError) { + }).catchError((onError) { // If it fails, dispatch a failure action. The reducer will // update the state with the error. - store.dispatch(FetchYourLocationsFailedAction(onError as Exception)); + store.dispatch(new FetchYourLocationsFailedAction(onError)); }); } if (action is FetchFireNotificationsAction) { - loadFireNotifications().then((List fireNotifications) { + loadFireNotifications().then((fireNotifications) { int unread = 0; - for (final FireNotification notif in fireNotifications) { + for (FireNotification notif in fireNotifications) { if (!notif.read) { unread++; } } store.dispatch( - FetchFireNotificationsSucceededAction(fireNotifications, unread)); + new FetchFireNotificationsSucceededAction(fireNotifications, unread)); persistFireNotifications(fireNotifications); }); } if (action is FetchMonitoredAreasAction) { - api.getMonitoredAreas(state: store.state).then((List result) { + api.getMonitoredAreas(state: store.state).then((result) { // store.dispatch() - store.dispatch(FetchMonitoredAreasSucceededAction(result)); + store.dispatch(new FetchMonitoredAreasSucceededAction(result)); }); } @@ -225,11 +213,11 @@ void fetchDataMiddleware( api .markFalsePositive(store.state, store.state.user.token, action.notif.sealed, action.type) - .then((bool result) { + .then((result) { if (result) { // Not necessary getFiresStatsInFire(store, action.notif); - store.dispatch(UpdatedFireNotificationAction(action.notif)); + store.dispatch(new UpdatedFireNotificationAction(action.notif)); } }); } @@ -245,10 +233,10 @@ void getFiresStatsInLocation(Store store, YourLocation loc) { lat: loc.lat, lon: loc.lon, distance: loc.distance) - .then((UpdateFireMapStatsAction result) { + .then((result) { store.dispatch(result); - store.dispatch(UpdateYourLocationAction( - loc.copyWith(currentNumFires: result.numFires))); + loc.currentNumFires = result.numFires; + store.dispatch(new UpdateYourLocationAction(loc)); }); } @@ -259,33 +247,34 @@ void getFiresStatsInFire(Store store, FireNotification notif) { lat: notif.lat, lon: notif.lon, distance: 1) // FalsePositive/server/publications.js - .then((UpdateFireMapStatsAction result) => store.dispatch(result)); + .then((result) => store.dispatch(result)); } -void unsubsViaApi( - Store store, ObjectId id, void Function() onUnsubs) { - api.unsubscribe(store.state, id.hexString).then((bool res) { +void unsubsViaApi(Store store, ObjectId id, onUnsubs) { + api.unsubscribe(store.state, id.toHexString()).then((res) { onUnsubs(); persistYourLocations(store.state.yourLocations); }); } -void subscribeViaApi(Store store, YourLocation loc, - void Function(YourLocation) onSubs) { - api.subscribe(store.state, loc).then((String subsId) { - final YourLocation sub = loc.copyWith(id: objectIdFromJson(subsId)); +void subscribeViaApi(Store store, YourLocation loc, onSubs) { + api.subscribe(store.state, loc).then((subsId) { + YourLocation sub = loc; + if (loc.id != subsId) { + sub.id = new ObjectId.fromHexString(subsId); + } onSubs(sub); persistYourLocations(store.state.yourLocations); }); } -void createUser(Store store, String lang, String token) { - assert(token != null, 'User lang is null'); - assert(token != null, 'User mobile token is null'); - api.createUser(store.state, token, lang).then((String userId) { - store.dispatch(OnUserCreatedAction(userId)); - store.dispatch(FetchYourLocationsAction()); - store.dispatch(FetchFireNotificationsAction()); - store.dispatch(FetchMonitoredAreasAction()); +void createUser(store, lang, token) { + assert(token != null, "User lang is null"); + assert(token != null, "User mobile token is null"); + api.createUser(store.state, token, lang).then((userId) { + store.dispatch(new OnUserCreatedAction(userId)); + store.dispatch(new FetchYourLocationsAction()); + store.dispatch(new FetchFireNotificationsAction()); + store.dispatch(new FetchMonitoredAreasAction()); }); } diff --git a/lib/redux/fire_map_actions.dart b/lib/redux/fireMapActions.dart similarity index 60% rename from lib/redux/fire_map_actions.dart rename to lib/redux/fireMapActions.dart index 8a785ab..a8c5524 100644 --- a/lib/redux/fire_map_actions.dart +++ b/lib/redux/fireMapActions.dart @@ -1,61 +1,57 @@ -import '../models/fire_map_state.dart'; -import '../models/fire_notification.dart'; -import '../models/your_location.dart'; +import 'package:fires_flutter/models/yourLocation.dart'; +import 'package:fires_flutter/models/fireNotification.dart'; +import 'package:meta/meta.dart'; abstract class FiresMapActions {} class UpdateYourLocationMapAction extends FiresMapActions { + final YourLocation loc; UpdateYourLocationMapAction( this.loc, ); - final YourLocation loc; } class UpdateFireMapStatsAction extends FiresMapActions { + int numFires; + List fires = []; + List falsePos = []; + List industries = []; UpdateFireMapStatsAction( - {required this.numFires, - required this.fires, - required this.falsePos, - required this.industries}); - int numFires; - List fires = []; - List falsePos = []; - List industries = []; + {@required this.numFires, + @required this.fires, + @required this.falsePos, + @required this.industries}); } class ShowYourLocationMapAction extends FiresMapActions { + YourLocation loc; ShowYourLocationMapAction(this.loc); - YourLocation loc; } class ShowFireNotificationMapAction extends FiresMapActions { + FireNotification notif; ShowFireNotificationMapAction(this.notif); - FireNotification notif; } class EditYourLocationAction extends FiresMapActions { + YourLocation loc; EditYourLocationAction(this.loc); - YourLocation loc; } -class EditConfirmYourLocationAction extends FiresMapActions { +class EditConfirmYourLocationAction extends FiresMapActions{ + YourLocation loc; EditConfirmYourLocationAction(this.loc); - YourLocation loc; } class EditCancelYourLocationAction extends FiresMapActions { - EditCancelYourLocationAction(this.loc); YourLocation loc; + EditCancelYourLocationAction(this.loc); } -class SelectMapLayerAction extends FiresMapActions { - - SelectMapLayerAction(this.layer); - final FireMapLayer layer; -} +class ToggleMapLayerAction extends FiresMapActions {} \ No newline at end of file diff --git a/lib/redux/fire_map_reducer.dart b/lib/redux/fireMapReducer.dart similarity index 56% rename from lib/redux/fire_map_reducer.dart rename to lib/redux/fireMapReducer.dart index 22185a5..1794b86 100644 --- a/lib/redux/fire_map_reducer.dart +++ b/lib/redux/fireMapReducer.dart @@ -1,28 +1,28 @@ -import 'package:objectid/objectid.dart'; import 'package:redux/redux.dart'; - -import '../models/fire_map_state.dart'; -import '../models/your_location.dart'; +import 'package:fires_flutter/models/yourLocation.dart'; +import '../models/fireMapState.dart'; import 'actions.dart'; -final Reducer fireMapReducer = - combineReducers(>[ - TypedReducer(_showYourLocationMap), - TypedReducer( - _showFireNotificationMap), - TypedReducer( +final fireMapReducer = combineReducers([ + new TypedReducer( + _showYourLocationMap), + new TypedReducer( + _showFireNotificationMap), + new TypedReducer( _updateYourLocationMapStats), - TypedReducer(_subscribeYourLocationMap), - TypedReducer( + new TypedReducer(_subscribeYourLocationMap), + new TypedReducer( _subscribeConfirmYourLocationMap), - TypedReducer(_unsubscribeYourLocationMap), - TypedReducer(_editYourLocationMap), - TypedReducer( + new TypedReducer( + _unsubscribeYourLocationMap), + new TypedReducer(_editYourLocationMap), + new TypedReducer( _editConfirmYourLocationMap), - TypedReducer( + new TypedReducer( _editCancelYourLocationMap), - TypedReducer(_selectMapLayer), - TypedReducer( + new TypedReducer( + _toggleMapLayer), + new TypedReducer( _updateYourLocationMap) ]); @@ -46,21 +46,15 @@ FireMapState _showYourLocationMap( status: action.loc.subscribed ? FireMapStatus.unsubscribe : FireMapStatus.view, - yourLocation: action.loc); + yourLocation: action.loc, fireNotication: null); } FireMapState _showFireNotificationMap( FireMapState state, ShowFireNotificationMapAction action) { - // TODO(developer): use here real location instead of notification location? - final YourLocation pseudoLoc = YourLocation( - id: ObjectId(), - lat: action.notif.lat, - lon: action.notif.lon, - description: action.notif.description); + // TODO: use here you real location? + YourLocation pseudoLoc = new YourLocation(lat: action.notif.lat, lon: action.notif.lon, description: action.notif.description); return state.copyWith( - status: FireMapStatus.viewFireNotification, - yourLocation: pseudoLoc, - fireNotication: action.notif); + status: FireMapStatus.viewFireNotification, yourLocation: pseudoLoc, fireNotication: action.notif); } FireMapState _subscribeYourLocationMap( @@ -95,9 +89,15 @@ FireMapState _editCancelYourLocationMap( status: restoreStatusAfterSave(action.loc), yourLocation: action.loc); } -FireMapStatus restoreStatusAfterSave(YourLocation loc) => +FireMapStatus restoreStatusAfterSave(loc) => loc.subscribed ? FireMapStatus.unsubscribe : FireMapStatus.view; -FireMapState _selectMapLayer(FireMapState state, SelectMapLayerAction action) { - return state.copyWith(layer: action.layer); -} +FireMapState _toggleMapLayer( + FireMapState state, ToggleMapLayerAction action) { + FireMapLayer currentLayer = state.layer; + List list = FireMapLayer.values; + int currentPos = list.indexOf(currentLayer); + currentPos++; + FireMapLayer next = list.elementAt(currentPos >= list.length ? 0: currentPos); + return state.copyWith(layer: next); +} \ No newline at end of file diff --git a/lib/redux/fire_notification_actions.dart b/lib/redux/fireNotificationActions.dart similarity index 92% rename from lib/redux/fire_notification_actions.dart rename to lib/redux/fireNotificationActions.dart index 71f0020..32dc82f 100644 --- a/lib/redux/fire_notification_actions.dart +++ b/lib/redux/fireNotificationActions.dart @@ -1,11 +1,12 @@ -import '../models/false_positive_types.dart'; -import '../models/fire_notification.dart'; +import 'package:fires_flutter/models/fireNotification.dart'; +import '../models/falsePositiveTypes.dart'; abstract class FireNotificationActions {} class DeleteFireNotificationAction extends FireNotificationActions { - DeleteFireNotificationAction(this.notif); final FireNotification notif; + + DeleteFireNotificationAction(this.notif); } class DeleteAllFireNotificationAction extends FireNotificationActions { @@ -13,13 +14,15 @@ class DeleteAllFireNotificationAction extends FireNotificationActions { } class AddFireNotificationAction extends FireNotificationActions { - AddFireNotificationAction(this.notif); final FireNotification notif; + + AddFireNotificationAction(this.notif); } class DeletedFireNotificationAction extends FireNotificationActions { - DeletedFireNotificationAction(this.notif); final FireNotification notif; + + DeletedFireNotificationAction(this.notif); } class DeletedAllFireNotificationAction extends FireNotificationActions { @@ -27,27 +30,32 @@ class DeletedAllFireNotificationAction extends FireNotificationActions { } class AddedFireNotificationAction extends FireNotificationActions { - AddedFireNotificationAction(this.notif); final FireNotification notif; + + AddedFireNotificationAction(this.notif); } class ReadFireNotificationAction extends FireNotificationActions { - ReadFireNotificationAction(this.notif); final FireNotification notif; + + ReadFireNotificationAction(this.notif); } class ReadedFireNotificationAction extends FireNotificationActions { - ReadedFireNotificationAction(this.notif); final FireNotification notif; + + ReadedFireNotificationAction(this.notif); } class MarkFireAsFalsePositiveAction extends FireNotificationActions { - MarkFireAsFalsePositiveAction(this.notif, this.type); final FireNotification notif; final FalsePositiveType type; + + MarkFireAsFalsePositiveAction(this.notif, this.type); } class UpdatedFireNotificationAction extends FireNotificationActions { - UpdatedFireNotificationAction(this.notif); final FireNotification notif; -} + + UpdatedFireNotificationAction(this.notif); +} \ No newline at end of file diff --git a/lib/redux/fire_notification_reducer.dart b/lib/redux/fireNotificationReducer.dart similarity index 51% rename from lib/redux/fire_notification_reducer.dart rename to lib/redux/fireNotificationReducer.dart index 1d5d4c1..a3cd083 100644 --- a/lib/redux/fire_notification_reducer.dart +++ b/lib/redux/fireNotificationReducer.dart @@ -1,46 +1,43 @@ +import 'package:fires_flutter/models/fireNotification.dart'; import 'package:redux/redux.dart'; -import '../models/fire_notification.dart'; import 'actions.dart'; -final Reducer> fireNotificationReducer = - combineReducers>(>>[ - TypedReducer, AddedFireNotificationAction>( +final fireNotificationReducer = combineReducers>([ + new TypedReducer, AddedFireNotificationAction>( _addedFireNotification), - TypedReducer, DeletedFireNotificationAction>( + new TypedReducer, DeletedFireNotificationAction>( _deletedFireNotification), - TypedReducer, UpdatedFireNotificationAction>( + new TypedReducer, UpdatedFireNotificationAction>( _updatedFireNotification), - TypedReducer, DeletedAllFireNotificationAction>( + new TypedReducer, DeletedAllFireNotificationAction>( _deletedAllFireNotifications), - TypedReducer, ReadedFireNotificationAction>( + new TypedReducer, ReadedFireNotificationAction>( _readedFireNotification) ]); List _addedFireNotification( List notifications, AddedFireNotificationAction action) { - return List.from(notifications)..insert(0, action.notif); + return new List.from(notifications)..insert(0, action.notif); } List _deletedFireNotification( List notifications, DeletedFireNotificationAction action) { - return List.from(notifications)..remove(action.notif); + return new List.from(notifications)..remove(action.notif); } List _updatedFireNotification( - List notifications, - UpdatedFireNotificationAction action) { + List notifications, UpdatedFireNotificationAction action) { return notifications - .map((FireNotification notif) => - notif.id == action.notif.id ? action.notif : notif) + .map((notif) => notif.id == action.notif.id ? action.notif : notif) .toList(); } List _readedFireNotification( List notifications, ReadedFireNotificationAction action) { return notifications - .map((FireNotification yourLocation) => + .map((yourLocation) => yourLocation.id == action.notif.id ? action.notif : yourLocation) .toList(); } @@ -48,5 +45,5 @@ List _readedFireNotification( List _deletedAllFireNotifications( List notifications, DeletedAllFireNotificationAction action) { - return []; + return new List(); } diff --git a/lib/redux/loadedReducer.dart b/lib/redux/loadedReducer.dart new file mode 100644 index 0000000..96b549a --- /dev/null +++ b/lib/redux/loadedReducer.dart @@ -0,0 +1,6 @@ +import 'actions.dart'; + +bool loadedReducer(isLoaded, action) { + if (action is FetchYourLocationsSucceededAction) return true; + return isLoaded; +} \ No newline at end of file diff --git a/lib/redux/loaded_reducer.dart b/lib/redux/loaded_reducer.dart deleted file mode 100644 index 9d54e51..0000000 --- a/lib/redux/loaded_reducer.dart +++ /dev/null @@ -1,8 +0,0 @@ -import 'actions.dart'; - -bool loadedReducer(bool isLoaded, dynamic action) { - if (action is FetchYourLocationsSucceededAction) { - return true; - } - return isLoaded; -} diff --git a/lib/redux/loadingReducer.dart b/lib/redux/loadingReducer.dart new file mode 100644 index 0000000..bbb62cc --- /dev/null +++ b/lib/redux/loadingReducer.dart @@ -0,0 +1,5 @@ +import 'actions.dart'; +bool loadingReducer(isLoading, action) { + if (action is FetchYourLocationsAction) return true; + return isLoading; +} \ No newline at end of file diff --git a/lib/redux/loading_reducer.dart b/lib/redux/loading_reducer.dart deleted file mode 100644 index a36fe06..0000000 --- a/lib/redux/loading_reducer.dart +++ /dev/null @@ -1,8 +0,0 @@ -import 'actions.dart'; - -bool loadingReducer(bool isLoading, dynamic action) { - if (action is FetchYourLocationsAction) { - return true; - } - return isLoading; -} diff --git a/lib/redux/reducers.dart b/lib/redux/reducers.dart index 8f6b99e..c867825 100644 --- a/lib/redux/reducers.dart +++ b/lib/redux/reducers.dart @@ -1,16 +1,16 @@ -import '../models/app_state.dart'; -import 'app_reducer.dart'; -import 'error_reducer.dart'; -import 'fire_map_reducer.dart'; -import 'fire_notification_reducer.dart'; -import 'loaded_reducer.dart'; -import 'loading_reducer.dart'; -import 'user_reducer.dart'; -import 'your_locations_reducer.dart'; +import '../models/appState.dart'; +import 'appReducer.dart'; +import 'errorReducer.dart'; +import 'fireMapReducer.dart'; +import 'fireNotificationReducer.dart'; +import 'loadedReducer.dart'; +import 'loadingReducer.dart'; +import 'userReducer.dart'; +import 'yourLocationsReducer.dart'; // We create the State reducer by combining many smaller reducers into one! -AppState appStateReducer(AppState prevState, dynamic action) { - final AppState state = appReducer(prevState, action); +AppState appStateReducer(AppState prevState, action) { + var state = appReducer(prevState, action); return AppState( yourLocations: yourLocationsReducer(state.yourLocations, action), fireNotifications: diff --git a/lib/redux/userReducer.dart b/lib/redux/userReducer.dart new file mode 100644 index 0000000..61c2b3a --- /dev/null +++ b/lib/redux/userReducer.dart @@ -0,0 +1,9 @@ +import 'actions.dart'; +import '../models/user.dart'; + +User userReducer(User user, action) { + if (action is OnUserCreatedAction) return user.copyWith(userId: action.userId); + if (action is OnUserTokenAction) return user.copyWith(token: action.token); + if (action is OnUserLangAction) return user.copyWith(lang: action.lang); + return user; +} diff --git a/lib/redux/user_reducer.dart b/lib/redux/user_reducer.dart deleted file mode 100644 index fca4dbc..0000000 --- a/lib/redux/user_reducer.dart +++ /dev/null @@ -1,15 +0,0 @@ -import '../models/user.dart'; -import 'actions.dart'; - -User userReducer(User user, dynamic action) { - if (action is OnUserCreatedAction) { - return user.copyWith(userId: action.userId); - } - if (action is OnUserTokenAction) { - return user.copyWith(token: action.token); - } - if (action is OnUserLangAction) { - return user.copyWith(lang: action.lang); - } - return user; -} diff --git a/lib/redux/your_location_actions.dart b/lib/redux/yourLocationActions.dart similarity index 92% rename from lib/redux/your_location_actions.dart rename to lib/redux/yourLocationActions.dart index a24b8f8..0fec9b7 100644 --- a/lib/redux/your_location_actions.dart +++ b/lib/redux/yourLocationActions.dart @@ -1,67 +1,66 @@ -import 'package:objectid/objectid.dart'; - -import '../models/your_location.dart'; +import 'package:bson_objectid/bson_objectid.dart'; +import 'package:fires_flutter/models/yourLocation.dart'; abstract class YourLocationActions {} class AddYourLocationAction extends YourLocationActions { + YourLocation loc; AddYourLocationAction(this.loc); - YourLocation loc; } class AddedYourLocationAction extends YourLocationActions { + YourLocation loc; AddedYourLocationAction(this.loc); - YourLocation loc; } class DeleteYourLocationAction extends YourLocationActions { + YourLocation loc; DeleteYourLocationAction(this.loc); - YourLocation loc; } class DeletedYourLocationAction extends YourLocationActions { + ObjectId id; DeletedYourLocationAction(this.id); - ObjectId id; } class UpdateYourLocationAction extends YourLocationActions { + YourLocation loc; UpdateYourLocationAction(this.loc); - YourLocation loc; } class UpdatedYourLocationAction extends YourLocationActions { + YourLocation loc; UpdatedYourLocationAction(this.loc); - YourLocation loc; } class ToggleSubscriptionAction extends YourLocationActions { + YourLocation loc; ToggleSubscriptionAction(this.loc); - YourLocation loc; } class ToggledSubscriptionAction extends YourLocationActions { + YourLocation loc; ToggledSubscriptionAction(this.loc); - YourLocation loc; } class SubscribeAction extends YourLocationActions {} class SubscribeConfirmAction extends YourLocationActions { + YourLocation loc; SubscribeConfirmAction(this.loc); - YourLocation loc; } class UnSubscribeAction extends YourLocationActions { + YourLocation loc; UnSubscribeAction(this.loc); - YourLocation loc; } diff --git a/lib/redux/your_locations_reducer.dart b/lib/redux/yourLocationsReducer.dart similarity index 57% rename from lib/redux/your_locations_reducer.dart rename to lib/redux/yourLocationsReducer.dart index c1e6515..aa45662 100644 --- a/lib/redux/your_locations_reducer.dart +++ b/lib/redux/yourLocationsReducer.dart @@ -1,35 +1,35 @@ +import 'package:fires_flutter/models/yourLocation.dart'; import 'package:redux/redux.dart'; -import '../models/your_location.dart'; import 'actions.dart'; -final Reducer> yourLocationsReducer = - combineReducers>(>>[ - TypedReducer, AddedYourLocationAction>(_addedYourLocation), - TypedReducer, DeletedYourLocationAction>( +final yourLocationsReducer = combineReducers>([ + new TypedReducer, AddedYourLocationAction>( + _addedYourLocation), + new TypedReducer, DeletedYourLocationAction>( _deletedYourLocation), - TypedReducer, UpdatedYourLocationAction>( + new TypedReducer, UpdatedYourLocationAction>( _updatedYourLocation), - TypedReducer, ToggledSubscriptionAction>( + new TypedReducer, ToggledSubscriptionAction>( _toggledSubscriptionAction) ]); List _addedYourLocation( List yourLocations, AddedYourLocationAction action) { - return List.from(yourLocations)..add(action.loc); + return new List.from(yourLocations)..add(action.loc); } List _deletedYourLocation( List yourLocations, DeletedYourLocationAction action) { return yourLocations - .where((YourLocation yourLocation) => yourLocation.id != action.id) + .where((yourLocation) => yourLocation.id != action.id) .toList(); } List _updatedYourLocation( List yourLocations, UpdatedYourLocationAction action) { return yourLocations - .map((YourLocation yourLocation) => + .map((yourLocation) => yourLocation.id == action.loc.id ? action.loc : yourLocation) .toList(); } @@ -37,7 +37,7 @@ List _updatedYourLocation( List _toggledSubscriptionAction( List yourLocations, ToggledSubscriptionAction action) { return yourLocations - .map((YourLocation yourLocation) => + .map((yourLocation) => yourLocation.id == action.loc.id ? action.loc : yourLocation) .toList(); } diff --git a/lib/sandbox.dart b/lib/sandbox.dart index 036ad01..ff0fc35 100644 --- a/lib/sandbox.dart +++ b/lib/sandbox.dart @@ -7,35 +7,34 @@ new RandomColorBlock( child */ class Sandbox extends StatelessWidget { - const Sandbox({super.key}); - static const String routeName = '/sandbox'; @override Widget build(BuildContext context) { //showDialog(context: context, child: builder(context)); return Scaffold( - appBar: AppBar( - title: TextField( + appBar: new AppBar( + title: new TextField( autofocus: true, - controller: TextEditingController(text: 'kk'), - onSubmitted: (String todoText) {}, + controller: new TextEditingController(text: "kk"), + decoration: new InputDecoration(), + onSubmitted: (todoText) {}, )), - body: ElevatedButton( - child: const Text('Press'), + body: new RaisedButton( + child: new Text('Press'), onPressed: () { - /*showDialog( + showDialog( context: context, - builder: (BuildContext context) { - return const SimpleDialog( - title: Text('title'), + builder: (context) { + return new SimpleDialog( + title: new Text('title'), children: [ - Padding( - padding: EdgeInsets.only(left: 16.0), - child: Text('hhh'), + new Padding( + padding: const EdgeInsets.only(left: 16.0), + child: new Text('hhh'), ), ]); - });*/ + }); })); } } diff --git a/lib/sentryReport.dart b/lib/sentryReport.dart new file mode 100644 index 0000000..a50455e --- /dev/null +++ b/lib/sentryReport.dart @@ -0,0 +1,25 @@ +import 'dart:async'; + +import 'package:flutter_simple_dependency_injection/injector.dart'; +import 'package:sentry/sentry.dart'; + +import 'globals.dart' as globals; + +var useSentry = !globals.isDevelopment; + +Future reportError(dynamic error, dynamic stackTrace) async { + // Print the exception to the console + + print('Caught error: $error'); + if (!useSentry) { + // Print the full stacktrace in debug mode + print(stackTrace); + return; + } else { + // Send the Exception and Stacktrace to Sentry in Production mode + Injector.getInjector().get().captureException( + exception: error, + stackTrace: stackTrace, + ); + } +} diff --git a/lib/sentry_report.dart b/lib/sentry_report.dart deleted file mode 100644 index eadcb27..0000000 --- a/lib/sentry_report.dart +++ /dev/null @@ -1,22 +0,0 @@ -import 'dart:async'; - -import 'package:sentry_flutter/sentry_flutter.dart'; - -import 'globals.dart' as globals; - -bool useSentry = !globals.isDevelopment; - -Future reportError(dynamic error, dynamic stackTrace) async { - // Print the exception to the console - - if (!useSentry) { - // Print the full stacktrace in debug mode - return; - } else { - // Send the Exception and Stacktrace to Sentry in Production mode - await Sentry.captureException( - error, - stackTrace: stackTrace, - ); - } -} diff --git a/lib/slider.dart b/lib/slider.dart index 0682828..637f31e 100644 --- a/lib/slider.dart +++ b/lib/slider.dart @@ -1,68 +1,66 @@ +import 'package:comunes_flutter/comunes_flutter.dart'; import 'package:flutter/material.dart'; import 'colors.dart'; import 'generated/i18n.dart'; -import 'utils/widget_utils.dart'; -typedef SlideCallback = void Function(int distance); +typedef void SlideCallback(int distance); class FireDistanceSlider extends StatefulWidget { - const FireDistanceSlider( - {super.key, required this.initialValue, required this.onSlide}); final int initialValue; final SlideCallback onSlide; + FireDistanceSlider({this.initialValue, this.onSlide}); + @override - // ignore: library_private_types_in_public_api - _FireDistanceSliderState createState() => _FireDistanceSliderState(); + _FireDistanceSliderState createState() => new _FireDistanceSliderState( + initialValue: initialValue, onSlide: onSlide); } class _FireDistanceSliderState extends State { - _FireDistanceSliderState(); - late int _sliderValue; - late final SlideCallback onSlide; + int _sliderValue; + final SlideCallback onSlide; - @override - void initState() { - super.initState(); - _sliderValue = widget.initialValue; - onSlide = widget.onSlide; + _FireDistanceSliderState({int initialValue = 10, this.onSlide}) { + this._sliderValue = initialValue; } - Widget sizeText(int sliderValue) => - Text(S.of(context).subscribeToValueAroundThisArea(sliderValue.toString()), - style: const TextStyle(color: Colors.black87)); + sizeText(sliderValue) => + new Text(S.of(context).subscribeToValueAroundThisArea(sliderValue.toString()), + style: new TextStyle(color: Colors.black87)); - Widget warningText(int sliderValue) => _sliderValue >= 50 - ? Text(S.of(context).warningThisIsAVeryLargeArea, - style: const TextStyle(color: fires900)) - : const Text(''); + warningText(sliderValue) => _sliderValue >= 50 + ? new Text(S.of(context).warningThisIsAVeryLargeArea, + style: new TextStyle(color: fires900)) + : new Text(''); @override Widget build(BuildContext context) { - final Slider slider = Slider( + var slider = new Slider( value: _sliderValue + 0.0, activeColor: fires900, inactiveColor: Colors.black45, min: 1.0, max: 100.0, divisions: 99, - label: '$_sliderValue', + label: '${_sliderValue.round()}', onChanged: (double value) { _sliderValue = value.round(); - onSlide(_sliderValue); + if (onSlide != null) { + onSlide(_sliderValue); + } setState(() {}); }, ); - return Column( + return new Column( mainAxisSize: MainAxisSize.min, - children: compactWidgets([ - const SizedBox(height: 50.0), - Row(children: [slider]), + children: listWithoutNulls([ + new SizedBox(height: 50.0), + new Row(mainAxisSize: MainAxisSize.max, children: [slider]), // new SizedBox(height: 5.0), - Column(children: [ + new Column(children: [ sizeText(_sliderValue), - const SizedBox(height: 5.0), + new SizedBox(height: 5.0), warningText(_sliderValue) ]) ]), diff --git a/lib/supportPage.dart b/lib/supportPage.dart new file mode 100644 index 0000000..41cee6f --- /dev/null +++ b/lib/supportPage.dart @@ -0,0 +1,100 @@ +import 'package:comunes_flutter/comunes_flutter.dart'; +import 'package:flutter/material.dart'; +import 'package:launch_review/launch_review.dart'; +import 'package:url_launcher/url_launcher.dart'; +import 'package:share/share.dart'; + +import 'generated/i18n.dart'; +import 'mainDrawer.dart'; + +class SupportPage extends StatefulWidget { + static const String routeName = '/support'; + + @override + _SupportPageState createState() => _SupportPageState(); +} + +class _SupportPageState extends State { + final GlobalKey _scaffoldKey = new GlobalKey(); + + Widget buildSupportButton() { + return new Align( + alignment: const Alignment(0.0, -0.2), + child: new OutlineButton.icon( + icon: const Icon(Icons.favorite_border), + label: new Text(S.of(context).comunesSupportBtn), + onPressed: () { + launch("https://comunes.org/"); + }, + ), + ); + } + + Widget buildShareButton() { + return new Align( + alignment: const Alignment(0.0, -0.2), + child: new OutlineButton.icon( + icon: const Icon(Icons.share), + label: new Text(S.of(context).shareAppBtn), + onPressed: () { + Share.share('https://play.google.com/store/apps/details?id=org.comunes.fires'); + }, + ), + ); + } + + Widget buildStarButton() { + return new Align( + alignment: const Alignment(0.0, -0.2), + child: new OutlineButton.icon( + icon: const Icon(Icons.star_border), + label: new Text(S.of(context).starAppBtn), + onPressed: () { + LaunchReview.launch(); + }, + ), + ); + } + + Widget buildTranslateButton() { + return new Align( + alignment: const Alignment(0.0, -0.2), + child: new OutlineButton.icon( + icon: const Icon(Icons.translate), + label: new Text(S.of(context).translateBtn), + onPressed: () { + launch("https://translate.comunes.org/projects/todos-contra-el-fuego/"); + }, + ), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + key: _scaffoldKey, + appBar: + new AppBar(title: new Text(S.of(context).supportThisInitiative)), + drawer: new MainDrawer(context, SupportPage.routeName), + body: Padding( + padding: const EdgeInsets.all(8.0), + child: new CenteredColumn(children: [ + new Flexible( + child: new CenteredColumn(children: [ + new Text( + S.of(context).supportPageDescription, + textAlign: TextAlign.center, + ), + new SizedBox(height: 20.0), + buildSupportButton(), + new SizedBox(height: 20.0), + buildTranslateButton(), + new SizedBox(height: 20.0), + buildStarButton(), + new SizedBox(height: 20.0), + buildShareButton() + ])) + ]), + )); + } +} diff --git a/lib/support_page.dart b/lib/support_page.dart deleted file mode 100644 index 0ca6c90..0000000 --- a/lib/support_page.dart +++ /dev/null @@ -1,120 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:in_app_review/in_app_review.dart'; -import 'package:share_plus/share_plus.dart'; -import 'package:url_launcher/url_launcher.dart'; - -import 'generated/i18n.dart'; -import 'main_drawer.dart'; - -class SupportPage extends StatefulWidget { - const SupportPage({super.key}); - - static const String routeName = '/support'; - - @override - // ignore: library_private_types_in_public_api - _SupportPageState createState() => _SupportPageState(); -} - -class _SupportPageState extends State { - final GlobalKey _scaffoldKey = GlobalKey(); - - Widget buildSupportButton() { - return Align( - alignment: const Alignment(0.0, -0.2), - child: OutlinedButton.icon( - icon: const Icon(Icons.favorite_border), - label: Text(S.of(context).comunesSupportBtn), - onPressed: () async { - // Play policy: la app fue retirada por enlazar a comunes.org (que - // tiene página de donaciones). Apuntamos a la página del proyecto - // (sin donaciones) para la re-aprobación. Reversible una vez aprobada. - final Uri url = Uri.parse( - 'https://git.comunes.org/comunes/todos-contra-el-fuego-mobile'); - if (await canLaunchUrl(url)) { - await launchUrl(url); - } - }, - ), - ); - } - - Widget buildShareButton() { - return Align( - alignment: const Alignment(0.0, -0.2), - child: OutlinedButton.icon( - icon: const Icon(Icons.share), - label: Text(S.of(context).shareAppBtn), - onPressed: () { - Share.shareWithResult( - 'https://play.google.com/store/apps/details?id=org.comunes.fires'); - }, - ), - ); - } - - Widget buildStarButton() { - return Align( - alignment: const Alignment(0.0, -0.2), - child: OutlinedButton.icon( - icon: const Icon(Icons.star_border), - label: Text(S.of(context).starAppBtn), - onPressed: () { - InAppReview.instance.requestReview(); - }, - ), - ); - } - - Widget buildTranslateButton() { - return Align( - alignment: const Alignment(0.0, -0.2), - child: OutlinedButton.icon( - icon: const Icon(Icons.translate), - label: Text(S.of(context).translateBtn), - onPressed: () async { - final Uri url = Uri.parse( - 'https://translate.comunes.org/projects/todos-contra-el-fuego/'); - if (await canLaunchUrl(url)) { - await launchUrl(url); - } - }, - ), - ); - } - - @override - @override - Widget build(BuildContext context) { - return Scaffold( - key: _scaffoldKey, - appBar: AppBar(title: Text(S.of(context).supportThisInitiative)), - drawer: MainDrawer(context, SupportPage.routeName), - body: Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Flexible( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - S.of(context).supportPageDescription, - textAlign: TextAlign.center, - ), - const SizedBox(height: 20.0), - buildSupportButton(), - const SizedBox(height: 20.0), - buildTranslateButton(), - const SizedBox(height: 20.0), - buildStarButton(), - const SizedBox(height: 20.0), - buildShareButton() - ], - )) - ], - ), - )); - } -} diff --git a/lib/theme.dart b/lib/theme.dart index 2ef6631..3f91ff8 100644 --- a/lib/theme.dart +++ b/lib/theme.dart @@ -2,298 +2,21 @@ import 'package:flutter/material.dart'; import 'colors.dart'; -// ignore_for_file: avoid_redundant_argument_values final ThemeData firesTheme = _buildFiresTheme(); ThemeData _buildFiresTheme() { - // Material 3 Color Scheme - Modern Warm Palette - const ColorScheme colorScheme = ColorScheme.light( - primary: m3Primary, - onPrimary: m3OnPrimary, - primaryContainer: m3PrimaryContainer, - onPrimaryContainer: m3OnPrimaryContainer, - secondary: m3Secondary, - onSecondary: m3OnSecondary, - secondaryContainer: m3SecondaryContainer, - onSecondaryContainer: m3OnSecondaryContainer, - tertiary: m3Tertiary, - onTertiary: m3OnTertiary, - tertiaryContainer: m3TertiaryContainer, - onTertiaryContainer: m3OnTertiaryContainer, - error: m3Error, - onError: m3OnError, - errorContainer: m3ErrorContainer, - onErrorContainer: m3OnErrorContainer, - surface: m3Surface, - onSurface: m3OnSurface, - surfaceContainerHighest: m3SurfaceVariant, - onSurfaceVariant: m3OnSurfaceVariant, - outline: m3Outline, - outlineVariant: m3OutlineVariant, - ); + final ThemeData base = ThemeData.light(); // light or dark + return base.copyWith( + accentColor: fires900, + primaryColor: fires600, + buttonColor: fires900, + scaffoldBackgroundColor: firesSurfaceWhite, + cardColor: firesBackgroundWhite, + textSelectionColor: fires300, + errorColor: firesErrorRed, - return ThemeData( - useMaterial3: true, - colorScheme: colorScheme, - scaffoldBackgroundColor: m3Surface, - cardColor: m3Surface, - - // ======================================================================== - // TEXT THEMES - Material 3 Complete - // ======================================================================== - textTheme: TextTheme( - // Display styles - Large, prominent text - displayLarge: TextStyle( - fontSize: 57, - fontWeight: FontWeight.w400, - letterSpacing: -0.25, - color: colorScheme.onSurface, - ), - displayMedium: TextStyle( - fontSize: 45, - fontWeight: FontWeight.w400, - letterSpacing: 0, - color: colorScheme.onSurface, - ), - displaySmall: TextStyle( - fontSize: 36, - fontWeight: FontWeight.w400, - letterSpacing: 0, - color: colorScheme.onSurface, - ), - - // Headline styles - Important section titles - headlineLarge: TextStyle( - fontSize: 32, - fontWeight: FontWeight.w600, - letterSpacing: 0, - color: colorScheme.onSurface, - ), - headlineMedium: TextStyle( - fontSize: 28, - fontWeight: FontWeight.w600, - letterSpacing: 0, - color: colorScheme.onSurface, - ), - headlineSmall: TextStyle( - fontSize: 24, - fontWeight: FontWeight.w600, - letterSpacing: 0, - color: colorScheme.onSurface, - ), - - // Title styles - Subheadings - titleLarge: TextStyle( - fontSize: 22, - fontWeight: FontWeight.w500, - letterSpacing: 0, - color: colorScheme.onSurface, - ), - titleMedium: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - letterSpacing: 0.15, - color: colorScheme.onSurface, - ), - titleSmall: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - letterSpacing: 0.1, - color: colorScheme.onSurface, - ), - - // Body styles - Main content text - bodyLarge: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w400, - letterSpacing: 0.5, - color: colorScheme.onSurface, - ), - bodyMedium: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w400, - letterSpacing: 0.25, - color: colorScheme.onSurface, - ), - bodySmall: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w400, - letterSpacing: 0.4, - color: colorScheme.onSurfaceVariant, - ), - - // Label styles - Buttons, badges, labels - labelLarge: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - letterSpacing: 0.1, - color: colorScheme.onSurface, - ), - labelMedium: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w500, - letterSpacing: 0.5, - color: colorScheme.onSurface, - ), - labelSmall: TextStyle( - fontSize: 11, - fontWeight: FontWeight.w500, - letterSpacing: 0.5, - color: colorScheme.onSurface, - ), - ), - - // ======================================================================== - // ICON THEMES - // ======================================================================== - iconTheme: IconThemeData( - color: colorScheme.primary, - size: 24, - ), - appBarTheme: AppBarTheme( - backgroundColor: colorScheme.primary, - foregroundColor: colorScheme.onPrimary, - elevation: 4, - shadowColor: colorScheme.shadow, - surfaceTintColor: colorScheme.primary, - iconTheme: IconThemeData(color: colorScheme.onPrimary), - titleTextStyle: TextStyle( - color: colorScheme.onPrimary, - fontSize: 20, - fontWeight: FontWeight.w500, - ), - ), - - // ======================================================================== - // INPUT DECORATION - Material 3 Filled Style - // ======================================================================== - inputDecorationTheme: InputDecorationTheme( - filled: true, - fillColor: colorScheme.surfaceContainerHighest.withValues(alpha: 0.5), - isDense: false, - contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - - // Enabled state - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: BorderSide(color: colorScheme.outline, width: 1), - ), - - // Focused state - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: BorderSide(color: colorScheme.primary, width: 2), - ), - - // Error state - errorBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: BorderSide(color: colorScheme.error, width: 1), - ), - focusedErrorBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: BorderSide(color: colorScheme.error, width: 2), - ), - - // Label and hint styles - labelStyle: TextStyle( - color: colorScheme.primary, - fontSize: 14, - fontWeight: FontWeight.w500, - ), - hintStyle: TextStyle( - color: colorScheme.onSurfaceVariant, - fontSize: 14, - ), - - // Error text style - errorStyle: TextStyle( - color: colorScheme.error, - fontSize: 12, - ), - ), - - // ======================================================================== - // BUTTON THEMES - // ======================================================================== - elevatedButtonTheme: ElevatedButtonThemeData( - style: ElevatedButton.styleFrom( - backgroundColor: colorScheme.primary, - foregroundColor: colorScheme.onPrimary, - elevation: 4, - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), - textStyle: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600), - ), - ), - outlinedButtonTheme: OutlinedButtonThemeData( - style: OutlinedButton.styleFrom( - backgroundColor: colorScheme.surface, - foregroundColor: colorScheme.primary, - side: BorderSide(color: colorScheme.outline, width: 1), - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), - textStyle: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600), - ), - ), - textButtonTheme: TextButtonThemeData( - style: TextButton.styleFrom( - backgroundColor: Colors.transparent, - foregroundColor: colorScheme.primary, - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), - textStyle: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600), - ), - ), - - // ======================================================================== - // FLOATING ACTION BUTTON THEME - // ======================================================================== - floatingActionButtonTheme: FloatingActionButtonThemeData( - backgroundColor: colorScheme.primary, - foregroundColor: colorScheme.onPrimary, - elevation: 6, - focusElevation: 8, - hoverElevation: 8, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - ), - - // ======================================================================== - // CARD THEME - // ======================================================================== - cardTheme: CardThemeData( - color: colorScheme.surface, - elevation: 1, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), - margin: const EdgeInsets.all(8), - shadowColor: colorScheme.shadow, - ), - - // ======================================================================== - // BOTTOM APP BAR THEME - // ======================================================================== - bottomAppBarTheme: BottomAppBarThemeData( - color: colorScheme.surface, - elevation: 4, - shadowColor: colorScheme.shadow, - surfaceTintColor: colorScheme.surface, - shape: const CircularNotchedRectangle(), - ), - - // ======================================================================== - // TEXT SELECTION THEME - // ======================================================================== - textSelectionTheme: TextSelectionThemeData( - cursorColor: colorScheme.primary, - selectionColor: colorScheme.primary.withValues(alpha: 0.3), - selectionHandleColor: colorScheme.primary, - ), - - // ======================================================================== - // ADDITIONAL THEMES - // ======================================================================== - canvasColor: colorScheme.surface, + //TODO: Add the text themes (103) + //TODO: Add the icon themes (103) + //TODO: Decorate the inputs (103) ); } diff --git a/lib/themeDev.dart b/lib/themeDev.dart new file mode 100644 index 0000000..025dfba --- /dev/null +++ b/lib/themeDev.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; + +import 'colors.dart'; + +final ThemeData devFiresTheme = _buildFiresTheme(); + +ThemeData _buildFiresTheme() { + final ThemeData base = ThemeData.light(); // light or dark + return base.copyWith( + accentColor: fires900, + primaryColor: Colors.pink, + buttonColor: fires900, + scaffoldBackgroundColor: firesSurfaceWhite, + cardColor: firesBackgroundWhite, + textSelectionColor: fires300, + errorColor: firesErrorRed, + + //TODO: Add the text themes (103) + //TODO: Add the icon themes (103) + //TODO: Decorate the inputs (103) + ); +} diff --git a/lib/theme_dev.dart b/lib/theme_dev.dart deleted file mode 100644 index 2155154..0000000 --- a/lib/theme_dev.dart +++ /dev/null @@ -1,300 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'colors.dart'; - -// ignore_for_file: avoid_redundant_argument_values -final ThemeData devFiresTheme = _buildFiresTheme(); - -ThemeData _buildFiresTheme() { - // Material 3 Color Scheme - Development Theme with Pink Primary - // Uses pink for easy visual identification of dev builds - const ColorScheme colorScheme = ColorScheme.light( - primary: m3DevPrimary, - onPrimary: m3DevOnPrimary, - primaryContainer: m3DevPrimaryContainer, - onPrimaryContainer: m3DevOnPrimaryContainer, - secondary: m3Secondary, - onSecondary: m3OnSecondary, - secondaryContainer: m3SecondaryContainer, - onSecondaryContainer: m3OnSecondaryContainer, - tertiary: m3Tertiary, - onTertiary: m3OnTertiary, - tertiaryContainer: m3TertiaryContainer, - onTertiaryContainer: m3OnTertiaryContainer, - error: m3Error, - onError: m3OnError, - errorContainer: m3ErrorContainer, - onErrorContainer: m3OnErrorContainer, - surface: m3Surface, - onSurface: m3OnSurface, - surfaceContainerHighest: m3SurfaceVariant, - onSurfaceVariant: m3OnSurfaceVariant, - outline: m3Outline, - outlineVariant: m3OutlineVariant, - ); - - return ThemeData( - useMaterial3: true, - colorScheme: colorScheme, - scaffoldBackgroundColor: m3Surface, - cardColor: m3Surface, - - // ======================================================================== - // TEXT THEMES - Material 3 Complete - // ======================================================================== - textTheme: TextTheme( - // Display styles - Large, prominent text - displayLarge: TextStyle( - fontSize: 57, - fontWeight: FontWeight.w400, - letterSpacing: -0.25, - color: colorScheme.onSurface, - ), - displayMedium: TextStyle( - fontSize: 45, - fontWeight: FontWeight.w400, - letterSpacing: 0, - color: colorScheme.onSurface, - ), - displaySmall: TextStyle( - fontSize: 36, - fontWeight: FontWeight.w400, - letterSpacing: 0, - color: colorScheme.onSurface, - ), - - // Headline styles - Important section titles - headlineLarge: TextStyle( - fontSize: 32, - fontWeight: FontWeight.w600, - letterSpacing: 0, - color: colorScheme.onSurface, - ), - headlineMedium: TextStyle( - fontSize: 28, - fontWeight: FontWeight.w600, - letterSpacing: 0, - color: colorScheme.onSurface, - ), - headlineSmall: TextStyle( - fontSize: 24, - fontWeight: FontWeight.w600, - letterSpacing: 0, - color: colorScheme.onSurface, - ), - - // Title styles - Subheadings - titleLarge: TextStyle( - fontSize: 22, - fontWeight: FontWeight.w500, - letterSpacing: 0, - color: colorScheme.onSurface, - ), - titleMedium: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - letterSpacing: 0.15, - color: colorScheme.onSurface, - ), - titleSmall: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - letterSpacing: 0.1, - color: colorScheme.onSurface, - ), - - // Body styles - Main content text - bodyLarge: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w400, - letterSpacing: 0.5, - color: colorScheme.onSurface, - ), - bodyMedium: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w400, - letterSpacing: 0.25, - color: colorScheme.onSurface, - ), - bodySmall: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w400, - letterSpacing: 0.4, - color: colorScheme.onSurfaceVariant, - ), - - // Label styles - Buttons, badges, labels - labelLarge: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - letterSpacing: 0.1, - color: colorScheme.onSurface, - ), - labelMedium: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w500, - letterSpacing: 0.5, - color: colorScheme.onSurface, - ), - labelSmall: TextStyle( - fontSize: 11, - fontWeight: FontWeight.w500, - letterSpacing: 0.5, - color: colorScheme.onSurface, - ), - ), - - // ======================================================================== - // ICON THEMES - // ======================================================================== - iconTheme: IconThemeData( - color: colorScheme.primary, - size: 24, - ), - appBarTheme: AppBarTheme( - backgroundColor: colorScheme.primary, - foregroundColor: colorScheme.onPrimary, - elevation: 4, - shadowColor: colorScheme.shadow, - surfaceTintColor: colorScheme.primary, - iconTheme: IconThemeData(color: colorScheme.onPrimary), - titleTextStyle: TextStyle( - color: colorScheme.onPrimary, - fontSize: 20, - fontWeight: FontWeight.w500, - ), - ), - - // ======================================================================== - // INPUT DECORATION - Material 3 Filled Style - // ======================================================================== - inputDecorationTheme: InputDecorationTheme( - filled: true, - fillColor: colorScheme.surfaceContainerHighest.withValues(alpha: 0.5), - isDense: false, - contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - - // Enabled state - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: BorderSide(color: colorScheme.outline, width: 1), - ), - - // Focused state - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: BorderSide(color: colorScheme.primary, width: 2), - ), - - // Error state - errorBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: BorderSide(color: colorScheme.error, width: 1), - ), - focusedErrorBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: BorderSide(color: colorScheme.error, width: 2), - ), - - // Label and hint styles - labelStyle: TextStyle( - color: colorScheme.primary, - fontSize: 14, - fontWeight: FontWeight.w500, - ), - hintStyle: TextStyle( - color: colorScheme.onSurfaceVariant, - fontSize: 14, - ), - - // Error text style - errorStyle: TextStyle( - color: colorScheme.error, - fontSize: 12, - ), - ), - - // ======================================================================== - // BUTTON THEMES - // ======================================================================== - elevatedButtonTheme: ElevatedButtonThemeData( - style: ElevatedButton.styleFrom( - backgroundColor: colorScheme.primary, - foregroundColor: colorScheme.onPrimary, - elevation: 4, - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), - textStyle: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600), - ), - ), - outlinedButtonTheme: OutlinedButtonThemeData( - style: OutlinedButton.styleFrom( - backgroundColor: colorScheme.surface, - foregroundColor: colorScheme.primary, - side: BorderSide(color: colorScheme.outline, width: 1), - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), - textStyle: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600), - ), - ), - textButtonTheme: TextButtonThemeData( - style: TextButton.styleFrom( - backgroundColor: Colors.transparent, - foregroundColor: colorScheme.primary, - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), - textStyle: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600), - ), - ), - - // ======================================================================== - // FLOATING ACTION BUTTON THEME - // ======================================================================== - floatingActionButtonTheme: FloatingActionButtonThemeData( - backgroundColor: colorScheme.primary, - foregroundColor: colorScheme.onPrimary, - elevation: 6, - focusElevation: 8, - hoverElevation: 8, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - ), - - // ======================================================================== - // CARD THEME - // ======================================================================== - cardTheme: CardThemeData( - color: colorScheme.surface, - elevation: 1, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), - margin: const EdgeInsets.all(8), - shadowColor: colorScheme.shadow, - ), - - // ======================================================================== - // BOTTOM APP BAR THEME - // ======================================================================== - bottomAppBarTheme: BottomAppBarThemeData( - color: colorScheme.surface, - elevation: 4, - shadowColor: colorScheme.shadow, - surfaceTintColor: colorScheme.surface, - shape: const CircularNotchedRectangle(), - ), - - // ======================================================================== - // TEXT SELECTION THEME - // ======================================================================== - textSelectionTheme: TextSelectionThemeData( - cursorColor: colorScheme.primary, - selectionColor: colorScheme.primary.withValues(alpha: 0.3), - selectionHandleColor: colorScheme.primary, - ), - - // ======================================================================== - // ADDITIONAL THEMES - // ======================================================================== - canvasColor: colorScheme.surface, - ); -} diff --git a/lib/utils/secret_loader.dart b/lib/utils/secret_loader.dart deleted file mode 100644 index a1965b8..0000000 --- a/lib/utils/secret_loader.dart +++ /dev/null @@ -1,20 +0,0 @@ -import 'dart:async' show Future; -import 'dart:convert' show json; -import 'package:flutter/services.dart' show rootBundle; - -/// Loads secret/config files from Flutter assets as JSON. -/// Used to load configuration files at app startup. -class SecretLoader { - SecretLoader({required this.secretPath}); - - final String secretPath; - - /// Load and parse JSON from asset bundle. - /// Returns a map of the parsed JSON content. - Future> load() { - return rootBundle.loadStructuredData>( - secretPath, - (String jsonStr) async => json.decode(jsonStr) as Map, - ); - } -} diff --git a/lib/utils/widget_utils.dart b/lib/utils/widget_utils.dart deleted file mode 100644 index a8f5f4b..0000000 --- a/lib/utils/widget_utils.dart +++ /dev/null @@ -1,13 +0,0 @@ -import 'package:flutter/material.dart'; - -/// Modern null-safe widget list compaction. -/// Filters out null values from a list of nullable widgets. -/// Example: compactWidgets([widget1, null, widget2]) → [widget1, widget2] -List compactWidgets(List children) => - children.whereType().toList(); - -/// String truncation for debug output and logging. -/// Truncates a string to [end] characters and appends '...' -/// Example: ellipse('abcdefgh', 4) → 'abcd...' -String ellipse(String s, [int end = 4]) => - s.length > end ? '${s.substring(0, end)}...' : s; diff --git a/lib/widgets/app_intro_page.dart b/lib/widgets/app_intro_page.dart deleted file mode 100644 index c573251..0000000 --- a/lib/widgets/app_intro_page.dart +++ /dev/null @@ -1,152 +0,0 @@ -// Copyright 2015 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:flutter/material.dart'; -import '../utils/widget_utils.dart'; - -class AppIntroItem { - AppIntroItem({required this.icon, required this.title, this.subTitle = ''}); - - final IconData icon; - final String title; - final String subTitle; -} - -typedef OnIntroFinish = void Function(BuildContext context); -typedef ListItems = List Function(BuildContext context); - -abstract class AppIntroPage extends StatelessWidget { - const AppIntroPage({ - super.key, - required this.items, - required this.onIntroFinish, - }); - - static const String routeName = '/appintro'; - final ListItems items; - final OnIntroFinish onIntroFinish; - - @override - Widget build(BuildContext context) { - return Scaffold( - body: DefaultTabController( - length: items(context).length, - child: _AppIntroPageSelector(items: items, onFinish: onIntroFinish), - ), - ); - } -} - -class _AppIntroPageSelector extends StatelessWidget { - const _AppIntroPageSelector({required this.items, required this.onFinish}); - - final ListItems items; - final OnIntroFinish onFinish; - - void _handleArrowButtonPress(BuildContext context, int delta) { - final TabController controller = DefaultTabController.of(context); - if (!controller.indexIsChanging) - controller.animateTo( - (controller.index + delta).clamp(0, items(context).length - 1)); - } - - @override - Widget build(BuildContext context) { - final TabController controller = DefaultTabController.of(context); - - final ThemeData theme = Theme.of(context); - final Color color = theme.colorScheme.secondary; - final TextStyle titleStyle = - (theme.textTheme.headlineSmall ?? const TextStyle()) - .copyWith(color: color); - final TextStyle subTitleStyle = - theme.textTheme.titleMedium ?? const TextStyle(); - - return SafeArea( - bottom: false, - child: Column( - children: [ - Row(mainAxisAlignment: MainAxisAlignment.end, children: [ - IconButton( - onPressed: () { - onFinish(context); - }, - icon: - const Icon(Icons.close, size: 30.0, color: Colors.black38)), - ]), - Expanded(child: OrientationBuilder( - builder: (BuildContext context, Orientation orientation) { - return IconTheme( - data: IconThemeData( - size: orientation == Orientation.portrait ? 200.0 : 100.0, - color: color, - ), - child: TabBarView( - children: items(context).map((AppIntroItem item) { - return Container( - padding: const EdgeInsets.all(12.0), - child: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Icon( - item.icon, - semanticLabel: item.title, - ), - Padding( - padding: - const EdgeInsets.only(left: 16.0, right: 16.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - item.title, - style: titleStyle, - textAlign: TextAlign.center, - ), - const SizedBox(height: 20.0), - Text( - item.subTitle, - style: subTitleStyle, - textAlign: TextAlign.center, - ) - ], - )), - ], - ), - ), - ); - }).toList()), - ); - })), - Container( - margin: const EdgeInsets.only(top: 16.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: compactWidgets([ - IconButton( - icon: const Icon(Icons.chevron_left), - color: color, - onPressed: () { - _handleArrowButtonPress(context, -1); - }, - tooltip: 'Page back'), - TabPageSelector(controller: controller), - IconButton( - icon: const Icon(Icons.chevron_right), - color: color, - onPressed: () { - _handleArrowButtonPress(context, 1); - if (!controller.indexIsChanging && - controller.index == items(context).length - 1) { - onFinish(context); - } - }, - tooltip: 'Page forward'), - ]))), - ], - ), - ); - } -} diff --git a/lib/widgets/material_app_with_intro.dart b/lib/widgets/material_app_with_intro.dart deleted file mode 100644 index 37b9540..0000000 --- a/lib/widgets/material_app_with_intro.dart +++ /dev/null @@ -1,96 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; -import 'package:shared_preferences/shared_preferences.dart'; - -@immutable -abstract class MaterialAppWithIntro extends StatelessWidget { - const MaterialAppWithIntro( - this.name, - this.theme, - this.routes, - this.introWidget, - this.continueWidget, - this.prefsKey, { - super.key, - }); - - final String name; - final ThemeData theme; - final Map routes; - final WidgetBuilder introWidget; - final WidgetBuilder continueWidget; - final String prefsKey; - - @override - Widget build(BuildContext context) { - return MaterialApp( - home: MaterialAppWithIntroHome(introWidget, continueWidget, prefsKey), - title: name, - theme: theme, - routes: routes, - ); - } -} - -class MaterialAppWithIntroHome extends StatefulWidget { - const MaterialAppWithIntroHome( - this.introWidget, - this.continueWidget, - this.prefsKey, { - super.key, - }); - - final WidgetBuilder introWidget; - final WidgetBuilder continueWidget; - final String prefsKey; - - @override - // ignore: library_private_types_in_public_api - _MaterialAppWithIntroState createState() => _MaterialAppWithIntroState(); -} - -class _MaterialAppWithIntroState extends State { - _MaterialAppWithIntroState(); - - late final WidgetBuilder introWidget = widget.introWidget; - late final WidgetBuilder continueWidget = widget.continueWidget; - late final String prefsKey = widget.prefsKey; - - @override - void initState() { - super.initState(); - Timer(const Duration(milliseconds: 1000), () { - checkFirstStart(); - }); - } - - // https://stackoverflow.com/questions/50654195/flutter-one-time-intro-screen - Future checkFirstStart() async { - final String initialWizardKey = prefsKey; - final SharedPreferences prefs = await SharedPreferences.getInstance(); - final bool showInitialWizard = prefs.getBool(initialWizardKey) ?? true; - - if (showInitialWizard) { - await prefs.setBool(initialWizardKey, false); - if (mounted) { - await Navigator.of(context) - .pushReplacement(MaterialPageRoute(builder: introWidget)); - } - } else { - if (mounted) { - await Navigator.of(context) - .pushReplacement(MaterialPageRoute(builder: continueWidget)); - } - } - } - - @override - Widget build(BuildContext context) { - return const Scaffold( - body: Center( - child: CircularProgressIndicator(), - ), - ); - } -} diff --git a/lib/widgets/rounded_btn.dart b/lib/widgets/rounded_btn.dart deleted file mode 100644 index 91e8a72..0000000 --- a/lib/widgets/rounded_btn.dart +++ /dev/null @@ -1,73 +0,0 @@ -import 'package:flutter/material.dart'; - -/// Rounded button widget with icon and text. -/// Used primarily in the active fires page. -class RoundedBtn extends StatelessWidget { - const RoundedBtn({ - super.key, - required this.icon, - required this.text, - required this.onPressed, - required this.backColor, - this.textStyle = const TextStyle(fontSize: 20.0, color: Colors.white), - this.fontColor = Colors.white, - }); - - factory RoundedBtn.nav({ - required IconData icon, - required String text, - required BuildContext context, - required String route, - required Color backColor, - Key? key, - TextStyle textStyle = const TextStyle(fontSize: 20.0, color: Colors.white), - Color fontColor = Colors.white, - }) { - return RoundedBtn( - key: key, - icon: icon, - text: text, - onPressed: () { - Navigator.pushNamed(context, route); - }, - backColor: backColor, - textStyle: textStyle, - fontColor: fontColor, - ); - } - - static const Radius btnRadius = Radius.circular(90.0); - - final IconData icon; - final String text; - final Color backColor; - final Color fontColor; - final TextStyle textStyle; - final VoidCallback onPressed; - - @override - Widget build(BuildContext context) { - return SizedBox( - child: ElevatedButton( - onPressed: onPressed, - style: ElevatedButton.styleFrom( - backgroundColor: backColor, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.all(btnRadius), - ), - ), - child: Padding( - padding: const EdgeInsets.all(10.0), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(icon, size: 32.0, color: fontColor), - const SizedBox(width: 10.0), - Text(text, style: textStyle), - ], - ), - ), - ), - ); - } -} diff --git a/lib/zoomMapPlugin.dart b/lib/zoomMapPlugin.dart new file mode 100644 index 0000000..fd29515 --- /dev/null +++ b/lib/zoomMapPlugin.dart @@ -0,0 +1,58 @@ +import 'package:flutter/src/widgets/framework.dart'; +import 'package:flutter_map/plugin_api.dart'; +import 'package:flutter/material.dart'; +import 'package:comunes_flutter/comunes_flutter.dart'; + +class ZoomMapPluginOptions extends LayerOptions { + final String text; + + ZoomMapPluginOptions({this.text = ""}); +} + +// https://github.com/apptreesoftware/flutter_map/blob/master/flutter_map_example/lib/pages/plugin_api.dart +class ZoomMapPlugin extends MapPlugin { + IconButton zoomButton(MapState mapState, IconData zoomIcon, int inc) { + return new IconButton( + icon: Icon(zoomIcon), + onPressed: () { + var currentZoom = mapState.zoom; + var currentCenter = mapState.center; + mapState.move(currentCenter, currentZoom + inc); + }); + } + + @override + Widget createLayer(LayerOptions options, MapState mapState) { + if (options is ZoomMapPluginOptions) { + /* print('point ${mapState + .getPixelBounds(mapState.zoom) + .bottomLeft}'); */ + return LayoutBuilder( + builder: (context, constraints) => + Stack(fit: StackFit.expand, children: [ + Positioned( + top: constraints.maxHeight - 100, + right: 10.0, + // left: 10.0, + child: new CenteredRow( + children: [ + new Column( + children: [ + zoomButton(mapState, Icons.zoom_in, 1), + zoomButton(mapState, Icons.zoom_out, -1) + ], + ) + ], + ), + ) + ])); + } + throw ("Unknown options type for ZoomMapPlugin" + "plugin: $options"); + } + + @override + bool supportsLayer(LayerOptions options) { + return options is ZoomMapPluginOptions; + } +} diff --git a/lib/zoom_map_plugin.dart b/lib/zoom_map_plugin.dart deleted file mode 100644 index 430fde4..0000000 --- a/lib/zoom_map_plugin.dart +++ /dev/null @@ -1,42 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_map/flutter_map.dart'; -import 'package:latlong2/latlong.dart'; - -/// Zoom control widget for flutter_map v6+ -class ZoomMapPluginWidget extends StatelessWidget { - const ZoomMapPluginWidget({super.key}); - - @override - Widget build(BuildContext context) { - return LayoutBuilder( - builder: (BuildContext context, BoxConstraints constraints) => - Stack(fit: StackFit.expand, children: [ - Positioned( - top: constraints.maxHeight - 100, - right: 10.0, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Column( - children: [ - _zoomButton(context, Icons.zoom_in, 1), - _zoomButton(context, Icons.zoom_out, -1) - ], - ) - ], - ), - ) - ])); - } - - IconButton _zoomButton(BuildContext context, IconData zoomIcon, int inc) { - return IconButton( - icon: Icon(zoomIcon), - onPressed: () { - final MapController controller = MapController.of(context); - final double currentZoom = controller.camera.zoom; - final LatLng currentCenter = controller.camera.center; - controller.move(currentCenter, currentZoom + inc); - }); - } -} diff --git a/pubspec.yaml b/pubspec.yaml index 45bad0c..0585ce9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,11 +1,5 @@ name: fires_flutter description: All Against Fire -# MAJOR.MINOR.PATCH+versionCode — Flutter maps +CODE to Android versionCode. -# Must be strictly greater than the highest versionCode ever uploaded to Play. -version: 1.10.0+10 - -environment: - sdk: ">=3.0.0 <4.0.0" dependencies: flutter: @@ -13,62 +7,68 @@ dependencies: # i18n flutter_localizations: sdk: flutter - intl: ^0.20.2 + intl: "^0.15.6" + + # https://pub.dartlang.org/packages/flutter # utils - timeago: ^3.6.1 - get_it: ^7.6.7 - sentry_flutter: ^7.18.0 - flutter_markdown: ^0.7.0 - url_launcher: ^6.2.5 - package_info_plus: ^6.0.0 + simple_moment: "^0.0.3" + just_debounce_it: "^1.0.4" + flutter_simple_dependency_injection: "^0.0.2" + sentry: "^2.0.2" + flutter_markdown: "^0.1.5" + url_launcher: "^3.0.2" + package_info: "^0.3.2" + launch_review: "^1.0.1" # net - http: ^1.2.1 - dio: ^5.4.3+1 - connectivity_plus: ^5.0.2 - share_plus: ^7.2.2 + http: "^0.11.3+16" + jaguar_resty: "^2.5.5" + connectivity: "^0.3.1" + share: "^0.5.2" # data - json_annotation: ^4.9.0 - shared_preferences: ^2.2.3 - objectid: ^2.1.0 + json_annotation: "^0.2.3" + shared_preferences: "^0.4.2" + bson_objectid: "^0.1.0" + comunes_flutter: + git: git@github.com:comunes/comunes-flutter.git + # path: /home/vjrj/dev/comunes_flutter + # version: "^0.0.12" # redux - redux: ^5.0.0 - flutter_redux: ^0.10.0 + redux: "^3.0.0" + flutter_redux: "^0.5.2" + redux_logging: "^0.3.0" # maps, geo, etc - flutter_map: ^6.1.0 - latlong2: ^0.9.1 - location: ^5.0.3 - geocoding: ^2.1.1 + flutter_map: "^0.0.10" + flutter_google_places_autocomplete: "^0.0.4" + location: "^1.3.4" + geocoder: "^0.0.1" # layout - padder: ^1.0.1 - flutter_speed_dial: ^7.0.0 - badges: ^3.1.2 - flutter_spinkit: ^5.2.0 + fluttery: "^0.0.7" + # https://pub.dartlang.org/packages/padder + padder: "^1.0.1" + flutter_fab_dialer: "^0.0.5" + badge: "^0.0.2" + flutter_spinkit: "^1.0.0" # firebase - firebase_core: ^2.27.1 - firebase_messaging: ^14.7.19 + firebase_messaging: "^1.0.4" # icons - cupertino_icons: ^1.0.8 - community_material_icon: ^5.9.55 + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: "^0.1.2" + community_material_icon: "^0.1.2" - # reviews - in_app_review: ^2.0.9 - - meta: any dev_dependencies: flutter_test: sdk: flutter - build_runner: ^2.4.8 - json_serializable: ^6.7.1 - test: ^1.25.2 - flutter_lints: ^3.0.1 + build_runner: ^0.8.0 + json_serializable: ^0.5.0 # For information on the generic Dart part of this file, see the # following page: https://www.dartlang.org/tools/pub/pubspec @@ -103,7 +103,8 @@ flutter: # To add custom fonts to your application, add a fonts section here, # in this "flutter" section. Each entry in this list should have a # "family" key with the font family name, and a "fonts" key with a - # list giving the asset and other descriptors. For example: + # list giving the asset and other descriptors for the font. For + # example: # fonts: # - family: Schyler # fonts: diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index 328a280..fcbd60b 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -15,10 +15,10 @@ "toFiresNotifications": "Subscribe to fires notifications", "confirm": "Confirm", "unsubscribe": "Unsubscribe", - "firesAroundThisArea": "$numFires fires at $kmAround km around this area", - "fireAroundThisArea": "A fire at $kmAround km around this area", - "noFiresAroundThisArea": "There are no fires at $kmAround km around this area", - "noFiresAround": "There are no fires", + "firesAroundThisArea": "$numFires fires at $kmAround км around this area", + "fireAroundThisArea": "A fire at $kmAround км around this area", + "noFiresAroundThisArea": "There is no fires at $kmAround км around this area", + "noFiresAround": "There is no fires", "activeFiresWorldWide": "$activeFires active fires worldwide", "updatedLastCheck": "Updated $lastCheck", "appLicense": "(c) 2017-$thisYear Comunes Association under the GNU Affero GPL v3", @@ -30,7 +30,7 @@ "getAlertsOfFiresinThatArea": "Get alerts of fires in that area", "alertWhenThereIsAFire": "Alert when there is a fire", "notPermsUbication": "We don't have permission to get your location", - "isYourUbicationEnabled": "I cannot get your current location. Is location enabled on your device?", + "isYourUbicationEnabled": "I cannot get your current location. It's your ubication enabled?", "typeTheNameOfAPlace": "Type the name of a place, region, etc", "subscribeToValueAroundThisArea": "Subscribe to $sliderValue км around this area", "warningThisIsAVeryLargeArea": "Warning: this is a very large area", diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index c3fb95d..b735530 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -15,8 +15,8 @@ "toFiresNotifications": "Suscríbete a alertas de fuegos", "confirm": "Confirmar", "unsubscribe": "Desuscríbete", - "firesAroundThisArea": "$numFires fuegos a $kmAround km a la redonda", - "fireAroundThisArea": "Un fuego a $kmAround km a la redonda", + "firesAroundThisArea": "$numFires fuegos a $kmAround км a la redonda", + "fireAroundThisArea": "Un fuego a $kmAround км a la redonda", "noFiresAround": "Sin fuegos", "noFiresAroundThisArea": "No hay fuegos a $kmAround км a la redonda", "activeFiresWorldWide": "$activeFires fuegos activos en el mundo", @@ -32,7 +32,7 @@ "notPermsUbication": "No tenemos permisos para conocer tu ubicación", "isYourUbicationEnabled": "No podemos saber tu ubicación actual. ¿Están los servicios de ubicación en tu móvil activados?", "typeTheNameOfAPlace": "Escribe el nombre de un lugar, zona, etc", - "subscribeToValueAroundThisArea": "Suscríbete a $sliderValue km a la redonda", + "subscribeToValueAroundThisArea": "Suscríbete a $sliderValue км a la redonda", "warningThisIsAVeryLargeArea": "Cuidado: esta zona es muy grande", "aF3wSeconds": "unos segundos", "aMinute": "un minuto", diff --git a/res/values/strings_gl.arb b/res/values/strings_gl.arb index 7871c59..2800f88 100644 --- a/res/values/strings_gl.arb +++ b/res/values/strings_gl.arb @@ -1,93 +1,4 @@ { - "AvoidThisStringsIfisNotPlural": "Cero Un Dous Poucos Moitos Outro", - "appName": "Tod@s contra o Lume!", - "addYourCurrentPosition": "Engade a túa localización actual", - "addSomePlace": "Engade outro lugar", - "addedThisLocation": "Xa engadiras este lugar antes", - "firesNearPlace": "Lumes cercanos a ti", - "firesInTheWorld": "Lumes no mundo", - "firesInYourPlaces": "Lumes nos teus lugares", - "youDeletedThisPlace": "Borraches este lugar", - "youDeletedThisNotification": "Borraches esta alerta", - "UNDO": "DESFACER", - "toDeleteThisPlace": "Desliza horizontalmente para borrar este lugar", - "toDeleteThisNotification": "Desliza horizontalmente para borrar esta notificación", - "toFiresNotifications": "Subscríbete a alertas de lumes", - "confirm": "Confirmar", - "unsubscribe": "Desuscríbete", - "firesAroundThisArea": "$numFires lumes a $kmAround km á redonda", - "fireAroundThisArea": "Un lume a $kmAround km á redonda", - "noFiresAround": "Sen lumes", - "noFiresAroundThisArea": "Non hai lumes a $kmAround km á redonda", - "activeFiresWorldWide": "$activeFires lumes activos no mundo", - "updatedLastCheck": "Actualizado $lastCheck", - "appLicense": "(c) 2017-$thisYear Asociación Comunes baixo licenza GNU Affero GPL v3", - "activeFires": "Lumes activos", - "notifyAFire": "Notificar un lume", - "supportThisInitiative": "Apoia esta iniciativa", - "chooseAPlace": "Escolle un lugar", - "chooseAWatchRadio": "Escolle un raio de vixilancia", - "getAlertsOfFiresinThatArea": "Recibe alertas de lumes nesa zona", - "alertWhenThereIsAFire": "Alerta cando hai un lume", - "notPermsUbication": "Non temos permisos para coñecer a túa localización", - "isYourUbicationEnabled": "Non podemos saber a túa localización actual. ¿Están os servizos de localización no teu móbil activados?", - "typeTheNameOfAPlace": "Escribe o nome dun lugar, zona, etc", - "subscribeToValueAroundThisArea": "Subscríbete a $sliderValue km á redonda", - "warningThisIsAVeryLargeArea": "Cuidado: esta zona é moi grande", - "aF3wSeconds": "uns segundos", - "aMinute": "un minuto", - "inMinutes": "$value minutos", - "anHour": "unha hora", - "inHours": "$value horas", - "aDay": "un día", - "inDays": "$value días", - "aMonth": "un mes", - "inMonths": "$value meses", - "aYear": "un ano", - "inYears": "$value anos", - "somethingAgo": "fai $something", - "inSomething": "en $something", - "SAVE": "GARDAR", - "CANCEL": "CANCELAR", - "privacyPolicy": "Política de Privacidade", - "errorFirePlaceDialog": "Non puidemos obter a localización do lume", - "callEmergencyServicesTitle": "Chama ao 112", - "callEmergencyServicesDescription": "Deberías chamar ao 112 se non o fixeches xa para avisar aos servizos de emerxencia.", - "notifyNeighbours": "Notifica a outros usuarios/as", - "notifyNeighboursDescription": "Alerta a outros usuarios da zona sobre o lume", - "tweetAboutAFireTitle": "Tuitea", - "tweetAboutAFireDescription": "Adicionalmente se usas twitter podes compartir información adicional cos servizos de emerxencia, por exemplo, adxuntando fotos ao tuit se tes boa visibilidade do lume, cando tiñas as fotos, localización exacta, etc. Usa #hashtags tipo #IFTerminoMunicipal por exemplo #IFXumilla (Incendio Forestal en Xumilla).", - "tweetAboutSelf": "Lume en $location $hash", - "supportPageDescription": "Podes apoiar esta iniciativa, dando difusión, axudándonos a desenvolver o noso software, traducindo esta aplicación á túa lingua, suxerindo outras fontes de datos públicos de lumes para usar, etc.", - "fireNotificationsTitle": "Notificacións de lumes", - "fireNotificationTitle": "Notificación de lume", - "fireNotificationsTitleShort": "Notificacións", - "fireNotificationsDescription": "Aquí recibirás as notificacións de lumes nos lugares aos que te subscribas", - "areYouSureTitle": "¿Seguro?", - "deleteAllFireNotificationsAlertDescription": "Isto borrará todas as notificacións de lumes", - "DELETE": "BORRAR", - "SHOW": "MOSTRAR", - "CLOSE": "PECHAR", - "noConnectivity": "Esta aplicación necesita unha conexión a Internet para funcionar", - "NASAAck": "Recoñecemos o uso de datos e imaxes de LANCE FIRMS operadas por NASA/GSFC/Earth Science Data and Information System (ESDIS) con fondos proporcionados por NASA/HQ.", - "appMoto": "Fortalecendo vecindaros contra incendios forestais", - "itSeemsNotAtForesFire": "Parece que este lume non é un lume forestal.", - "notAWildfire": "¿Non é un lume forestal?", - "itSeemsAIndustry": "É unha industria", - "itSeemsAControlledBurning": "É unha quema controlada", - "itSeemsAFalseAlarm": "Parece unha falsa alarma", - "thanksForParticipating": "Grazas pola participación", - "inGreenMonitoredAreas": "En verde, as zonas vixiadas polos nosos usuarios/as actualmente", - "monitoredAreasTitle": "Zonas vixiadas", - "mapPrivacy": "Para preservar a privacidade dos nosos usuarios/as, os datos reflectidos están aleatoriamente alterados e son só orientativos.", - "comunesSupportBtn": "Coñece e apoia o noso traballo", - "shareAppBtn": "Comparte a nosa app", - "starAppBtn": "Puntúa a nosa app", - "unsubscribedToFires": "Desuscrito a notificacións de lumes", - "subscribedToFires": "Suscrito a notificacións de lumes", - "inDevelopment": "En desenvolvemento", - "translateBtn": "Axuda coas traducións", - "additionalInfoAboutFire": "Información adicional sobre lume detectado en $where $when $by", - "byNASAsatellites": "por satélites da NASA", - "byOurUsers": "por un dos nosos usuarios/as" + "AvoidThisStringsIfisNotPlural": "No hace falta traducir esto", + "appName": "Tod@s contra o Lume!" } diff --git a/test/file_test.dart b/test/file_test.dart index d42e4f9..f584d1b 100644 --- a/test/file_test.dart +++ b/test/file_test.dart @@ -1,30 +1,30 @@ -import 'package:fires_flutter/file_utils.dart'; import 'package:test/test.dart'; +import 'package:fires_flutter/fileUtils.dart'; void main() { test('test es-ES fallback', () { - final String answer = getFallbackLang('es-ES'); + String answer = getFallbackLang('es-ES'); expect(answer, 'es'); }); test('test en-GB fallback', () { - final String answer = getFallbackLang('en-GB'); + String answer = getFallbackLang('en-GB'); expect(answer, 'en'); }); test('test gl fallback', () { - final String answer = getFallbackLang('gl'); + String answer = getFallbackLang('gl'); expect(answer, 'es'); }); test('test fr fallback', () { - final String answer = getFallbackLang('fr'); + String answer = getFallbackLang('fr'); expect(answer, 'en'); }); test('test privacy English md page', () async { - final String answer = await getFileNameOfLang( - dir: 'assets/pages', fileName: 'privacy', ext: 'md', lang: 'en'); + String answer = await getFileNameOfLang(dir: 'assets/pages', fileName: 'privacy', ext: 'md', lang: 'en'); expect(answer, 'assets/pages/privacy-en.md'); }); + }