diff --git a/.github/workflows/build-apk.yml b/.github/workflows/build-apk.yml deleted file mode 100644 index 4a8f838..0000000 --- a/.github/workflows/build-apk.yml +++ /dev/null @@ -1,166 +0,0 @@ -name: Build APK & Release - -on: - workflow_dispatch: - inputs: - release_tag: - description: 'Release tag (e.g. v1.2.0)' - required: false - default: '' - push: - branches: - - main - paths: - - 'app.json' - - 'src/**' - - 'App.tsx' - - 'package.json' - - 'android/**' - -permissions: - contents: write - -jobs: - build: - runs-on: ubuntu-latest - - outputs: - version: ${{ steps.meta.outputs.version }} - tag: ${{ steps.meta.outputs.tag }} - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup Node - uses: actions/setup-node@v4 - with: - node-version: 22 - cache: npm - - - name: Setup Java - uses: actions/setup-java@v4 - with: - distribution: temurin - java-version: 21 - - - name: Cache Gradle - uses: actions/cache@v4 - with: - path: | - ~/.gradle/caches - ~/.gradle/wrapper - key: gradle-${{ hashFiles('android/**/*.gradle*', 'android/gradle.properties') }} - restore-keys: gradle- - - - name: Setup Android SDK - uses: android-actions/setup-android@v3 - - - name: Install SDK components - run: | - echo "y" | sdkmanager \ - "ndk;27.1.12297006" \ - "build-tools;36.0.0" \ - "platforms;android-36" \ - "cmake;3.22.1" - - - name: Install dependencies - run: npm install --legacy-peer-deps - - - name: Fix expo-modules-core Gradle bugs - run: | - FILE="node_modules/expo-modules-core/android/build.gradle" - sed -i "s|apply plugin: 'com.android.library'|// Fix: project-level re-declaration\ndef _coreFeatures = project.findProperty(\"coreFeatures\") ?: []\next.shouldIncludeCompose = _coreFeatures.contains(\"compose\")\n\napply plugin: 'com.android.library'|" "$FILE" - sed -i 's/^\s*compose shouldIncludeCompose\s*$/ compose = shouldIncludeCompose/' "$FILE" - - - name: Read version & compute tag - id: meta - run: | - VERSION=$(node -p "require('./app.json').expo.version") - TAG="${{ github.event.inputs.release_tag }}" - if [ -z "$TAG" ]; then - TAG="v${VERSION}" - fi - echo "version=$VERSION" >> "$GITHUB_OUTPUT" - echo "tag=$TAG" >> "$GITHUB_OUTPUT" - echo "Building AeroStaff Pro $VERSION (tag: $TAG)" - - - name: Make gradlew executable - run: chmod +x android/gradlew - - - name: Generate debug keystore (used by release signing config) - run: | - mkdir -p android/app - keytool -genkey -v \ - -keystore android/app/debug.keystore \ - -storepass android \ - -alias androiddebugkey \ - -keypass android \ - -keyalg RSA \ - -keysize 2048 \ - -validity 10000 \ - -dname "CN=Android Debug,O=Android,C=US" - - - name: Build release APK (standalone, JS bundle embedded) - run: | - cd android - ./gradlew :app:assembleRelease \ - --no-daemon \ - -Pandroid.overridePathCheck=true - env: - ANDROID_HOME: ${{ env.ANDROID_SDK_ROOT }} - GRADLE_OPTS: "-Xmx4g -XX:MaxMetaspaceSize=512m" - - - name: Find & rename APK - id: apk - run: | - APK=$(find android/app/build/outputs/apk/release -name "*.apk" -type f | head -1) - echo "Found: $APK" - VERSION=${{ steps.meta.outputs.version }} - DEST="AeroStaffPro-v${VERSION}.apk" - mkdir -p artifacts - cp "$APK" "artifacts/$DEST" - echo "apk_path=artifacts/$DEST" >> "$GITHUB_OUTPUT" - echo "apk_name=$DEST" >> "$GITHUB_OUTPUT" - ls -lh artifacts/ - - - name: Upload artifact - uses: actions/upload-artifact@v4 - with: - name: AeroStaffPro-v${{ steps.meta.outputs.version }} - path: ${{ steps.apk.outputs.apk_path }} - - release: - needs: build - runs-on: ubuntu-latest - - steps: - - name: Download APK artifact - uses: actions/download-artifact@v4 - with: - name: AeroStaffPro-v${{ needs.build.outputs.version }} - path: artifacts/ - - - name: Create or update GitHub release - uses: softprops/action-gh-release@v2 - with: - tag_name: ${{ needs.build.outputs.tag }} - name: "AeroStaff Pro ${{ needs.build.outputs.version }} β€” Liquid Glass + Orange" - body: | - ## 🍊 AeroStaff Pro ${{ needs.build.outputs.version }} - - ### Design Overhaul: Liquid Glass + Orange - - Nuovo colore primario arancione `#F47B16` - - Stile iOS 26 liquid glass (BlurView + shimmer cards) - - Nuovo logo AeroStaff PRO con gradiente arancione - - GlassCard component con fallback Android - - Tab bar, AppBar e DrawerMenu aggiornati - - Tutti gli hardcoded blue rimossi - - ### βœ… APK Standalone - Bundle JavaScript incorporato β€” funziona senza Metro dev server. - - ### Download - Installa `AeroStaffPro-v${{ needs.build.outputs.version }}.apk` sul tuo dispositivo Android. - prerelease: true - files: artifacts/*.apk \ No newline at end of file diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index 2ae356c..7eaf6fb 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -4,6 +4,12 @@ on: push: tags: - 'v*' + branches: + - 'claude/fix-shift-permissions-Cn8gk' + workflow_dispatch: + +permissions: + contents: write jobs: build: @@ -13,78 +19,116 @@ jobs: - name: Checkout uses: actions/checkout@v4 - - name: Set up Node.js + - name: Setup Node uses: actions/setup-node@v4 with: - node-version: '20' - cache: 'npm' + node-version: 22 + cache: npm - - name: Set up Java 17 + - name: Setup Java uses: actions/setup-java@v4 with: - java-version: '17' - distribution: 'temurin' + distribution: temurin + java-version: 21 + + - name: Cache Gradle + uses: actions/cache@v4 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: gradle-${{ hashFiles('android/**/*.gradle*', 'android/gradle.properties') }} + restore-keys: gradle- - - name: Set up Android SDK + - name: Setup Android SDK uses: android-actions/setup-android@v3 + - name: Install SDK components + run: | + echo "y" | sdkmanager \ + "ndk;27.1.12297006" \ + "build-tools;36.0.0" \ + "platforms;android-36" \ + "cmake;3.22.1" + - name: Install dependencies - run: npm ci + run: npm install --legacy-peer-deps + + - name: Fix expo-modules-core Gradle bugs + run: | + FILE="node_modules/expo-modules-core/android/build.gradle" + sed -i "s|apply plugin: 'com.android.library'|// Fix: project-level re-declaration\ndef _coreFeatures = project.findProperty(\"coreFeatures\") ?: []\next.shouldIncludeCompose = _coreFeatures.contains(\"compose\")\n\napply plugin: 'com.android.library'|" "$FILE" + sed -i 's/^\s*compose shouldIncludeCompose\s*$/ compose = shouldIncludeCompose/' "$FILE" - - name: Bundle JS for Android + - name: Compute release tag + id: meta run: | - mkdir -p android/app/src/main/assets - node_modules/.bin/expo export:embed \ - --platform android \ - --entry-file index.ts \ - --bundle-output android/app/src/main/assets/index.android.bundle \ - --assets-dest android/app/src/main/res - - - name: Generate debug keystore (for signing) + VERSION=$(node -p "require('./app.json').expo.version") + if [[ "${{ github.ref }}" == refs/tags/* ]]; then + TAG="${{ github.ref_name }}" + else + TAG="v${VERSION}" + fi + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "tag=$TAG" >> "$GITHUB_OUTPUT" + + - name: Make gradlew executable + run: chmod +x android/gradlew + + - name: Restore stable keystore run: | - keytool -genkey -v \ - -keystore android/app/debug.keystore \ - -storepass android \ - -alias androiddebugkey \ - -keypass android \ - -keyalg RSA \ - -keysize 2048 \ - -validity 10000 \ - -dname "CN=Android Debug,O=Android,C=US" + mkdir -p android/app + echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 -d > android/app/debug.keystore - name: Build release APK run: | cd android - chmod +x gradlew - ./gradlew assembleRelease - - - name: Get version name - id: version - run: | - VERSION=$(grep versionName android/app/build.gradle | sed "s/.*versionName \"\\(.*\\)\"/\\1/") - echo "version=$VERSION" >> $GITHUB_OUTPUT + ./gradlew :app:assembleRelease \ + --no-daemon \ + -Pandroid.overridePathCheck=true + env: + ANDROID_HOME: ${{ env.ANDROID_SDK_ROOT }} + GRADLE_OPTS: "-Xmx4g -XX:MaxMetaspaceSize=512m" - name: Rename APK run: | - mv android/app/build/outputs/apk/release/app-release.apk \ - FlightWorkApp-${{ github.ref_name }}.apk + APK=$(find android/app/build/outputs/apk/release -name "*.apk" -type f | head -1) + DEST="AeroStaffPro-${{ steps.meta.outputs.tag }}.apk" + cp "$APK" "$DEST" + echo "apk=$DEST" >> "$GITHUB_ENV" - name: Create GitHub Release uses: softprops/action-gh-release@v2 with: - name: "${{ github.ref_name }} - Shift Permissions & Widget Fix" + tag_name: ${{ steps.meta.outputs.tag }} + name: "AeroStaff Pro ${{ steps.meta.outputs.tag }}" body: | - ## πŸ› Bug Fixes - - πŸ”§ **Permessi calendario** β€” Quando il permesso Γ¨ rifiutato permanentemente, ora mostra un tasto "Apri Impostazioni" per abilitarlo direttamente - πŸ”§ **Modal Aggiungi Turno** β€” Il form Γ¨ ora scrollabile: il tasto "Salva Turno" Γ¨ sempre raggiungibile su schermi piccoli - πŸ”§ **Widget auto-aggiornante** β€” Il widget ora scarica dati freschi da FR24 ad ogni aggiornamento Android (30 min) senza dover aprire l'app - πŸ”§ **Widget si aggiorna dopo salvataggio turno** β€” Salvando un turno dal CalendarScreen il widget si aggiorna immediatamente - - ## πŸ“¦ Technical - - - versionCode bumped to 7 - - versionName ${{ steps.version.outputs.version }} - files: FlightWorkApp-${{ github.ref_name }}.apk + ## πŸ“¦ AeroStaff Pro ${{ steps.meta.outputs.tag }} + + ### NovitΓ  + - **Backup / Ripristino**: esporta tutti i dati dell'app in un file JSON e reimportali in qualsiasi momento da Impostazioni + - **Aggiornamento in-app**: popup stile Mihon con changelog e download APK diretto; controllo automatico all'avvio ogni 24h + - **Impostazioni riprogettate**: sezione aggiornamenti con card dedicata e badge NEW; backup con due tile colorate (esporta verde, importa blu) + + ### Bug fix – Turni e Home + - **HomeScreen "Giorno di riposo" errato**: l'evento Lavoro nel calendario ora ha sempre la precedenza su un eventuale evento Riposo dello stesso giorno + - **Widget "Giorno di riposo" errato**: WIDGET_SHIFT_KEY Γ¨ ora la fonte autoritativa per classificare lavoro/riposo, impedendo che dati in cache stantii sovrascrivano il turno reale + - **FlightScreen**: trovato evento Lavoro nel calendario non azzerava `isRestDay` se preceduto da un evento Riposo; corretto + + ### Bug fix – Voli + - **Aggiornamento automatico voli**: i dati FR24 ora si ricaricano ogni 2 minuti senza riaprire l'app + - **Duplicati voli**: chiave di merge stabilizzata su `numeroVolo_tsPartenza` (prima usava `identification.id` che FR24 a volte omette) + + ### Bug fix – StaffMonitor stand / gate + - **Colonne non rilevate**: il parser non richiedeva piΓΉ la presenza di colonne stand/gate/belt per avviare il parsing; prima saltava tutte le righe + - **Nomi handler nella colonna stand**: `cell()` ora estrae solo il primo token alfanumerico (`17`, `674`, `4`) scartando tutto ciΓ² che segue (es. `17β—† Federico`, `674 RICCARDO F`) + - **Numeri di telefono come stand**: `isPhoneOrJunk` ora individua sequenze di 8+ cifre ovunque nella stringa (prima cercava solo stringhe composte da soli numeri) + - **Colonna "ADDETTO STAND" rilevata come stand**: ora si usa una word-boundary (`\bstand\b`) per evitare falsi positivi + - **Arrivi sempre in errore (AbortError)**: 7 varianti URL con `nature=A` vengono ora inviate in parallelo (race); la prima risposta valida vince in ≀30s invece di 3Γ—20s sequenziali. Cache AsyncStorage da 20 minuti: un fetch riuscito sopravvive ai cali di rete successivi + - **Numeri volo corrotti negli arrivi**: rimossi URL di fallback senza `nature=A` (restituivano dati partenze interpretati come arrivi) + - **Timeout partenze regredito**: ripristinato timeout a 25s (il server Pisa Γ¨ lento; 12s era troppo corto) + + Scarica `AeroStaffPro-${{ steps.meta.outputs.tag }}.apk` e installalo sul tuo dispositivo Android. + files: ${{ env.apk }} draft: false prerelease: false diff --git a/.github/workflows/release-snapshot.yml b/.github/workflows/release-snapshot.yml deleted file mode 100644 index b19cb75..0000000 --- a/.github/workflows/release-snapshot.yml +++ /dev/null @@ -1,33 +0,0 @@ -name: Release Snapshot - -on: - workflow_dispatch: - push: - branches: - - main - -permissions: - contents: write - -jobs: - release: - runs-on: ubuntu-latest - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Build release metadata - id: meta - run: | - echo "tag=main-${GITHUB_RUN_NUMBER}" >> "$GITHUB_OUTPUT" - echo "name=Main snapshot #${GITHUB_RUN_NUMBER}" >> "$GITHUB_OUTPUT" - - - name: Create GitHub prerelease - uses: softprops/action-gh-release@v2 - with: - tag_name: ${{ steps.meta.outputs.tag }} - name: ${{ steps.meta.outputs.name }} - target_commitish: ${{ github.sha }} - generate_release_notes: true - prerelease: true diff --git a/App.tsx b/App.tsx index 63cdb27..bd5d2a3 100644 --- a/App.tsx +++ b/App.tsx @@ -1,7 +1,7 @@ import React, { useState, useRef, useMemo, useEffect } from 'react'; import { View, Text, StyleSheet, TouchableOpacity, Platform, StatusBar, PanResponder, Animated, Dimensions, BackHandler } from 'react-native'; import { LinearGradient } from 'expo-linear-gradient'; -import { BlurView } from 'expo-blur'; +import { BlurView as ExpoBlurView } from 'expo-blur'; import * as Haptics from 'expo-haptics'; import { MaterialIcons } from '@expo/vector-icons'; import { ThemeProvider, useAppTheme } from './src/context/ThemeContext'; @@ -18,6 +18,8 @@ import SettingsScreen from './src/screens/SettingsScreen'; import PasswordScreen from './src/screens/PasswordScreen'; import DrawerMenu from './src/components/DrawerMenu'; import { autoScheduleNotifications } from './src/utils/autoNotifications'; +import { checkForUpdate, wasUpdateSeen, markUpdateSeen, type UpdateInfo } from './src/utils/updateChecker'; +import UpdateModal from './src/components/UpdateModal'; type Tab = 'Shifts' | 'Calendar' | 'Flights' | 'TravelDoc'; type OverlayScreen = 'Notepad' | 'Phonebook' | 'Passwords' | 'Manuals' | 'Settings' | null; @@ -82,6 +84,7 @@ function AppInner() { const [activeTab, setActiveTab] = useState('Shifts'); const [drawerOpen, setDrawerOpen] = useState(false); const [overlay, setOverlay] = useState(null); + const [pendingUpdate, setPendingUpdate] = useState(null); const tabLabels: Record = { Shifts: t('tabHome'), Calendar: t('tabShifts'), Flights: t('tabFlights'), TravelDoc: t('tabTravelDoc'), @@ -99,6 +102,12 @@ function AppInner() { autoScheduleNotifications().then(count => { if (count > 0 && __DEV__) console.log(`Auto-scheduled ${count} notifications`); }).catch(() => {}); + // Check for updates; show modal once per new version + checkForUpdate().then(async info => { + if (!info?.available) return; + const seen = await wasUpdateSeen(info.latestVersion); + if (!seen) setPendingUpdate(info); + }).catch(() => {}); }, []); // ─── Android back button: overlay β†’ home, drawer β†’ close ─────────────────── @@ -167,7 +176,7 @@ function AppInner() { const renderTabScreen = (tab: Tab) => { switch (tab) { - case 'Shifts': return ; + case 'Shifts': return ; case 'Calendar': return ; case 'Flights': return ; case 'TravelDoc': return ; @@ -186,7 +195,7 @@ function AppInner() { /> {/* Top App Bar β€” liquid glass */} - MR - + {/* Screen Content */} {isWeather ? ( @@ -248,25 +257,32 @@ function AppInner() { {/* Bottom Nav β€” Glassmorphic Floating Pill (hidden on overlay screens) */} {!overlay && ( - - {TABS.map(tab => { - const active = activeTab === tab.id; - return ( - { - Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); - goToTab(TABS.findIndex(t => t.id === tab.id)); - }} - /> - ); - })} - + + + + {TABS.map(tab => { + const active = activeTab === tab.id; + return ( + { + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); + goToTab(TABS.findIndex(t => t.id === tab.id)); + }} + /> + ); + })} + + )} @@ -276,6 +292,15 @@ function AppInner() { onClose={() => setDrawerOpen(false)} onSelect={handleDrawerSelect} /> + {pendingUpdate && ( + { + markUpdateSeen(pendingUpdate.latestVersion).catch(() => {}); + setPendingUpdate(null); + }} + /> + )} ); } @@ -330,15 +355,19 @@ const styles = StyleSheet.create({ right: 16, }, tabBarBlur: { - flexDirection: 'row', height: 66, borderRadius: 33, - justifyContent: 'space-around', - alignItems: 'center', overflow: 'hidden', borderWidth: 0.75, borderColor: 'rgba(255,255,255,0.22)', }, + tabBarRow: { + flex: 1, + flexDirection: 'row', + height: 66, + justifyContent: 'space-around', + alignItems: 'center', + }, glassTab: { alignItems: 'center', justifyContent: 'center', diff --git a/android/app/build.gradle b/android/app/build.gradle index 77fd0c0..9c0fbb6 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -89,11 +89,11 @@ android { namespace 'com.anonymous.FlightWorkApp' defaultConfig { - applicationId 'com.anonymous.FlightWorkApp' + applicationId 'com.aerostaffpro.app' minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 7 - versionName "1.3.6" + versionCode 11 + versionName "2.5.0" buildConfigField "String", "REACT_NATIVE_RELEASE_LEVEL", "\"${findProperty('reactNativeReleaseLevel') ?: 'stable'}\"" } diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index f119c49..043c5e2 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -35,7 +35,7 @@ - + diff --git a/android/app/src/main/res/drawable/shiftflights_preview.png b/android/app/src/main/res/drawable/shiftflights_preview.png index 7165a53..e891be1 100644 Binary files a/android/app/src/main/res/drawable/shiftflights_preview.png and b/android/app/src/main/res/drawable/shiftflights_preview.png differ diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp index a858de0..6760a36 100644 Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_background.webp b/android/app/src/main/res/mipmap-hdpi/ic_launcher_background.webp index c02225f..ffa39e6 100644 Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher_background.webp and b/android/app/src/main/res/mipmap-hdpi/ic_launcher_background.webp differ diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp b/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp index 49a260f..a2b89f3 100644 Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp and b/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp differ diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.webp b/android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.webp index fc1087b..c2139c6 100644 Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.webp and b/android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.webp differ diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp index 7b9ac97..881d205 100644 Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp and b/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp index c75dbb9..f7b7a04 100644 Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_background.webp b/android/app/src/main/res/mipmap-mdpi/ic_launcher_background.webp index 1e9b3d1..b8c9601 100644 Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher_background.webp and b/android/app/src/main/res/mipmap-mdpi/ic_launcher_background.webp differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp b/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp index 46ea7d0..beafe69 100644 Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp and b/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.webp b/android/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.webp index 12a3278..581377b 100644 Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.webp and b/android/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.webp differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp index a9b305d..006f0eb 100644 Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp and b/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp index b2658c0..615b6ab 100644 Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_background.webp b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_background.webp index dde133a..dfd17e7 100644 Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_background.webp and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_background.webp differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp index 671b75f..89599c4 100644 Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.webp b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.webp index 2809d31..dc354fd 100644 Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.webp and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.webp differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp index 535e6d7..15e79c2 100644 Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp index 858ddc8..498934d 100644 Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.webp b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.webp index 55a3581..65409c5 100644 Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.webp and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.webp differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp index ba86612..b67f11b 100644 Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.webp b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.webp index 9dbe470..dc5094b 100644 Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.webp and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.webp differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp index 357395c..c92bb64 100644 Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp index d31ce8f..4cc55a9 100644 Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.webp b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.webp index d190cdd..1a53ca8 100644 Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.webp and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.webp differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp index 3468026..a3a34b3 100644 Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.webp b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.webp index 8736a27..f5cc80c 100644 Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.webp and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.webp differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp index 4185f23..800242b 100644 Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index 686e992..9454458 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -1,5 +1,5 @@ - FlightWorkApp + AeroStaff Pro contain false Voli del turno corrente con orari CI e Gate diff --git a/app.json b/app.json index 4ac6610..44de1a9 100644 --- a/app.json +++ b/app.json @@ -1,8 +1,8 @@ { "expo": { - "name": "FlightWorkApp", - "slug": "FlightWorkApp", - "version": "1.3.5", + "name": "AeroStaff Pro", + "slug": "AeroStaffPro", + "version": "2.6.2", "orientation": "portrait", "icon": "./assets/icon.png", "userInterfaceStyle": "light", @@ -16,13 +16,13 @@ }, "android": { "adaptiveIcon": { - "backgroundColor": "#FFF3E0", + "backgroundColor": "#F47B16", "foregroundImage": "./assets/android-icon-foreground.png", "backgroundImage": "./assets/android-icon-background.png", "monochromeImage": "./assets/android-icon-monochrome.png" }, "predictiveBackGestureEnabled": false, - "package": "com.anonymous.FlightWorkApp" + "package": "com.aerostaffpro.app" }, "web": { "favicon": "./assets/favicon.png" diff --git a/assets/android-icon-background.png b/assets/android-icon-background.png index 5ffefc5..4bdece7 100644 Binary files a/assets/android-icon-background.png and b/assets/android-icon-background.png differ diff --git a/assets/android-icon-foreground.png b/assets/android-icon-foreground.png index 3a9e501..3eaaeb6 100644 Binary files a/assets/android-icon-foreground.png and b/assets/android-icon-foreground.png differ diff --git a/assets/android-icon-monochrome.png b/assets/android-icon-monochrome.png index 77484eb..1e2ea1a 100644 Binary files a/assets/android-icon-monochrome.png and b/assets/android-icon-monochrome.png differ diff --git a/assets/favicon.png b/assets/favicon.png index 408bd74..c0d5451 100644 Binary files a/assets/favicon.png and b/assets/favicon.png differ diff --git a/assets/icon.png b/assets/icon.png index 7165a53..9092595 100644 Binary files a/assets/icon.png and b/assets/icon.png differ diff --git a/assets/widget-preview.png b/assets/widget-preview.png index 7165a53..e891be1 100644 Binary files a/assets/widget-preview.png and b/assets/widget-preview.png differ diff --git a/package-lock.json b/package-lock.json index 9503e29..9d971c6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { - "name": "flightworkapp", - "version": "1.1.0", + "name": "aerostaff-pro", + "version": "2.5.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "flightworkapp", - "version": "1.1.0", + "name": "aerostaff-pro", + "version": "2.5.0", "dependencies": { "@expo/vector-icons": "^15.0.3", "@react-native-async-storage/async-storage": "2.2.0", @@ -1884,14 +1884,14 @@ "version": "9.3.0", "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==", - "devOptional": true, + "dev": true, "license": "BSD-3-Clause" }, "node_modules/@hapi/topo": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", - "devOptional": true, + "dev": true, "license": "BSD-3-Clause", "dependencies": { "@hapi/hoek": "^9.0.0" @@ -2378,7 +2378,7 @@ "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@nodelib/fs.stat": "2.0.5", @@ -2392,7 +2392,7 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">= 8" @@ -2402,7 +2402,7 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@nodelib/fs.scandir": "2.1.5", @@ -2428,7 +2428,7 @@ "version": "20.1.3", "resolved": "https://registry.npmjs.org/@react-native-community/cli/-/cli-20.1.3.tgz", "integrity": "sha512-sLo8cu9JyFNfuuF1C+8NJ4DHE/PEFaXGd4enkcxi/OJjGG8+sOQrdjNQ4i+cVh/2c+ah1mEMwsYjc3z0+/MqSg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@react-native-community/cli-clean": "20.1.3", @@ -2458,7 +2458,7 @@ "version": "20.1.3", "resolved": "https://registry.npmjs.org/@react-native-community/cli-clean/-/cli-clean-20.1.3.tgz", "integrity": "sha512-sFLdLzapfC0scjgzBJJWYDY2RhHPjuuPkA5r6q0gc/UQH/izXpMpLrhh1DW84cMDraNACK0U62tU7ebNaQ1LMQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@react-native-community/cli-tools": "20.1.3", @@ -2471,7 +2471,7 @@ "version": "20.1.3", "resolved": "https://registry.npmjs.org/@react-native-community/cli-config/-/cli-config-20.1.3.tgz", "integrity": "sha512-n73nW0cG92oNF0r994pPqm0DjAShOm3F8LSffDYhJqNAno+h/csmv/37iL4NtSpmKIO8xqsG3uVTXz9X/hzNaQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@react-native-community/cli-tools": "20.1.3", @@ -2486,7 +2486,7 @@ "version": "20.1.3", "resolved": "https://registry.npmjs.org/@react-native-community/cli-config-android/-/cli-config-android-20.1.3.tgz", "integrity": "sha512-DNHDP+OWLyhKShGciBqPcxhxfp1Z/7GQcb4F+TGyCeKQAr+JdnUjRXN3X+YCU/v+g2kbYYyRJKlGabzkVvdrAw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@react-native-community/cli-tools": "20.1.3", @@ -2499,7 +2499,7 @@ "version": "20.1.3", "resolved": "https://registry.npmjs.org/@react-native-community/cli-config-apple/-/cli-config-apple-20.1.3.tgz", "integrity": "sha512-QX9B83nAfCPs0KiaYz61kAEHWr9sttooxzRzNdQwvZTwnsIpvWOT9GvMMj/19OeXiQzMJBzZX0Pgt6+spiUsDQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@react-native-community/cli-tools": "20.1.3", @@ -2512,7 +2512,7 @@ "version": "20.1.3", "resolved": "https://registry.npmjs.org/@react-native-community/cli-doctor/-/cli-doctor-20.1.3.tgz", "integrity": "sha512-EI+mAPWn255/WZ4CQohy1I049yiaxVr41C3BeQ2BCyhxODIDR8XRsLzYb1t9MfqK/C3ZncUN2mPSRXFeKPPI1w==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@react-native-community/cli-config": "20.1.3", @@ -2536,7 +2536,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "restore-cursor": "^3.1.0" @@ -2549,7 +2549,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "chalk": "^4.1.0", @@ -2566,7 +2566,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -2576,7 +2576,7 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "mimic-fn": "^2.1.0" @@ -2592,7 +2592,7 @@ "version": "5.4.1", "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "bl": "^4.1.0", @@ -2616,7 +2616,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "onetime": "^5.1.0", @@ -2630,7 +2630,7 @@ "version": "20.1.3", "resolved": "https://registry.npmjs.org/@react-native-community/cli-platform-android/-/cli-platform-android-20.1.3.tgz", "integrity": "sha512-bzB9ELPOISuqgtDZXFPQlkuxx1YFkNx3cNgslc5ElCrk+5LeCLQLIBh/dmIuK8rwUrPcrramjeBj++Noc+TaAA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@react-native-community/cli-config-android": "20.1.3", @@ -2644,7 +2644,7 @@ "version": "20.1.3", "resolved": "https://registry.npmjs.org/@react-native-community/cli-platform-apple/-/cli-platform-apple-20.1.3.tgz", "integrity": "sha512-XJ+DqAD4hkplWVXK5AMgN7pP9+4yRSe5KfZ/b42+ofkDBI55ALlUmX+9HWE3fMuRjcotTCoNZqX2ov97cFDXpQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@react-native-community/cli-config-apple": "20.1.3", @@ -2658,7 +2658,7 @@ "version": "20.1.3", "resolved": "https://registry.npmjs.org/@react-native-community/cli-platform-ios/-/cli-platform-ios-20.1.3.tgz", "integrity": "sha512-2qL48SINotuHbZO73cgqSwqd/OWNx0xTbFSdujhpogV4p8BNwYYypfjh4vJY5qJEB5PxuoVkMXT+aCADpg9nBg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@react-native-community/cli-platform-apple": "20.1.3" @@ -2668,7 +2668,7 @@ "version": "20.1.3", "resolved": "https://registry.npmjs.org/@react-native-community/cli-server-api/-/cli-server-api-20.1.3.tgz", "integrity": "sha512-hsNsdUKZDd2T99OuNuiXz4VuvLa1UN0zcxefmPjXQgI0byrBLzzDr+o7p03sKuODSzKi2h+BMnUxiS07HACQLA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@react-native-community/cli-tools": "20.1.3", @@ -2688,7 +2688,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", "integrity": "sha512-gfygJYZ2gLTDlmbWMI0CE2MwnFzSN/2SZfkMlItC4K/JBlsWVDB0bO6XhqcY13YXE7iMcAJnzTCJjPiTeJJ0Mw==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -2698,7 +2698,7 @@ "version": "6.4.0", "resolved": "https://registry.npmjs.org/open/-/open-6.4.0.tgz", "integrity": "sha512-IFenVPgF70fSm1keSd2iDBIDIBZkroLeuffXq+wKTzTJlBpesFWojV9lb8mzOfaAzM1sr7HQHuO0vtV0zYekGg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "is-wsl": "^1.1.0" @@ -2711,7 +2711,7 @@ "version": "6.2.3", "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.3.tgz", "integrity": "sha512-jmTjYU0j60B+vHey6TfR3Z7RD61z/hmxBS3VMSGIrroOWXQEneK1zNuotOUrGyBHQj0yrpsLHPWtigEFd13ndA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "async-limiter": "~1.0.0" @@ -2721,7 +2721,7 @@ "version": "20.1.3", "resolved": "https://registry.npmjs.org/@react-native-community/cli-tools/-/cli-tools-20.1.3.tgz", "integrity": "sha512-EAn0vPCMxtHhfWk2UwLmSUfPfLUnFgC7NjiVJVTKJyVk5qGnkPfoT8te/1IUXFTysUB0F0RIi+NgDB4usFOLeA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@vscode/sudo-prompt": "^9.0.0", @@ -2740,7 +2740,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "restore-cursor": "^3.1.0" @@ -2753,7 +2753,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "locate-path": "^6.0.0", @@ -2770,7 +2770,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "p-locate": "^5.0.0" @@ -2786,7 +2786,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "chalk": "^4.1.0", @@ -2803,7 +2803,7 @@ "version": "2.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", - "devOptional": true, + "dev": true, "license": "MIT", "bin": { "mime": "cli.js" @@ -2816,7 +2816,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -2826,7 +2826,7 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "mimic-fn": "^2.1.0" @@ -2842,7 +2842,7 @@ "version": "5.4.1", "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "bl": "^4.1.0", @@ -2866,7 +2866,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "yocto-queue": "^0.1.0" @@ -2882,7 +2882,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "p-limit": "^3.0.2" @@ -2898,7 +2898,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "onetime": "^5.1.0", @@ -2912,7 +2912,7 @@ "version": "20.1.3", "resolved": "https://registry.npmjs.org/@react-native-community/cli-types/-/cli-types-20.1.3.tgz", "integrity": "sha512-IdAcegf0pH1hVraxWTG1ACLkYC0LDQfqtaEf42ESyLIF3Xap70JzL/9tAlxw7lSCPZPFWhrcgU0TBc4SkC/ecw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "joi": "^17.2.1" @@ -2922,7 +2922,7 @@ "version": "9.5.0", "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": "^12.20.0 || >=14" @@ -2932,7 +2932,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "locate-path": "^6.0.0", @@ -2949,7 +2949,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "p-locate": "^5.0.0" @@ -2965,7 +2965,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "yocto-queue": "^0.1.0" @@ -2981,7 +2981,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "p-limit": "^3.0.2" @@ -3264,7 +3264,7 @@ "version": "4.1.5", "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", "integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==", - "devOptional": true, + "dev": true, "license": "BSD-3-Clause", "dependencies": { "@hapi/hoek": "^9.0.0" @@ -3274,14 +3274,14 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==", - "devOptional": true, + "dev": true, "license": "BSD-3-Clause" }, "node_modules/@sideway/pinpoint": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==", - "devOptional": true, + "dev": true, "license": "BSD-3-Clause" }, "node_modules/@sinclair/typebox": { @@ -3395,7 +3395,7 @@ "version": "19.1.17", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.17.tgz", "integrity": "sha512-Qec1E3mhALmaspIrhWt9jkQMNdw6bReVu64mjvhbhq2NFPftLPVr+l1SZgmw/66WwBNpDh7ao5AT6gF5v41PFA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "csstype": "^3.0.2" @@ -3464,7 +3464,7 @@ "version": "9.3.2", "resolved": "https://registry.npmjs.org/@vscode/sudo-prompt/-/sudo-prompt-9.3.2.tgz", "integrity": "sha512-gcXoCN00METUNFeQOFJ+C9xUI0DKB+0EGMVg7wbVYRHBw2Eq3fKisDZOkRdOz3kqXRKOENMfShPOmypw1/8nOw==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/@xmldom/xmldom": { @@ -3559,7 +3559,7 @@ "version": "0.2.1", "resolved": "https://registry.npmjs.org/ansi-fragments/-/ansi-fragments-0.2.1.tgz", "integrity": "sha512-DykbNHxuXQwUDRv5ibc2b0x7uw7wmwOGLBUd5RmaQ5z8Lhx19vwvKV+FAsM5rEA6dEcHxX+/Ad5s9eF2k2bB+w==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "colorette": "^1.0.7", @@ -3571,7 +3571,7 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -3581,7 +3581,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^4.1.0" @@ -3649,7 +3649,7 @@ "version": "1.2.7", "resolved": "https://registry.npmjs.org/appdirsjs/-/appdirsjs-1.2.7.tgz", "integrity": "sha512-Quji6+8kLBC3NnBeo14nPDq0+2jUs5s3/xEye+udFHumHhRk4M7aAMXp/PBJqkKYGuuyR9M/6Dq7d2AViiGmhw==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/arg": { @@ -3690,7 +3690,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -3993,7 +3993,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "buffer": "^5.5.0", @@ -4011,7 +4011,7 @@ "version": "2.2.2", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "bytes": "^3.1.2", @@ -4036,7 +4036,7 @@ "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "ee-first": "1.1.1" @@ -4219,7 +4219,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -4389,14 +4389,14 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz", "integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/command-exists": { "version": "1.2.9", "resolved": "https://registry.npmjs.org/command-exists/-/command-exists-1.2.9.tgz", "integrity": "sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/commander": { @@ -4502,7 +4502,7 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -4531,7 +4531,7 @@ "version": "9.0.1", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.1.tgz", "integrity": "sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "env-paths": "^2.2.1", @@ -4558,14 +4558,14 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "devOptional": true, + "dev": true, "license": "Python-2.0" }, "node_modules/cosmiconfig/node_modules/js-yaml": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -4592,14 +4592,14 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/dayjs": { "version": "1.11.20", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.20.tgz", "integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/debug": { @@ -4623,7 +4623,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -4811,7 +4811,7 @@ "version": "2.2.1", "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -4821,7 +4821,7 @@ "version": "7.21.0", "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.21.0.tgz", "integrity": "sha512-Lw7I8Zp5YKHFCXL7+Dz95g4CcbMEpgvqZNNq3AmlT5XAV6CgAAk6gyAMqn2zjw08K9BHfcNuKrMiCPLByGafow==", - "devOptional": true, + "dev": true, "license": "MIT", "bin": { "envinfo": "dist/cli.js" @@ -4834,7 +4834,7 @@ "version": "1.3.4", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "is-arrayish": "^0.2.1" @@ -4853,7 +4853,7 @@ "version": "1.5.2", "resolved": "https://registry.npmjs.org/errorhandler/-/errorhandler-1.5.2.tgz", "integrity": "sha512-kNAL7hESndBCrWwS72QyV3IVOTrVmj9D062FV5BQswNL5zEdeRmz/WJFyh6Aj/plvvSOrzddkxW57HgkZcR9Fw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "accepts": "~1.3.8", @@ -4959,7 +4959,7 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "cross-spawn": "^7.0.3", @@ -4983,7 +4983,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -4993,7 +4993,7 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "mimic-fn": "^2.1.0" @@ -5550,7 +5550,7 @@ "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@nodelib/fs.stat": "^2.0.2", @@ -5573,7 +5573,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.4.tgz", "integrity": "sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg==", - "devOptional": true, + "dev": true, "funding": [ { "type": "github", @@ -5589,7 +5589,7 @@ "version": "5.5.9", "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.5.9.tgz", "integrity": "sha512-jldvxr1MC6rtiZKgrFnDSvT8xuH+eJqxqOBThUVjYrxssYTo1avZLGql5l0a0BAERR01CadYzZ83kVEkbyDg+g==", - "devOptional": true, + "dev": true, "funding": [ { "type": "github", @@ -5610,7 +5610,7 @@ "version": "1.20.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", - "devOptional": true, + "dev": true, "license": "ISC", "dependencies": { "reusify": "^1.0.4" @@ -5749,7 +5749,7 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "graceful-fs": "^4.2.0", @@ -5866,7 +5866,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -5905,7 +5905,7 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "devOptional": true, + "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.1" @@ -6122,7 +6122,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "engines": { "node": ">=10.17.0" @@ -6132,7 +6132,7 @@ "version": "0.7.2", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" @@ -6199,7 +6199,7 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "parent-module": "^1.0.0", @@ -6216,7 +6216,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -6283,7 +6283,7 @@ "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/is-callable": { @@ -6332,7 +6332,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -6370,7 +6370,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" @@ -6383,7 +6383,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -6445,7 +6445,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -6473,7 +6473,7 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -6735,7 +6735,7 @@ "version": "17.13.3", "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz", "integrity": "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==", - "devOptional": true, + "dev": true, "license": "BSD-3-Clause", "dependencies": { "@hapi/hoek": "^9.3.0", @@ -6786,7 +6786,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/json5": { @@ -6805,7 +6805,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", - "devOptional": true, + "dev": true, "license": "MIT", "optionalDependencies": { "graceful-fs": "^4.1.6" @@ -6833,7 +6833,7 @@ "version": "2.13.2", "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.13.2.tgz", "integrity": "sha512-4VVDnbOpLXy/s8rdRCSXb+zfMeFR0WlJWpET1iA9CQdlZDfwyLjUuGQzXU4VeOoey6AicSAluWan7Etga6Kcmg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "picocolors": "^1.1.1", @@ -7246,7 +7246,7 @@ "version": "0.7.1", "resolved": "https://registry.npmjs.org/logkitty/-/logkitty-0.7.1.tgz", "integrity": "sha512-/3ER20CTTbahrCrpYfPn7Xavv9diBROZpoXGVZDWMw4b/X4uuUwAC0ki85tgsdMRONURyIJbcOvS94QsUBYPbQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "ansi-fragments": "^0.2.1", @@ -7261,7 +7261,7 @@ "version": "5.3.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -7271,7 +7271,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", - "devOptional": true, + "dev": true, "license": "ISC", "dependencies": { "string-width": "^4.2.0", @@ -7283,7 +7283,7 @@ "version": "6.2.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -7298,14 +7298,14 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", - "devOptional": true, + "dev": true, "license": "ISC" }, "node_modules/logkitty/node_modules/yargs": { "version": "15.4.1", "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "cliui": "^6.0.0", @@ -7328,7 +7328,7 @@ "version": "18.1.3", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", - "devOptional": true, + "dev": true, "license": "ISC", "dependencies": { "camelcase": "^5.0.0", @@ -7387,7 +7387,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -7421,7 +7421,7 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">= 8" @@ -7884,7 +7884,7 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/nocache/-/nocache-3.0.4.tgz", "integrity": "sha512-WDD0bdg9mbq6F4mRxEYcPWwfA1vxd0mrvKOyxI7Xj/atfRHVeutzuWByG//jfm4uPzp0y4Kj051EORCBSQMycw==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=12.0.0" @@ -7943,7 +7943,7 @@ "version": "1.15.0", "resolved": "https://registry.npmjs.org/node-stream-zip/-/node-stream-zip-1.15.0.tgz", "integrity": "sha512-LN4fydt9TqhZhThkZIVQnF9cwjU3qmUH9h78Mx/K7d3VvfRqqwthLwJEUOEL0QPZ0XQmNN7be5Ggit5+4dq3Bw==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=0.12.0" @@ -7981,7 +7981,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.0.0" @@ -8021,7 +8021,7 @@ "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -8291,7 +8291,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "callsites": "^3.0.0" @@ -8304,7 +8304,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.0.0", @@ -8353,7 +8353,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.2.0.tgz", "integrity": "sha512-DwmPWeFn+tq7TiyJ2CxezCAirXjFxvaiD03npak3cRjlP9+OjTmSy1EpIrEbh+l6JgUundniloMLDQ/6VTdhLQ==", - "devOptional": true, + "dev": true, "funding": [ { "type": "github", @@ -8631,7 +8631,7 @@ "version": "6.15.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", - "devOptional": true, + "dev": true, "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" @@ -8656,7 +8656,7 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "devOptional": true, + "dev": true, "funding": [ { "type": "github", @@ -8686,7 +8686,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "bytes": "~3.1.2", @@ -8959,7 +8959,7 @@ "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "inherits": "^2.0.3", @@ -9066,7 +9066,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", - "devOptional": true, + "dev": true, "license": "ISC" }, "node_modules/requireg": { @@ -9152,7 +9152,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "iojs": ">=1.0.0", @@ -9222,7 +9222,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "devOptional": true, + "dev": true, "funding": [ { "type": "github", @@ -9283,7 +9283,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/sax": { @@ -9419,7 +9419,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", - "devOptional": true, + "dev": true, "license": "ISC" }, "node_modules/set-function-length": { @@ -9482,7 +9482,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -9502,7 +9502,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -9519,7 +9519,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -9538,7 +9538,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -9602,7 +9602,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^3.2.0", @@ -9617,7 +9617,7 @@ "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "color-convert": "^1.9.0" @@ -9630,7 +9630,7 @@ "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "color-name": "1.1.3" @@ -9640,14 +9640,14 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/slice-ansi/node_modules/is-fullwidth-code-point": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -9766,14 +9766,14 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/strict-url-sanitise/-/strict-url-sanitise-0.0.1.tgz", "integrity": "sha512-nuFtF539K8jZg3FjaWH/L8eocCR6gegz5RDOsaWxfdbF5Jqr2VXWxZayjTwUzsWJDC91k2EbnJXp6FuWW+Z4hg==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "safe-buffer": "~5.2.0" @@ -9809,7 +9809,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -9828,7 +9828,7 @@ "version": "2.2.2", "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.2.tgz", "integrity": "sha512-DnR90I+jtXNSTXWdwrEy9FakW7UX+qUZg28gj5fk2vxxl7uS/3bpI4fjFYVmdK9etptYBPNkpahuQnEwhwECqA==", - "devOptional": true, + "dev": true, "funding": [ { "type": "github", @@ -10167,7 +10167,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "content-type": "^1.0.5", @@ -10182,7 +10182,7 @@ "version": "1.54.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -10192,7 +10192,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "mime-db": "^1.54.0" @@ -10278,7 +10278,7 @@ "version": "0.1.2", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">= 4.0.0" @@ -10340,7 +10340,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/utils-merge": { @@ -10473,7 +10473,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", - "devOptional": true, + "dev": true, "license": "ISC" }, "node_modules/which-typed-array": { diff --git a/package.json b/package.json index 3f8b1c4..7a6b66c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { - "name": "flightworkapp", - "version": "1.1.0", + "name": "aerostaff-pro", + "version": "2.5.0", "main": "index.ts", "scripts": { "start": "expo start", diff --git a/src/components/AeroStaffLogo.tsx b/src/components/AeroStaffLogo.tsx index 86a5f2a..f7beeba 100644 --- a/src/components/AeroStaffLogo.tsx +++ b/src/components/AeroStaffLogo.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { View, Text, StyleSheet, Platform } from 'react-native'; +import { View, Text, StyleSheet, Platform, Image } from 'react-native'; import { LinearGradient } from 'expo-linear-gradient'; type Size = 'small' | 'large'; @@ -12,9 +12,15 @@ interface Props { } export default function AeroStaffLogo({ variant = 'large', monochrome = false }: Props) { + const iconSize = variant === 'small' ? 36 : 44; return ( - + {variant === 'large' && ( AERO diff --git a/src/components/ShiftTimeline.tsx b/src/components/ShiftTimeline.tsx index 9c7f60b..da7be7d 100644 --- a/src/components/ShiftTimeline.tsx +++ b/src/components/ShiftTimeline.tsx @@ -3,6 +3,7 @@ import { View, Text, StyleSheet, Modal, ScrollView, TouchableOpacity, ActivityIndicator, Dimensions, LayoutAnimation, Platform, UIManager, } from 'react-native'; +import AsyncStorage from '@react-native-async-storage/async-storage'; import { MaterialIcons } from '@expo/vector-icons'; import { useAppTheme, type ThemeColors } from '../context/ThemeContext'; import { useAirport } from '../context/AirportContext'; @@ -23,6 +24,7 @@ type Props = { shiftStart: Date; shiftEnd: Date; inline?: boolean; + refreshKey?: number; }; type Flight = { @@ -54,7 +56,7 @@ function parseFlight(item: any): Flight | null { }; } -export default function ShiftTimeline({ visible, onClose, shiftStart, shiftEnd, inline }: Props) { +export default function ShiftTimeline({ visible, onClose, shiftStart, shiftEnd, inline, refreshKey }: Props) { const { colors } = useAppTheme(); const { t } = useLanguage(); const { airportCode, isLoading: airportLoading } = useAirport(); @@ -74,10 +76,18 @@ export default function ShiftTimeline({ visible, onClose, shiftStart, shiftEnd, setLoading(true); setError(false); try { - const { departures } = await fetchAirportScheduleRaw(airportCode); + const [{ departures }, filterRaw] = await Promise.all([ + fetchAirportScheduleRaw(airportCode), + AsyncStorage.getItem('aerostaff_flight_filter_v1'), + ]); + const selectedAirlines: string[] = filterRaw ? JSON.parse(filterRaw) : []; const filtered = departures .map(parseFlight) - .filter((f): f is Flight => f !== null && f.departureTs >= startSec && f.departureTs <= endSec) + .filter((f): f is Flight => { + if (!f || f.departureTs < startSec || f.departureTs > endSec) return false; + if (selectedAirlines.length > 0 && !selectedAirlines.some(k => f.airlineName.toLowerCase().includes(k))) return false; + return true; + }) .sort((a, b) => a.departureTs - b.departureTs); setFlights(filtered); } catch { @@ -85,7 +95,7 @@ export default function ShiftTimeline({ visible, onClose, shiftStart, shiftEnd, } finally { setLoading(false); } - }, [airportCode, airportLoading, startSec, endSec]); + }, [airportCode, airportLoading, startSec, endSec, refreshKey]); // Inline: carica subito; Modal: carica quando visibile useEffect(() => { diff --git a/src/components/UpdateModal.tsx b/src/components/UpdateModal.tsx new file mode 100644 index 0000000..b4e2392 --- /dev/null +++ b/src/components/UpdateModal.tsx @@ -0,0 +1,157 @@ +import React from 'react'; +import { + Modal, View, Text, TouchableOpacity, ScrollView, StyleSheet, Linking, +} from 'react-native'; +import { MaterialIcons } from '@expo/vector-icons'; +import { useAppTheme } from '../context/ThemeContext'; +import { type UpdateInfo, APP_VERSION } from '../utils/updateChecker'; + +interface Props { + info: UpdateInfo; + onDismiss: () => void; +} + +function renderNotes(raw: string): string { + return raw + .replace(/^#{1,3} ?/gm, '') // strip markdown headers + .replace(/\*\*(.+?)\*\*/g, '$1') // strip bold + .replace(/`(.+?)`/g, '$1') // strip inline code + .replace(/πŸ“¦ AeroStaff Pro [^\n]+\n?/, '') // strip redundant title line + .replace(/\n{3,}/g, '\n\n') + .trim(); +} + +export default function UpdateModal({ info, onDismiss }: Props) { + const { colors } = useAppTheme(); + + const notes = renderNotes(info.releaseNotes); + + return ( + + + + {/* Header */} + + + + Aggiornamento disponibile + + v{APP_VERSION} β†’ {info.latestVersion} + + + + + {/* Release notes */} + {notes.length > 0 && ( + + {notes} + + )} + + {/* Buttons */} + + + PiΓΉ tardi + + + { Linking.openURL(info.releaseUrl); onDismiss(); }} + activeOpacity={0.8} + > + + GitHub + + + { Linking.openURL(info.downloadUrl); onDismiss(); }} + activeOpacity={0.8} + > + + Scarica APK + + + + + + ); +} + +const styles = StyleSheet.create({ + overlay: { + flex: 1, + backgroundColor: 'rgba(0,0,0,0.6)', + justifyContent: 'center', + alignItems: 'center', + padding: 24, + }, + sheet: { + width: '100%', + maxWidth: 420, + borderRadius: 20, + overflow: 'hidden', + maxHeight: '80%', + }, + header: { + flexDirection: 'row', + alignItems: 'center', + padding: 20, + }, + headerTitle: { + color: '#fff', + fontSize: 16, + fontWeight: '800', + }, + headerSub: { + color: 'rgba(255,255,255,0.8)', + fontSize: 12, + marginTop: 2, + }, + notesScroll: { + maxHeight: 320, + }, + notesContent: { + padding: 20, + }, + notesText: { + fontSize: 13, + lineHeight: 20, + }, + footer: { + flexDirection: 'row', + alignItems: 'center', + gap: 8, + padding: 16, + borderTopWidth: 1, + }, + btnLater: { + flex: 1, + paddingVertical: 10, + alignItems: 'center', + }, + btnLaterText: { + fontSize: 14, + fontWeight: '500', + }, + btn: { + flexDirection: 'row', + alignItems: 'center', + gap: 6, + paddingHorizontal: 14, + paddingVertical: 10, + borderRadius: 10, + borderWidth: 1.5, + }, + btnPrimary: { + borderWidth: 0, + }, + btnText: { + fontSize: 13, + fontWeight: '700', + }, +}); diff --git a/src/context/ThemeContext.tsx b/src/context/ThemeContext.tsx index 6d99c9e..4e316ef 100644 --- a/src/context/ThemeContext.tsx +++ b/src/context/ThemeContext.tsx @@ -42,18 +42,18 @@ export type ThemeColors = { // ─── Tema Chiaro ────────────────────────────────────────────────────────────── const LIGHT: ThemeColors = { bg: '#F2F2F7', - card: 'rgba(255,255,255,0.72)', - cardSecondary: 'rgba(242,242,247,0.80)', + card: '#FFFFFF', + cardSecondary: '#F2F2F7', text: '#1C1C1E', textSub: '#48484A', textMuted: 'rgba(60,60,67,0.45)', primary: '#F47B16', primaryDark: '#C2520A', primaryLight: '#FFEDD5', - glass: 'rgba(255,255,255,0.58)', - glassBorder: 'rgba(255,255,255,0.88)', - glassStrong: 'rgba(255,255,255,0.84)', - border: 'rgba(60,60,67,0.18)', + glass: '#FFFFFF', + glassBorder: 'transparent', + glassStrong: '#FFFFFF', + border: 'rgba(60,60,67,0.12)', appBar: 'rgba(242,242,247,0.85)', tabBar: 'rgba(255,255,255,0.90)', tabIconActive: '#F47B16', @@ -67,20 +67,20 @@ const LIGHT: ThemeColors = { // ─── Tema Scuro ─────────────────────────────────────────────────────────────── const DARK: ThemeColors = { bg: '#0A0A0C', - card: 'rgba(255,255,255,0.07)', - cardSecondary: 'rgba(255,255,255,0.04)', + card: '#1C1C1E', + cardSecondary: '#2C2C2E', text: '#FFFFFF', textSub: 'rgba(235,235,245,0.75)', textMuted: 'rgba(235,235,245,0.38)', primary: '#FF9A42', primaryDark: '#F47B16', primaryLight: 'rgba(255,154,66,0.20)', - glass: 'rgba(255,255,255,0.06)', - glassBorder: 'rgba(255,255,255,0.13)', - glassStrong: 'rgba(28,28,30,0.84)', + glass: '#1C1C1E', + glassBorder: 'transparent', + glassStrong: '#2C2C2E', border: 'rgba(255,255,255,0.11)', - appBar: 'rgba(10,10,12,0.82)', - tabBar: 'rgba(10,10,12,0.90)', + appBar: '#0A0A0C', + tabBar: '#111113', tabIconActive: '#FF9A42', tabIconInactive:'rgba(235,235,245,0.35)', tabLabelActive: '#FF9A42', diff --git a/src/i18n/translations.ts b/src/i18n/translations.ts index 8ccd1e2..777bce5 100644 --- a/src/i18n/translations.ts +++ b/src/i18n/translations.ts @@ -151,11 +151,13 @@ const it = { // ShiftTimeline shiftUnknown: 'Sconosciuta', // FlightScreen filter menu - flightFilterTitle: 'Mostra voli', + flightFilterTitle: 'Filtra compagnie', flightFilterMine: 'Solo mie compagnie', flightFilterMineSub: 'Wizz, EasyJet e altri operatori configurati', flightFilterAll: 'Tutti i voli', flightFilterAllSub: 'Mostra tutti gli operatori presenti', + flightFilterSelAll: 'Seleziona tutto', + flightFilterDeselAll: 'Deseleziona tutto', }; const en: typeof it = { @@ -309,11 +311,13 @@ const en: typeof it = { // ShiftTimeline shiftUnknown: 'Unknown', // FlightScreen filter menu - flightFilterTitle: 'Show flights', + flightFilterTitle: 'Filter airlines', flightFilterMine: 'My airlines only', flightFilterMineSub: 'Wizz, EasyJet and other configured operators', flightFilterAll: 'All flights', flightFilterAllSub: 'Show all operators at this airport', + flightFilterSelAll: 'Select all', + flightFilterDeselAll: 'Deselect all', }; export const translations: Record = { it, en }; diff --git a/src/screens/FlightScreen.tsx b/src/screens/FlightScreen.tsx index 7b37ca6..e200b90 100644 --- a/src/screens/FlightScreen.tsx +++ b/src/screens/FlightScreen.tsx @@ -1,6 +1,6 @@ import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react'; import { - View, Text, StyleSheet, ActivityIndicator, Modal, + View, Text, StyleSheet, ActivityIndicator, Modal, ScrollView, FlatList, TouchableOpacity, RefreshControl, Image, Alert, Animated, PanResponder, NativeModules, Platform, } from 'react-native'; @@ -10,15 +10,16 @@ import AsyncStorage from '@react-native-async-storage/async-storage'; import { MaterialIcons } from '@expo/vector-icons'; import { useAppTheme, type ThemeColors } from '../context/ThemeContext'; import { useAirport } from '../context/AirportContext'; -import { getAirlineOps, getAirlineColor } from '../utils/airlineOps'; +import { getAirlineOps, getAirlineColor, AIRLINE_COLORS, AIRLINE_DISPLAY_NAMES } from '../utils/airlineOps'; import { fetchAirportScheduleRaw } from '../utils/fr24api'; import { fetchStaffMonitorData, normalizeFlightNumber, type StaffMonitorFlight } from '../utils/staffMonitor'; -import { formatAirportHeader } from '../utils/airportSettings'; +import { formatAirportHeader, getAirportAirlines } from '../utils/airportSettings'; import { requestWidgetUpdate } from 'react-native-android-widget'; import { WIDGET_CACHE_KEY, WIDGET_SHIFT_KEY } from '../widgets/widgetTaskHandler'; import type { WidgetData, WidgetFlight, WidgetShiftData } from '../widgets/widgetTaskHandler'; import { ShiftWidget } from '../widgets/ShiftWidget'; import { useLanguage } from '../context/LanguageContext'; +import type { TranslationKey } from '../i18n/translations'; const WearDataSender = Platform.OS === 'android' ? NativeModules.WearDataSender : null; @@ -27,6 +28,23 @@ const NOTIF_ENABLED_KEY = 'aerostaff_notif_enabled'; const PINNED_FLIGHT_KEY = 'pinned_flight_v1'; const PINNED_NOTIF_IDS_KEY = 'pinned_notif_ids_v1'; const FLIGHT_FILTER_KEY = 'aerostaff_flight_filter_v1'; +const FLIGHTS_CACHE_KEY = 'aerostaff_flights_cache_v2'; + +function flightKey(item: any, tsField: string): string { + // Use flight number + scheduled time as a stable key. + // Avoid using identification.id: FR24 sometimes omits it, which would cause + // the same flight to be stored under two different keys (one per fetch). + const fn = item.flight?.identification?.number?.default ?? ''; + const ts = item.flight?.time?.scheduled?.[tsField] ?? ''; + return `${fn}_${ts}`; +} + +function mergeFlights(cached: any[], fresh: any[], tsField: string): any[] { + const map = new Map(); + for (const item of cached) map.set(flightKey(item, tsField), item); + for (const item of fresh) map.set(flightKey(item, tsField), item); + return Array.from(map.values()); +} // Handler: mostra notifiche anche con app aperta (wrapped for Expo Go compat) try { Notifications.setNotificationHandler({ @@ -104,6 +122,197 @@ function SwipeableFlightCardComponent({ // Performance optimization: memoize flatlist item to prevent unnecessary re-renders const SwipeableFlightCard = React.memo(SwipeableFlightCardComponent); +// ─── FlightRow ──────────────────────────────────────────────────────────────── +interface FlightRowProps { + item: any; + activeTab: 'arrivals' | 'departures'; + userShift: { start: number; end: number } | null; + pinnedFlightId: string | null; + onPin: (item: any) => void; + onUnpin: () => void; + inboundArrivals: Record; + colors: ThemeColors; + s: ReturnType; + smPool: StaffMonitorFlight[]; + locale: string; + t: (key: TranslationKey) => string; +} + +function FlightRowComponent({ item, activeTab, userShift, pinnedFlightId, onPin, onUnpin, inboundArrivals, colors, s, smPool, locale, t }: FlightRowProps) { + const flightNumber = item.flight?.identification?.number?.default || 'N/A'; + const airline = item.flight?.airline?.name || 'Sconosciuta'; + const iataCode = item.flight?.airline?.code?.iata || ''; + const statusText = item.flight?.status?.text || 'Scheduled'; + const raw = item.flight?.status?.generic?.status?.color || 'gray'; + const statusColor = raw === 'green' ? '#10b981' : raw === 'red' ? '#ef4444' : raw === 'yellow' ? '#f59e0b' : '#6b7280'; + const originDest = activeTab === 'arrivals' + ? (item.flight?.airport?.origin?.name || item.flight?.airport?.origin?.code?.iata || 'N/A') + : (item.flight?.airport?.destination?.name || item.flight?.airport?.destination?.code?.iata || 'N/A'); + const ts = activeTab === 'arrivals' ? item.flight?.time?.scheduled?.arrival : item.flight?.time?.scheduled?.departure; + const time = ts ? new Date(ts * 1000).toLocaleTimeString(locale, { hour: '2-digit', minute: '2-digit' }) : 'N/A'; + const duringShift = userShift && ts && (() => { + if (activeTab === 'arrivals') return ts >= userShift.start && ts <= userShift.end; + const opsData = getAirlineOps(airline); + const ciOpen = ts - opsData.checkInOpen * 60; + const ciClose = ts - opsData.checkInClose * 60; + const gOpen = ts - opsData.gateOpen * 60; + const gClose = ts - opsData.gateClose * 60; + const ciOverlap = ciOpen <= userShift.end && ciClose >= userShift.start; + const gateOverlap = gOpen <= userShift.end && gClose >= userShift.start; + return ciOverlap || gateOverlap; + })(); + const color = getAirlineColor(airline); + const ops = activeTab === 'departures' && ts ? getAirlineOps(airline) : null; + const fmt = (offsetMin: number) => + ts ? new Date((ts - offsetMin * 60) * 1000).toLocaleTimeString(locale, { hour: '2-digit', minute: '2-digit' }) : ''; + const fmtTs = (t: number) => + new Date(t * 1000).toLocaleTimeString(locale, { hour: '2-digit', minute: '2-digit' }); + + const reg = item.flight?.aircraft?.registration; + const inboundTs = reg ? inboundArrivals[reg] : undefined; + const gateOpenFromInbound = activeTab === 'departures' && ts && inboundTs ? inboundTs : undefined; + + const flightId = item.flight?.identification?.number?.default || null; + const isPinned = flightId !== null && flightId === pinnedFlightId; + + const normFn = normalizeFlightNumber(flightNumber); + const normalizeForMatching = (s: string) => s.replace(/[\s\-_]/g, '').toUpperCase(); + const normFnStripped = normalizeForMatching(normFn); + const smFlight = + smPool.find(sm => sm.flightNumber === normFn) ?? + smPool.find(sm => normalizeForMatching(sm.flightNumber) === normFnStripped); + + return ( + isPinned ? onUnpin() : onPin(item)} + > + + {isPinned && {t('flightPinned')}} + {/* Header */} + + + + + {flightNumber} + {airline} + + + + {time} + {originDest} + + + {/* Body */} + + {activeTab === 'departures' && ops ? ( + + + + + {t('flightCheckin')} + {fmt(ops.checkInOpen)} – {fmt(ops.checkInClose)} + + + + + + {t('flightGate')} + + {gateOpenFromInbound ? fmtTs(gateOpenFromInbound) : fmt(ops.gateOpen)} – {fmt(ops.gateClose)} + + + + + ) : activeTab === 'arrivals' && ts ? (() => { + const realDep = item.flight?.time?.real?.departure; + const realArr = item.flight?.time?.real?.arrival; + const estArr = item.flight?.time?.estimated?.arrival; + const bestArr = realArr || estArr || ts; + const delayMin = Math.round((bestArr - ts) / 60); + const landed = !!realArr; + const departed = !!realDep; + + const landColor = landed ? '#10B981' + : delayMin > 20 ? '#EF4444' + : delayMin > 5 ? '#F59E0B' + : colors.primary; + const landLabel = landed ? t('flightLanded') : t('flightEstimated'); + + return ( + + + + + {t('flightDeparted')} + + {departed ? fmtTs(realDep) : '--:--'} + + + + + + + {landLabel} + {fmtTs(bestArr)} + + + + ); + })() : ( + {`Da: ${originDest}`} + )} + {/* Status pill β€” own row, right-aligned */} + {activeTab === 'arrivals' && ts ? (() => { + const rArr = item.flight?.time?.real?.arrival; + const eArr = item.flight?.time?.estimated?.arrival; + const bArr = rArr || eArr || ts; + const dMin = Math.round((bArr - ts) / 60); + const isLanded = !!rArr; + const dText = isLanded ? 'Atterrato' : dMin > 0 ? `+${dMin} min` : 'In orario'; + const dColor = isLanded ? '#10B981' : dMin > 20 ? '#EF4444' : dMin > 5 ? '#F59E0B' : '#10B981'; + return ( + + {dText} + + ); + })() : ( + + {statusText} + + )} + + {/* StaffMonitor footer β€” inside card so border-radius applies */} + + + + Stand {smFlight?.stand ?? 'β€”'} + + {activeTab === 'departures' ? ( + <> + + + {t('flightCheckin')} {smFlight?.checkin ?? 'β€”'} + + + + {t('flightGate')} {smFlight?.gate ?? 'β€”'} + + + ) : ( + + + {t('flightBelt')} {smFlight?.belt ?? 'β€”'} + + )} + + + + ); +} + +const FlightRow = React.memo(FlightRowComponent); + // ─── Helpers notifiche ───────────────────────────────────────────────────────── async function cancelPreviousNotifications() { const raw = await AsyncStorage.getItem(NOTIF_IDS_KEY); @@ -253,19 +462,45 @@ export default function FlightScreen() { const [scheduledCount, setScheduledCount] = useState(0); const [pinnedFlightId, setPinnedFlightId] = useState(null); const [inboundArrivals, setInboundArrivals] = useState>({}); - const [filterMode, setFilterMode] = useState<'mine' | 'all'>('mine'); const [filterMenuVisible, setFilterMenuVisible] = useState(false); const [allArrivalsFull, setAllArrivalsFull] = useState([]); const [allDeparturesFull, setAllDeparturesFull] = useState([]); + const [airportAirlines, setAirportAirlines] = useState([]); + const [selectedAirlines, setSelectedAirlines] = useState([]); const [staffMonitorDeps, setStaffMonitorDeps] = useState([]); const [staffMonitorArrs, setStaffMonitorArrs] = useState([]); - // Carica preferenza notifiche salvata useEffect(() => { AsyncStorage.getItem(NOTIF_ENABLED_KEY).then(v => setNotifsEnabled(v === 'true')); - AsyncStorage.getItem(FLIGHT_FILTER_KEY).then(v => { if (v === 'all' || v === 'mine') setFilterMode(v); }); + // Carica voli accumulati oggi cosΓ¬ sono visibili prima del primo fetch + const today = new Date().toISOString().split('T')[0]; + AsyncStorage.getItem(FLIGHTS_CACHE_KEY).then(raw => { + if (!raw) return; + try { + const cache = JSON.parse(raw); + if (cache.date === today) { + setAllArrivalsFull(cache.arrivals ?? []); + setAllDeparturesFull(cache.departures ?? []); + } + } catch {} + }); }, []); + // Carica lista compagnie per aeroporto + selezione salvata + useEffect(() => { + const airlines = getAirportAirlines(airportCode); + setAirportAirlines(airlines); + AsyncStorage.getItem(FLIGHT_FILTER_KEY).then(raw => { + try { + const saved: string[] = JSON.parse(raw ?? '[]'); + const valid = saved.filter(k => airlines.includes(k)); + setSelectedAirlines(valid.length > 0 ? valid : [...airlines]); + } catch { + setSelectedAirlines([...airlines]); + } + }); + }, [airportCode]); + const fetchAll = useCallback(async () => { if (airportLoading) return; @@ -276,8 +511,22 @@ export default function FlightScreen() { departures: fetchedDepartures, arrivals: fetchedArrivals, } = await fetchAirportScheduleRaw(airportCode); - setAllArrivalsFull(allArrivals); - setAllDeparturesFull(allDepartures); + // Accumula voli: fonde i dati freschi con quelli giΓ  visti oggi + // cosΓ¬ i voli rimossi da FR24 dopo la partenza restano visibili fino a mezzanotte + const today = new Date().toISOString().split('T')[0]; + let cachedArrs: any[] = [], cachedDeps: any[] = []; + try { + const raw = await AsyncStorage.getItem(FLIGHTS_CACHE_KEY); + if (raw) { + const cache = JSON.parse(raw); + if (cache.date === today) { cachedArrs = cache.arrivals ?? []; cachedDeps = cache.departures ?? []; } + } + } catch {} + const mergedArrs = mergeFlights(cachedArrs, allArrivals, 'arrival'); + const mergedDeps = mergeFlights(cachedDeps, allDepartures, 'departure'); + setAllArrivalsFull(mergedArrs); + setAllDeparturesFull(mergedDeps); + AsyncStorage.setItem(FLIGHTS_CACHE_KEY, JSON.stringify({ date: today, arrivals: mergedArrs, departures: mergedDeps })).catch(() => {}); // Build inbound arrival map: registration β†’ best known arrival timestamp const inboundMap: Record = {}; @@ -338,8 +587,10 @@ export default function FlightScreen() { const s = new Date(e.startDate).getTime() / 1000; const en = new Date(e.endDate).getTime() / 1000; const evtDay = new Date(e.startDate); - if (evtDay >= todayStart && evtDay <= todayEnd) shiftToday = { start: s, end: en }; - else if (evtDay >= tomorrowStart && evtDay <= tomorrowEnd) shiftTomorrow = { start: s, end: en }; + if (evtDay >= todayStart && evtDay <= todayEnd) { + shiftToday = { start: s, end: en }; + isRestDay = false; // Lavoro event overrides any stale Riposo marker for the same day + } else if (evtDay >= tomorrowStart && evtDay <= tomorrowEnd) shiftTomorrow = { start: s, end: en }; } } } @@ -371,11 +622,14 @@ export default function FlightScreen() { if (pinnedRawW) { try { pinnedFn = JSON.parse(pinnedRawW).flight?.identification?.number?.default || null; } catch {} } + const wFilterRaw = await AsyncStorage.getItem(FLIGHT_FILTER_KEY); + const wAllowedAirlines: string[] = wFilterRaw ? JSON.parse(wFilterRaw) : []; const wFlights: WidgetFlight[] = fetchedDepartures .filter(item => { const ts = item.flight?.time?.scheduled?.departure; if (ts == null) return false; const airline = item.flight?.airline?.name || ''; + if (wAllowedAirlines.length > 0 && !wAllowedAirlines.some(k => airline.toLowerCase().includes(k))) return false; const ops = getAirlineOps(airline); const ciO = ts - ops.checkInOpen * 60, ciC = ts - ops.checkInClose * 60; const gO = ts - ops.gateOpen * 60, gC = ts - ops.gateClose * 60; @@ -386,6 +640,11 @@ export default function FlightScreen() { const airline = item.flight?.airline?.name || 'Sconosciuta'; const ops = getAirlineOps(airline); const fn = item.flight?.identification?.number?.default || 'N/A'; + const normFn = normalizeFlightNumber(fn); + const strip = (s: string) => s.replace(/[\s\-_]/g, '').toUpperCase(); + const smDeps = staffMonitorDepsRef.current; + const sm = smDeps.find(x => x.flightNumber === normFn) + ?? smDeps.find(x => strip(x.flightNumber) === strip(normFn)); return { flightNumber: fn, destinationIata: item.flight?.airport?.destination?.code?.iata || '???', @@ -395,6 +654,9 @@ export default function FlightScreen() { gateOpen: fmtOff(ts, ops.gateOpen), gateClose: fmtOff(ts, ops.gateClose), airlineColor: getAirlineColor(airline), isPinned: fn === pinnedFn, + stand: sm?.stand, + checkin: sm?.checkin, + gate: sm?.gate, }; }) .sort((a, b) => a.departureTs - b.departureTs); @@ -431,6 +693,13 @@ export default function FlightScreen() { fetchAll(); }, [airportLoading, fetchAll]); + // Auto-refresh flight data every 2 minutes so status/times stay current + useEffect(() => { + if (airportLoading) return; + const iv = setInterval(() => { fetchAll(); }, 2 * 60 * 1000); + return () => clearInterval(iv); + }, [airportLoading, fetchAll]); + useEffect(() => { AsyncStorage.getItem(PINNED_FLIGHT_KEY).then(raw => { if (!raw) return; @@ -442,6 +711,9 @@ export default function FlightScreen() { }); }, []); + const staffMonitorDepsRef = useRef([]); + const staffMonitorArrsRef = useRef([]); + // staffMonitor: poll stand / gate / belt every 60 s useEffect(() => { const load = async () => { @@ -450,6 +722,8 @@ export default function FlightScreen() { fetchStaffMonitorData('D'), fetchStaffMonitorData('A'), ]); + staffMonitorDepsRef.current = deps; + staffMonitorArrsRef.current = arrs; setStaffMonitorDeps(deps); setStaffMonitorArrs(arrs); } catch {} @@ -542,210 +816,41 @@ export default function FlightScreen() { const isSameDay = (d1: Date, d2: Date) => d1.getFullYear() === d2.getFullYear() && d1.getMonth() === d2.getMonth() && d1.getDate() === d2.getDate(); + const allSelected = airportAirlines.length > 0 && airportAirlines.every(k => selectedAirlines.includes(k)); + const currentData = (() => { - const source = filterMode === 'all' - ? (activeTab === 'arrivals' ? allArrivalsFull : allDeparturesFull) - : (activeTab === 'arrivals' ? arrivals : departures); + const source = activeTab === 'arrivals' ? allArrivalsFull : allDeparturesFull; + const seen = new Set(); return source.filter(item => { const ts = activeTab === 'arrivals' ? item.flight?.time?.scheduled?.arrival : item.flight?.time?.scheduled?.departure; - return ts && isSameDay(new Date(ts * 1000), selectedDate); + if (!ts || !isSameDay(new Date(ts * 1000), selectedDate)) return false; + const dedupeKey = `${item.flight?.identification?.number?.default ?? ''}_${ts}`; + if (seen.has(dedupeKey)) return false; + seen.add(dedupeKey); + if (selectedAirlines.length === 0) return true; + const name = (item.flight?.airline?.name || '').toLowerCase(); + return selectedAirlines.some(key => name.includes(key)); }); })(); - const renderFlight = useCallback(({ item }: { item: any }) => { - const flightNumber = item.flight?.identification?.number?.default || 'N/A'; - const airline = item.flight?.airline?.name || 'Sconosciuta'; - const iataCode = item.flight?.airline?.code?.iata || ''; - const statusText = item.flight?.status?.text || 'Scheduled'; - const raw = item.flight?.status?.generic?.status?.color || 'gray'; - const statusColor = raw === 'green' ? '#10b981' : raw === 'red' ? '#ef4444' : raw === 'yellow' ? '#f59e0b' : '#6b7280'; - const originDest = activeTab === 'arrivals' - ? (item.flight?.airport?.origin?.name || item.flight?.airport?.origin?.code?.iata || 'N/A') - : (item.flight?.airport?.destination?.name || item.flight?.airport?.destination?.code?.iata || 'N/A'); - const ts = activeTab === 'arrivals' ? item.flight?.time?.scheduled?.arrival : item.flight?.time?.scheduled?.departure; - const time = ts ? new Date(ts * 1000).toLocaleTimeString(locale, { hour: '2-digit', minute: '2-digit' }) : 'N/A'; - const duringShift = userShift && ts && (() => { - if (activeTab === 'arrivals') return ts >= userShift.start && ts <= userShift.end; - // Departures: CI or Gate window overlaps with shift (even 1 min) - const opsData = getAirlineOps(airline); - const ciOpen = ts - opsData.checkInOpen * 60; - const ciClose = ts - opsData.checkInClose * 60; - const gOpen = ts - opsData.gateOpen * 60; - const gClose = ts - opsData.gateClose * 60; - const ciOverlap = ciOpen <= userShift.end && ciClose >= userShift.start; - const gateOverlap = gOpen <= userShift.end && gClose >= userShift.start; - return ciOverlap || gateOverlap; - })(); - const color = getAirlineColor(airline); - // ops is null when ts is falsy β€” fmt is only called when ops is truthy - const ops = activeTab === 'departures' && ts ? getAirlineOps(airline) : null; - const fmt = (offsetMin: number) => - ts ? new Date((ts - offsetMin * 60) * 1000).toLocaleTimeString(locale, { hour: '2-digit', minute: '2-digit' }) : ''; - const fmtTs = (t: number) => - new Date(t * 1000).toLocaleTimeString(locale, { hour: '2-digit', minute: '2-digit' }); - - // Gate open = inbound aircraft arrival time (if available) - const reg = item.flight?.aircraft?.registration; - const inboundTs = reg ? inboundArrivals[reg] : undefined; - const gateOpenFromInbound = activeTab === 'departures' && ts && inboundTs ? inboundTs : undefined; - - const flightId = item.flight?.identification?.number?.default || null; - const isPinned = flightId !== null && flightId === pinnedFlightId; - - const normFn = normalizeFlightNumber(flightNumber); - const normalizeForMatching = (s: string) => s.replace(/[\s\-_]/g, '').toUpperCase(); - const normFnStripped = normalizeForMatching(normFn); - const smPool = activeTab === 'departures' ? staffMonitorDeps : staffMonitorArrs; - const smFlight = - smPool.find(sm => sm.flightNumber === normFn) ?? - smPool.find(sm => normalizeForMatching(sm.flightNumber) === normFnStripped); - if (__DEV__ && !smFlight && smPool.length > 0) { - console.log(`[FlightScreen] No staffMonitor match for "${normFn}" (stripped: "${normFnStripped}") in ${activeTab}`); - } - - return ( - isPinned ? unpinFlight() : pinFlight(item)} - > - - {isPinned && {t('flightPinned')}} - {/* Header */} - - - - - {flightNumber} - {airline} - - - - {time} - {originDest} - - - {/* Body */} - - {activeTab === 'departures' && ops ? ( - - - - - {t('flightCheckin')} - {fmt(ops.checkInOpen)} – {fmt(ops.checkInClose)} - - - - - - {t('flightGate')} - - {gateOpenFromInbound ? fmtTs(gateOpenFromInbound) : fmt(ops.gateOpen)} – {fmt(ops.gateClose)} - - - - - ) : activeTab === 'arrivals' && ts ? (() => { - const realDep = item.flight?.time?.real?.departure; - const realArr = item.flight?.time?.real?.arrival; - const estArr = item.flight?.time?.estimated?.arrival; - const bestArr = realArr || estArr || ts; - const delayMin = Math.round((bestArr - ts) / 60); - const landed = !!realArr; - const departed = !!realDep; - - // Color logic for landing badge - const landColor = landed ? '#10B981' - : delayMin > 20 ? '#EF4444' - : delayMin > 5 ? '#F59E0B' - : colors.primary; - const landLabel = landed ? t('flightLanded') : t('flightEstimated'); - - // Delay pill - const delayText = landed ? 'Atterrato' - : delayMin > 0 ? `+${delayMin} min` - : t('flightOnTime'); - const delayColor = landed ? '#10B981' - : delayMin > 20 ? '#EF4444' - : delayMin > 5 ? '#F59E0B' - : '#10B981'; - - return ( - - - - - {t('flightDeparted')} - - {departed ? fmtTs(realDep) : '--:--'} - - - - - - - {landLabel} - {fmtTs(bestArr)} - - - - ); - })() : ( - {`Da: ${originDest}`} - )} - {activeTab === 'arrivals' && ts ? (() => { - const rArr = item.flight?.time?.real?.arrival; - const eArr = item.flight?.time?.estimated?.arrival; - const bArr = rArr || eArr || ts; - const dMin = Math.round((bArr - ts) / 60); - const isLanded = !!rArr; - const dText = isLanded ? 'Atterrato' : dMin > 0 ? `+${dMin} min` : 'In orario'; - const dColor = isLanded ? '#10B981' : dMin > 20 ? '#EF4444' : dMin > 5 ? '#F59E0B' : '#10B981'; - return ( - - {dText} - - ); - })() : ( - - {statusText} - - )} - - - {smFlight && (smFlight.stand || smFlight.checkin || smFlight.gate || smFlight.belt) && ( - - {smFlight.stand && ( - - - Stand {smFlight.stand} - - )} - {smFlight.checkin && ( - - - {t('flightCheckin')} {smFlight.checkin} - - )} - {smFlight.gate && ( - - - {t('flightGate')} {smFlight.gate} - - )} - {smFlight.belt && ( - - - {t('flightBelt')} {smFlight.belt} - - )} - - )} - - ); - }, [activeTab, userShift, s, pinnedFlightId, pinFlight, unpinFlight, inboundArrivals, colors, staffMonitorDeps, staffMonitorArrs]); + const renderFlight = useCallback(({ item }: { item: any }) => ( + + ), [activeTab, userShift, s, pinnedFlightId, pinFlight, unpinFlight, inboundArrivals, colors, staffMonitorDeps, staffMonitorArrs, locale, t]); return ( @@ -756,13 +861,13 @@ export default function FlightScreen() { {formatAirportHeader(airport.code)} setFilterMenuVisible(true)} activeOpacity={0.8} accessibilityLabel={t('flightFilterTitle')} accessibilityRole="button" > - + setFilterMenuVisible(false)} > setFilterMenuVisible(false)} > - + true}> - {t('flightFilterTitle')} - {(['mine', 'all'] as const).map(mode => ( + + {t('flightFilterTitle')} { - setFilterMode(mode); - AsyncStorage.setItem(FLIGHT_FILTER_KEY, mode); - setFilterMenuVisible(false); + const next = allSelected ? [] : [...airportAirlines]; + setSelectedAirlines(next); + AsyncStorage.setItem(FLIGHT_FILTER_KEY, JSON.stringify(next)); }} > - - - - {mode === 'mine' ? t('flightFilterMine') : t('flightFilterAll')} - - - {mode === 'mine' ? t('flightFilterMineSub') : t('flightFilterAllSub')} - - - {filterMode === mode && ( - - )} + + {allSelected ? t('flightFilterDeselAll') : t('flightFilterSelAll')} + - ))} + + + {airportAirlines.map(key => { + const checked = selectedAirlines.includes(key); + const dot = AIRLINE_COLORS[key] ?? '#2563EB'; + const label = AIRLINE_DISPLAY_NAMES[key] ?? key; + return ( + { + const next = checked + ? selectedAirlines.filter(k => k !== key) + : [...selectedAirlines, key]; + setSelectedAirlines(next); + AsyncStorage.setItem(FLIGHT_FILTER_KEY, JSON.stringify(next)); + }} + > + + {label} + + + ); + })} + @@ -894,7 +1012,7 @@ function makeStyles(c: ThemeColors) { cardPinned: { borderWidth: 2, borderColor: '#F59E0B' }, pinBanner: { backgroundColor: '#F59E0B', paddingVertical: 5, paddingHorizontal: 12 }, pinBannerText: { color: '#fff', fontWeight: 'bold', fontSize: 11, letterSpacing: 0.5 }, - statusPill: { paddingHorizontal: 8, paddingVertical: 3, borderRadius: 20, marginTop: 5 }, + statusPill: { paddingHorizontal: 10, paddingVertical: 4, borderRadius: 20, marginTop: 8, alignSelf: 'flex-end' }, statusText: { fontSize: 10, fontWeight: '700' }, cardHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingVertical: 10, paddingHorizontal: 14 }, headerLeft: { flexDirection: 'row', alignItems: 'center', gap: 10 }, @@ -902,10 +1020,10 @@ function makeStyles(c: ThemeColors) { headerAirlineName: { color: 'rgba(255,255,255,0.8)', fontSize: 10 }, headerTime: { color: '#fff', fontWeight: '900', fontSize: 18, lineHeight: 20, textAlign: 'right' }, headerDest: { color: 'rgba(255,255,255,0.8)', fontSize: 10, textAlign: 'right' }, - cardBody: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingVertical: 10, paddingHorizontal: 14, backgroundColor: c.card }, - bodyInfo: { flex: 1, fontSize: 11, color: c.textSub }, + cardBody: { flexDirection: 'column', paddingVertical: 10, paddingHorizontal: 14, backgroundColor: c.card }, + bodyInfo: { fontSize: 11, color: c.textSub }, bodyTime: { fontWeight: '700', color: c.text }, - opsRow: { flex: 1, flexDirection: 'row', gap: 8 }, + opsRow: { flexDirection: 'row', gap: 8 }, opsBadge: { flex: 1, flexDirection: 'row', alignItems: 'center', gap: 8, backgroundColor: c.primaryLight, borderRadius: 10, paddingHorizontal: 10, paddingVertical: 8 }, opsIcon: { fontSize: 16 }, opsLabel: { fontSize: 10, fontWeight: '600', color: c.textSub, letterSpacing: 0.5 }, diff --git a/src/screens/HomeScreen.tsx b/src/screens/HomeScreen.tsx index 8475bcb..0f8e1d5 100644 --- a/src/screens/HomeScreen.tsx +++ b/src/screens/HomeScreen.tsx @@ -150,9 +150,11 @@ function PinnedFlightCardComponent({ item, colors }: { item: any; colors: any }) // Performance optimization: memoize flatlist item to prevent unnecessary re-renders const PinnedFlightCard = React.memo(PinnedFlightCardComponent); -export default function HomeScreen() { +export default function HomeScreen({ isFocused }: { isFocused?: boolean }) { const { colors } = useAppTheme(); const { t, months, locale, weatherMap } = useLanguage(); + const [timelineKey, setTimelineKey] = React.useState(0); + React.useEffect(() => { if (isFocused) setTimelineKey(k => k + 1); }, [isFocused]); const HOME_SHIFT_TITLES = { work: t('homeShiftWork'), rest: '🌴 Riposo' }; const today = new Date(); const [shiftEvent, setShiftEvent] = useState(null); @@ -257,8 +259,9 @@ export default function HomeScreen() { d.setHours(0, 0, 0, 0); const dEnd = new Date(); dEnd.setHours(23, 59, 59, 999); const events = await Calendar.getEventsAsync([cal.id], d, dEnd); - const shift = events.find(e => e.title.includes('Lavoro') || e.title.includes('Riposo')); - setShiftEvent(shift || null); + const lavoro = events.find(e => e.title.includes('Lavoro')); + const riposo = events.find(e => e.title.includes('Riposo')); + setShiftEvent(lavoro ?? riposo ?? null); } catch (e) { if (__DEV__) console.error('[shift]', e); } finally { setLoadingShift(false); } }; @@ -428,6 +431,7 @@ export default function HomeScreen() { shiftStart={new Date(shiftEvent.startDate)} shiftEnd={new Date(shiftEvent.endDate)} inline + refreshKey={timelineKey} /> )} diff --git a/src/screens/PasswordScreen.tsx b/src/screens/PasswordScreen.tsx index c5338f1..a9611d8 100644 --- a/src/screens/PasswordScreen.tsx +++ b/src/screens/PasswordScreen.tsx @@ -9,21 +9,44 @@ import { MaterialIcons } from '@expo/vector-icons'; import { useAppTheme, type ThemeColors } from '../context/ThemeContext'; import { useLanguage } from '../context/LanguageContext'; -const PASSWORDS_KEY = 'aerostaff_passwords_v1'; -const PIN_KEY = 'aerostaff_pin_v1'; +const PASSWORDS_KEY = 'aerostaff_passwords_v1'; +const PIN_KEY = 'aerostaff_pin_v1'; const PIN_ENABLED_KEY = 'aerostaff_pin_enabled_v1'; -// Secure helpers β€” PIN is stored in the OS keychain, not plain AsyncStorage. +// ── Secure helpers β€” all sensitive data goes through the OS keychain ────────── async function getSecurePin(): Promise { try { return await SecureStore.getItemAsync(PIN_KEY); } - catch { return AsyncStorage.getItem(PIN_KEY); } // fallback for older installs + catch { return AsyncStorage.getItem(PIN_KEY); } } async function setSecurePin(pin: string): Promise { await SecureStore.setItemAsync(PIN_KEY, pin); } async function deleteSecurePin(): Promise { await SecureStore.deleteItemAsync(PIN_KEY).catch(() => {}); - await AsyncStorage.removeItem(PIN_KEY).catch(() => {}); // clean up legacy + await AsyncStorage.removeItem(PIN_KEY).catch(() => {}); +} + +// Passwords are stored encrypted in SecureStore. +// On first access we migrate any legacy plaintext AsyncStorage data. +async function loadPasswords(): Promise { + try { + const secure = await SecureStore.getItemAsync(PASSWORDS_KEY); + if (secure) return JSON.parse(secure); + } catch {} + // Migration: if data exists only in AsyncStorage, move it to SecureStore + try { + const legacy = await AsyncStorage.getItem(PASSWORDS_KEY); + if (legacy) { + const parsed: PasswordEntry[] = JSON.parse(legacy); + await SecureStore.setItemAsync(PASSWORDS_KEY, legacy); + await AsyncStorage.removeItem(PASSWORDS_KEY); + return parsed; + } + } catch {} + return []; +} +async function savePasswords(entries: PasswordEntry[]): Promise { + await SecureStore.setItemAsync(PASSWORDS_KEY, JSON.stringify(entries)); } type PasswordEntry = { @@ -149,8 +172,8 @@ export default function PasswordScreen() { // Load on mount useEffect(() => { (async () => { - const raw = await AsyncStorage.getItem(PASSWORDS_KEY); - if (raw) setEntries(JSON.parse(raw)); + const loaded = await loadPasswords(); + setEntries(loaded); const enabled = await AsyncStorage.getItem(PIN_ENABLED_KEY); const isEnabled = enabled === 'true'; setPinEnabled(isEnabled); @@ -160,7 +183,7 @@ export default function PasswordScreen() { const persist = useCallback(async (next: PasswordEntry[]) => { setEntries(next); - await AsyncStorage.setItem(PASSWORDS_KEY, JSON.stringify(next)); + await savePasswords(next); }, []); // PIN toggle diff --git a/src/screens/SettingsScreen.tsx b/src/screens/SettingsScreen.tsx index d4f8bcc..8b35d7d 100644 --- a/src/screens/SettingsScreen.tsx +++ b/src/screens/SettingsScreen.tsx @@ -1,7 +1,7 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect, useCallback } from 'react'; import { View, Text, StyleSheet, ScrollView, TouchableOpacity, Switch, ActivityIndicator, - Alert, Modal, KeyboardAvoidingView, Platform, TextInput, + Alert, Modal, KeyboardAvoidingView, Platform, TextInput, Linking, } from 'react-native'; import { LinearGradient } from 'expo-linear-gradient'; import { MaterialIcons } from '@expo/vector-icons'; @@ -14,6 +14,16 @@ import { normalizeAirportCode, isValidAirportCode, } from '../utils/airportSettings'; +import { + APP_VERSION, + checkForUpdate, + getCachedUpdateInfo, + markUpdateSeen, + type UpdateInfo, +} from '../utils/updateChecker'; +import UpdateModal from '../components/UpdateModal'; +import { exportBackup, importBackup } from '../utils/backupManager'; +import { getStaffMonitorDebugStatus, getStaffMonitorDebugColumns, getStaffMonitorDebugFlights } from '../utils/staffMonitor'; // ─── Tema picker ────────────────────────────────────────────────────────────── type ThemeOption = { @@ -172,6 +182,68 @@ export default function SettingsScreen() { sublabel: opt.id === 'light' ? t('themeLightSub') : opt.id === 'dark' ? t('themeDarkSub') : t('themeWeatherSub'), })); const [airportInput, setAirportInput] = useState(airportCode); + const [updateInfo, setUpdateInfo] = useState(null); + const [checkingUpdate, setCheckingUpdate] = useState(false); + const [showUpdateModal, setShowUpdateModal] = useState(false); + const [exportingBackup, setExportingBackup] = useState(false); + const [importingBackup, setImportingBackup] = useState(false); + + useEffect(() => { + getCachedUpdateInfo().then(setUpdateInfo); + }, []); + + const handleCheckUpdate = useCallback(async () => { + setCheckingUpdate(true); + const info = await checkForUpdate(true); + setUpdateInfo(info); + setCheckingUpdate(false); + if (!info) { + Alert.alert('Errore', 'Impossibile contattare GitHub. Riprova piΓΉ tardi.'); + } else if (info.available) { + setShowUpdateModal(true); + } else { + Alert.alert('Sei aggiornato!', `AeroStaff Pro v${APP_VERSION} Γ¨ l'ultima versione.`); + } + }, []); + + const handleDownload = useCallback(() => { + if (updateInfo?.downloadUrl) Linking.openURL(updateInfo.downloadUrl); + }, [updateInfo]); + + const handleExport = useCallback(async () => { + setExportingBackup(true); + const result = await exportBackup(); + setExportingBackup(false); + if (result.ok) { + Alert.alert('Backup esportato', 'File salvato nella cartella selezionata.'); + } else if (result.error !== 'Permesso negato' && result.error !== 'Annullato') { + Alert.alert('Errore', result.error); + } + }, []); + + const handleImport = useCallback(async () => { + Alert.alert( + 'Importa backup', + 'I dati attuali (note, password, blocco note) saranno sovrascritti con quelli del backup. Continuare?', + [ + { text: 'Annulla', style: 'cancel' }, + { + text: 'Importa', + style: 'destructive', + onPress: async () => { + setImportingBackup(true); + const result = await importBackup(); + setImportingBackup(false); + if (result.ok) { + Alert.alert('Backup importato', 'Riavvia l\'app per applicare tutte le modifiche.'); + } else if (result.error !== 'Annullato') { + Alert.alert('Errore', result.error); + } + }, + }, + ], + ); + }, []); const openAirportModal = () => { setAirportInput(airportCode); @@ -213,7 +285,7 @@ export default function SettingsScreen() { {t('settingsTitle')} - AeroStaff Pro Β· v1.1.0 + AeroStaff Pro Β· v{APP_VERSION} @@ -275,9 +347,107 @@ export default function SettingsScreen() { {/* ── Info app ── */} {t('sectionApp')} - + + + Alert.alert('StaffMonitor debug', `Stato: ${getStaffMonitorDebugStatus()}\n\nColonne:\n${getStaffMonitorDebugColumns()}\n\nVoli (D, primi 5):\n${getStaffMonitorDebugFlights()}`)} activeOpacity={0.8}> + + + + + Debug StaffMonitor + Tocca per vedere colonne rilevate + + + + + {/* ── Sezione Aggiornamenti ── */} + AGGIORNAMENTI + + {/* Version row */} + + + + + + + {updateInfo?.available ? `Aggiornamento disponibile` : 'AeroStaff Pro'} + + + {updateInfo?.available + ? `Versione ${updateInfo.latestVersion} pronta` + : `v${APP_VERSION} Β· versione installata`} + + + {updateInfo?.available && ( + + NEW + + )} + + {/* Action buttons */} + + + {checkingUpdate + ? + : } + + {checkingUpdate ? 'Controllo…' : 'Controlla'} + + + {updateInfo?.available && ( + + + + Scarica v{updateInfo.latestVersion} + + + )} + + {/* ── Sezione Backup ── */} + BACKUP + + {/* Esporta */} + + + {exportingBackup + ? + : } + + Esporta + Salva su file + + {/* Importa */} + + + {importingBackup + ? + : } + + Importa + Ripristina da file + + {/* ── Sezione Lingua ── */} {t('sectionLanguage')} @@ -307,6 +477,16 @@ export default function SettingsScreen() { + {showUpdateModal && updateInfo && ( + { + markUpdateSeen(updateInfo.latestVersion).catch(() => {}); + setShowUpdateModal(false); + }} + /> + )} + { if (!loading) return; const timer = setTimeout(() => { setLoading(false); setLoadError(true); }, 15_000); @@ -20,13 +30,11 @@ export default function TraveldocScreen() { return ( - {/* Header */} TravelDoc {t('traveldocSub')} - {/* WebView */} {loading && ( @@ -42,11 +50,13 @@ export default function TraveldocScreen() { )} { setLoading(false); setLoadError(false); }} onError={() => { setLoading(false); setLoadError(true); }} javaScriptEnabled domStorageEnabled + injectedJavaScriptBeforeContentLoaded={colors.isDark ? DARK_CSS_JS : undefined} + injectedJavaScript={colors.isDark ? DARK_CSS_JS : undefined} /> ); diff --git a/src/utils/airlineOps.ts b/src/utils/airlineOps.ts index 7edf78a..788af39 100644 --- a/src/utils/airlineOps.ts +++ b/src/utils/airlineOps.ts @@ -10,8 +10,12 @@ export type AirlineOps = { export const DEFAULT_OPS: AirlineOps = { checkInOpen: 120, checkInClose: 40, gateOpen: 30, gateClose: 20 }; export const AIRLINE_OPS: Array<{ key: string; ops: AirlineOps }> = [ + { key: 'ryanair', ops: { checkInOpen: 150, checkInClose: 40, gateOpen: 30, gateClose: 20 } }, { key: 'easyjet', ops: { checkInOpen: 120, checkInClose: 40, gateOpen: 30, gateClose: 20 } }, { key: 'wizz', ops: { checkInOpen: 120, checkInClose: 40, gateOpen: 30, gateClose: 15 } }, + { key: 'volotea', ops: { checkInOpen: 120, checkInClose: 40, gateOpen: 30, gateClose: 20 } }, + { key: 'vueling', ops: { checkInOpen: 120, checkInClose: 45, gateOpen: 35, gateClose: 20 } }, + { key: 'transavia', ops: { checkInOpen: 120, checkInClose: 40, gateOpen: 30, gateClose: 20 } }, { key: 'aer lingus', ops: { checkInOpen: 150, checkInClose: 40, gateOpen: 30, gateClose: 20 } }, { key: 'british airways', ops: { checkInOpen: 180, checkInClose: 45, gateOpen: 45, gateClose: 20 } }, { key: 'sas', ops: { checkInOpen: 120, checkInClose: 40, gateOpen: 30, gateClose: 20 } }, @@ -25,8 +29,10 @@ export function getAirlineOps(name: string): AirlineOps { } export const AIRLINE_COLORS: Record = { - 'wizz': '#C6006E', 'easyjet': '#FF6600', 'aer lingus': '#006E44', - 'british airways': '#075AAA', 'sas': '#003E7E', 'scandinavian': '#003E7E', 'flydubai': '#CC1E42', + 'ryanair': '#073590', 'easyjet': '#FF6600', 'wizz': '#C6006E', + 'volotea': '#3C0F8B', 'vueling': '#FFB300', 'transavia': '#00A650', + 'aer lingus': '#006E44', 'british airways': '#075AAA', + 'sas': '#003E7E', 'scandinavian': '#003E7E', 'flydubai': '#CC1E42', }; export function getAirlineColor(name: string): HexColor { @@ -35,4 +41,21 @@ export function getAirlineColor(name: string): HexColor { return '#2563EB'; } -export const ALLOWED_AIRLINES = ['wizz', 'aer lingus', 'easyjet', 'british airways', 'sas', 'scandinavian', 'flydubai']; +export const AIRLINE_DISPLAY_NAMES: Record = { + 'ryanair': 'Ryanair', + 'easyjet': 'easyJet', + 'wizz': 'Wizz Air', + 'volotea': 'Volotea', + 'vueling': 'Vueling', + 'transavia': 'Transavia', + 'aer lingus': 'Aer Lingus', + 'british airways': 'British Airways', + 'sas': 'SAS', + 'scandinavian': 'Scandinavian Airlines', + 'flydubai': 'flydubai', +}; + +export const ALLOWED_AIRLINES = [ + 'ryanair', 'easyjet', 'wizz', 'volotea', 'vueling', 'transavia', + 'aer lingus', 'british airways', 'sas', 'scandinavian', 'flydubai', +]; diff --git a/src/utils/airportSettings.ts b/src/utils/airportSettings.ts index 3294454..981a5e1 100644 --- a/src/utils/airportSettings.ts +++ b/src/utils/airportSettings.ts @@ -1,4 +1,5 @@ import AsyncStorage from '@react-native-async-storage/async-storage'; +import { ALLOWED_AIRLINES } from './airlineOps'; export type AirportPreset = { code: string; @@ -29,6 +30,26 @@ export const AIRPORT_PRESETS: AirportPreset[] = [ { code: 'PMO', name: 'Palermo Falcone Borsellino', city: 'Palermo', icao: 'LICJ' }, ]; +export const AIRPORT_AIRLINES: Record = { + PSA: ['ryanair', 'easyjet', 'wizz', 'aer lingus', 'transavia', 'volotea'], + FCO: ['ryanair', 'easyjet', 'wizz', 'aer lingus', 'british airways', 'sas', 'flydubai', 'volotea', 'vueling', 'transavia'], + CIA: ['ryanair', 'easyjet', 'wizz'], + MXP: ['ryanair', 'easyjet', 'wizz', 'aer lingus', 'british airways', 'flydubai', 'vueling', 'volotea'], + LIN: ['british airways', 'aer lingus', 'sas'], + BGY: ['ryanair', 'wizz', 'easyjet', 'vueling', 'volotea'], + BLQ: ['ryanair', 'easyjet', 'wizz', 'vueling', 'volotea', 'transavia'], + VCE: ['ryanair', 'easyjet', 'wizz', 'british airways', 'volotea', 'vueling'], + FLR: ['ryanair', 'easyjet', 'volotea', 'vueling'], + NAP: ['ryanair', 'easyjet', 'wizz', 'volotea', 'vueling'], + CTA: ['ryanair', 'easyjet', 'wizz', 'volotea', 'vueling'], + PMO: ['ryanair', 'easyjet', 'wizz', 'volotea', 'vueling'], +}; + +export function getAirportAirlines(code: string | null | undefined): string[] { + const normalized = normalizeAirportCode(code); + return AIRPORT_AIRLINES[normalized] ?? ALLOWED_AIRLINES; +} + const AIRPORT_MAP = Object.fromEntries( AIRPORT_PRESETS.map(airport => [airport.code, airport] as const), ) as Record; diff --git a/src/utils/backupManager.ts b/src/utils/backupManager.ts new file mode 100644 index 0000000..cf2815f --- /dev/null +++ b/src/utils/backupManager.ts @@ -0,0 +1,81 @@ +import AsyncStorage from '@react-native-async-storage/async-storage'; +import * as DocumentPicker from 'expo-document-picker'; +import * as FileSystem from 'expo-file-system/legacy'; + +const BACKUP_VERSION = 1; + +// Keys that represent user data worth preserving across reinstalls +const BACKUP_KEYS = [ + 'aerostaff_notepad_v1', + 'aerostaff_passwords_v1', + 'aerostaff_pin_v1', + 'aerostaff_pin_enabled_v1', + 'aerostaff_phonebook_v1', + 'aerostaff_airport_code_v1', + 'aerostaff_language_v1', + 'aerostaff_theme_mode', + 'aerostaff_flight_filter_v1', + 'manuals_data_v2', + '@shift_import_name', + 'aerostaff_notif_enabled', +]; + +export type BackupResult = { ok: true } | { ok: false; error: string }; + +export async function exportBackup(): Promise { + try { + const pairs = await AsyncStorage.multiGet(BACKUP_KEYS); + const data: Record = {}; + for (const [key, value] of pairs) data[key] = value; + + const payload = JSON.stringify({ version: BACKUP_VERSION, exportedAt: Date.now(), data }, null, 2); + const dateStr = new Date().toISOString().slice(0, 10); + const filename = `AeroStaffPro-backup-${dateStr}.json`; + + // Ask user to choose a folder via SAF + const perms = await FileSystem.StorageAccessFramework.requestDirectoryPermissionsAsync(); + if (!perms.granted) return { ok: false, error: 'Permesso negato' }; + + const fileUri = await FileSystem.StorageAccessFramework.createFileAsync( + perms.directoryUri, + filename, + 'application/json', + ); + await FileSystem.writeAsStringAsync(fileUri, payload, { encoding: FileSystem.EncodingType.UTF8 }); + return { ok: true }; + } catch (e: any) { + return { ok: false, error: e?.message ?? 'Errore sconosciuto' }; + } +} + +export async function importBackup(): Promise { + try { + const result = await DocumentPicker.getDocumentAsync({ type: 'application/json', copyToCacheDirectory: true }); + if (result.canceled) return { ok: false, error: 'Annullato' }; + + const uri = result.assets[0].uri; + const raw = await FileSystem.readAsStringAsync(uri, { encoding: FileSystem.EncodingType.UTF8 }); + + let parsed: any; + try { + parsed = JSON.parse(raw); + } catch { + return { ok: false, error: 'File non valido' }; + } + + if (!parsed.version || !parsed.data || typeof parsed.data !== 'object') { + return { ok: false, error: 'Formato backup non riconosciuto' }; + } + + const pairs: [string, string][] = Object.entries(parsed.data) + .filter(([key, val]) => BACKUP_KEYS.includes(key) && val !== null && val !== undefined) + .map(([key, val]) => [key, val as string]); + + if (pairs.length === 0) return { ok: false, error: 'Nessun dato trovato nel backup' }; + + await AsyncStorage.multiSet(pairs); + return { ok: true }; + } catch (e: any) { + return { ok: false, error: e?.message ?? 'Errore sconosciuto' }; + } +} diff --git a/src/utils/fr24api.ts b/src/utils/fr24api.ts index a560d89..e6061a0 100644 --- a/src/utils/fr24api.ts +++ b/src/utils/fr24api.ts @@ -1,6 +1,6 @@ -import { ALLOWED_AIRLINES } from './airlineOps'; import { buildFr24ScheduleUrl, + getAirportAirlines, getAirportInfo, getStoredAirportCode, isValidAirportCode, @@ -26,9 +26,9 @@ export type FR24ScheduleRaw = { airport: AirportInfo; }; -function filterAirlines(data: any[]) { +function filterAirlines(data: any[], allowedList: string[]) { return data.filter(item => - ALLOWED_AIRLINES.some(key => (item.flight?.airline?.name || '').toLowerCase().includes(key)), + allowedList.some(key => (item.flight?.airline?.name || '').toLowerCase().includes(key)), ); } @@ -56,9 +56,10 @@ export async function fetchAirportSchedule(code?: string): Promise const allArrivals = json.result?.response?.airport?.pluginData?.schedule?.arrivals?.data || []; const allDepartures = json.result?.response?.airport?.pluginData?.schedule?.departures?.data || []; + const airlines = getAirportAirlines(airportCode); return { - arrivals: filterAirlines(allArrivals), - departures: filterAirlines(allDepartures), + arrivals: filterAirlines(allArrivals, airlines), + departures: filterAirlines(allDepartures, airlines), airportCode, airport: getAirportInfo(airportCode), }; @@ -86,11 +87,12 @@ export async function fetchAirportScheduleRaw(code?: string): Promise') + .replace(/\s+/g, ' ') .trim(); } -function extractTDCells(trHTML: string): string[] { - const cells: string[] = []; - const regex = /]*>([\s\S]*?)<\/td>/gi; +type RawCell = { text: string; colspan: number }; + +function extractCellsRaw(trHTML: string): RawCell[] { + const cells: RawCell[] = []; + const regex = /]*)>([\s\S]*?)<\/t[dh]>/gi; let m: RegExpExecArray | null; while ((m = regex.exec(trHTML)) !== null) { - cells.push(stripHTML(m[1])); + const attrs = m[1]; + const text = stripHTML(m[2]); + const csMatch = /colspan\s*=\s*["']?(\d+)/i.exec(attrs); + const colspan = csMatch ? Math.max(1, parseInt(csMatch[1], 10)) : 1; + cells.push({ text, colspan }); } return cells; } +/** Reject phone numbers, names-with-phones, and other junk. */ +function isPhoneOrJunk(val: string): boolean { + if (!val) return false; + if (val.length > 30) return true; + // Catch "Albe 3284693677" style: any run of 8+ consecutive digits anywhere in the value + if (/\d{8,}/.test(val)) return true; + return false; +} + +type ColMap = { + flight: number; + stand?: number; + checkin?: number; + gate?: number; + belt?: number; +}; + /** - * Fetch and parse stand/gate/check-in data from the Pisa Airport staffMonitor. - * - * Departures columns (0-indexed): - * 0=logo, 1=flight, 2=ACtype, 3=TRtype, 4=REG, 5=dest, 6=SLOT, 7=SCHED, - * 8=EXP, 9=BLKOFF, 10=TKOFF, 11=STATUS, 12=STAND, 13=CHECKIN, 14=GATE - * - * Arrivals columns (0-indexed): - * 0=logo, 1=flight, 2=ACtype, 3=TRtype, 4=REG, 5=origin, 6=SCHED, - * 7=EXP, 8=LAND, 9=BLKON, 10=STATUS, 11=STAND, 12=BELT + * Build absolute data-column positions from a header row. + * Headers with colspan>1 span multiple data columns β€” e.g. "VOLO / FLIGHT" + * with colspan=2 covers [logo_col, flight_number_col]. For the flight column + * specifically, data rows put the flight code in the LAST sub-column. */ -export async function fetchStaffMonitorData(nature: 'D' | 'A'): Promise { +function detectColumns(headerRow: RawCell[]): ColMap | null { + type HPos = { name: string; start: number; span: number }; + const positions: HPos[] = []; + let col = 0; + for (const h of headerRow) { + positions.push({ name: h.text.toLowerCase().trim(), start: col, span: h.colspan }); + col += h.colspan; + } + + const findPos = (pred: (n: string) => boolean) => positions.find(p => pred(p.name)); + + const flightH = findPos(n => n.includes('volo') || n.includes('flight') || n.includes('flt') || n === 'num' || n.includes('numero')); + if (!flightH) return null; + + // For "VOLO / FLIGHT" with colspan=2, logo is first sub-col, flight # is last. + const flightCol = flightH.start + flightH.span - 1; + + // Use word-boundary checks: 'stand' as a whole word to avoid matching "addetto stand" / "standby" + const standH = findPos(n => n === 'stand' || /\bstand\b/.test(n) || n.includes('parch') || n.includes('posiz') || n.includes('piazzola') || n === 'park'); + const checkinH = findPos(n => n.includes('check') || n === 'c/i' || n === 'ci' || n === 'banco' || n.includes('desk') || n.includes('bancone')); + // gate: match 'gate', 'uscita', 'imbarco' exactly (avoid partial matches on status cols) + const gateH = findPos(n => n === 'gate' || n === 'uscita' || n === 'imbarco' || n.includes('uscit')); + const beltH = findPos(n => n.includes('belt') || n.includes('nastro') || n.includes('tapis') || n.includes('baggage') || n.includes('reclam') || n.includes('bggl')); + + const map: ColMap = { flight: flightCol }; + if (standH) map.stand = standH.start; + if (checkinH) map.checkin = checkinH.start; + if (gateH) map.gate = gateH.start; + if (beltH) map.belt = beltH.start; + // Do NOT return null when only flight column is found β€” better to parse flight numbers + // with empty stand/gate/belt than to skip every row because keyword didn't match. + + const headerStr = positions.map(p => `${p.start}:"${p.name}"`).join(' '); + _lastDebugColumns = `map=${JSON.stringify(map)} | ${headerStr}`; + console.warn('[staffMonitor] columns:', _lastDebugColumns); + return map; +} + +function cell(cells: string[], idx: number | undefined): string | undefined { + if (idx === undefined) return undefined; + const v = cells[idx]?.trim(); + if (!v || isPhoneOrJunk(v)) return undefined; + // Extract only the leading operational code (stand "17", gate "674", desk "4"). + // Cells often contain extra text after the code: "17β—† Federico" or "674 RICCARDO F". + const m = /^([A-Z0-9][A-Z0-9\-\/]{0,8})/i.exec(v); + if (!m) return undefined; + const code = m[1]; + // Reject pure-letter tokens of 3+ chars β€” handler abbreviations like "ana", "FEDE", "MARCO" + if (/^[A-Za-z]{3,}$/.test(code)) return undefined; + return code; +} + +/** Extract flight number from a cell that may contain "FR03747 B738" β€” take first token only. */ +function extractFlightCode(raw: string): string | null { + const token = raw.trim().split(/\s+/)[0]; + if (!/^[A-Z0-9]{1,4}\d/i.test(token)) return null; + // Require at least one letter to avoid matching pure numbers + if (!/[A-Z]/i.test(token)) return null; + return normalizeFlightNumber(token); +} + +function parseSection(sectionHTML: string): StaffMonitorFlight[] { + const results: StaffMonitorFlight[] = []; + const trRegex = /]*>([\s\S]*?)<\/tr>/gi; + let match: RegExpExecArray | null; + let colMap: ColMap | null = null; + + while ((match = trRegex.exec(sectionHTML)) !== null) { + const rawCells = extractCellsRaw(match[1]); + if (rawCells.length < 2) continue; + + if (!colMap) { + colMap = detectColumns(rawCells); + // Always skip this row whether it was the header or a pre-header title row. + // If colMap is still null we'll keep trying on the next row. + continue; + } + + const cells = rawCells.map(c => c.text); + const rawFlight = cells[colMap.flight] ?? ''; + const flightNumber = extractFlightCode(rawFlight); + if (!flightNumber) continue; + + results.push({ + flightNumber, + stand: cell(cells, colMap.stand), + checkin: cell(cells, colMap.checkin), + gate: cell(cells, colMap.gate), + belt: cell(cells, colMap.belt), + }); + } + + if (!colMap) { _lastDebugColumns = 'NESSUNA intestazione trovata'; console.warn('[staffMonitor] header row never detected'); } + return results; +} + +// ─── AsyncStorage flight cache (20-min TTL) ────────────────────────────────────── +const SM_CACHE_KEY = 'sm_flights_v2'; + +async function loadCached(nature: 'D' | 'A'): Promise { try { - const url = `https://servizi.pisa-airport.com/staffMonitor/staffMonitor?trans=true&nature=${nature}`; - const resp = await fetch(url); - if (!resp.ok) { - console.warn(`[staffMonitor] HTTP error for nature=${nature}: ${resp.status} ${resp.statusText}`); - return []; + const raw = await AsyncStorage.getItem(SM_CACHE_KEY); + if (!raw) return null; + const c = JSON.parse(raw); + const e = c[nature]; + if (!e || Date.now() - e.ts > 20 * 60 * 1000) return null; + return e.flights as StaffMonitorFlight[]; + } catch { return null; } +} + +async function saveCache(nature: 'D' | 'A', flights: StaffMonitorFlight[]): Promise { + try { + const raw = await AsyncStorage.getItem(SM_CACHE_KEY) ?? '{}'; + const c = JSON.parse(raw); + c[nature] = { flights, ts: Date.now() }; + await AsyncStorage.setItem(SM_CACHE_KEY, JSON.stringify(c)); + } catch {} +} + +// ─── Fetch helpers ──────────────────────────────────────────────────────────────── +const FETCH_HEADERS = { + 'User-Agent': 'Mozilla/5.0 (Linux; Android 10) AppleWebKit/537.36 Chrome/120 Safari/537.36', + 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', + 'Accept-Language': 'it-IT,it;q=0.9,en;q=0.8', + 'Referer': 'https://servizi.pisa-airport.com/staffMonitor/staffMonitor.html', +}; + +// Tomcat JSESSIONID captured from D responses and forwarded to A requests. +// The arrivals servlet likely requires an active session; departures may not. +let _sessionCookie: string | null = null; + +function captureSessionCookie(resp: Response): void { + const raw = resp.headers.get('set-cookie') ?? ''; + const m = /JSESSIONID=([^;,\s]+)/i.exec(raw); + if (m) { + _sessionCookie = `JSESSIONID=${m[1]}`; + console.warn('[staffMonitor] JSESSIONID captured'); + } +} + +async function tryFetch(url: string, timeoutMs: number): Promise { + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), timeoutMs); + try { + const headers: Record = { ...FETCH_HEADERS }; + if (_sessionCookie) headers['Cookie'] = _sessionCookie; + + const resp = await fetch(url, { signal: controller.signal, headers }); + clearTimeout(timer); + captureSessionCookie(resp); + const body = await resp.text(); + if (!resp.ok || body.length < 200) throw new Error(`${resp.status} len=${body.length}`); + return body; + } catch (e) { + clearTimeout(timer); + throw e; + } +} + +/** + * Fire all URLs simultaneously and resolve with the first successful HTML. + * Returns null only if every URL fails or times out. + */ +function raceUrls(urls: string[], timeoutMs: number): Promise { + return new Promise(resolve => { + let done = false; + let pending = urls.length; + for (const url of urls) { + tryFetch(url, timeoutMs) + .then(html => { if (!done) { done = true; resolve(html); } }) + .catch(() => { pending--; if (pending === 0 && !done) { done = true; resolve(null); } }); } - const html = await resp.text(); + }); +} - const results: StaffMonitorFlight[] = []; - const trRegex = /]*>([\s\S]*?)<\/tr>/gi; - let match: RegExpExecArray | null; +/** + * When a combined page (no nature filter) is fetched, split at boundaries + * and return the chunk whose surrounding text best matches the requested nature. + */ +function extractSectionFor(html: string, nature: 'D' | 'A'): string { + const arrRx = /arriv[oi]|arrival|inbound/i; + const depRx = /partenz|departur|outbound/i; + const chunks = html.split(/(?=])/i).filter(s => /]/i.test(s)); + if (chunks.length < 2) return html; - while ((match = trRegex.exec(html)) !== null) { - const rowHTML = match[1]; - // Only rows that carry a flight number cell (match clsFlight as a substring of the class attribute) - if (!/class\s*=\s*["'][^"']*clsFlight[^"']*["']/i.test(rowHTML)) continue; + let bestChunk = html; + let bestScore = -99; + for (const chunk of chunks) { + const header = chunk.slice(0, 500); + const score = nature === 'A' + ? (arrRx.test(header) ? 2 : depRx.test(header) ? -1 : 0) + : (depRx.test(header) ? 2 : arrRx.test(header) ? -1 : 0); + if (score > bestScore) { bestScore = score; bestChunk = chunk; } + } + return bestScore > 0 ? bestChunk : html; +} + +// ─── Debug state ────────────────────────────────────────────────────────────────── +let _lastDebugStatus = 'init'; +let _lastDebugHtml = ''; +let _lastDebugColumns = 'non ancora rilevate'; +let _lastDebugFlightsD = 'nessun volo'; +let _lastDebugFlightsA = 'nessun volo'; +export function getStaffMonitorDebugStatus(): string { return _lastDebugStatus; } +export function getStaffMonitorDebugHtml(): string { return _lastDebugHtml; } +export function getStaffMonitorDebugColumns(): string { return _lastDebugColumns; } +export function getStaffMonitorDebugFlights(): string { + return `D:\n${_lastDebugFlightsD}\n\nA:\n${_lastDebugFlightsA}`; +} + +export async function fetchStaffMonitorData(nature: 'D' | 'A'): Promise { + const base = 'https://servizi.pisa-airport.com/staffMonitor/staffMonitor'; - const cells = extractTDCells(rowHTML); - if (cells.length < 2) continue; + // Primary URLs for the requested nature + const primaryUrls = [ + `${base}?trans=true&nature=${nature}`, + `${base}?nature=${nature}`, + `${base}?nature=${nature}&aviation=1`, + ]; - const rawFlight = cells[1]; - if (!rawFlight) continue; + // Extra variants for arrivals β€” all keep nature=A so the servlet returns arrivals data. + // Do NOT add URLs without nature=A: they return departures and produce garbage parse results. + const arrivalExtras = [ + `${base}?nature=A&trans=false`, + `${base}?nature=A&airport=PSA`, + `${base}?nature=A&refresh=1`, + `${base}?nature=A&_=${Date.now()}`, + ]; - const flightNumber = normalizeFlightNumber(rawFlight); + try { + let html = ''; + + if (nature === 'D') { + // Departures: sequential β€” server is slow, use 25s to avoid false timeouts + for (const url of primaryUrls) { + try { + html = await tryFetch(url, 25_000); + _lastDebugStatus = `D:200 len=${html.length}`; + _lastDebugHtml = html.replace(/\s+/g, ' ').slice(0, 300); + break; + } catch (e: any) { + _lastDebugStatus = `D:ERR ${String(e).slice(0, 60)}`; + console.warn(`[staffMonitor] D fetch error: ${_lastDebugStatus}`); + } + } + } else { + // Prime session cookie via a quick D request if we don't have one yet. + // The Tomcat arrivals servlet likely requires an active JSESSIONID. + if (!_sessionCookie) { + try { + await tryFetch(`${base}?trans=true&nature=D`, 12_000); + console.warn('[staffMonitor] session primed for A:', _sessionCookie ?? 'none'); + } catch { + console.warn('[staffMonitor] session prime failed, proceeding anyway'); + } + } - if (nature === 'D') { - results.push({ - flightNumber, - stand: cells[12] || undefined, - checkin: cells[13] || undefined, - gate: cells[14] || undefined, - }); + // Race all nature=A variants in parallel β€” fastest wins + html = await raceUrls([...primaryUrls, ...arrivalExtras], 40_000) ?? ''; + if (html) { + _lastDebugStatus = `A:200 len=${html.length} cookie=${_sessionCookie ? 'yes' : 'no'}`; + console.warn(`[staffMonitor] A parallel race succeeded len=${html.length}`); } else { - results.push({ - flightNumber, - stand: cells[11] || undefined, - belt: cells[12] || undefined, - }); + _lastDebugStatus = `A:ERR all ${primaryUrls.length + arrivalExtras.length} URLs failed cookie=${_sessionCookie ? 'yes' : 'no'}`; + console.warn(`[staffMonitor] ${_lastDebugStatus}`); } } - if (__DEV__) { - console.log(`[staffMonitor] nature=${nature} parsed ${results.length} flights.`, results.slice(0, 5).map(r => r.flightNumber)); + if (!html) { + console.warn(`[staffMonitor] all URLs failed for ${nature} β€” trying cache`); + const cached = await loadCached(nature); + if (cached) { + _lastDebugStatus = `${nature}:CACHE(${cached.length})`; + return cached; + } + return []; + } + + if (__DEV__) console.log(`[staffMonitor] nature=${nature} HTML sample:\n`, html.slice(0, 2000)); + + const results = parseSection(html); + + const summary = results.length === 0 + ? 'nessun volo parsato' + : results.slice(0, 5).map(f => `${f.flightNumber} S=${f.stand ?? '-'} CI=${f.checkin ?? '-'} G=${f.gate ?? '-'} B=${f.belt ?? '-'}`).join('\n'); + if (nature === 'D') _lastDebugFlightsD = summary; + else _lastDebugFlightsA = summary; + + if (results.length > 0) { + await saveCache(nature, results); + return results; } + // Parse returned nothing β€” fall back to cache if available + const cached = await loadCached(nature); + if (cached && cached.length > 0) { + _lastDebugStatus += '+CACHE'; + return cached; + } return results; } catch (e) { - console.error(`[staffMonitor] fetch/parse error for nature=${nature}:`, e); - return []; + console.error(`[staffMonitor] error for nature=${nature}:`, e); + const cached = await loadCached(nature); + return cached ?? []; } } diff --git a/src/utils/updateChecker.ts b/src/utils/updateChecker.ts new file mode 100644 index 0000000..1bfb9d4 --- /dev/null +++ b/src/utils/updateChecker.ts @@ -0,0 +1,99 @@ +import AsyncStorage from '@react-native-async-storage/async-storage'; + +export const APP_VERSION = '2.6.0'; +const REPO = 'targetmisser/flightworkapp'; +const CHECK_KEY = 'aerostaff_update_check_v1'; +const SEEN_KEY = 'aerostaff_update_seen_v1'; + +export type UpdateInfo = { + available: boolean; + latestVersion: string; + downloadUrl: string; + releaseUrl: string; + releaseNotes: string; + checkedAt: number; +}; + +function parseVersion(v: string): number[] { + return v.replace(/^v/, '').split('.').map(n => parseInt(n, 10) || 0); +} + +function isNewer(remote: string, current: string): boolean { + const r = parseVersion(remote); + const c = parseVersion(current); + for (let i = 0; i < Math.max(r.length, c.length); i++) { + const diff = (r[i] ?? 0) - (c[i] ?? 0); + if (diff !== 0) return diff > 0; + } + return false; +} + +export async function checkForUpdate(force = false): Promise { + try { + const now = Date.now(); + + if (!force) { + const raw = await AsyncStorage.getItem(CHECK_KEY); + if (raw) { + const cached: UpdateInfo = JSON.parse(raw); + if (now - cached.checkedAt < 24 * 60 * 60 * 1000) return cached; + } + } + + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), 10_000); + let json: any; + try { + const resp = await fetch(`https://api.github.com/repos/${REPO}/releases/latest`, { + signal: controller.signal, + headers: { Accept: 'application/vnd.github+json' }, + }); + clearTimeout(timer); + if (!resp.ok) return null; + json = await resp.json(); + } catch { + clearTimeout(timer); + return null; + } + + const tag: string = json.tag_name ?? ''; + const apkAsset = json.assets?.find((a: any) => (a.name as string).endsWith('.apk')); + + const info: UpdateInfo = { + available: isNewer(tag, APP_VERSION), + latestVersion: tag, + downloadUrl: apkAsset?.browser_download_url ?? json.html_url, + releaseUrl: json.html_url ?? '', + releaseNotes: json.body ?? '', + checkedAt: now, + }; + + await AsyncStorage.setItem(CHECK_KEY, JSON.stringify(info)); + return info; + } catch { + return null; + } +} + +export async function getCachedUpdateInfo(): Promise { + try { + const raw = await AsyncStorage.getItem(CHECK_KEY); + return raw ? JSON.parse(raw) : null; + } catch { + return null; + } +} + +/** Returns true if this version was already shown to the user */ +export async function wasUpdateSeen(version: string): Promise { + try { + const seen = await AsyncStorage.getItem(SEEN_KEY); + return seen === version; + } catch { + return false; + } +} + +export async function markUpdateSeen(version: string): Promise { + await AsyncStorage.setItem(SEEN_KEY, version); +} diff --git a/src/widgets/ShiftWidget.tsx b/src/widgets/ShiftWidget.tsx index 1c61e02..6523542 100644 --- a/src/widgets/ShiftWidget.tsx +++ b/src/widgets/ShiftWidget.tsx @@ -76,50 +76,45 @@ function FlightRow({ flight, index }: { flight: WidgetFlight; index: number }) { /> - {/* Bottom row: CI pill + Gate pill */} + {/* Bottom row 1: CI timing + Gate timing */} - {/* CI pill */} - - + + - {/* Gate pill */} - - + + + + + + {/* Bottom row 2: stand / check-in desk / gate number from staffMonitor */} + + + + + + + + + + + + diff --git a/src/widgets/widgetTaskHandler.tsx b/src/widgets/widgetTaskHandler.tsx index 6881820..93e86d9 100644 --- a/src/widgets/widgetTaskHandler.tsx +++ b/src/widgets/widgetTaskHandler.tsx @@ -2,8 +2,8 @@ import React from 'react'; import AsyncStorage from '@react-native-async-storage/async-storage'; import type { WidgetTaskHandlerProps } from 'react-native-android-widget'; import type { HexColor } from '../utils/airlineOps'; -import { getAirlineOps, getAirlineColor, ALLOWED_AIRLINES } from '../utils/airlineOps'; -import { getStoredAirportCode, buildFr24ScheduleUrl } from '../utils/airportSettings'; +import { getAirlineOps, getAirlineColor } from '../utils/airlineOps'; +import { getStoredAirportCode, buildFr24ScheduleUrl, getAirportAirlines } from '../utils/airportSettings'; import { ShiftWidget } from './ShiftWidget'; /** Key used by the main app (FlightScreen) to push pre-built widget data */ @@ -24,6 +24,9 @@ export type WidgetFlight = { airlineColor: HexColor; departureTs: number; isPinned?: boolean; + stand?: string; + checkin?: string; + gate?: string; }; export type WidgetData = @@ -39,11 +42,44 @@ export type WidgetShiftData = { isRestDay: boolean; }; +function fmtTs(ts: number): string { + const d = new Date(ts * 1000); + return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`; +} + // ─── Read cached data written by the main app ────────────────────────────────── async function getWidgetData(): Promise { try { + const todayIso = new Date().toISOString().split('T')[0]; + const shiftRaw = await AsyncStorage.getItem(WIDGET_SHIFT_KEY); + + if (shiftRaw) { + const shiftData: WidgetShiftData = JSON.parse(shiftRaw); + if (shiftData.date === todayIso) { + // Shift key is authoritative for today's work/rest classification. + if (shiftData.isRestDay) return { state: 'rest' }; + if (!shiftData.shiftToday) return { state: 'no_shift' }; + + // It's a work day β€” return cached flight data only if it's also a 'work' state. + // A stale 'rest' in the cache must not override the shift key. + const cached = await AsyncStorage.getItem(WIDGET_CACHE_KEY); + if (cached) { + const data: WidgetData = JSON.parse(cached); + if (data.state === 'work' || data.state === 'work_empty') return data; + } + // Cache is stale or missing β€” show work_empty until periodic update runs. + const { start, end } = shiftData.shiftToday; + return { state: 'work_empty', shiftLabel: `${fmtTs(start)} – ${fmtTs(end)}`, updatedAt: '' }; + } + } + + // Shift key is missing or from a different day β€” fall back to cache. const cached = await AsyncStorage.getItem(WIDGET_CACHE_KEY); - if (cached) return JSON.parse(cached); + if (!cached) return { state: 'error' }; + const data: WidgetData = JSON.parse(cached); + // Rest/no_shift cached but shift key is stale β†’ treat as no_shift. + if (data.state === 'rest' || data.state === 'no_shift') return { state: 'no_shift' }; + return data; } catch {} return { state: 'error' }; } @@ -64,6 +100,9 @@ async function fetchFreshWidgetData(): Promise { const shiftToday = shiftData.shiftToday; const airportCode = await getStoredAirportCode(); + const allAirlines = getAirportAirlines(airportCode); + const filterRaw = await AsyncStorage.getItem('aerostaff_flight_filter_v1'); + const allowedAirlines: string[] = filterRaw ? JSON.parse(filterRaw) : allAirlines; const url = buildFr24ScheduleUrl(airportCode); const controller = new AbortController(); @@ -77,16 +116,12 @@ async function fetchFreshWidgetData(): Promise { clearTimeout(timer); } - const fmtT = (ts: number) => { - const d = new Date(ts * 1000); - return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`; - }; - const fmtOff = (dep: number, off: number) => fmtT(dep - off * 60); - const nowHH = fmtT(Date.now() / 1000); - const shiftLabel = `${fmtT(shiftToday.start)} – ${fmtT(shiftToday.end)}`; + const fmtOff = (dep: number, off: number) => fmtTs(dep - off * 60); + const nowHH = fmtTs(Date.now() / 1000); + const shiftLabel = `${fmtTs(shiftToday.start)} – ${fmtTs(shiftToday.end)}`; const filteredDeps = allDepartures.filter(item => - ALLOWED_AIRLINES.some(key => (item.flight?.airline?.name || '').toLowerCase().includes(key)), + allowedAirlines.some(key => (item.flight?.airline?.name || '').toLowerCase().includes(key)), ); const wFlights: WidgetFlight[] = filteredDeps @@ -108,7 +143,7 @@ async function fetchFreshWidgetData(): Promise { flightNumber: fn, destinationIata: item.flight?.airport?.destination?.code?.iata || '???', departureTs: ts, - departureTime: fmtT(ts), + departureTime: fmtTs(ts), ciOpen: fmtOff(ts, ops.checkInOpen), ciClose: fmtOff(ts, ops.checkInClose), gateOpen: fmtOff(ts, ops.gateOpen), gateClose: fmtOff(ts, ops.gateClose), airlineColor: getAirlineColor(airline),