diff --git a/README.md b/README.md
index bf87585..3c9a1f8 100644
--- a/README.md
+++ b/README.md
@@ -2,15 +2,15 @@
Piattaforma mobile-first per streaming di partite sportive giovanili — MVP su **Match Live TV** (HLS sul nostro sito); YouTube/Facebook/Twitch come integrazioni premium.
-**Slogan:** *Lo streaming che non muore*
+**Slogan:** *Ogni partita, ogni evento, per i tuoi tifosi.*
## Struttura monorepo
```
-├── backend/ # Rails 7.2 API + Action Cable + Sidekiq
-├── mobile/ # Flutter app (Camera + Regia)
-├── infra/ # Docker Compose, MediaMTX, Nginx, Kamal
-└── docs/ # Documentazione
+├── backend/ # Rails 7.2 API + Action Cable + Sidekiq
+├── native/android/ # App Android nativa (Kotlin + Compose)
+├── infra/ # Docker Compose, MediaMTX, Nginx, Kamal
+└── docs/ # Documentazione
```
## Deploy produzione (server 192.168.1.146)
@@ -45,15 +45,16 @@ email: coach@matchlivetv.test
password: password123
```
-## Mobile
+## App Android nativa
```bash
-cd mobile
-flutter pub get
-flutter run
+./scripts/build_native_android_apk_prod.sh
+adb install -r native/android/app/build/outputs/apk/release/app-release.apk
```
-Configura `API_BASE_URL` in `lib/core/config.dart` (default `http://10.0.2.2:3000` per emulatore Android).
+API produzione: `https://www.matchlivetv.it` (override con `-PAPI_BASE_URL=...` in Gradle).
+
+Vedi [native/android/README.md](native/android/README.md).
## Architettura
@@ -62,9 +63,9 @@ Configura `API_BASE_URL` in `lib/core/config.dart` (default `http://10.0.2.2:300
- Controllo: Phone ↔ Action Cable ↔ Rails ↔ PostgreSQL
- Disconnessioni: MediaMTX `alwaysAvailable` slate + timeout 5 min (Sidekiq)
-### Mobile produzione
+### App Android produzione
```bash
-./scripts/run_mobile_android_prod.sh
+./scripts/build_native_android_apk_prod.sh
# API: https://www.matchlivetv.it
```
diff --git a/docs/infrastructure/SERVER_DEPLOYMENT.md b/docs/infrastructure/SERVER_DEPLOYMENT.md
index 8d2f1a1..729aa16 100644
--- a/docs/infrastructure/SERVER_DEPLOYMENT.md
+++ b/docs/infrastructure/SERVER_DEPLOYMENT.md
@@ -156,7 +156,7 @@ Dalla macchina di sviluppo:
```bash
# Tar + scp (rsync non installato sul server)
tar -C /path/to/MatchLiveTV/src -czf /tmp/matchlivetv-deploy.tar.gz \
- --exclude='./mobile/build' --exclude='./backend/log' --exclude='./.git' .
+ --exclude='./native/android/build' --exclude='./backend/log' --exclude='./.git' .
scp /tmp/matchlivetv-deploy.tar.gz eminux@192.168.1.146:~/
ssh eminux@192.168.1.146 'tar -xzf ~/matchlivetv-deploy.tar.gz -C ~/matchlivetv'
diff --git a/mobile/.gitignore b/mobile/.gitignore
deleted file mode 100644
index 3820a95..0000000
--- a/mobile/.gitignore
+++ /dev/null
@@ -1,45 +0,0 @@
-# Miscellaneous
-*.class
-*.log
-*.pyc
-*.swp
-.DS_Store
-.atom/
-.build/
-.buildlog/
-.history
-.svn/
-.swiftpm/
-migrate_working_dir/
-
-# IntelliJ related
-*.iml
-*.ipr
-*.iws
-.idea/
-
-# The .vscode folder contains launch configuration and tasks you configure in
-# VS Code which you may wish to be included in version control, so this line
-# is commented out by default.
-#.vscode/
-
-# Flutter/Dart/Pub related
-**/doc/api/
-**/ios/Flutter/.last_build_id
-.dart_tool/
-.flutter-plugins-dependencies
-.pub-cache/
-.pub/
-/build/
-/coverage/
-
-# Symbolication related
-app.*.symbols
-
-# Obfuscation related
-app.*.map.json
-
-# Android Studio will place build artifacts here
-/android/app/debug
-/android/app/profile
-/android/app/release
diff --git a/mobile/.metadata b/mobile/.metadata
deleted file mode 100644
index a906de7..0000000
--- a/mobile/.metadata
+++ /dev/null
@@ -1,45 +0,0 @@
-# This file tracks properties of this Flutter project.
-# Used by Flutter tool to assess capabilities and perform upgrades etc.
-#
-# This file should be version controlled and should not be manually edited.
-
-version:
- revision: "559ffa3f75e7402d65a8def9c28389a9b2e6fe42"
- channel: "[user-branch]"
-
-project_type: app
-
-# Tracks metadata for the flutter migrate command
-migration:
- platforms:
- - platform: root
- create_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42
- base_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42
- - platform: android
- create_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42
- base_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42
- - platform: ios
- create_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42
- base_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42
- - platform: linux
- create_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42
- base_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42
- - platform: macos
- create_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42
- base_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42
- - platform: web
- create_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42
- base_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42
- - platform: windows
- create_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42
- base_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42
-
- # User provided section
-
- # List of Local paths (relative to this file) that should be
- # ignored by the migrate tool.
- #
- # Files that are not part of the templates will be ignored by default.
- unmanaged_files:
- - 'lib/main.dart'
- - 'ios/Runner.xcodeproj/project.pbxproj'
diff --git a/mobile/README.md b/mobile/README.md
deleted file mode 100644
index 15db89a..0000000
--- a/mobile/README.md
+++ /dev/null
@@ -1,17 +0,0 @@
-# match_live_tv
-
-A new Flutter project.
-
-## Getting Started
-
-This project is a starting point for a Flutter application.
-
-A few resources to get you started if this is your first Flutter project:
-
-- [Learn Flutter](https://docs.flutter.dev/get-started/learn-flutter)
-- [Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
-- [Flutter learning resources](https://docs.flutter.dev/reference/learning-resources)
-
-For help getting started with Flutter development, view the
-[online documentation](https://docs.flutter.dev/), which offers tutorials,
-samples, guidance on mobile development, and a full API reference.
diff --git a/mobile/analysis_options.yaml b/mobile/analysis_options.yaml
deleted file mode 100644
index 0d29021..0000000
--- a/mobile/analysis_options.yaml
+++ /dev/null
@@ -1,28 +0,0 @@
-# This file configures the analyzer, which statically analyzes Dart code to
-# check for errors, warnings, and lints.
-#
-# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
-# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
-# invoked from the command line by running `flutter analyze`.
-
-# The following line activates a set of recommended lints for Flutter apps,
-# packages, and plugins designed to encourage good coding practices.
-include: package:flutter_lints/flutter.yaml
-
-linter:
- # The lint rules applied to this project can be customized in the
- # section below to disable rules from the `package:flutter_lints/flutter.yaml`
- # included above or to enable additional rules. A list of all available lints
- # and their documentation is published at https://dart.dev/lints.
- #
- # Instead of disabling a lint rule for the entire project in the
- # section below, it can also be suppressed for a single line of code
- # or a specific dart file by using the `// ignore: name_of_lint` and
- # `// ignore_for_file: name_of_lint` syntax on the line or in the file
- # producing the lint.
- rules:
- # avoid_print: false # Uncomment to disable the `avoid_print` rule
- # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
-
-# Additional information about this file can be found at
-# https://dart.dev/guides/language/analysis-options
diff --git a/mobile/android/.gitignore b/mobile/android/.gitignore
deleted file mode 100644
index be3943c..0000000
--- a/mobile/android/.gitignore
+++ /dev/null
@@ -1,14 +0,0 @@
-gradle-wrapper.jar
-/.gradle
-/captures/
-/gradlew
-/gradlew.bat
-/local.properties
-GeneratedPluginRegistrant.java
-.cxx/
-
-# Remember to never publicly share your keystore.
-# See https://flutter.dev/to/reference-keystore
-key.properties
-**/*.keystore
-**/*.jks
diff --git a/mobile/android/app/build.gradle.kts b/mobile/android/app/build.gradle.kts
deleted file mode 100644
index 46895e5..0000000
--- a/mobile/android/app/build.gradle.kts
+++ /dev/null
@@ -1,59 +0,0 @@
-plugins {
- id("com.android.application")
- // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
- id("dev.flutter.flutter-gradle-plugin")
-}
-
-android {
- namespace = "com.matchlivetv.match_live_tv"
- compileSdk = flutter.compileSdkVersion
- ndkVersion = flutter.ndkVersion
-
- compileOptions {
- sourceCompatibility = JavaVersion.VERSION_17
- targetCompatibility = JavaVersion.VERSION_17
- }
-
- defaultConfig {
- // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
- applicationId = "com.matchlivetv.match_live_tv"
- // You can update the following values to match your application needs.
- // For more information, see: https://flutter.dev/to/review-gradle-config.
- minSdk = maxOf(flutter.minSdkVersion, 21)
- targetSdk = flutter.targetSdkVersion
- versionCode = flutter.versionCode
- versionName = flutter.versionName
- }
-
- buildTypes {
- release {
- // TODO: Add your own signing config for the release build.
- // Signing with the debug keys for now, so `flutter run --release` works.
- signingConfig = signingConfigs.getByName("debug")
- proguardFiles(
- getDefaultProguardFile("proguard-android-optimize.txt"),
- "proguard-rules.pro"
- )
- }
- }
-}
-
-kotlin {
- compilerOptions {
- jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17
- }
-}
-
-flutter {
- source = "../.."
-}
-
-dependencies {
- val cameraxVersion = "1.4.1"
-
- implementation("androidx.camera:camera-core:$cameraxVersion")
- implementation("androidx.camera:camera-camera2:$cameraxVersion")
- implementation("androidx.camera:camera-lifecycle:$cameraxVersion")
- implementation("androidx.camera:camera-view:$cameraxVersion")
- implementation("com.github.pedroSG94.RootEncoder:library:2.5.5")
-}
diff --git a/mobile/android/app/proguard-rules.pro b/mobile/android/app/proguard-rules.pro
deleted file mode 100644
index 57c43b6..0000000
--- a/mobile/android/app/proguard-rules.pro
+++ /dev/null
@@ -1,2 +0,0 @@
-# RootEncoder / SLF4J (R8 release)
--dontwarn org.slf4j.impl.StaticLoggerBinder
diff --git a/mobile/android/app/src/debug/AndroidManifest.xml b/mobile/android/app/src/debug/AndroidManifest.xml
deleted file mode 100644
index 399f698..0000000
--- a/mobile/android/app/src/debug/AndroidManifest.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
diff --git a/mobile/android/app/src/main/AndroidManifest.xml b/mobile/android/app/src/main/AndroidManifest.xml
deleted file mode 100644
index 8c44282..0000000
--- a/mobile/android/app/src/main/AndroidManifest.xml
+++ /dev/null
@@ -1,66 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/mobile/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/MainActivity.kt b/mobile/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/MainActivity.kt
deleted file mode 100644
index d6348bd..0000000
--- a/mobile/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/MainActivity.kt
+++ /dev/null
@@ -1,27 +0,0 @@
-package com.matchlivetv.match_live_tv
-
-import android.content.res.Configuration
-import android.os.Bundle
-import io.flutter.embedding.android.FlutterFragmentActivity
-import io.flutter.embedding.engine.FlutterEngine
-
-class MainActivity : FlutterFragmentActivity() {
-
- override fun onConfigurationChanged(newConfig: Configuration) {
- StreamingEngineHolder.engine?.pauseGlDuringRotation()
- super.onConfigurationChanged(newConfig)
- }
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- // Termina eventuale diretta rimasta attiva da sessione precedente
- StreamingEngineHolder.engine?.stopStream()
- StreamingEngineHolder.release()
- startService(StreamingForegroundService.stopIntent(this))
- }
-
- override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
- super.configureFlutterEngine(flutterEngine)
- flutterEngine.plugins.add(StreamingPlugin())
- }
-}
diff --git a/mobile/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/StreamPreviewHolder.kt b/mobile/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/StreamPreviewHolder.kt
deleted file mode 100644
index c93c24a..0000000
--- a/mobile/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/StreamPreviewHolder.kt
+++ /dev/null
@@ -1,9 +0,0 @@
-package com.matchlivetv.match_live_tv
-
-import com.pedro.library.view.OpenGlView
-
-/** Riferimento condiviso alla surface di preview tra PlatformView e StreamingEngine. */
-object StreamPreviewHolder {
- @Volatile
- var openGlView: OpenGlView? = null
-}
diff --git a/mobile/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/StreamingEngine.kt b/mobile/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/StreamingEngine.kt
deleted file mode 100644
index f52af47..0000000
--- a/mobile/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/StreamingEngine.kt
+++ /dev/null
@@ -1,1065 +0,0 @@
-package com.matchlivetv.match_live_tv
-
-import android.content.Context
-import android.media.MediaCodecInfo
-import android.os.Build
-import android.os.Handler
-import android.os.Looper
-import android.os.SystemClock
-import android.util.Log
-import androidx.annotation.RequiresApi
-import com.pedro.common.ConnectChecker
-import com.pedro.library.generic.GenericStream
-import com.pedro.library.util.BitrateAdapter
-import com.pedro.library.util.SensorRotationManager
-import com.pedro.library.view.OpenGlView
-import java.util.concurrent.atomic.AtomicBoolean
-import java.util.concurrent.atomic.AtomicInteger
-
-data class StreamingConfig(
- val rtmpUrl: String,
- val width: Int = 1280,
- val height: Int = 720,
- val videoBitrate: Int = 3_000_000,
- val audioBitrate: Int = 128_000,
- val fps: Int = 30,
- val rotation: Int = 0,
- val portrait: Boolean = false,
- // Allineato a MediaMTX alwaysAvailable slate (offline.mp4: 48 kHz mono)
- val sampleRate: Int = 48_000,
- val stereo: Boolean = false,
- val maxReconnectAttempts: Int = 10,
- val reconnectDelayMs: Long = 5_000L,
-)
-
-data class StreamingMetrics(
- val isStreaming: Boolean,
- val isPreviewActive: Boolean,
- val isConnected: Boolean,
- val state: String,
- val bitrateKbps: Long,
- val fps: Int,
- val durationMs: Long,
- val reconnectAttempts: Int,
- val lastError: String?,
-) {
- fun toMap(): Map = mapOf(
- "isStreaming" to isStreaming,
- "isPreviewActive" to isPreviewActive,
- "isConnected" to isConnected,
- "state" to state,
- "bitrateKbps" to bitrateKbps,
- "fps" to fps,
- "durationMs" to durationMs,
- "reconnectAttempts" to reconnectAttempts,
- "lastError" to lastError,
- )
-}
-
-enum class StreamingState {
- IDLE,
- PREVIEWING,
- CONNECTING,
- STREAMING,
- RECONNECTING,
- STOPPING,
- ERROR,
-}
-
-interface StreamingEngineListener {
- fun onStateChanged(state: StreamingState, message: String? = null)
- fun onMetricsUpdated(metrics: StreamingMetrics)
- fun onError(message: String)
-}
-
-@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
-class StreamingEngine(
- private val appContext: Context,
-) : ConnectChecker {
-
- private val mainHandler = Handler(Looper.getMainLooper())
- private val listeners = mutableSetOf()
-
- private var genericStream: GenericStream? = null
- private var previewSurface: OpenGlView? = null
-
- private var config: StreamingConfig? = null
- private var state: StreamingState = StreamingState.IDLE
- private var streamStartedAtMs: Long = 0L
- private var currentBitrateKbps: Long = 0L
- private var currentFps: Int = 0
- private var lastError: String? = null
- private var lastVideoFrameCount: Long = 0L
- private var lastFpsSampleAtMs: Long = 0L
- private var videoStallChecks: Int = 0
- private var lastStallFrameCount: Long = -1L
- private val reconnectAttempts = AtomicInteger(0)
- private val isReleased = AtomicBoolean(false)
- private val pausingIntentionally = AtomicBoolean(false)
- private var previewRebindUntilMs = 0L
- private var previewRebindAttempts = 0
- private var sensorRotationManager: SensorRotationManager? = null
- private var surfaceLost = false
- private var rebindInProgress = false
- private var pendingRotation: Int? = null
- private var pendingIsPortrait: Boolean? = null
- private var lastAppliedRotation = -1
-
- private val bitrateAdapter = BitrateAdapter { adaptedBitrate ->
- genericStream?.setVideoBitrateOnFly(adaptedBitrate)
- }
-
- private val idlePreviewRunnable = Runnable {
- val glView = previewSurface ?: return@Runnable
- if (!canAttachPreview(glView)) {
- if (previewRebindAttempts < PREVIEW_REBIND_MAX_ATTEMPTS) {
- scheduleIdlePreviewAttach(previewRebindAttempts + 1)
- }
- return@Runnable
- }
- finishIdlePreviewBind(glView)
- }
-
- private val surfaceReadyRunnable = Runnable {
- if (isReleased.get() || rebindInProgress) {
- return@Runnable
- }
- val glView = previewSurface ?: return@Runnable
- if (!canAttachPreview(glView)) {
- if (previewRebindAttempts < PREVIEW_REBIND_MAX_ATTEMPTS) {
- scheduleSurfaceReady(previewRebindAttempts + 1)
- }
- return@Runnable
- }
- surfaceLost = false
- val stream = genericStream
- if (stream != null && (stream.isStreaming || stream.isOnPreview)) {
- safeReattachPreview(glView)
- flushPendingOrientation()
- } else {
- attachPreviewToStream()
- }
- }
- private val metricsRunnable = object : Runnable {
- override fun run() {
- emitMetrics()
- if (state == StreamingState.STREAMING ||
- state == StreamingState.CONNECTING ||
- state == StreamingState.RECONNECTING ||
- state == StreamingState.PREVIEWING
- ) {
- mainHandler.postDelayed(this, METRICS_INTERVAL_MS)
- }
- }
- }
-
- fun addListener(listener: StreamingEngineListener) {
- listeners.add(listener)
- }
-
- fun removeListener(listener: StreamingEngineListener) {
- listeners.remove(listener)
- }
-
- fun setupPreviewGlView(glView: StreamingOpenGlView) {
- ensureMainThread()
- updatePreservePipelineFlag(glView)
- glView.onPreviewSurfaceLost = { onPlatformSurfaceDestroyed() }
- }
-
- fun attachPreviewHost(glView: StreamingOpenGlView) {
- ensureMainThread()
- setupPreviewGlView(glView)
- bindStreamPreview(glView)
- }
-
- private fun updatePreservePipelineFlag(glView: StreamingOpenGlView?) {
- glView?.preservePipelineOnSurfaceLoss =
- state == StreamingState.STREAMING ||
- state == StreamingState.CONNECTING ||
- state == StreamingState.RECONNECTING
- }
-
- /** Surface OpenGL pronta (debounced). */
- fun onPlatformSurfaceReady(glView: OpenGlView) {
- ensureMainThread()
- previewSurface = glView
- StreamPreviewHolder.openGlView = glView
- previewRebindAttempts = 0
- mainHandler.removeCallbacks(surfaceReadyRunnable)
- val live = state == StreamingState.STREAMING ||
- state == StreamingState.CONNECTING ||
- state == StreamingState.RECONNECTING
- if (live) {
- scheduleSurfaceReady(0)
- return
- }
- scheduleIdlePreviewAttach(0)
- }
-
- /**
- * Chiamato da MainActivity.onConfigurationChanged *prima* di super, per fermare drawScreen
- * sul thread GL encoder prima che Flutter ricrei le surface (evita GL_OUT_OF_MEMORY).
- */
- fun pauseGlDuringRotation() {
- ensureMainThread()
- if (isReleased.get()) {
- return
- }
- val active = state == StreamingState.STREAMING ||
- state == StreamingState.CONNECTING ||
- state == StreamingState.RECONNECTING ||
- state == StreamingState.PREVIEWING
- if (!active) {
- return
- }
- Log.i(TAG, "pauseGlDuringRotation state=$state")
- beginSurfaceTransition(SURFACE_TRANSITION_GRACE_MS)
- }
-
- /** Chiamato quando Flutter distrugge la surface PlatformView durante la rotazione. */
- fun onPlatformSurfaceDestroyed() {
- ensureMainThread()
- beginSurfaceTransition(SURFACE_TRANSITION_GRACE_MS)
- }
-
- private fun beginSurfaceTransition(graceMs: Long) {
- surfaceLost = true
- markPreviewRebinding(graceMs)
- mainHandler.removeCallbacks(surfaceReadyRunnable)
- mainHandler.removeCallbacks(idlePreviewRunnable)
- mainHandler.removeCallbacks(orientationApplyRunnable)
- mainHandler.removeCallbacks(postRebindOrientationRunnable)
- stopPreviewGlRender()
- try {
- genericStream?.getGlInterface()?.setForceRender(false)
- genericStream?.getGlInterface()?.deAttachPreview()
- } catch (exception: Exception) {
- Log.w(TAG, "beginSurfaceTransition: ${exception.message}")
- }
- }
-
- private fun stopPreviewGlRender() {
- listOfNotNull(previewSurface, StreamPreviewHolder.openGlView)
- .distinct()
- .forEach { glView ->
- (glView as? StreamingOpenGlView)?.let { updatePreservePipelineFlag(it) }
- try {
- glView.setForceRender(false)
- } catch (_: Exception) {
- }
- }
- }
-
- /** Solo orientamento encoder: niente rebind preview (evita crash GL in rotazione). */
- fun applyOrientationFromSensor() {
- ensureMainThread()
- refreshOrientationSensor()
- }
-
- fun bindStreamPreview(glView: OpenGlView) {
- ensureMainThread()
- previewSurface = glView
- StreamPreviewHolder.openGlView = glView
- (glView as? StreamingOpenGlView)?.let { updatePreservePipelineFlag(it) }
- val live = state == StreamingState.STREAMING ||
- state == StreamingState.CONNECTING ||
- state == StreamingState.RECONNECTING
- if (live) {
- onPlatformSurfaceReady(glView)
- return
- }
- bindPreviewForIdle(glView)
- }
-
- private fun finishIdlePreviewBind(glView: OpenGlView) {
- val stream = genericStream ?: return
- if (stream.isOnPreview) {
- return
- }
- try {
- attachPreviewToStream()
- ensureOrientationSensor()
- updateState(StreamingState.PREVIEWING)
- startMetricsLoop()
- } catch (exception: Exception) {
- Log.w(TAG, "finishIdlePreviewBind failed: ${exception.message}")
- if (previewRebindAttempts < PREVIEW_REBIND_MAX_ATTEMPTS) {
- scheduleIdlePreviewAttach(previewRebindAttempts + 1)
- }
- }
- }
-
- private fun bindPreviewForIdle(glView: OpenGlView) {
- val surfaceChanged = previewSurface != null && previewSurface !== glView
- previewSurface = glView
-
- if (state == StreamingState.STREAMING ||
- state == StreamingState.CONNECTING ||
- state == StreamingState.RECONNECTING
- ) {
- return
- }
-
- if (surfaceChanged) {
- genericStream?.let { stream ->
- if (stream.isOnPreview) {
- stream.stopPreview()
- }
- }
- }
-
- val cfg = config ?: previewConfig()
- val stream = genericStream ?: obtainGenericStream(cfg)
- genericStream = stream
-
- if (!stream.isOnPreview) {
- stopPreviewAndStreamForPrepare(stream)
- val prepared = try {
- stream.prepareVideo(
- width = cfg.width,
- height = cfg.height,
- bitrate = cfg.videoBitrate,
- fps = cfg.fps,
- iFrameInterval = 1,
- rotation = cfg.rotation,
- profile = MediaCodecInfo.CodecProfileLevel.AVCProfileBaseline,
- level = MediaCodecInfo.CodecProfileLevel.AVCLevel31,
- ) && stream.prepareAudio(
- cfg.sampleRate,
- cfg.stereo,
- cfg.audioBitrate,
- )
- } catch (_: Exception) {
- false
- }
- if (!prepared) {
- notifyError("Impossibile preparare la preview camera")
- return
- }
- }
-
- if (!canAttachPreview(glView)) {
- Log.i(TAG, "bindPreviewForIdle waiting for surface state=$state")
- scheduleIdlePreviewAttach(0)
- return
- }
- finishIdlePreviewBind(glView)
- }
-
- fun unbindStreamPreview() {
- ensureMainThread()
- previewSurface = null
- genericStream?.let { stream ->
- if (stream.isOnPreview) {
- stream.stopPreview()
- }
- }
- if (state == StreamingState.PREVIEWING) {
- updateState(StreamingState.IDLE)
- }
- }
-
- /** Ripresa dopo pausa: stesso percorso di startStream (evita RTMP solo-audio dopo reconnect). */
- fun resumeStream(streamConfig: StreamingConfig) {
- ensureMainThread()
- Log.i(TAG, "resumeStream reset url=${streamConfig.rtmpUrl}")
- stopStream()
- updateState(StreamingState.IDLE)
- mainHandler.postDelayed({
- if (!isReleased.get()) {
- startStream(streamConfig)
- }
- }, 250)
- }
-
- fun startStream(streamConfig: StreamingConfig) {
- ensureMainThread()
- Log.i(TAG, "startStream state=$state url=${streamConfig.rtmpUrl} bitrate=${streamConfig.videoBitrate} fps=${streamConfig.fps}")
- if (isReleased.get()) {
- notifyError("StreamingEngine rilasciato")
- return
- }
- // Già in onda sullo stesso endpoint RTMP.
- if (state == StreamingState.STREAMING &&
- config?.rtmpUrl == streamConfig.rtmpUrl &&
- genericStream?.isStreaming == true
- ) {
- return
- }
- // Nuova sessione o retry dopo errore: reset completo (evita startStream ignorato).
- if (state != StreamingState.IDLE && state != StreamingState.PREVIEWING) {
- stopStream()
- }
-
- config = streamConfig
- lastError = null
- reconnectAttempts.set(0)
-
- val stream = genericStream ?: obtainGenericStream(streamConfig)
- genericStream = stream
- stopPreviewAndStreamForPrepare(stream)
-
- val prepared = try {
- stream.prepareVideo(
- width = streamConfig.width,
- height = streamConfig.height,
- bitrate = streamConfig.videoBitrate,
- fps = streamConfig.fps,
- iFrameInterval = 1,
- rotation = streamConfig.rotation,
- profile = MediaCodecInfo.CodecProfileLevel.AVCProfileBaseline,
- level = MediaCodecInfo.CodecProfileLevel.AVCLevel31,
- ) && stream.prepareAudio(
- streamConfig.sampleRate,
- streamConfig.stereo,
- streamConfig.audioBitrate,
- )
- } catch (exception: IllegalArgumentException) {
- false
- } catch (exception: IllegalStateException) {
- false
- }
-
- if (!prepared) {
- notifyError("Configurazione audio/video non valida")
- updateState(StreamingState.ERROR, "prepare failed")
- return
- }
-
- attachPreviewToStream()
- lastAppliedRotation = -1
- val portraitHint = streamConfig.portrait || streamConfig.height > streamConfig.width
- val cameraRotation = if (portraitHint && streamConfig.rotation == 0) 90 else streamConfig.rotation
- applyGlOrientation(
- rotation = cameraRotation,
- isPortrait = portraitHint,
- logLabel = "startStream",
- )
- ensureOrientationSensor()
- resetFpsTracking()
- videoStallChecks = 0
- lastStallFrameCount = -1L
-
- stream.getStreamClient().setReTries(streamConfig.maxReconnectAttempts)
- bitrateAdapter.setMaxBitrate(streamConfig.videoBitrate + streamConfig.audioBitrate)
- waitForPreviewThenPublish(streamConfig, attempt = 0)
- }
-
- private fun waitForPreviewThenPublish(streamConfig: StreamingConfig, attempt: Int) {
- if (isReleased.get()) {
- return
- }
- attachPreviewToStream()
- val stream = genericStream
- val previewReady = stream?.isOnPreview == true && previewGlView() != null
- if (!previewReady) {
- if (attempt >= 40) {
- notifyError("Anteprima camera non pronta — impossibile inviare video")
- updateState(StreamingState.ERROR, "preview_not_ready")
- return
- }
- mainHandler.postDelayed({
- waitForPreviewThenPublish(streamConfig, attempt + 1)
- }, 50L)
- return
- }
-
- mainHandler.postDelayed({
- if (isReleased.get()) {
- return@postDelayed
- }
- val activeStream = genericStream ?: return@postDelayed
- streamStartedAtMs = SystemClock.elapsedRealtime()
- updateState(StreamingState.CONNECTING)
- Log.i(TAG, "publish RTMP url=${streamConfig.rtmpUrl}")
- activeStream.startStream(streamConfig.rtmpUrl)
- startMetricsLoop()
- }, 350L)
- }
-
- fun pauseStream() {
- ensureMainThread()
- Log.i(TAG, "pauseStream state=$state")
- if (state != StreamingState.STREAMING &&
- state != StreamingState.CONNECTING &&
- state != StreamingState.RECONNECTING
- ) {
- return
- }
-
- stopMetricsLoop()
- pausingIntentionally.set(true)
- genericStream?.let { stream ->
- if (stream.isStreaming) {
- stream.stopStream()
- }
- }
- pausingIntentionally.set(false)
-
- streamStartedAtMs = 0L
- currentBitrateKbps = 0L
- currentFps = 0
- reconnectAttempts.set(0)
- attachPreviewToStream()
- updateState(StreamingState.PREVIEWING)
- startMetricsLoop()
- }
-
- fun stopStream() {
- ensureMainThread()
- Log.i(TAG, "stopStream state=$state")
- if (state == StreamingState.IDLE || state == StreamingState.STOPPING) {
- return
- }
-
- updateState(StreamingState.STOPPING)
- stopMetricsLoop()
- stopOrientationSensor()
- (previewSurface as? StreamingOpenGlView)?.preservePipelineOnSurfaceLoss = false
-
- genericStream?.let { stream ->
- if (stream.isStreaming) {
- stream.stopStream()
- }
- if (stream.isOnPreview) {
- stream.stopPreview()
- }
- stream.release()
- }
- genericStream = null
-
- streamStartedAtMs = 0L
- currentBitrateKbps = 0L
- currentFps = 0
- reconnectAttempts.set(0)
- updateState(StreamingState.IDLE)
- emitMetrics()
- }
-
- fun release() {
- ensureMainThread()
- if (!isReleased.compareAndSet(false, true)) {
- return
- }
- stopStream()
- unbindStreamPreview()
- listeners.clear()
- }
-
- fun getMetrics(): StreamingMetrics = buildMetrics()
-
- override fun onConnectionStarted(url: String) {
- postMain {
- Log.i(TAG, "onConnectionStarted url=$url")
- updateState(StreamingState.CONNECTING, url)
- }
- }
-
- override fun onConnectionSuccess() {
- postMain {
- Log.i(TAG, "onConnectionSuccess")
- reconnectAttempts.set(0)
- lastError = null
- resetFpsTracking()
- videoStallChecks = 0
- lastStallFrameCount = genericStream?.getStreamClient()?.getSentVideoFrames() ?: 0L
- (previewSurface as? StreamingOpenGlView)?.let { updatePreservePipelineFlag(it) }
- updateState(StreamingState.STREAMING)
- }
- }
-
- override fun onConnectionFailed(reason: String) {
- postMain {
- if (pausingIntentionally.get()) {
- return@postMain
- }
- Log.w(TAG, "onConnectionFailed reason=$reason state=$state attempts=${reconnectAttempts.get()}")
- lastError = reason
- val stream = genericStream ?: return@postMain
- val currentConfig = config ?: return@postMain
-
- updateState(StreamingState.RECONNECTING, reason)
- val attempt = reconnectAttempts.incrementAndGet()
- val retryScheduled = stream.getStreamClient().reTry(
- currentConfig.reconnectDelayMs,
- reason,
- null,
- )
-
- if (retryScheduled) {
- emitMetrics()
- } else {
- updateState(StreamingState.ERROR, reason)
- notifyError("Connessione RTMP fallita: $reason")
- stream.stopStream()
- }
- }
- }
-
- override fun onNewBitrate(bitrate: Long) {
- postMain {
- currentBitrateKbps = bitrate / 1000
- // Bitrate fissa: l'adattamento on-the-fly causa glitch e GOP instabili su MediaMTX/HLS.
- emitMetrics()
- }
- }
-
- override fun onDisconnect() {
- postMain {
- if (pausingIntentionally.get()) {
- return@postMain
- }
- Log.w(TAG, "onDisconnect state=$state")
- if (state == StreamingState.STREAMING || state == StreamingState.RECONNECTING) {
- updateState(StreamingState.RECONNECTING, "disconnected")
- }
- }
- }
-
- override fun onAuthError() {
- postMain {
- Log.w(TAG, "onAuthError")
- lastError = "auth_error"
- updateState(StreamingState.ERROR, "auth_error")
- genericStream?.stopStream()
- notifyError("Autenticazione RTMP fallita")
- }
- }
-
- override fun onAuthSuccess() {
- postMain {
- Log.i(TAG, "onAuthSuccess")
- updateState(StreamingState.STREAMING)
- }
- }
-
- private fun stopPreviewAndStreamForPrepare(stream: GenericStream) {
- if (stream.isStreaming) {
- stream.stopStream()
- }
- if (stream.isOnPreview) {
- stream.stopPreview()
- }
- }
-
- private fun previewGlView(): OpenGlView? =
- previewSurface ?: StreamPreviewHolder.openGlView
-
- private fun resetFpsTracking() {
- lastFpsSampleAtMs = 0L
- lastVideoFrameCount = 0L
- currentFps = 0
- }
-
- private fun attachPreviewToStream(forceRebind: Boolean = false) {
- val glView = previewGlView() ?: return
- previewSurface = glView
- val stream = genericStream ?: return
- val liveState = state == StreamingState.STREAMING ||
- state == StreamingState.CONNECTING ||
- state == StreamingState.RECONNECTING
- if (liveState && (stream.isStreaming || stream.isOnPreview)) {
- safeReattachPreview(glView)
- return
- }
- if (forceRebind && stream.isOnPreview && !liveState) {
- stream.stopPreview()
- }
- if (!stream.isOnPreview) {
- if (!canAttachPreview(glView)) {
- return
- }
- try {
- stream.startPreview(glView, autoHandle = false)
- ensureOrientationSensor()
- } catch (exception: Exception) {
- Log.w(TAG, "attachPreviewToStream failed: ${exception.message}")
- }
- }
- }
-
- private fun scheduleIdlePreviewAttach(attempt: Int) {
- previewRebindAttempts = attempt
- if (attempt >= PREVIEW_REBIND_MAX_ATTEMPTS) {
- Log.w(TAG, "idle preview attach gave up after $attempt attempts state=$state")
- return
- }
- val delayMs = if (attempt == 0) IDLE_PREVIEW_ATTACH_MS else PREVIEW_REBIND_RETRY_MS
- mainHandler.removeCallbacks(idlePreviewRunnable)
- mainHandler.postDelayed(idlePreviewRunnable, delayMs)
- }
-
- private fun scheduleSurfaceReady(attempt: Int) {
- previewRebindAttempts = attempt
- if (attempt >= PREVIEW_REBIND_MAX_ATTEMPTS) {
- Log.w(TAG, "surface rebind gave up after $attempt attempts state=$state")
- return
- }
- val delayMs = when {
- attempt == 0 -> SURFACE_READY_DEBOUNCE_MS
- else -> PREVIEW_REBIND_RETRY_MS
- }
- mainHandler.postDelayed(surfaceReadyRunnable, delayMs)
- }
-
- private fun markPreviewRebinding(durationMs: Long = PREVIEW_REBIND_GRACE_MS) {
- previewRebindUntilMs = SystemClock.elapsedRealtime() + durationMs
- videoStallChecks = 0
- lastStallFrameCount = -1L
- }
-
- private fun isPreviewRebinding(): Boolean =
- SystemClock.elapsedRealtime() < previewRebindUntilMs
-
- private fun canAttachPreview(glView: OpenGlView): Boolean {
- if (!glView.isAttachedToWindow) {
- return false
- }
- return try {
- val surface = glView.holder?.surface
- surface != null && surface.isValid && glView.width > 0 && glView.height > 0
- } catch (_: Exception) {
- false
- }
- }
-
- private fun safeReattachPreview(glView: OpenGlView) {
- if (rebindInProgress) {
- return
- }
- val stream = genericStream ?: return
- previewSurface = glView
- if (!canAttachPreview(glView)) {
- scheduleSurfaceReady(previewRebindAttempts + 1)
- return
- }
- val surface = glView.holder?.surface
- if (surface == null || !surface.isValid) {
- scheduleSurfaceReady(previewRebindAttempts + 1)
- return
- }
- rebindInProgress = true
- markPreviewRebinding()
- try {
- Log.i(
- TAG,
- "safeReattachPreview state=$state isPreview=${stream.isOnPreview} isStreaming=${stream.isStreaming}",
- )
- val glInterface = stream.getGlInterface()
- val width = glView.width.coerceAtLeast(2)
- val height = glView.height.coerceAtLeast(2)
- glInterface.setForceRender(false)
- glInterface.deAttachPreview()
- glInterface.attachPreview(surface)
- glInterface.setPreviewResolution(width, height)
- glInterface.setForceRender(true, 30)
- } catch (exception: Exception) {
- Log.w(TAG, "safeReattachPreview failed: ${exception.message}")
- scheduleSurfaceReady(previewRebindAttempts + 1)
- } finally {
- rebindInProgress = false
- surfaceLost = false
- finishRotationTransition()
- }
- }
-
- /** Orientamento camera/preview/stream via GL (senza metadata RTMP rotation). */
- private fun applyGlOrientation(
- rotation: Int,
- isPortrait: Boolean,
- logLabel: String,
- forceRender: Boolean = true,
- ) {
- if (surfaceLost || rebindInProgress || isPreviewRebinding()) {
- pendingRotation = rotation
- pendingIsPortrait = isPortrait
- Log.i(TAG, "$logLabel deferred rotation=$rotation portrait=$isPortrait")
- return
- }
- val stream = genericStream ?: return
- val glView = previewSurface
- if (glView != null && !canAttachPreview(glView)) {
- return
- }
- try {
- val glInterface = stream.getGlInterface()
- if (forceRender) {
- glInterface.setForceRender(false)
- }
- glInterface.autoHandleOrientation = false
- glInterface.setIsPortrait(isPortrait)
- glInterface.setCameraOrientation(rotation)
- glInterface.setPreviewRotation(0)
- glInterface.setStreamRotation(0)
- lastAppliedRotation = rotation
- pendingIsPortrait = isPortrait
- Log.i(
- TAG,
- "$logLabel orientation rotation=$rotation portrait=$isPortrait state=$state",
- )
- } catch (exception: Exception) {
- Log.w(TAG, "$logLabel orientation failed: ${exception.message}")
- } finally {
- if (forceRender) {
- try {
- stream.getGlInterface().setForceRender(true, 30)
- } catch (_: Exception) {
- }
- }
- }
- }
-
- private fun ensureOrientationSensor() {
- if (sensorRotationManager != null) {
- return
- }
- sensorRotationManager = SensorRotationManager(appContext, true, true) { rotation, isPortrait ->
- postMain { applySensorRotation(rotation, isPortrait) }
- }
- sensorRotationManager?.start()
- }
-
- private fun refreshOrientationSensor() {
- sensorRotationManager?.stop()
- sensorRotationManager = null
- ensureOrientationSensor()
- }
-
- private fun stopOrientationSensor() {
- sensorRotationManager?.stop()
- sensorRotationManager = null
- }
-
- private val orientationApplyRunnable = Runnable {
- val rotation = pendingRotation ?: return@Runnable
- val isPortrait = pendingIsPortrait ?: return@Runnable
- applySensorRotationNow(rotation, isPortrait)
- }
-
- private fun applySensorRotation(rotation: Int, isPortrait: Boolean) {
- pendingRotation = rotation
- pendingIsPortrait = isPortrait
- mainHandler.removeCallbacks(orientationApplyRunnable)
- mainHandler.postDelayed(orientationApplyRunnable, ORIENTATION_DEBOUNCE_MS)
- }
-
- private fun applySensorRotationNow(rotation: Int, isPortrait: Boolean) {
- if (rotation == lastAppliedRotation && !surfaceLost) {
- return
- }
- if (surfaceLost || rebindInProgress || isPreviewRebinding()) {
- Log.i(TAG, "sensorOrientation queued rotation=$rotation portrait=$isPortrait")
- return
- }
- applyGlOrientation(rotation, isPortrait, "sensorOrientation")
- }
-
- private val postRebindOrientationRunnable = Runnable {
- val rotation = pendingRotation ?: return@Runnable
- val isPortrait = pendingIsPortrait ?: return@Runnable
- if (surfaceLost || rebindInProgress) {
- return@Runnable
- }
- applyGlOrientation(rotation, isPortrait, "postRebind")
- }
-
- private fun finishRotationTransition() {
- mainHandler.removeCallbacks(postRebindOrientationRunnable)
- mainHandler.postDelayed(postRebindOrientationRunnable, POST_REBIND_ORIENTATION_MS)
- }
-
- private fun flushPendingOrientation() {
- mainHandler.removeCallbacks(orientationApplyRunnable)
- mainHandler.postDelayed(orientationApplyRunnable, ORIENTATION_DEBOUNCE_MS)
- }
-
- private fun previewConfig(): StreamingConfig {
- return config ?: StreamingConfig(
- rtmpUrl = "rtmp://127.0.0.1/preview",
- width = 1280,
- height = 720,
- )
- }
-
- private fun obtainGenericStream(streamConfig: StreamingConfig): GenericStream {
- return GenericStream(
- appContext,
- this,
- ).apply {
- getGlInterface().autoHandleOrientation = false
- getStreamClient().setBitrateExponentialFactor(0.5f)
- getStreamClient().setReTries(streamConfig.maxReconnectAttempts)
- }
- }
-
- private fun buildMetrics(): StreamingMetrics {
- val durationMs = if (streamStartedAtMs > 0L) {
- SystemClock.elapsedRealtime() - streamStartedAtMs
- } else {
- 0L
- }
-
- return StreamingMetrics(
- isStreaming = state == StreamingState.STREAMING ||
- state == StreamingState.RECONNECTING ||
- state == StreamingState.CONNECTING,
- isPreviewActive = state == StreamingState.PREVIEWING ||
- (genericStream?.isOnPreview == true),
- isConnected = state == StreamingState.STREAMING,
- state = state.name.lowercase(),
- bitrateKbps = currentBitrateKbps,
- fps = currentFps,
- durationMs = durationMs,
- reconnectAttempts = reconnectAttempts.get(),
- lastError = lastError,
- )
- }
-
- private fun updateState(newState: StreamingState, message: String? = null) {
- state = newState
- listeners.forEach { it.onStateChanged(newState, message) }
- emitMetrics()
- }
-
- private fun emitMetrics() {
- updateFpsEstimate()
- checkVideoStallAndRecover()
- val metrics = buildMetrics()
- listeners.forEach { it.onMetricsUpdated(metrics) }
- }
-
- /** Se RTMP è connesso ma non partono frame video, ripubblica con preview attiva. */
- private fun checkVideoStallAndRecover() {
- if (isPreviewRebinding()) {
- return
- }
- if (state != StreamingState.STREAMING && state != StreamingState.RECONNECTING) {
- videoStallChecks = 0
- lastStallFrameCount = -1L
- return
- }
- val stream = genericStream ?: return
- if (!stream.isStreaming) {
- return
- }
- val frames = stream.getStreamClient().getSentVideoFrames()
- if (lastStallFrameCount < 0L) {
- lastStallFrameCount = frames
- return
- }
- if (frames > lastStallFrameCount) {
- videoStallChecks = 0
- lastStallFrameCount = frames
- return
- }
- videoStallChecks += 1
- if (videoStallChecks < 12) {
- return
- }
- videoStallChecks = 0
- lastStallFrameCount = -1L
- val cfg = config ?: return
- recoverVideoStream(cfg)
- }
-
- private fun recoverVideoStream(streamConfig: StreamingConfig) {
- val stream = genericStream ?: return
- Log.w(TAG, "recoverVideoStream state=$state url=${streamConfig.rtmpUrl}")
- pausingIntentionally.set(true)
- try {
- if (stream.isStreaming) {
- stream.stopStream()
- }
- } finally {
- pausingIntentionally.set(false)
- }
- stopPreviewAndStreamForPrepare(stream)
- val prepared = try {
- stream.prepareVideo(
- width = streamConfig.width,
- height = streamConfig.height,
- bitrate = streamConfig.videoBitrate,
- fps = streamConfig.fps,
- iFrameInterval = 1,
- rotation = streamConfig.rotation,
- profile = MediaCodecInfo.CodecProfileLevel.AVCProfileBaseline,
- level = MediaCodecInfo.CodecProfileLevel.AVCLevel31,
- ) && stream.prepareAudio(
- streamConfig.sampleRate,
- streamConfig.stereo,
- streamConfig.audioBitrate,
- )
- } catch (_: Exception) {
- false
- }
- if (!prepared) {
- notifyError("Ripristino video fallito")
- updateState(StreamingState.ERROR, "video_recover_failed")
- return
- }
- attachPreviewToStream()
- ensureOrientationSensor()
- resetFpsTracking()
- waitForPreviewThenPublish(streamConfig, attempt = 0)
- }
-
- private fun updateFpsEstimate() {
- val stream = genericStream ?: return
- val now = SystemClock.elapsedRealtime()
- val sentFrames = stream.getStreamClient().getSentVideoFrames()
- if (lastFpsSampleAtMs == 0L) {
- lastFpsSampleAtMs = now
- lastVideoFrameCount = sentFrames
- return
- }
-
- val elapsedMs = now - lastFpsSampleAtMs
- if (elapsedMs >= 1_000L) {
- val frameDelta = (sentFrames - lastVideoFrameCount).coerceAtLeast(0L)
- currentFps = ((frameDelta * 1000L) / elapsedMs).toInt().coerceIn(0, 120)
- lastVideoFrameCount = sentFrames
- lastFpsSampleAtMs = now
- }
- }
-
- private fun notifyError(message: String) {
- listeners.forEach { it.onError(message) }
- }
-
- private fun startMetricsLoop() {
- mainHandler.removeCallbacks(metricsRunnable)
- mainHandler.post(metricsRunnable)
- }
-
- private fun stopMetricsLoop() {
- mainHandler.removeCallbacks(metricsRunnable)
- }
-
- private fun postMain(block: () -> Unit) {
- if (Looper.myLooper() == Looper.getMainLooper()) {
- block()
- } else {
- mainHandler.post(block)
- }
- }
-
- private fun ensureMainThread() {
- check(Looper.myLooper() == Looper.getMainLooper()) {
- "StreamingEngine must be used on the main thread"
- }
- }
-
- companion object {
- private const val TAG = "StreamingEngine"
- private const val METRICS_INTERVAL_MS = 1_000L
- private const val SURFACE_READY_DEBOUNCE_MS = 700L
- private const val IDLE_PREVIEW_ATTACH_MS = 80L
- private const val POST_REBIND_ORIENTATION_MS = 350L
- private const val SURFACE_TRANSITION_GRACE_MS = 8_000L
- private const val ORIENTATION_DEBOUNCE_MS = 700L
- private const val PREVIEW_REBIND_RETRY_MS = 120L
- private const val PREVIEW_REBIND_MAX_ATTEMPTS = 15
- private const val PREVIEW_REBIND_GRACE_MS = 6_000L
- }
-}
diff --git a/mobile/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/StreamingEngineHolder.kt b/mobile/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/StreamingEngineHolder.kt
deleted file mode 100644
index ea2bcf1..0000000
--- a/mobile/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/StreamingEngineHolder.kt
+++ /dev/null
@@ -1,22 +0,0 @@
-package com.matchlivetv.match_live_tv
-
-import android.content.Context
-
-/** Motore streaming condiviso tra preview UI e RTMP (deve restare legato all'Activity). */
-object StreamingEngineHolder {
- @Volatile
- var engine: StreamingEngine? = null
-
- fun getOrCreate(context: Context): StreamingEngine {
- val existing = engine
- if (existing != null) {
- return existing
- }
- return StreamingEngine(context.applicationContext).also { engine = it }
- }
-
- fun release() {
- engine?.release()
- engine = null
- }
-}
diff --git a/mobile/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/StreamingForegroundService.kt b/mobile/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/StreamingForegroundService.kt
deleted file mode 100644
index b3e9ef3..0000000
--- a/mobile/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/StreamingForegroundService.kt
+++ /dev/null
@@ -1,243 +0,0 @@
-package com.matchlivetv.match_live_tv
-
-import android.app.Notification
-import android.app.NotificationChannel
-import android.app.NotificationManager
-import android.app.PendingIntent
-import android.app.Service
-import android.content.Context
-import android.content.Intent
-import android.content.pm.ServiceInfo
-import android.os.Binder
-import android.os.Build
-import android.os.Handler
-import android.os.IBinder
-import android.os.Looper
-import android.os.PowerManager
-import android.os.SystemClock
-import androidx.core.app.NotificationCompat
-import androidx.core.app.ServiceCompat
-
-/** Mantiene in foreground la diretta (notifica + wake lock). Il motore RTMP vive nel plugin. */
-class StreamingForegroundService : Service() {
-
- private val binder = LocalBinder()
- private val mainHandler = Handler(Looper.getMainLooper())
-
- private var wakeLock: PowerManager.WakeLock? = null
- private var sessionStartedAtMs: Long = 0L
- private var autoStopTriggered = false
-
- private val sessionTimeoutRunnable = Runnable {
- if (!autoStopTriggered) {
- autoStopTriggered = true
- ServiceEventBus.emit(
- mapOf(
- "type" to "stopped",
- "reason" to "session_timeout",
- "durationMs" to elapsedMs(),
- ),
- )
- stopForegroundInternal("session_timeout")
- }
- }
-
- inner class LocalBinder : Binder() {
- fun getService(): StreamingForegroundService = this@StreamingForegroundService
- }
-
- override fun onCreate() {
- super.onCreate()
- createNotificationChannel()
- }
-
- override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
- when (intent?.action) {
- ACTION_START -> startForegroundSession()
- ACTION_STOP -> stopForegroundInternal("user_stop")
- }
- return START_STICKY
- }
-
- override fun onBind(intent: Intent?): IBinder = binder
-
- override fun onDestroy() {
- stopForegroundInternal("service_destroy")
- super.onDestroy()
- }
-
- fun getMetrics(): Map =
- StreamingEngineHolder.engine?.getMetrics()?.toMap() ?: emptyMap()
-
- private fun startForegroundSession() {
- autoStopTriggered = false
- sessionStartedAtMs = SystemClock.elapsedRealtime()
- acquireWakeLock()
- promoteToForeground(getString(R.string.streaming_notification_active))
- mainHandler.removeCallbacks(sessionTimeoutRunnable)
- mainHandler.postDelayed(sessionTimeoutRunnable, SESSION_MAX_MS)
- }
-
- private fun stopForegroundInternal(reason: String) {
- mainHandler.removeCallbacks(sessionTimeoutRunnable)
- releaseWakeLock()
- if (sessionStartedAtMs > 0L) {
- ServiceEventBus.emit(
- mapOf(
- "type" to "stopped",
- "reason" to reason,
- "durationMs" to elapsedMs(),
- ),
- )
- }
- sessionStartedAtMs = 0L
- stopForeground(STOP_FOREGROUND_REMOVE)
- stopSelf()
- }
-
- private fun elapsedMs(): Long {
- return if (sessionStartedAtMs > 0L) {
- SystemClock.elapsedRealtime() - sessionStartedAtMs
- } else {
- 0L
- }
- }
-
- private fun promoteToForeground(content: String) {
- val notification = buildNotification(content)
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
- ServiceCompat.startForeground(
- this,
- NOTIFICATION_ID,
- notification,
- ServiceInfo.FOREGROUND_SERVICE_TYPE_CAMERA or
- ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE,
- )
- } else {
- startForeground(NOTIFICATION_ID, notification)
- }
- }
-
- fun updateNotificationForState(state: StreamingState) {
- val content = when (state) {
- StreamingState.CONNECTING -> getString(R.string.streaming_notification_connecting)
- StreamingState.STREAMING -> getString(R.string.streaming_notification_streaming)
- StreamingState.RECONNECTING -> getString(R.string.streaming_notification_reconnecting)
- StreamingState.ERROR -> getString(R.string.streaming_notification_error)
- else -> getString(R.string.streaming_notification_active)
- }
- val manager = getSystemService(NotificationManager::class.java)
- manager.notify(NOTIFICATION_ID, buildNotification(content))
- }
-
- private fun buildNotification(content: String): Notification {
- val launchIntent = packageManager.getLaunchIntentForPackage(packageName)
- val contentIntent = PendingIntent.getActivity(
- this,
- 0,
- launchIntent,
- PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
- )
- val stopIntent = PendingIntent.getService(
- this,
- 1,
- Companion.stopIntent(this),
- PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
- )
-
- return NotificationCompat.Builder(this, CHANNEL_ID)
- .setContentTitle(getString(R.string.streaming_notification_title))
- .setContentText(content)
- .setSmallIcon(R.mipmap.ic_launcher)
- .setOngoing(true)
- .setContentIntent(contentIntent)
- .addAction(
- android.R.drawable.ic_media_pause,
- getString(R.string.streaming_notification_stop),
- stopIntent,
- )
- .setCategory(NotificationCompat.CATEGORY_SERVICE)
- .setPriority(NotificationCompat.PRIORITY_LOW)
- .build()
- }
-
- private fun createNotificationChannel() {
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
- return
- }
- val manager = getSystemService(NotificationManager::class.java)
- val channel = NotificationChannel(
- CHANNEL_ID,
- getString(R.string.streaming_notification_channel),
- NotificationManager.IMPORTANCE_LOW,
- ).apply {
- description = getString(R.string.streaming_notification_channel_desc)
- }
- manager.createNotificationChannel(channel)
- }
-
- private fun acquireWakeLock() {
- if (wakeLock?.isHeld == true) {
- return
- }
- val powerManager = getSystemService(Context.POWER_SERVICE) as PowerManager
- wakeLock = powerManager.newWakeLock(
- PowerManager.PARTIAL_WAKE_LOCK,
- "$packageName:StreamingWakeLock",
- ).apply {
- setReferenceCounted(false)
- acquire(SESSION_MAX_MS + 60_000L)
- }
- }
-
- private fun releaseWakeLock() {
- wakeLock?.let { lock ->
- if (lock.isHeld) {
- lock.release()
- }
- }
- wakeLock = null
- }
-
- companion object {
- const val ACTION_START = "com.matchlivetv.match_live_tv.action.START_STREAM"
- const val ACTION_STOP = "com.matchlivetv.match_live_tv.action.STOP_STREAM"
-
- private const val CHANNEL_ID = "match_live_tv_streaming"
- private const val NOTIFICATION_ID = 1001
- private val SESSION_MAX_MS = 90L * 60L * 1000L
-
- fun startIntent(context: Context): Intent {
- return Intent(context, StreamingForegroundService::class.java).apply {
- action = ACTION_START
- }
- }
-
- fun stopIntent(context: Context): Intent {
- return Intent(context, StreamingForegroundService::class.java).apply {
- action = ACTION_STOP
- }
- }
- }
-}
-
-object ServiceEventBus {
- private val listeners = mutableSetOf<(Map) -> Unit>()
-
- fun addListener(listener: (Map) -> Unit) {
- synchronized(listeners) {
- listeners.add(listener)
- }
- }
-
- fun removeListener(listener: (Map) -> Unit) {
- synchronized(listeners) {
- listeners.remove(listener)
- }
- }
-
- fun emit(event: Map) {
- val snapshot = synchronized(listeners) { listeners.toList() }
- snapshot.forEach { it(event) }
- }
-}
diff --git a/mobile/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/StreamingOpenGlView.kt b/mobile/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/StreamingOpenGlView.kt
deleted file mode 100644
index ffcfc83..0000000
--- a/mobile/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/StreamingOpenGlView.kt
+++ /dev/null
@@ -1,30 +0,0 @@
-package com.matchlivetv.match_live_tv
-
-import android.content.Context
-import android.util.AttributeSet
-import android.view.SurfaceHolder
-import com.pedro.library.view.OpenGlView
-
-/**
- * OpenGlView che non distrugge encoder/GL quando la surface viene persa in rotazione.
- * La SurfaceView standard di Pedro chiama stop() in surfaceDestroyed e uccide tutto il pipeline.
- */
-class StreamingOpenGlView @JvmOverloads constructor(
- context: Context,
- attrs: AttributeSet? = null,
-) : OpenGlView(context, attrs) {
-
- @Volatile
- var preservePipelineOnSurfaceLoss: Boolean = false
-
- var onPreviewSurfaceLost: (() -> Unit)? = null
-
- override fun surfaceDestroyed(holder: SurfaceHolder) {
- if (preservePipelineOnSurfaceLoss && isRunning) {
- setForceRender(false)
- onPreviewSurfaceLost?.invoke()
- return
- }
- super.surfaceDestroyed(holder)
- }
-}
diff --git a/mobile/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/StreamingPlugin.kt b/mobile/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/StreamingPlugin.kt
deleted file mode 100644
index 89e22d1..0000000
--- a/mobile/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/StreamingPlugin.kt
+++ /dev/null
@@ -1,365 +0,0 @@
-package com.matchlivetv.match_live_tv
-
-import android.content.ComponentName
-import android.content.Context
-import android.content.Intent
-import android.content.ServiceConnection
-import android.os.Build
-import android.os.IBinder
-import io.flutter.embedding.engine.plugins.FlutterPlugin
-import io.flutter.embedding.engine.plugins.activity.ActivityAware
-import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
-import io.flutter.plugin.common.EventChannel
-import io.flutter.plugin.common.MethodCall
-import io.flutter.plugin.common.MethodChannel
-import io.flutter.plugin.common.MethodChannel.MethodCallHandler
-import io.flutter.plugin.common.MethodChannel.Result
-
-class StreamingPlugin : FlutterPlugin, MethodCallHandler, ActivityAware, EventChannel.StreamHandler {
-
- private lateinit var appContext: Context
- private lateinit var methodChannel: MethodChannel
- private lateinit var eventChannel: EventChannel
-
- private var activityBinding: ActivityPluginBinding? = null
- private var eventSink: EventChannel.EventSink? = null
- private var boundService: StreamingForegroundService? = null
- private var serviceBound = false
-
- private val engineListener = object : StreamingEngineListener {
- override fun onStateChanged(state: StreamingState, message: String?) {
- ServiceEventBus.emit(
- mapOf(
- "type" to "state",
- "state" to state.name.lowercase(),
- "message" to message,
- ),
- )
- boundService?.updateNotificationForState(state)
- }
-
- override fun onMetricsUpdated(metrics: StreamingMetrics) {
- ServiceEventBus.emit(
- mapOf(
- "type" to "metrics",
- "metrics" to metrics.toMap(),
- ),
- )
- }
-
- override fun onError(message: String) {
- ServiceEventBus.emit(
- mapOf(
- "type" to "error",
- "message" to message,
- ),
- )
- }
- }
-
- private val serviceConnection = object : ServiceConnection {
- override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
- val binder = service as? StreamingForegroundService.LocalBinder ?: return
- boundService = binder.getService()
- serviceBound = true
- }
-
- override fun onServiceDisconnected(name: ComponentName?) {
- boundService = null
- serviceBound = false
- }
- }
-
- private val busListener: (Map) -> Unit = { event ->
- eventSink?.success(event)
- }
-
- override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
- appContext = binding.applicationContext
- methodChannel = MethodChannel(binding.binaryMessenger, METHOD_CHANNEL)
- methodChannel.setMethodCallHandler(this)
-
- eventChannel = EventChannel(binding.binaryMessenger, EVENT_CHANNEL)
- eventChannel.setStreamHandler(this)
-
- binding.platformViewRegistry.registerViewFactory(
- PREVIEW_VIEW_TYPE,
- StreamingPreviewFactory(
- onSurfaceReady = { glView ->
- android.os.Handler(android.os.Looper.getMainLooper()).post {
- val engine = getOrCreateEngine()
- engine.setupPreviewGlView(glView)
- engine.onPlatformSurfaceReady(glView)
- }
- },
- onSurfaceDestroyed = {
- android.os.Handler(android.os.Looper.getMainLooper()).post {
- getEngine()?.onPlatformSurfaceDestroyed()
- }
- },
- ),
- )
-
- ServiceEventBus.addListener(busListener)
- }
-
- override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
- methodChannel.setMethodCallHandler(null)
- eventChannel.setStreamHandler(null)
- ServiceEventBus.removeListener(busListener)
- releaseEngine()
- unbindServiceIfNeeded()
- }
-
- override fun onAttachedToActivity(binding: ActivityPluginBinding) {
- activityBinding = binding
- }
-
- override fun onDetachedFromActivityForConfigChanges() {
- activityBinding = null
- }
-
- override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
- activityBinding = binding
- }
-
- private fun attachPreviewIfPossible() {
- val glView = StreamPreviewHolder.openGlView as? StreamingOpenGlView ?: return
- val engine = getOrCreateEngine()
- engine.setupPreviewGlView(glView)
- engine.bindStreamPreview(glView)
- }
-
- private fun previewIsReady(): Boolean {
- val engine = getEngine() ?: return false
- val metrics = engine.getMetrics()
- return metrics.isPreviewActive
- }
-
- override fun onDetachedFromActivity() {
- activityBinding = null
- unbindServiceIfNeeded()
- }
-
- override fun onMethodCall(call: MethodCall, result: Result) {
- when (call.method) {
- "startStream" -> startStream(call, result)
- "resumeStream" -> resumeStream(call, result)
- "stopStream" -> stopStream(result)
- "pauseStream" -> pauseStream(result)
- "getMetrics" -> getMetrics(result)
- "startPreview" -> startPreview(result)
- "stopPreview" -> stopPreview(result)
- "bindPreview" -> bindPreview(result)
- "syncOrientation" -> syncOrientation(result)
- "unbindPreview" -> unbindPreview(result)
- else -> result.notImplemented()
- }
- }
-
- override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
- eventSink = events
- }
-
- override fun onCancel(arguments: Any?) {
- eventSink = null
- }
-
- private fun startStream(call: MethodCall, result: Result) {
- if (activityBinding?.activity == null) {
- result.error("no_activity", "Activity non disponibile per lo streaming", null)
- return
- }
-
- val args = call.arguments as? Map<*, *>
- val rtmpUrl = args?.get("rtmpUrl") as? String
- if (rtmpUrl.isNullOrBlank()) {
- result.error("invalid_args", "rtmpUrl is required", null)
- return
- }
-
- val argWidth = (args["width"] as? Number)?.toInt() ?: 1280
- val argHeight = (args["height"] as? Number)?.toInt() ?: 720
- val portrait = args["portrait"] as? Boolean ?: (argHeight > argWidth)
- val config = StreamingConfig(
- rtmpUrl = rtmpUrl,
- width = (args["width"] as? Number)?.toInt() ?: if (portrait) 720 else 1280,
- height = (args["height"] as? Number)?.toInt() ?: if (portrait) 1280 else 720,
- videoBitrate = (args["videoBitrate"] as? Number)?.toInt() ?: 2_500_000,
- audioBitrate = (args["audioBitrate"] as? Number)?.toInt() ?: 128_000,
- fps = (args["fps"] as? Number)?.toInt() ?: 30,
- rotation = (args["rotation"] as? Number)?.toInt() ?: 0,
- portrait = portrait,
- maxReconnectAttempts = (args["maxReconnectAttempts"] as? Number)?.toInt() ?: 10,
- reconnectDelayMs = (args["reconnectDelayMs"] as? Number)?.toLong() ?: 5_000L,
- )
-
- val engine = getOrCreateEngine()
- engine.removeListener(engineListener)
- engine.addListener(engineListener)
-
- attachPreviewIfPossible()
-
- val intent = StreamingForegroundService.startIntent(appContext)
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- appContext.startForegroundService(intent)
- } else {
- appContext.startService(intent)
- }
- bindServiceIfNeeded()
-
- try {
- engine.startStream(config)
- result.success(true)
- } catch (exception: Exception) {
- appContext.startService(StreamingForegroundService.stopIntent(appContext))
- result.error("start_failed", exception.message, null)
- }
- }
-
- private fun resumeStream(call: MethodCall, result: Result) {
- if (activityBinding?.activity == null) {
- result.error("no_activity", "Activity non disponibile per lo streaming", null)
- return
- }
-
- val args = call.arguments as? Map<*, *>
- val rtmpUrl = args?.get("rtmpUrl") as? String
- if (rtmpUrl.isNullOrBlank()) {
- result.error("invalid_args", "rtmpUrl is required", null)
- return
- }
-
- val argWidth = (args["width"] as? Number)?.toInt() ?: 1280
- val argHeight = (args["height"] as? Number)?.toInt() ?: 720
- val portrait = args["portrait"] as? Boolean ?: (argHeight > argWidth)
- val config = StreamingConfig(
- rtmpUrl = rtmpUrl,
- width = (args["width"] as? Number)?.toInt() ?: if (portrait) 720 else 1280,
- height = (args["height"] as? Number)?.toInt() ?: if (portrait) 1280 else 720,
- videoBitrate = (args["videoBitrate"] as? Number)?.toInt() ?: 2_500_000,
- audioBitrate = (args["audioBitrate"] as? Number)?.toInt() ?: 128_000,
- fps = (args["fps"] as? Number)?.toInt() ?: 30,
- rotation = (args["rotation"] as? Number)?.toInt() ?: 0,
- portrait = portrait,
- maxReconnectAttempts = (args["maxReconnectAttempts"] as? Number)?.toInt() ?: 10,
- reconnectDelayMs = (args["reconnectDelayMs"] as? Number)?.toLong() ?: 5_000L,
- )
-
- val engine = getOrCreateEngine()
- engine.removeListener(engineListener)
- engine.addListener(engineListener)
- attachPreviewIfPossible()
-
- val intent = StreamingForegroundService.startIntent(appContext)
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- appContext.startForegroundService(intent)
- } else {
- appContext.startService(intent)
- }
- bindServiceIfNeeded()
-
- try {
- engine.resumeStream(config)
- result.success(true)
- } catch (exception: Exception) {
- appContext.startService(StreamingForegroundService.stopIntent(appContext))
- result.error("resume_failed", exception.message, null)
- }
- }
-
- private fun stopStream(result: Result) {
- appContext.startService(StreamingForegroundService.stopIntent(appContext))
- getEngine()?.let { engine ->
- engine.removeListener(engineListener)
- engine.stopStream()
- }
- unbindServiceIfNeeded()
- result.success(true)
- }
-
- private fun pauseStream(result: Result) {
- getEngine()?.pauseStream()
- result.success(true)
- }
-
- private fun getMetrics(result: Result) {
- val metrics = boundService?.getMetrics()
- ?: getEngine()?.getMetrics()?.toMap()
- ?: emptyMap()
- result.success(metrics)
- }
-
- private fun bindPreview(result: Result) {
- attachPreviewIfPossible()
- result.success(previewIsReady())
- }
-
- private fun syncOrientation(result: Result) {
- getEngine()?.applyOrientationFromSensor()
- result.success(true)
- }
-
- private fun unbindPreview(result: Result) {
- getEngine()?.unbindStreamPreview()
- result.success(true)
- }
-
- private fun startPreview(result: Result) {
- attachPreviewIfPossible()
- result.success(StreamPreviewHolder.openGlView != null)
- }
-
- private fun stopPreview(result: Result) {
- getEngine()?.unbindStreamPreview()
- result.success(true)
- }
-
- private fun getOrCreateEngine(): StreamingEngine {
- val context = activityBinding?.activity ?: appContext
- return StreamingEngineHolder.getOrCreate(context).also { engine ->
- engine.removeListener(engineListener)
- engine.addListener(engineListener)
- }
- }
-
- private fun getEngine(): StreamingEngine? = StreamingEngineHolder.engine
-
- private fun releaseEngine() {
- getEngine()?.let { engine ->
- engine.removeListener(engineListener)
- engine.stopStream()
- }
- StreamingEngineHolder.release()
- }
-
- private fun bindServiceIfNeeded() {
- if (serviceBound) {
- return
- }
- val intent = Intent(appContext, StreamingForegroundService::class.java)
- appContext.bindService(
- intent,
- serviceConnection,
- Context.BIND_AUTO_CREATE,
- )
- }
-
- private fun unbindServiceIfNeeded() {
- if (!serviceBound) {
- return
- }
- try {
- appContext.unbindService(serviceConnection)
- } catch (_: IllegalArgumentException) {
- }
- serviceBound = false
- boundService = null
- }
-
- companion object {
- private const val METHOD_CHANNEL = "com.matchlivetv.match_live_tv/streaming"
- private const val EVENT_CHANNEL = "com.matchlivetv.match_live_tv/streaming_events"
- const val PREVIEW_VIEW_TYPE = "match_live_tv/streaming_preview"
- }
-}
diff --git a/mobile/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/StreamingPreviewPlatformView.kt b/mobile/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/StreamingPreviewPlatformView.kt
deleted file mode 100644
index 8fff3d1..0000000
--- a/mobile/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/StreamingPreviewPlatformView.kt
+++ /dev/null
@@ -1,95 +0,0 @@
-package com.matchlivetv.match_live_tv
-
-import android.content.Context
-import android.view.SurfaceHolder
-import android.view.View
-import android.widget.FrameLayout
-import io.flutter.plugin.common.StandardMessageCodec
-import io.flutter.plugin.platform.PlatformView
-import io.flutter.plugin.platform.PlatformViewFactory
-
-class StreamingPreviewPlatformView(
- context: Context,
- private val onSurfaceReady: (StreamingOpenGlView) -> Unit,
- private val onSurfaceDestroyed: () -> Unit,
-) : PlatformView {
-
- private val openGlView = StreamingOpenGlView(context).apply {
- layoutParams = FrameLayout.LayoutParams(
- FrameLayout.LayoutParams.MATCH_PARENT,
- FrameLayout.LayoutParams.MATCH_PARENT,
- )
- }
-
- private var lastNotifiedWidth = 0
- private var lastNotifiedHeight = 0
-
- private val notifyReadyRunnable = Runnable {
- if (openGlView.width <= 0 || openGlView.height <= 0) {
- return@Runnable
- }
- val sizeChanged = openGlView.width != lastNotifiedWidth ||
- openGlView.height != lastNotifiedHeight
- if (!sizeChanged && lastNotifiedWidth > 0) {
- return@Runnable
- }
- lastNotifiedWidth = openGlView.width
- lastNotifiedHeight = openGlView.height
- onSurfaceReady(openGlView)
- }
-
- init {
- StreamPreviewHolder.openGlView = openGlView
- openGlView.holder.addCallback(object : SurfaceHolder.Callback {
- override fun surfaceCreated(holder: SurfaceHolder) {
- StreamPreviewHolder.openGlView = openGlView
- scheduleReadyDebounced()
- }
-
- override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
- StreamPreviewHolder.openGlView = openGlView
- if (width > 0 && height > 0) {
- scheduleReadyDebounced()
- }
- }
-
- override fun surfaceDestroyed(holder: SurfaceHolder) {
- openGlView.removeCallbacks(notifyReadyRunnable)
- lastNotifiedWidth = 0
- lastNotifiedHeight = 0
- onSurfaceDestroyed()
- }
- })
- }
-
- private fun scheduleReadyDebounced() {
- openGlView.removeCallbacks(notifyReadyRunnable)
- openGlView.postDelayed(notifyReadyRunnable, SURFACE_NOTIFY_DEBOUNCE_MS)
- }
-
- override fun getView(): View = openGlView
-
- override fun dispose() {
- openGlView.removeCallbacks(notifyReadyRunnable)
- lastNotifiedWidth = 0
- lastNotifiedHeight = 0
- onSurfaceDestroyed()
- if (StreamPreviewHolder.openGlView === openGlView) {
- StreamPreviewHolder.openGlView = null
- }
- }
-
- companion object {
- private const val SURFACE_NOTIFY_DEBOUNCE_MS = 600L
- }
-}
-
-class StreamingPreviewFactory(
- private val onSurfaceReady: (StreamingOpenGlView) -> Unit,
- private val onSurfaceDestroyed: () -> Unit,
-) : PlatformViewFactory(StandardMessageCodec.INSTANCE) {
-
- override fun create(context: Context, viewId: Int, args: Any?): PlatformView {
- return StreamingPreviewPlatformView(context, onSurfaceReady, onSurfaceDestroyed)
- }
-}
diff --git a/mobile/android/app/src/main/res/drawable-v21/launch_background.xml b/mobile/android/app/src/main/res/drawable-v21/launch_background.xml
deleted file mode 100644
index f74085f..0000000
--- a/mobile/android/app/src/main/res/drawable-v21/launch_background.xml
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
-
-
-
-
diff --git a/mobile/android/app/src/main/res/drawable/launch_background.xml b/mobile/android/app/src/main/res/drawable/launch_background.xml
deleted file mode 100644
index 304732f..0000000
--- a/mobile/android/app/src/main/res/drawable/launch_background.xml
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
-
-
-
-
diff --git a/mobile/android/app/src/main/res/values-night/styles.xml b/mobile/android/app/src/main/res/values-night/styles.xml
deleted file mode 100644
index 06952be..0000000
--- a/mobile/android/app/src/main/res/values-night/styles.xml
+++ /dev/null
@@ -1,18 +0,0 @@
-
-
-
-
-
-
-
diff --git a/mobile/android/app/src/main/res/values/strings.xml b/mobile/android/app/src/main/res/values/strings.xml
deleted file mode 100644
index 0b09da7..0000000
--- a/mobile/android/app/src/main/res/values/strings.xml
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
- Match Live TV
- Streaming live
- Notifiche durante lo streaming Match Live TV
- Match Live TV
- Streaming attivo
- Connessione al server RTMP…
- Trasmissione in corso
- Riconnessione in corso…
- Errore di streaming
- Stop
-
diff --git a/mobile/android/app/src/main/res/values/styles.xml b/mobile/android/app/src/main/res/values/styles.xml
deleted file mode 100644
index cb1ef88..0000000
--- a/mobile/android/app/src/main/res/values/styles.xml
+++ /dev/null
@@ -1,18 +0,0 @@
-
-
-
-
-
-
-
diff --git a/mobile/android/app/src/main/res/xml/network_security_config.xml b/mobile/android/app/src/main/res/xml/network_security_config.xml
deleted file mode 100644
index 82f15b7..0000000
--- a/mobile/android/app/src/main/res/xml/network_security_config.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
-
- 10.0.2.2
- localhost
- 127.0.0.1
-
-
diff --git a/mobile/android/app/src/profile/AndroidManifest.xml b/mobile/android/app/src/profile/AndroidManifest.xml
deleted file mode 100644
index 399f698..0000000
--- a/mobile/android/app/src/profile/AndroidManifest.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
diff --git a/mobile/android/build.gradle.kts b/mobile/android/build.gradle.kts
deleted file mode 100644
index d08ad63..0000000
--- a/mobile/android/build.gradle.kts
+++ /dev/null
@@ -1,25 +0,0 @@
-allprojects {
- repositories {
- google()
- mavenCentral()
- maven { url = uri("https://jitpack.io") }
- }
-}
-
-val newBuildDir: Directory =
- rootProject.layout.buildDirectory
- .dir("../../build")
- .get()
-rootProject.layout.buildDirectory.value(newBuildDir)
-
-subprojects {
- val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
- project.layout.buildDirectory.value(newSubprojectBuildDir)
-}
-subprojects {
- project.evaluationDependsOn(":app")
-}
-
-tasks.register("clean") {
- delete(rootProject.layout.buildDirectory)
-}
diff --git a/mobile/android/gradle.properties b/mobile/android/gradle.properties
deleted file mode 100644
index e96108c..0000000
--- a/mobile/android/gradle.properties
+++ /dev/null
@@ -1,6 +0,0 @@
-org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
-android.useAndroidX=true
-# This newDsl flag was added by the Flutter template
-android.newDsl=false
-# This builtInKotlin flag was added by the Flutter template
-android.builtInKotlin=false
diff --git a/mobile/android/gradle/wrapper/gradle-wrapper.properties b/mobile/android/gradle/wrapper/gradle-wrapper.properties
deleted file mode 100644
index 2d428bf..0000000
--- a/mobile/android/gradle/wrapper/gradle-wrapper.properties
+++ /dev/null
@@ -1,5 +0,0 @@
-distributionBase=GRADLE_USER_HOME
-distributionPath=wrapper/dists
-zipStoreBase=GRADLE_USER_HOME
-zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-all.zip
diff --git a/mobile/android/settings.gradle.kts b/mobile/android/settings.gradle.kts
deleted file mode 100644
index 2786c72..0000000
--- a/mobile/android/settings.gradle.kts
+++ /dev/null
@@ -1,35 +0,0 @@
-pluginManagement {
- val flutterSdkPath =
- run {
- val properties = java.util.Properties()
- file("local.properties").inputStream().use { properties.load(it) }
- val flutterSdkPath = properties.getProperty("flutter.sdk")
- require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
- flutterSdkPath
- }
-
- includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
-
- repositories {
- google()
- mavenCentral()
- gradlePluginPortal()
- }
-}
-
-plugins {
- id("dev.flutter.flutter-plugin-loader") version "1.0.0"
- id("com.android.application") version "9.0.1" apply false
- id("org.jetbrains.kotlin.android") version "2.3.20" apply false
-}
-
-dependencyResolutionManagement {
- repositoriesMode.set(RepositoriesMode.PREFER_PROJECT)
- repositories {
- google()
- mavenCentral()
- maven { url = uri("https://jitpack.io") }
- }
-}
-
-include(":app")
diff --git a/mobile/ios/.gitignore b/mobile/ios/.gitignore
deleted file mode 100644
index 7a7f987..0000000
--- a/mobile/ios/.gitignore
+++ /dev/null
@@ -1,34 +0,0 @@
-**/dgph
-*.mode1v3
-*.mode2v3
-*.moved-aside
-*.pbxuser
-*.perspectivev3
-**/*sync/
-.sconsign.dblite
-.tags*
-**/.vagrant/
-**/DerivedData/
-Icon?
-**/Pods/
-**/.symlinks/
-profile
-xcuserdata
-**/.generated/
-Flutter/App.framework
-Flutter/Flutter.framework
-Flutter/Flutter.podspec
-Flutter/Generated.xcconfig
-Flutter/ephemeral/
-Flutter/app.flx
-Flutter/app.zip
-Flutter/flutter_assets/
-Flutter/flutter_export_environment.sh
-ServiceDefinitions.json
-Runner/GeneratedPluginRegistrant.*
-
-# Exceptions to above rules.
-!default.mode1v3
-!default.mode2v3
-!default.pbxuser
-!default.perspectivev3
diff --git a/mobile/ios/Flutter/AppFrameworkInfo.plist b/mobile/ios/Flutter/AppFrameworkInfo.plist
deleted file mode 100644
index 391a902..0000000
--- a/mobile/ios/Flutter/AppFrameworkInfo.plist
+++ /dev/null
@@ -1,24 +0,0 @@
-
-
-
-
- CFBundleDevelopmentRegion
- en
- CFBundleExecutable
- App
- CFBundleIdentifier
- io.flutter.flutter.app
- CFBundleInfoDictionaryVersion
- 6.0
- CFBundleName
- App
- CFBundlePackageType
- FMWK
- CFBundleShortVersionString
- 1.0
- CFBundleSignature
- ????
- CFBundleVersion
- 1.0
-
-
diff --git a/mobile/ios/Flutter/Debug.xcconfig b/mobile/ios/Flutter/Debug.xcconfig
deleted file mode 100644
index 592ceee..0000000
--- a/mobile/ios/Flutter/Debug.xcconfig
+++ /dev/null
@@ -1 +0,0 @@
-#include "Generated.xcconfig"
diff --git a/mobile/ios/Flutter/Release.xcconfig b/mobile/ios/Flutter/Release.xcconfig
deleted file mode 100644
index 592ceee..0000000
--- a/mobile/ios/Flutter/Release.xcconfig
+++ /dev/null
@@ -1 +0,0 @@
-#include "Generated.xcconfig"
diff --git a/mobile/ios/Runner.xcodeproj/project.pbxproj b/mobile/ios/Runner.xcodeproj/project.pbxproj
deleted file mode 100644
index abfdfb5..0000000
--- a/mobile/ios/Runner.xcodeproj/project.pbxproj
+++ /dev/null
@@ -1,644 +0,0 @@
-// !$*UTF8*$!
-{
- archiveVersion = 1;
- classes = {
- };
- objectVersion = 54;
- objects = {
-
-/* Begin PBXBuildFile section */
- 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
- 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
- 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
- 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
- 7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */; };
- 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; };
- 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
- 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
- 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
-/* End PBXBuildFile section */
-
-/* Begin PBXContainerItemProxy section */
- 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {
- isa = PBXContainerItemProxy;
- containerPortal = 97C146E61CF9000F007C117D /* Project object */;
- proxyType = 1;
- remoteGlobalIDString = 97C146ED1CF9000F007C117D;
- remoteInfo = Runner;
- };
-/* End PBXContainerItemProxy section */
-
-/* Begin PBXCopyFilesBuildPhase section */
- 9705A1C41CF9048500538489 /* Embed Frameworks */ = {
- isa = PBXCopyFilesBuildPhase;
- buildActionMask = 2147483647;
- dstPath = "";
- dstSubfolderSpec = 10;
- files = (
- );
- name = "Embed Frameworks";
- runOnlyForDeploymentPostprocessing = 0;
- };
-/* End PBXCopyFilesBuildPhase section */
-
-/* Begin PBXFileReference section */
- 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; };
- 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; };
- 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; };
- 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
- 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; };
- 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; };
- 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
- 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; };
- 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; };
- 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; };
- 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; };
- 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; };
- 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
- 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; };
- 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
- 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
- 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
-/* End PBXFileReference section */
-
-/* Begin PBXFrameworksBuildPhase section */
- 97C146EB1CF9000F007C117D /* Frameworks */ = {
- isa = PBXFrameworksBuildPhase;
- buildActionMask = 2147483647;
- files = (
- 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */,
- );
- runOnlyForDeploymentPostprocessing = 0;
- };
-/* End PBXFrameworksBuildPhase section */
-
-/* Begin PBXGroup section */
- 331C8082294A63A400263BE5 /* RunnerTests */ = {
- isa = PBXGroup;
- children = (
- 331C807B294A618700263BE5 /* RunnerTests.swift */,
- );
- path = RunnerTests;
- sourceTree = "";
- };
- 9740EEB11CF90186004384FC /* Flutter */ = {
- isa = PBXGroup;
- children = (
- 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */,
- 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
- 9740EEB21CF90195004384FC /* Debug.xcconfig */,
- 7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
- 9740EEB31CF90195004384FC /* Generated.xcconfig */,
- );
- name = Flutter;
- sourceTree = "";
- };
- 97C146E51CF9000F007C117D = {
- isa = PBXGroup;
- children = (
- 9740EEB11CF90186004384FC /* Flutter */,
- 97C146F01CF9000F007C117D /* Runner */,
- 97C146EF1CF9000F007C117D /* Products */,
- 331C8082294A63A400263BE5 /* RunnerTests */,
- );
- sourceTree = "";
- };
- 97C146EF1CF9000F007C117D /* Products */ = {
- isa = PBXGroup;
- children = (
- 97C146EE1CF9000F007C117D /* Runner.app */,
- 331C8081294A63A400263BE5 /* RunnerTests.xctest */,
- );
- name = Products;
- sourceTree = "";
- };
- 97C146F01CF9000F007C117D /* Runner */ = {
- isa = PBXGroup;
- children = (
- 97C146FA1CF9000F007C117D /* Main.storyboard */,
- 97C146FD1CF9000F007C117D /* Assets.xcassets */,
- 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
- 97C147021CF9000F007C117D /* Info.plist */,
- 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
- 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
- 74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
- 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */,
- 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
- );
- path = Runner;
- sourceTree = "";
- };
-/* End PBXGroup section */
-
-/* Begin PBXNativeTarget section */
- 331C8080294A63A400263BE5 /* RunnerTests */ = {
- isa = PBXNativeTarget;
- buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
- buildPhases = (
- 331C807D294A63A400263BE5 /* Sources */,
- 331C807F294A63A400263BE5 /* Resources */,
- );
- buildRules = (
- );
- dependencies = (
- 331C8086294A63A400263BE5 /* PBXTargetDependency */,
- );
- name = RunnerTests;
- productName = RunnerTests;
- productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
- productType = "com.apple.product-type.bundle.unit-test";
- };
- 97C146ED1CF9000F007C117D /* Runner */ = {
- isa = PBXNativeTarget;
- buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
- buildPhases = (
- 9740EEB61CF901F6004384FC /* Run Script */,
- 97C146EA1CF9000F007C117D /* Sources */,
- 97C146EB1CF9000F007C117D /* Frameworks */,
- 97C146EC1CF9000F007C117D /* Resources */,
- 9705A1C41CF9048500538489 /* Embed Frameworks */,
- 3B06AD1E1E4923F5004D2608 /* Thin Binary */,
- );
- buildRules = (
- );
- dependencies = (
- );
- name = Runner;
- packageProductDependencies = (
- 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */,
- );
- productName = Runner;
- productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
- productType = "com.apple.product-type.application";
- };
-/* End PBXNativeTarget section */
-
-/* Begin PBXProject section */
- 97C146E61CF9000F007C117D /* Project object */ = {
- isa = PBXProject;
- attributes = {
- BuildIndependentTargetsInParallel = YES;
- LastUpgradeCheck = 1510;
- ORGANIZATIONNAME = "";
- TargetAttributes = {
- 331C8080294A63A400263BE5 = {
- CreatedOnToolsVersion = 14.0;
- TestTargetID = 97C146ED1CF9000F007C117D;
- };
- 97C146ED1CF9000F007C117D = {
- CreatedOnToolsVersion = 7.3.1;
- LastSwiftMigration = 1100;
- };
- };
- };
- buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
- compatibilityVersion = "Xcode 9.3";
- developmentRegion = en;
- hasScannedForEncodings = 0;
- knownRegions = (
- en,
- Base,
- );
- mainGroup = 97C146E51CF9000F007C117D;
- packageReferences = (
- 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */,
- );
- productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
- projectDirPath = "";
- projectRoot = "";
- targets = (
- 97C146ED1CF9000F007C117D /* Runner */,
- 331C8080294A63A400263BE5 /* RunnerTests */,
- );
- };
-/* End PBXProject section */
-
-/* Begin PBXResourcesBuildPhase section */
- 331C807F294A63A400263BE5 /* Resources */ = {
- isa = PBXResourcesBuildPhase;
- buildActionMask = 2147483647;
- files = (
- );
- runOnlyForDeploymentPostprocessing = 0;
- };
- 97C146EC1CF9000F007C117D /* Resources */ = {
- isa = PBXResourcesBuildPhase;
- buildActionMask = 2147483647;
- files = (
- 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
- 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
- 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
- 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
- );
- runOnlyForDeploymentPostprocessing = 0;
- };
-/* End PBXResourcesBuildPhase section */
-
-/* Begin PBXShellScriptBuildPhase section */
- 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
- isa = PBXShellScriptBuildPhase;
- alwaysOutOfDate = 1;
- buildActionMask = 2147483647;
- files = (
- );
- inputPaths = (
- "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
- );
- name = "Thin Binary";
- outputPaths = (
- );
- runOnlyForDeploymentPostprocessing = 0;
- shellPath = /bin/sh;
- shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
- };
- 9740EEB61CF901F6004384FC /* Run Script */ = {
- isa = PBXShellScriptBuildPhase;
- alwaysOutOfDate = 1;
- buildActionMask = 2147483647;
- files = (
- );
- inputPaths = (
- );
- name = "Run Script";
- outputPaths = (
- );
- runOnlyForDeploymentPostprocessing = 0;
- shellPath = /bin/sh;
- shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
- };
-/* End PBXShellScriptBuildPhase section */
-
-/* Begin PBXSourcesBuildPhase section */
- 331C807D294A63A400263BE5 /* Sources */ = {
- isa = PBXSourcesBuildPhase;
- buildActionMask = 2147483647;
- files = (
- 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */,
- );
- runOnlyForDeploymentPostprocessing = 0;
- };
- 97C146EA1CF9000F007C117D /* Sources */ = {
- isa = PBXSourcesBuildPhase;
- buildActionMask = 2147483647;
- files = (
- 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
- 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
- 7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */,
- );
- runOnlyForDeploymentPostprocessing = 0;
- };
-/* End PBXSourcesBuildPhase section */
-
-/* Begin PBXTargetDependency section */
- 331C8086294A63A400263BE5 /* PBXTargetDependency */ = {
- isa = PBXTargetDependency;
- target = 97C146ED1CF9000F007C117D /* Runner */;
- targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
- };
-/* End PBXTargetDependency section */
-
-/* Begin PBXVariantGroup section */
- 97C146FA1CF9000F007C117D /* Main.storyboard */ = {
- isa = PBXVariantGroup;
- children = (
- 97C146FB1CF9000F007C117D /* Base */,
- );
- name = Main.storyboard;
- sourceTree = "";
- };
- 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
- isa = PBXVariantGroup;
- children = (
- 97C147001CF9000F007C117D /* Base */,
- );
- name = LaunchScreen.storyboard;
- sourceTree = "";
- };
-/* End PBXVariantGroup section */
-
-/* Begin XCBuildConfiguration section */
- 249021D3217E4FDB00AE95B9 /* Profile */ = {
- isa = XCBuildConfiguration;
- buildSettings = {
- ALWAYS_SEARCH_USER_PATHS = NO;
- ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
- CLANG_ANALYZER_NONNULL = YES;
- CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
- CLANG_CXX_LIBRARY = "libc++";
- CLANG_ENABLE_MODULES = YES;
- CLANG_ENABLE_OBJC_ARC = YES;
- CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
- CLANG_WARN_BOOL_CONVERSION = YES;
- CLANG_WARN_COMMA = YES;
- CLANG_WARN_CONSTANT_CONVERSION = YES;
- CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
- CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
- CLANG_WARN_EMPTY_BODY = YES;
- CLANG_WARN_ENUM_CONVERSION = YES;
- CLANG_WARN_INFINITE_RECURSION = YES;
- CLANG_WARN_INT_CONVERSION = YES;
- CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
- CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
- CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
- CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
- CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
- CLANG_WARN_STRICT_PROTOTYPES = YES;
- CLANG_WARN_SUSPICIOUS_MOVE = YES;
- CLANG_WARN_UNREACHABLE_CODE = YES;
- CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
- "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
- COPY_PHASE_STRIP = NO;
- DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
- ENABLE_NS_ASSERTIONS = NO;
- ENABLE_STRICT_OBJC_MSGSEND = YES;
- ENABLE_USER_SCRIPT_SANDBOXING = NO;
- GCC_C_LANGUAGE_STANDARD = gnu99;
- GCC_NO_COMMON_BLOCKS = YES;
- GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
- GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
- GCC_WARN_UNDECLARED_SELECTOR = YES;
- GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
- GCC_WARN_UNUSED_FUNCTION = YES;
- GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 13.0;
- MTL_ENABLE_DEBUG_INFO = NO;
- SDKROOT = iphoneos;
- SUPPORTED_PLATFORMS = iphoneos;
- TARGETED_DEVICE_FAMILY = "1,2";
- VALIDATE_PRODUCT = YES;
- };
- name = Profile;
- };
- 249021D4217E4FDB00AE95B9 /* Profile */ = {
- isa = XCBuildConfiguration;
- baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
- buildSettings = {
- ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
- CLANG_ENABLE_MODULES = YES;
- CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
- ENABLE_BITCODE = NO;
- INFOPLIST_FILE = Runner/Info.plist;
- LD_RUNPATH_SEARCH_PATHS = (
- "$(inherited)",
- "@executable_path/Frameworks",
- );
- PRODUCT_BUNDLE_IDENTIFIER = com.matchlivetv.matchLiveTv;
- PRODUCT_NAME = "$(TARGET_NAME)";
- SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
- SWIFT_VERSION = 5.0;
- VERSIONING_SYSTEM = "apple-generic";
- };
- name = Profile;
- };
- 331C8088294A63A400263BE5 /* Debug */ = {
- isa = XCBuildConfiguration;
- buildSettings = {
- BUNDLE_LOADER = "$(TEST_HOST)";
- CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 1;
- GENERATE_INFOPLIST_FILE = YES;
- MARKETING_VERSION = 1.0;
- PRODUCT_BUNDLE_IDENTIFIER = com.matchlivetv.matchLiveTv.RunnerTests;
- PRODUCT_NAME = "$(TARGET_NAME)";
- SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
- SWIFT_OPTIMIZATION_LEVEL = "-Onone";
- SWIFT_VERSION = 5.0;
- TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
- };
- name = Debug;
- };
- 331C8089294A63A400263BE5 /* Release */ = {
- isa = XCBuildConfiguration;
- buildSettings = {
- BUNDLE_LOADER = "$(TEST_HOST)";
- CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 1;
- GENERATE_INFOPLIST_FILE = YES;
- MARKETING_VERSION = 1.0;
- PRODUCT_BUNDLE_IDENTIFIER = com.matchlivetv.matchLiveTv.RunnerTests;
- PRODUCT_NAME = "$(TARGET_NAME)";
- SWIFT_VERSION = 5.0;
- TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
- };
- name = Release;
- };
- 331C808A294A63A400263BE5 /* Profile */ = {
- isa = XCBuildConfiguration;
- buildSettings = {
- BUNDLE_LOADER = "$(TEST_HOST)";
- CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 1;
- GENERATE_INFOPLIST_FILE = YES;
- MARKETING_VERSION = 1.0;
- PRODUCT_BUNDLE_IDENTIFIER = com.matchlivetv.matchLiveTv.RunnerTests;
- PRODUCT_NAME = "$(TARGET_NAME)";
- SWIFT_VERSION = 5.0;
- TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
- };
- name = Profile;
- };
- 97C147031CF9000F007C117D /* Debug */ = {
- isa = XCBuildConfiguration;
- buildSettings = {
- ALWAYS_SEARCH_USER_PATHS = NO;
- ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
- CLANG_ANALYZER_NONNULL = YES;
- CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
- CLANG_CXX_LIBRARY = "libc++";
- CLANG_ENABLE_MODULES = YES;
- CLANG_ENABLE_OBJC_ARC = YES;
- CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
- CLANG_WARN_BOOL_CONVERSION = YES;
- CLANG_WARN_COMMA = YES;
- CLANG_WARN_CONSTANT_CONVERSION = YES;
- CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
- CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
- CLANG_WARN_EMPTY_BODY = YES;
- CLANG_WARN_ENUM_CONVERSION = YES;
- CLANG_WARN_INFINITE_RECURSION = YES;
- CLANG_WARN_INT_CONVERSION = YES;
- CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
- CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
- CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
- CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
- CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
- CLANG_WARN_STRICT_PROTOTYPES = YES;
- CLANG_WARN_SUSPICIOUS_MOVE = YES;
- CLANG_WARN_UNREACHABLE_CODE = YES;
- CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
- "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
- COPY_PHASE_STRIP = NO;
- DEBUG_INFORMATION_FORMAT = dwarf;
- ENABLE_STRICT_OBJC_MSGSEND = YES;
- ENABLE_TESTABILITY = YES;
- ENABLE_USER_SCRIPT_SANDBOXING = NO;
- GCC_C_LANGUAGE_STANDARD = gnu99;
- GCC_DYNAMIC_NO_PIC = NO;
- GCC_NO_COMMON_BLOCKS = YES;
- GCC_OPTIMIZATION_LEVEL = 0;
- GCC_PREPROCESSOR_DEFINITIONS = (
- "DEBUG=1",
- "$(inherited)",
- );
- GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
- GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
- GCC_WARN_UNDECLARED_SELECTOR = YES;
- GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
- GCC_WARN_UNUSED_FUNCTION = YES;
- GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 13.0;
- MTL_ENABLE_DEBUG_INFO = YES;
- ONLY_ACTIVE_ARCH = YES;
- SDKROOT = iphoneos;
- TARGETED_DEVICE_FAMILY = "1,2";
- };
- name = Debug;
- };
- 97C147041CF9000F007C117D /* Release */ = {
- isa = XCBuildConfiguration;
- buildSettings = {
- ALWAYS_SEARCH_USER_PATHS = NO;
- ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
- CLANG_ANALYZER_NONNULL = YES;
- CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
- CLANG_CXX_LIBRARY = "libc++";
- CLANG_ENABLE_MODULES = YES;
- CLANG_ENABLE_OBJC_ARC = YES;
- CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
- CLANG_WARN_BOOL_CONVERSION = YES;
- CLANG_WARN_COMMA = YES;
- CLANG_WARN_CONSTANT_CONVERSION = YES;
- CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
- CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
- CLANG_WARN_EMPTY_BODY = YES;
- CLANG_WARN_ENUM_CONVERSION = YES;
- CLANG_WARN_INFINITE_RECURSION = YES;
- CLANG_WARN_INT_CONVERSION = YES;
- CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
- CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
- CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
- CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
- CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
- CLANG_WARN_STRICT_PROTOTYPES = YES;
- CLANG_WARN_SUSPICIOUS_MOVE = YES;
- CLANG_WARN_UNREACHABLE_CODE = YES;
- CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
- "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
- COPY_PHASE_STRIP = NO;
- DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
- ENABLE_NS_ASSERTIONS = NO;
- ENABLE_STRICT_OBJC_MSGSEND = YES;
- ENABLE_USER_SCRIPT_SANDBOXING = NO;
- GCC_C_LANGUAGE_STANDARD = gnu99;
- GCC_NO_COMMON_BLOCKS = YES;
- GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
- GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
- GCC_WARN_UNDECLARED_SELECTOR = YES;
- GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
- GCC_WARN_UNUSED_FUNCTION = YES;
- GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 13.0;
- MTL_ENABLE_DEBUG_INFO = NO;
- SDKROOT = iphoneos;
- SUPPORTED_PLATFORMS = iphoneos;
- SWIFT_COMPILATION_MODE = wholemodule;
- SWIFT_OPTIMIZATION_LEVEL = "-O";
- TARGETED_DEVICE_FAMILY = "1,2";
- VALIDATE_PRODUCT = YES;
- };
- name = Release;
- };
- 97C147061CF9000F007C117D /* Debug */ = {
- isa = XCBuildConfiguration;
- baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
- buildSettings = {
- ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
- CLANG_ENABLE_MODULES = YES;
- CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
- ENABLE_BITCODE = NO;
- INFOPLIST_FILE = Runner/Info.plist;
- LD_RUNPATH_SEARCH_PATHS = (
- "$(inherited)",
- "@executable_path/Frameworks",
- );
- PRODUCT_BUNDLE_IDENTIFIER = com.matchlivetv.matchLiveTv;
- PRODUCT_NAME = "$(TARGET_NAME)";
- SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
- SWIFT_OPTIMIZATION_LEVEL = "-Onone";
- SWIFT_VERSION = 5.0;
- VERSIONING_SYSTEM = "apple-generic";
- };
- name = Debug;
- };
- 97C147071CF9000F007C117D /* Release */ = {
- isa = XCBuildConfiguration;
- baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
- buildSettings = {
- ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
- CLANG_ENABLE_MODULES = YES;
- CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
- ENABLE_BITCODE = NO;
- INFOPLIST_FILE = Runner/Info.plist;
- LD_RUNPATH_SEARCH_PATHS = (
- "$(inherited)",
- "@executable_path/Frameworks",
- );
- PRODUCT_BUNDLE_IDENTIFIER = com.matchlivetv.matchLiveTv;
- PRODUCT_NAME = "$(TARGET_NAME)";
- SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
- SWIFT_VERSION = 5.0;
- VERSIONING_SYSTEM = "apple-generic";
- };
- name = Release;
- };
-/* End XCBuildConfiguration section */
-
-/* Begin XCConfigurationList section */
- 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
- isa = XCConfigurationList;
- buildConfigurations = (
- 331C8088294A63A400263BE5 /* Debug */,
- 331C8089294A63A400263BE5 /* Release */,
- 331C808A294A63A400263BE5 /* Profile */,
- );
- defaultConfigurationIsVisible = 0;
- defaultConfigurationName = Release;
- };
- 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
- isa = XCConfigurationList;
- buildConfigurations = (
- 97C147031CF9000F007C117D /* Debug */,
- 97C147041CF9000F007C117D /* Release */,
- 249021D3217E4FDB00AE95B9 /* Profile */,
- );
- defaultConfigurationIsVisible = 0;
- defaultConfigurationName = Release;
- };
- 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
- isa = XCConfigurationList;
- buildConfigurations = (
- 97C147061CF9000F007C117D /* Debug */,
- 97C147071CF9000F007C117D /* Release */,
- 249021D4217E4FDB00AE95B9 /* Profile */,
- );
- defaultConfigurationIsVisible = 0;
- defaultConfigurationName = Release;
- };
-/* End XCConfigurationList section */
-
-/* Begin XCLocalSwiftPackageReference section */
- 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = {
- isa = XCLocalSwiftPackageReference;
- relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage;
- };
-/* End XCLocalSwiftPackageReference section */
-
-/* Begin XCSwiftPackageProductDependency section */
- 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = {
- isa = XCSwiftPackageProductDependency;
- productName = FlutterGeneratedPluginSwiftPackage;
- };
-/* End XCSwiftPackageProductDependency section */
- };
- rootObject = 97C146E61CF9000F007C117D /* Project object */;
-}
diff --git a/mobile/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/mobile/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
deleted file mode 100644
index 919434a..0000000
--- a/mobile/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
-
diff --git a/mobile/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/mobile/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
deleted file mode 100644
index 18d9810..0000000
--- a/mobile/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
- IDEDidComputeMac32BitWarning
-
-
-
diff --git a/mobile/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/mobile/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
deleted file mode 100644
index f9b0d7c..0000000
--- a/mobile/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
- PreviewsEnabled
-
-
-
diff --git a/mobile/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/mobile/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
deleted file mode 100644
index c3fedb2..0000000
--- a/mobile/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
+++ /dev/null
@@ -1,119 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/mobile/ios/Runner.xcworkspace/contents.xcworkspacedata b/mobile/ios/Runner.xcworkspace/contents.xcworkspacedata
deleted file mode 100644
index 1d526a1..0000000
--- a/mobile/ios/Runner.xcworkspace/contents.xcworkspacedata
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
-
diff --git a/mobile/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/mobile/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
deleted file mode 100644
index 18d9810..0000000
--- a/mobile/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
- IDEDidComputeMac32BitWarning
-
-
-
diff --git a/mobile/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/mobile/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
deleted file mode 100644
index f9b0d7c..0000000
--- a/mobile/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
- PreviewsEnabled
-
-
-
diff --git a/mobile/ios/Runner/AppDelegate.swift b/mobile/ios/Runner/AppDelegate.swift
deleted file mode 100644
index c30b367..0000000
--- a/mobile/ios/Runner/AppDelegate.swift
+++ /dev/null
@@ -1,16 +0,0 @@
-import Flutter
-import UIKit
-
-@main
-@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
- override func application(
- _ application: UIApplication,
- didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
- ) -> Bool {
- return super.application(application, didFinishLaunchingWithOptions: launchOptions)
- }
-
- func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
- GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
- }
-}
diff --git a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
deleted file mode 100644
index d36b1fa..0000000
--- a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
+++ /dev/null
@@ -1,122 +0,0 @@
-{
- "images" : [
- {
- "size" : "20x20",
- "idiom" : "iphone",
- "filename" : "Icon-App-20x20@2x.png",
- "scale" : "2x"
- },
- {
- "size" : "20x20",
- "idiom" : "iphone",
- "filename" : "Icon-App-20x20@3x.png",
- "scale" : "3x"
- },
- {
- "size" : "29x29",
- "idiom" : "iphone",
- "filename" : "Icon-App-29x29@1x.png",
- "scale" : "1x"
- },
- {
- "size" : "29x29",
- "idiom" : "iphone",
- "filename" : "Icon-App-29x29@2x.png",
- "scale" : "2x"
- },
- {
- "size" : "29x29",
- "idiom" : "iphone",
- "filename" : "Icon-App-29x29@3x.png",
- "scale" : "3x"
- },
- {
- "size" : "40x40",
- "idiom" : "iphone",
- "filename" : "Icon-App-40x40@2x.png",
- "scale" : "2x"
- },
- {
- "size" : "40x40",
- "idiom" : "iphone",
- "filename" : "Icon-App-40x40@3x.png",
- "scale" : "3x"
- },
- {
- "size" : "60x60",
- "idiom" : "iphone",
- "filename" : "Icon-App-60x60@2x.png",
- "scale" : "2x"
- },
- {
- "size" : "60x60",
- "idiom" : "iphone",
- "filename" : "Icon-App-60x60@3x.png",
- "scale" : "3x"
- },
- {
- "size" : "20x20",
- "idiom" : "ipad",
- "filename" : "Icon-App-20x20@1x.png",
- "scale" : "1x"
- },
- {
- "size" : "20x20",
- "idiom" : "ipad",
- "filename" : "Icon-App-20x20@2x.png",
- "scale" : "2x"
- },
- {
- "size" : "29x29",
- "idiom" : "ipad",
- "filename" : "Icon-App-29x29@1x.png",
- "scale" : "1x"
- },
- {
- "size" : "29x29",
- "idiom" : "ipad",
- "filename" : "Icon-App-29x29@2x.png",
- "scale" : "2x"
- },
- {
- "size" : "40x40",
- "idiom" : "ipad",
- "filename" : "Icon-App-40x40@1x.png",
- "scale" : "1x"
- },
- {
- "size" : "40x40",
- "idiom" : "ipad",
- "filename" : "Icon-App-40x40@2x.png",
- "scale" : "2x"
- },
- {
- "size" : "76x76",
- "idiom" : "ipad",
- "filename" : "Icon-App-76x76@1x.png",
- "scale" : "1x"
- },
- {
- "size" : "76x76",
- "idiom" : "ipad",
- "filename" : "Icon-App-76x76@2x.png",
- "scale" : "2x"
- },
- {
- "size" : "83.5x83.5",
- "idiom" : "ipad",
- "filename" : "Icon-App-83.5x83.5@2x.png",
- "scale" : "2x"
- },
- {
- "size" : "1024x1024",
- "idiom" : "ios-marketing",
- "filename" : "Icon-App-1024x1024@1x.png",
- "scale" : "1x"
- }
- ],
- "info" : {
- "version" : 1,
- "author" : "xcode"
- }
-}
diff --git a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
deleted file mode 100644
index dc9ada4..0000000
Binary files a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png and /dev/null differ
diff --git a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
deleted file mode 100644
index 7353c41..0000000
Binary files a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png and /dev/null differ
diff --git a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
deleted file mode 100644
index 797d452..0000000
Binary files a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png and /dev/null differ
diff --git a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
deleted file mode 100644
index 6ed2d93..0000000
Binary files a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png and /dev/null differ
diff --git a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
deleted file mode 100644
index 4cd7b00..0000000
Binary files a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png and /dev/null differ
diff --git a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
deleted file mode 100644
index fe73094..0000000
Binary files a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png and /dev/null differ
diff --git a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
deleted file mode 100644
index 321773c..0000000
Binary files a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png and /dev/null differ
diff --git a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
deleted file mode 100644
index 797d452..0000000
Binary files a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png and /dev/null differ
diff --git a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
deleted file mode 100644
index 502f463..0000000
Binary files a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png and /dev/null differ
diff --git a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
deleted file mode 100644
index 0ec3034..0000000
Binary files a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png and /dev/null differ
diff --git a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
deleted file mode 100644
index 0ec3034..0000000
Binary files a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png and /dev/null differ
diff --git a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
deleted file mode 100644
index e9f5fea..0000000
Binary files a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png and /dev/null differ
diff --git a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
deleted file mode 100644
index 84ac32a..0000000
Binary files a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png and /dev/null differ
diff --git a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
deleted file mode 100644
index 8953cba..0000000
Binary files a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png and /dev/null differ
diff --git a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
deleted file mode 100644
index 0467bf1..0000000
Binary files a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png and /dev/null differ
diff --git a/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
deleted file mode 100644
index 0bedcf2..0000000
--- a/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
+++ /dev/null
@@ -1,23 +0,0 @@
-{
- "images" : [
- {
- "idiom" : "universal",
- "filename" : "LaunchImage.png",
- "scale" : "1x"
- },
- {
- "idiom" : "universal",
- "filename" : "LaunchImage@2x.png",
- "scale" : "2x"
- },
- {
- "idiom" : "universal",
- "filename" : "LaunchImage@3x.png",
- "scale" : "3x"
- }
- ],
- "info" : {
- "version" : 1,
- "author" : "xcode"
- }
-}
diff --git a/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
deleted file mode 100644
index 9da19ea..0000000
Binary files a/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png and /dev/null differ
diff --git a/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
deleted file mode 100644
index 9da19ea..0000000
Binary files a/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png and /dev/null differ
diff --git a/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
deleted file mode 100644
index 9da19ea..0000000
Binary files a/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png and /dev/null differ
diff --git a/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
deleted file mode 100644
index 89c2725..0000000
--- a/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
+++ /dev/null
@@ -1,5 +0,0 @@
-# Launch Screen Assets
-
-You can customize the launch screen with your own desired assets by replacing the image files in this directory.
-
-You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.
\ No newline at end of file
diff --git a/mobile/ios/Runner/Base.lproj/LaunchScreen.storyboard b/mobile/ios/Runner/Base.lproj/LaunchScreen.storyboard
deleted file mode 100644
index f2e259c..0000000
--- a/mobile/ios/Runner/Base.lproj/LaunchScreen.storyboard
+++ /dev/null
@@ -1,37 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/mobile/ios/Runner/Base.lproj/Main.storyboard b/mobile/ios/Runner/Base.lproj/Main.storyboard
deleted file mode 100644
index f3c2851..0000000
--- a/mobile/ios/Runner/Base.lproj/Main.storyboard
+++ /dev/null
@@ -1,26 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/mobile/ios/Runner/Info.plist b/mobile/ios/Runner/Info.plist
deleted file mode 100644
index 2189020..0000000
--- a/mobile/ios/Runner/Info.plist
+++ /dev/null
@@ -1,70 +0,0 @@
-
-
-
-
- CADisableMinimumFrameDurationOnPhone
-
- CFBundleDevelopmentRegion
- $(DEVELOPMENT_LANGUAGE)
- CFBundleDisplayName
- Match Live TV
- CFBundleExecutable
- $(EXECUTABLE_NAME)
- CFBundleIdentifier
- $(PRODUCT_BUNDLE_IDENTIFIER)
- CFBundleInfoDictionaryVersion
- 6.0
- CFBundleName
- match_live_tv
- CFBundlePackageType
- APPL
- CFBundleShortVersionString
- $(FLUTTER_BUILD_NAME)
- CFBundleSignature
- ????
- CFBundleVersion
- $(FLUTTER_BUILD_NUMBER)
- LSRequiresIPhoneOS
-
- UIApplicationSceneManifest
-
- UIApplicationSupportsMultipleScenes
-
- UISceneConfigurations
-
- UIWindowSceneSessionRoleApplication
-
-
- UISceneClassName
- UIWindowScene
- UISceneConfigurationName
- flutter
- UISceneDelegateClassName
- $(PRODUCT_MODULE_NAME).SceneDelegate
- UISceneStoryboardFile
- Main
-
-
-
-
- UIApplicationSupportsIndirectInputEvents
-
- UILaunchStoryboardName
- LaunchScreen
- UIMainStoryboardFile
- Main
- UISupportedInterfaceOrientations
-
- UIInterfaceOrientationPortrait
- UIInterfaceOrientationLandscapeLeft
- UIInterfaceOrientationLandscapeRight
-
- UISupportedInterfaceOrientations~ipad
-
- UIInterfaceOrientationPortrait
- UIInterfaceOrientationPortraitUpsideDown
- UIInterfaceOrientationLandscapeLeft
- UIInterfaceOrientationLandscapeRight
-
-
-
diff --git a/mobile/ios/Runner/Runner-Bridging-Header.h b/mobile/ios/Runner/Runner-Bridging-Header.h
deleted file mode 100644
index 308a2a5..0000000
--- a/mobile/ios/Runner/Runner-Bridging-Header.h
+++ /dev/null
@@ -1 +0,0 @@
-#import "GeneratedPluginRegistrant.h"
diff --git a/mobile/ios/Runner/SceneDelegate.swift b/mobile/ios/Runner/SceneDelegate.swift
deleted file mode 100644
index b9ce8ea..0000000
--- a/mobile/ios/Runner/SceneDelegate.swift
+++ /dev/null
@@ -1,6 +0,0 @@
-import Flutter
-import UIKit
-
-class SceneDelegate: FlutterSceneDelegate {
-
-}
diff --git a/mobile/ios/RunnerTests/RunnerTests.swift b/mobile/ios/RunnerTests/RunnerTests.swift
deleted file mode 100644
index 86a7c3b..0000000
--- a/mobile/ios/RunnerTests/RunnerTests.swift
+++ /dev/null
@@ -1,12 +0,0 @@
-import Flutter
-import UIKit
-import XCTest
-
-class RunnerTests: XCTestCase {
-
- func testExample() {
- // If you add code to the Runner application, consider adding tests here.
- // See https://developer.apple.com/documentation/xctest for more information about using XCTest.
- }
-
-}
diff --git a/mobile/lib/app/router.dart b/mobile/lib/app/router.dart
deleted file mode 100644
index 4e62848..0000000
--- a/mobile/lib/app/router.dart
+++ /dev/null
@@ -1,84 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:flutter_riverpod/flutter_riverpod.dart';
-import 'package:go_router/go_router.dart';
-
-import '../features/auth/login_screen.dart';
-import '../features/auth/splash_screen.dart';
-import '../features/camera_mode/camera_screen.dart';
-import '../features/archive/archive_screen.dart';
-import '../features/matches/matches_screen.dart';
-import '../features/setup_wizard/wizard_shell.dart';
-import '../providers/auth_provider.dart';
-
-/// Notifica GoRouter solo su cambi auth rilevanti (non su ogni loading tick).
-class _AuthRefreshNotifier extends ChangeNotifier {
- _AuthRefreshNotifier(this._ref) {
- _ref.listen(authProvider, (prev, next) {
- final authChanged = prev?.isAuthenticated != next.isAuthenticated;
- if (authChanged) notifyListeners();
- });
- }
-
- final Ref _ref;
-}
-
-final routerProvider = Provider((ref) {
- final refresh = _AuthRefreshNotifier(ref);
-
- return GoRouter(
- initialLocation: '/',
- refreshListenable: refresh,
- redirect: (context, state) {
- final auth = ref.read(authProvider);
- final loggingIn = state.matchedLocation == '/login';
- final onSplash = state.matchedLocation == '/';
-
- if (onSplash) return null;
-
- if (!auth.isAuthenticated && !loggingIn) {
- return '/login';
- }
-
- if (auth.isAuthenticated && loggingIn) {
- return '/matches';
- }
-
- return null;
- },
- routes: [
- GoRoute(
- path: '/',
- builder: (context, state) => const SplashScreen(),
- ),
- GoRoute(
- path: '/login',
- builder: (context, state) => LoginScreen(
- inviteToken: state.uri.queryParameters['invite'],
- ),
- ),
- GoRoute(
- path: '/matches',
- builder: (context, state) => const MatchesScreen(),
- ),
- GoRoute(
- path: '/archive',
- builder: (context, state) => const ArchiveScreen(),
- ),
- GoRoute(
- path: '/setup/:matchId/:step',
- builder: (context, state) {
- final matchId = state.pathParameters['matchId']!;
- final step = int.tryParse(state.pathParameters['step'] ?? '1') ?? 1;
- return WizardShell(matchId: matchId, step: step);
- },
- ),
- GoRoute(
- path: '/session/:id/camera',
- builder: (context, state) {
- final sessionId = state.pathParameters['id']!;
- return CameraScreen(sessionId: sessionId);
- },
- ),
- ],
- );
-});
diff --git a/mobile/lib/app/theme.dart b/mobile/lib/app/theme.dart
deleted file mode 100644
index 64bd13c..0000000
--- a/mobile/lib/app/theme.dart
+++ /dev/null
@@ -1,93 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:google_fonts/google_fonts.dart';
-
-/// Brand Match Live TV — dark theme, rosso #FF2D2D, Barlow Condensed.
-class AppTheme {
- AppTheme._();
-
- static const Color primaryRed = Color(0xFFFF2D2D);
- static const Color background = Color(0xFF0A0A0A);
- static const Color surface = Color(0xFF1E1E1E);
- static const Color surfaceElevated = Color(0xFF2A2A2A);
- static const Color accentYellow = Color(0xFFF5C518);
- static const Color successGreen = Color(0xFF22C55E);
- static const Color textSecondary = Color(0xFF9CA3AF);
-
- static TextTheme get _textTheme {
- final base = GoogleFonts.barlowCondensedTextTheme(
- ThemeData.dark().textTheme,
- );
- return base.copyWith(
- displayLarge: base.displayLarge?.copyWith(
- fontWeight: FontWeight.w900,
- letterSpacing: -0.5,
- ),
- displayMedium: base.displayMedium?.copyWith(fontWeight: FontWeight.w900),
- displaySmall: base.displaySmall?.copyWith(fontWeight: FontWeight.w900),
- headlineLarge: base.headlineLarge?.copyWith(fontWeight: FontWeight.w900),
- headlineMedium: base.headlineMedium?.copyWith(fontWeight: FontWeight.w800),
- headlineSmall: base.headlineSmall?.copyWith(fontWeight: FontWeight.w800),
- titleLarge: base.titleLarge?.copyWith(fontWeight: FontWeight.w700),
- titleMedium: base.titleMedium?.copyWith(fontWeight: FontWeight.w700),
- labelLarge: base.labelLarge?.copyWith(
- fontWeight: FontWeight.w800,
- letterSpacing: 1.2,
- ),
- bodyLarge: base.bodyLarge?.copyWith(fontWeight: FontWeight.w500),
- bodyMedium: base.bodyMedium?.copyWith(
- fontWeight: FontWeight.w500,
- color: textSecondary,
- ),
- );
- }
-
- static ThemeData get dark {
- return ThemeData(
- useMaterial3: true,
- brightness: Brightness.dark,
- scaffoldBackgroundColor: background,
- colorScheme: const ColorScheme.dark(
- primary: primaryRed,
- secondary: accentYellow,
- surface: surface,
- error: primaryRed,
- onPrimary: Colors.white,
- onSecondary: Colors.black,
- onSurface: Colors.white,
- ),
- textTheme: _textTheme,
- appBarTheme: AppBarTheme(
- backgroundColor: background,
- elevation: 0,
- centerTitle: true,
- titleTextStyle: _textTheme.titleLarge?.copyWith(color: Colors.white),
- iconTheme: const IconThemeData(color: Colors.white),
- ),
- cardTheme: CardThemeData(
- color: surface,
- elevation: 0,
- shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
- ),
- inputDecorationTheme: InputDecorationTheme(
- filled: true,
- fillColor: surfaceElevated,
- border: OutlineInputBorder(
- borderRadius: BorderRadius.circular(10),
- borderSide: BorderSide.none,
- ),
- labelStyle: _textTheme.bodyMedium,
- hintStyle: _textTheme.bodyMedium,
- ),
- segmentedButtonTheme: SegmentedButtonThemeData(
- style: ButtonStyle(
- backgroundColor: WidgetStateProperty.resolveWith((states) {
- if (states.contains(WidgetState.selected)) return primaryRed;
- return surfaceElevated;
- }),
- foregroundColor: WidgetStateProperty.all(Colors.white),
- ),
- ),
- dividerColor: surfaceElevated,
- );
- }
-}
diff --git a/mobile/lib/core/auth_storage.dart b/mobile/lib/core/auth_storage.dart
deleted file mode 100644
index ed379dc..0000000
--- a/mobile/lib/core/auth_storage.dart
+++ /dev/null
@@ -1,64 +0,0 @@
-import 'package:shared_preferences/shared_preferences.dart';
-
-import '../shared/models/user.dart';
-
-class AuthStorage {
- static const _keyAccess = 'auth_access_token';
- static const _keyRefresh = 'auth_refresh_token';
- static const _keyUserId = 'auth_user_id';
- static const _keyUserEmail = 'auth_user_email';
- static const _keyUserName = 'auth_user_name';
- static const _keyUserRole = 'auth_user_role';
-
- static Future<({User user, String accessToken, String refreshToken})?> load() async {
- final prefs = await SharedPreferences.getInstance();
- final access = prefs.getString(_keyAccess);
- final refresh = prefs.getString(_keyRefresh);
- final userId = prefs.getString(_keyUserId);
- final email = prefs.getString(_keyUserEmail);
- final name = prefs.getString(_keyUserName);
- if (access == null ||
- access.isEmpty ||
- refresh == null ||
- refresh.isEmpty ||
- userId == null ||
- email == null ||
- name == null) {
- return null;
- }
- return (
- user: User(
- id: userId,
- email: email,
- name: name,
- role: prefs.getString(_keyUserRole) ?? 'coach',
- ),
- accessToken: access,
- refreshToken: refresh,
- );
- }
-
- static Future save({
- required User user,
- required String accessToken,
- required String refreshToken,
- }) async {
- final prefs = await SharedPreferences.getInstance();
- await prefs.setString(_keyAccess, accessToken);
- await prefs.setString(_keyRefresh, refreshToken);
- await prefs.setString(_keyUserId, user.id);
- await prefs.setString(_keyUserEmail, user.email);
- await prefs.setString(_keyUserName, user.name);
- await prefs.setString(_keyUserRole, user.role);
- }
-
- static Future clear() async {
- final prefs = await SharedPreferences.getInstance();
- await prefs.remove(_keyAccess);
- await prefs.remove(_keyRefresh);
- await prefs.remove(_keyUserId);
- await prefs.remove(_keyUserEmail);
- await prefs.remove(_keyUserName);
- await prefs.remove(_keyUserRole);
- }
-}
diff --git a/mobile/lib/core/config.dart b/mobile/lib/core/config.dart
deleted file mode 100644
index 035886f..0000000
--- a/mobile/lib/core/config.dart
+++ /dev/null
@@ -1,24 +0,0 @@
-/// Configurazione globale dell'app Match Live TV.
-class AppConfig {
- AppConfig._();
-
- /// URL base API Rails. Default emulatore Android → host localhost.
- static const String apiBaseUrl = String.fromEnvironment(
- 'API_BASE_URL',
- defaultValue: 'http://10.0.2.2:3000',
- );
-
- static String get apiV1 => '$apiBaseUrl/api/v1';
-
- static String get cableUrl {
- final uri = Uri.parse(apiBaseUrl);
- final wsScheme = uri.scheme == 'https' ? 'wss' : 'ws';
- final defaultPort = uri.scheme == 'https' ? 443 : 80;
- final port = uri.hasPort ? uri.port : defaultPort;
- // Evita :443/:80 espliciti — alcuni client WebSocket/NPM li gestiscono male
- if (port == defaultPort) {
- return '$wsScheme://${uri.host}/cable';
- }
- return '$wsScheme://${uri.host}:$port/cable';
- }
-}
diff --git a/mobile/lib/core/deep_link_listener.dart b/mobile/lib/core/deep_link_listener.dart
deleted file mode 100644
index f9a5c1f..0000000
--- a/mobile/lib/core/deep_link_listener.dart
+++ /dev/null
@@ -1,77 +0,0 @@
-import 'dart:async';
-
-import 'package:app_links/app_links.dart';
-import 'package:flutter/material.dart';
-import 'package:flutter_riverpod/flutter_riverpod.dart';
-import 'package:go_router/go_router.dart';
-
-import '../features/matches/team_providers.dart';
-import 'invite_storage.dart';
-
-/// Gestisce link invito (join) e ritorno OAuth YouTube dall'app.
-class DeepLinkListener extends ConsumerStatefulWidget {
- const DeepLinkListener({super.key, required this.child});
-
- final Widget child;
-
- @override
- ConsumerState createState() => _DeepLinkListenerState();
-}
-
-class _DeepLinkListenerState extends ConsumerState {
- final _appLinks = AppLinks();
- StreamSubscription? _sub;
-
- @override
- void initState() {
- super.initState();
- _handleInitialLink();
- _sub = _appLinks.uriLinkStream.listen(_onUri, onError: (_) {});
- }
-
- Future _handleInitialLink() async {
- final uri = await _appLinks.getInitialLink();
- if (uri != null) await _onUri(uri);
- }
-
- Future _onUri(Uri uri) async {
- final inviteToken = _extractInviteToken(uri);
- if (inviteToken != null) {
- await InviteStorage.saveToken(inviteToken);
- if (!mounted) return;
- context.go('/login?invite=$inviteToken');
- return;
- }
-
- if (uri.scheme == 'matchlivetv' && uri.host == 'youtube-connected') {
- ref.invalidate(teamsProvider);
- if (!mounted) return;
- ScaffoldMessenger.of(context).showSnackBar(
- const SnackBar(
- content: Text('Canale YouTube collegato. Puoi selezionare YouTube Live.'),
- duration: Duration(seconds: 4),
- ),
- );
- }
- }
-
- String? _extractInviteToken(Uri uri) {
- if (uri.scheme == 'matchlivetv' && uri.host == 'join') {
- final segment = uri.pathSegments.isNotEmpty ? uri.pathSegments.first : uri.path.replaceFirst('/', '');
- return segment.isEmpty ? null : segment;
- }
- if (uri.pathSegments.length >= 2 && uri.pathSegments.first == 'join') {
- return uri.pathSegments[1];
- }
- return null;
- }
-
- @override
- void dispose() {
- _sub?.cancel();
- super.dispose();
- }
-
- @override
- Widget build(BuildContext context) => widget.child;
-}
diff --git a/mobile/lib/core/invite_storage.dart b/mobile/lib/core/invite_storage.dart
deleted file mode 100644
index 587edc4..0000000
--- a/mobile/lib/core/invite_storage.dart
+++ /dev/null
@@ -1,20 +0,0 @@
-import 'package:shared_preferences/shared_preferences.dart';
-
-class InviteStorage {
- static const _keyToken = 'pending_invite_token';
-
- static Future saveToken(String token) async {
- final prefs = await SharedPreferences.getInstance();
- await prefs.setString(_keyToken, token);
- }
-
- static Future readToken() async {
- final prefs = await SharedPreferences.getInstance();
- return prefs.getString(_keyToken);
- }
-
- static Future clear() async {
- final prefs = await SharedPreferences.getInstance();
- await prefs.remove(_keyToken);
- }
-}
diff --git a/mobile/lib/core/team_selection_storage.dart b/mobile/lib/core/team_selection_storage.dart
deleted file mode 100644
index f442fbd..0000000
--- a/mobile/lib/core/team_selection_storage.dart
+++ /dev/null
@@ -1,22 +0,0 @@
-import 'package:shared_preferences/shared_preferences.dart';
-
-class TeamSelectionStorage {
- static const _keySelectedTeamId = 'selected_team_id';
-
- static Future load() async {
- final prefs = await SharedPreferences.getInstance();
- final id = prefs.getString(_keySelectedTeamId);
- if (id == null || id.isEmpty) return null;
- return id;
- }
-
- static Future save(String teamId) async {
- final prefs = await SharedPreferences.getInstance();
- await prefs.setString(_keySelectedTeamId, teamId);
- }
-
- static Future clear() async {
- final prefs = await SharedPreferences.getInstance();
- await prefs.remove(_keySelectedTeamId);
- }
-}
diff --git a/mobile/lib/features/archive/archive_screen.dart b/mobile/lib/features/archive/archive_screen.dart
deleted file mode 100644
index fbdbea0..0000000
--- a/mobile/lib/features/archive/archive_screen.dart
+++ /dev/null
@@ -1,216 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:flutter_riverpod/flutter_riverpod.dart';
-import 'package:go_router/go_router.dart';
-import 'package:intl/intl.dart';
-import 'package:url_launcher/url_launcher.dart';
-
-import '../../app/theme.dart';
-import '../../shared/api_client.dart';
-import '../../shared/models/recording_item.dart';
-import '../../shared/premium_dialog.dart';
-import '../matches/team_providers.dart';
-
-class ArchiveScreen extends ConsumerWidget {
- const ArchiveScreen({super.key});
-
- @override
- Widget build(BuildContext context, WidgetRef ref) {
- final teamAsync = ref.watch(activeTeamProvider);
- final theme = Theme.of(context);
-
- return Scaffold(
- appBar: AppBar(
- title: const Text('Replay'),
- leading: IconButton(
- icon: const Icon(Icons.arrow_back),
- onPressed: () => context.go('/matches'),
- ),
- ),
- body: teamAsync.when(
- loading: () => const Center(
- child: CircularProgressIndicator(color: AppTheme.primaryRed),
- ),
- error: (e, _) => Center(child: Text('Errore: $e')),
- data: (team) {
- if (team == null) {
- return const Center(child: Text('Nessuna squadra'));
- }
- if (!team.canUseRecordings) {
- return Center(
- child: Padding(
- padding: const EdgeInsets.all(24),
- child: Column(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- Text(
- 'Replay disponibili con Premium Light o Full',
- style: theme.textTheme.titleMedium,
- textAlign: TextAlign.center,
- ),
- const SizedBox(height: 8),
- Text(
- 'Light: 30 giorni · Full: 90 giorni',
- style: theme.textTheme.bodyMedium?.copyWith(color: Colors.white70),
- textAlign: TextAlign.center,
- ),
- const SizedBox(height: 16),
- FilledButton(
- onPressed: () => showPremiumRequiredDialog(
- context,
- message: 'Salva e rivedi le gare con il piano Premium.',
- billingUrl: team.billingUrl,
- ),
- style: FilledButton.styleFrom(
- backgroundColor: AppTheme.primaryRed,
- ),
- child: const Text('Scopri Premium'),
- ),
- ],
- ),
- ),
- );
- }
-
- final recsAsync = ref.watch(recordingsProvider(team.id));
- return recsAsync.when(
- loading: () => const Center(
- child: CircularProgressIndicator(color: AppTheme.primaryRed),
- ),
- error: (e, _) => Center(child: Text('$e')),
- data: (recs) {
- if (recs.isEmpty) {
- return const Center(
- child: Padding(
- padding: EdgeInsets.all(24),
- child: Text(
- 'Nessun replay ancora.\nAl termine delle dirette Premium le registrazioni compaiono qui.',
- textAlign: TextAlign.center,
- ),
- ),
- );
- }
- final df = DateFormat('d MMM yyyy · HH:mm', 'it_IT');
- return ListView.builder(
- padding: const EdgeInsets.all(16),
- itemCount: recs.length,
- itemBuilder: (context, i) {
- final r = recs[i];
- final when = r.recordedAt ?? r.endedAt;
- final subtitle = [
- if (when != null) df.format(when.toLocal()),
- if (r.durationLabel != null) r.durationLabel,
- if (r.viewsLabel != null) r.viewsLabel,
- r.statusLabel,
- if (r.expiresAt != null)
- 'Scade ${DateFormat('d MMM', 'it_IT').format(r.expiresAt!.toLocal())}',
- ].whereType().join(' · ');
-
- return Card(
- color: AppTheme.surface,
- child: ListTile(
- leading: r.thumbnailUrl != null
- ? ClipRRect(
- borderRadius: BorderRadius.circular(6),
- child: Image.network(
- r.thumbnailUrl!,
- width: 72,
- height: 40,
- fit: BoxFit.cover,
- errorBuilder: (context, error, stackTrace) =>
- const Icon(Icons.movie),
- ),
- )
- : const Icon(Icons.movie_outlined, size: 40),
- title: Text(r.title),
- subtitle: Text(subtitle),
- isThreeLine: true,
- trailing: r.isProcessing
- ? const SizedBox(
- width: 24,
- height: 24,
- child: CircularProgressIndicator(strokeWidth: 2),
- )
- : PopupMenuButton(
- onSelected: (action) => _onMenuAction(
- context,
- ref,
- action,
- r,
- team.canDownloadOnPhone,
- ),
- itemBuilder: (_) => [
- if (r.isReady)
- const PopupMenuItem(
- value: 'play',
- child: Text('Guarda replay'),
- ),
- if (r.isReady && r.downloadEnabled && team.canDownloadOnPhone)
- const PopupMenuItem(
- value: 'download',
- child: Text('Scarica MP4'),
- ),
- if (r.youtubeWatchUrl != null)
- const PopupMenuItem(
- value: 'youtube',
- child: Text('Apri su YouTube'),
- ),
- ],
- ),
- onTap: r.isReady ? () => _openReplay(r.replayUrl) : null,
- ),
- );
- },
- );
- },
- );
- },
- ),
- );
- }
-
- Future _openReplay(String? url) async {
- if (url == null) return;
- final uri = Uri.parse(url);
- if (await canLaunchUrl(uri)) {
- await launchUrl(uri, mode: LaunchMode.inAppBrowserView);
- }
- }
-
- Future _onMenuAction(
- BuildContext context,
- WidgetRef ref,
- String action,
- RecordingItem r,
- bool canDownload,
- ) async {
- switch (action) {
- case 'play':
- await _openReplay(r.replayUrl);
- break;
- case 'download':
- if (!canDownload) return;
- try {
- final url = await ref.read(apiClientProvider).fetchRecordingDownloadUrl(r.id);
- final uri = Uri.parse(url);
- if (await canLaunchUrl(uri)) {
- await launchUrl(uri, mode: LaunchMode.externalApplication);
- }
- } catch (e) {
- if (context.mounted) {
- ScaffoldMessenger.of(context).showSnackBar(
- SnackBar(content: Text('Download: $e')),
- );
- }
- }
- break;
- case 'youtube':
- final yt = r.youtubeWatchUrl;
- if (yt == null) return;
- final uri = Uri.parse(yt);
- if (await canLaunchUrl(uri)) {
- await launchUrl(uri, mode: LaunchMode.externalApplication);
- }
- break;
- }
- }
-}
diff --git a/mobile/lib/features/auth/login_screen.dart b/mobile/lib/features/auth/login_screen.dart
deleted file mode 100644
index f4ebed7..0000000
--- a/mobile/lib/features/auth/login_screen.dart
+++ /dev/null
@@ -1,267 +0,0 @@
-import 'package:connectivity_plus/connectivity_plus.dart';
-import 'package:dio/dio.dart';
-import 'package:flutter/material.dart';
-import 'package:flutter_riverpod/flutter_riverpod.dart';
-import 'package:go_router/go_router.dart';
-
-import '../../app/theme.dart';
-import '../../core/invite_storage.dart';
-import '../../features/matches/team_providers.dart';
-import '../../providers/auth_provider.dart';
-import '../../shared/api_client.dart';
-import '../../shared/models/invite_preview.dart';
-import '../../shared/widgets/match_live_wordmark.dart';
-import '../../shared/widgets/primary_cta.dart';
-import '../../shared/widgets/screen_insets.dart';
-
-class LoginScreen extends ConsumerStatefulWidget {
- const LoginScreen({super.key, this.inviteToken});
-
- final String? inviteToken;
-
- @override
- ConsumerState createState() => _LoginScreenState();
-}
-
-class _LoginScreenState extends ConsumerState {
- final _formKey = GlobalKey();
- final _emailController = TextEditingController();
- final _passwordController = TextEditingController(text: 'password123');
- bool _obscure = true;
- InvitePreview? _invite;
- String? _inviteToken;
- bool _loadingInvite = false;
-
- @override
- void initState() {
- super.initState();
- _bootstrapInvite();
- }
-
- Future _bootstrapInvite() async {
- _inviteToken = widget.inviteToken ?? await InviteStorage.readToken();
- if (_inviteToken == null) return;
-
- setState(() => _loadingInvite = true);
- try {
- final preview = await ApiClient().fetchInvitePreview(_inviteToken!);
- if (!mounted) return;
- setState(() {
- _invite = preview;
- if (preview.email.isNotEmpty) {
- _emailController.text = preview.email;
- }
- });
- } catch (_) {
- if (mounted) {
- ScaffoldMessenger.of(context).showSnackBar(
- const SnackBar(content: Text('Invito non valido o scaduto')),
- );
- }
- await InviteStorage.clear();
- } finally {
- if (mounted) setState(() => _loadingInvite = false);
- }
- }
-
- @override
- void dispose() {
- _emailController.dispose();
- _passwordController.dispose();
- super.dispose();
- }
-
- Future _acceptInviteIfNeeded(ApiClient client) async {
- final token = _inviteToken;
- if (token == null) return;
-
- try {
- final message = await client.acceptInvitation(token);
- await InviteStorage.clear();
- if (mounted) {
- ScaffoldMessenger.of(context).showSnackBar(
- SnackBar(content: Text(message)),
- );
- }
- } on DioException catch (e) {
- final err = e.response?.data;
- final msg = err is Map ? (err['error'] as String?) : null;
- if (mounted && msg != null) {
- ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(msg)));
- }
- rethrow;
- }
- }
-
- Future _login() async {
- if (!_formKey.currentState!.validate()) return;
-
- final connectivity = await Connectivity().checkConnectivity();
- if (connectivity.contains(ConnectivityResult.none)) {
- ref.read(authProvider.notifier).setError(
- 'Nessuna connessione. Disattiva la modalità aereo e verifica Wi‑Fi.',
- );
- return;
- }
-
- ref.read(authProvider.notifier).setLoading(true);
- try {
- final client = ApiClient();
- final result = await client.login(
- email: _emailController.text.trim(),
- password: _passwordController.text,
- );
- ref.read(authProvider.notifier).setSession(
- user: result.user,
- accessToken: result.tokens.accessToken,
- refreshToken: result.tokens.refreshToken,
- );
-
- final authed = ApiClient(accessToken: result.tokens.accessToken);
- if (_inviteToken != null) {
- await _acceptInviteIfNeeded(authed);
- }
-
- ref.invalidate(teamsProvider);
- ref.invalidate(matchesProvider);
- ref.read(authProvider.notifier).setLoading(false);
- if (mounted) context.go('/matches');
- return;
- } on DioException catch (e) {
- if (e.response?.statusCode == 422) {
- ref.read(authProvider.notifier).setLoading(false);
- return;
- }
- final message = switch (e.type) {
- DioExceptionType.connectionTimeout ||
- DioExceptionType.receiveTimeout ||
- DioExceptionType.connectionError =>
- 'Server non raggiungibile. Verifica che il backend sia avviato (docker compose up).',
- DioExceptionType.badResponse when e.response?.statusCode == 401 =>
- 'Email o password non corretti',
- _ => 'Errore di rete: ${e.message}',
- };
- ref.read(authProvider.notifier).setError(message);
- } catch (e) {
- ref.read(authProvider.notifier).setError('Errore imprevisto: $e');
- }
- ref.read(authProvider.notifier).setLoading(false);
- }
-
- @override
- Widget build(BuildContext context) {
- final auth = ref.watch(authProvider);
- final theme = Theme.of(context);
-
- return Scaffold(
- body: SingleChildScrollView(
- padding: ScreenInsets.contentPadding(context, horizontal: 24, vertical: 16),
- child: Form(
- key: _formKey,
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.stretch,
- children: [
- const SizedBox(height: 32),
- const Center(child: MatchLiveWordmark(showSlogan: true)),
- const SizedBox(height: 48),
- if (_loadingInvite)
- const Padding(
- padding: EdgeInsets.only(bottom: 16),
- child: Center(
- child: CircularProgressIndicator(color: AppTheme.primaryRed),
- ),
- ),
- if (_invite != null) ...[
- Container(
- padding: const EdgeInsets.all(14),
- decoration: BoxDecoration(
- color: AppTheme.surfaceElevated,
- borderRadius: BorderRadius.circular(10),
- border: Border.all(color: AppTheme.primaryRed.withValues(alpha: 0.35)),
- ),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(
- 'Invito staff',
- style: theme.textTheme.titleMedium?.copyWith(color: AppTheme.primaryRed),
- ),
- const SizedBox(height: 6),
- Text(
- 'Entra in ${_invite!.teamName} come responsabile trasmissione.',
- style: theme.textTheme.bodyMedium,
- ),
- const SizedBox(height: 4),
- Text(
- 'Accedi con ${_invite!.email}',
- style: theme.textTheme.bodySmall,
- ),
- ],
- ),
- ),
- const SizedBox(height: 20),
- ],
- Text(
- 'ACCEDI',
- style: theme.textTheme.headlineMedium,
- textAlign: TextAlign.center,
- ),
- const SizedBox(height: 8),
- Text(
- _invite != null
- ? 'Usa l\'email dell\'invito, poi collega YouTube e vai in diretta'
- : 'Gestisci le dirette della tua squadra',
- style: theme.textTheme.bodyMedium,
- textAlign: TextAlign.center,
- ),
- const SizedBox(height: 32),
- TextFormField(
- controller: _emailController,
- keyboardType: TextInputType.emailAddress,
- readOnly: _invite != null,
- decoration: const InputDecoration(
- labelText: 'Email',
- hintText: 'coach@squadra.it',
- ),
- validator: (v) =>
- v == null || v.isEmpty ? 'Inserisci l\'email' : null,
- ),
- const SizedBox(height: 16),
- TextFormField(
- controller: _passwordController,
- obscureText: _obscure,
- decoration: InputDecoration(
- labelText: 'Password',
- suffixIcon: IconButton(
- icon: Icon(
- _obscure ? Icons.visibility : Icons.visibility_off,
- ),
- onPressed: () => setState(() => _obscure = !_obscure),
- ),
- ),
- validator: (v) =>
- v == null || v.isEmpty ? 'Inserisci la password' : null,
- ),
- if (auth.error != null) ...[
- const SizedBox(height: 12),
- Text(
- auth.error!,
- style: theme.textTheme.bodyMedium?.copyWith(
- color: AppTheme.primaryRed,
- ),
- textAlign: TextAlign.center,
- ),
- ],
- const SizedBox(height: 32),
- PrimaryCta(
- label: _invite != null ? 'ACCEDI E ACCETTA INVITO' : 'ACCEDI',
- loading: auth.loading,
- onPressed: _login,
- ),
- ],
- ),
- ),
- ),
- );
- }
-}
diff --git a/mobile/lib/features/auth/splash_screen.dart b/mobile/lib/features/auth/splash_screen.dart
deleted file mode 100644
index 2b25bdc..0000000
--- a/mobile/lib/features/auth/splash_screen.dart
+++ /dev/null
@@ -1,68 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:flutter_riverpod/flutter_riverpod.dart';
-import 'package:go_router/go_router.dart';
-
-import '../../app/theme.dart';
-import '../../providers/auth_provider.dart';
-import '../../shared/widgets/match_live_wordmark.dart';
-import '../../shared/widgets/screen_insets.dart';
-
-/// Splash con wordmark e slogan.
-class SplashScreen extends ConsumerStatefulWidget {
- const SplashScreen({super.key});
-
- @override
- ConsumerState createState() => _SplashScreenState();
-}
-
-class _SplashScreenState extends ConsumerState {
- @override
- void initState() {
- super.initState();
- _bootstrap();
- }
-
- Future _bootstrap() async {
- final notifier = ref.read(authProvider.notifier);
- await notifier.restoreSession();
- await Future.delayed(const Duration(milliseconds: 400));
- if (!mounted) return;
-
- var auth = ref.read(authProvider);
- if (auth.isAuthenticated) {
- final ok = await notifier.validateOrRefreshSession();
- if (!mounted) return;
- auth = ref.read(authProvider);
- if (!ok || !auth.isAuthenticated) {
- context.go('/login');
- return;
- }
- }
-
- if (!mounted) return;
- if (auth.isAuthenticated) {
- context.go('/matches');
- } else {
- context.go('/login');
- }
- }
-
- @override
- Widget build(BuildContext context) {
- return Scaffold(
- backgroundColor: AppTheme.background,
- body: AppSafeArea(
- child: Center(
- child: Column(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- const MatchLiveWordmark(showSlogan: true),
- const SizedBox(height: 48),
- const CircularProgressIndicator(color: AppTheme.primaryRed),
- ],
- ),
- ),
- ),
- );
- }
-}
diff --git a/mobile/lib/features/camera_mode/camera_screen.dart b/mobile/lib/features/camera_mode/camera_screen.dart
deleted file mode 100644
index 4031562..0000000
--- a/mobile/lib/features/camera_mode/camera_screen.dart
+++ /dev/null
@@ -1,923 +0,0 @@
-import 'dart:async';
-import 'dart:math' as math;
-
-import 'package:battery_plus/battery_plus.dart';
-import 'package:flutter/material.dart';
-import 'package:flutter/services.dart';
-import 'package:flutter_riverpod/flutter_riverpod.dart';
-import 'package:go_router/go_router.dart';
-import 'package:permission_handler/permission_handler.dart';
-import 'package:wakelock_plus/wakelock_plus.dart';
-
-import '../../app/theme.dart';
-import '../../platform/streaming_channel.dart';
-import '../../platform/streaming_preview.dart';
-import '../../providers/match_rules_provider.dart';
-import '../../providers/score_provider.dart';
-import '../../providers/score_sync_provider.dart';
-import '../../shared/api_client.dart';
-import '../../shared/regia_share.dart';
-import '../../shared/watch_share.dart';
-import '../../shared/models/score_state.dart';
-import '../../providers/session_provider.dart';
-import '../../shared/websocket_service.dart';
-import '../../shared/widgets/camera_compact_metrics.dart';
-import '../../shared/widgets/live_score_controls.dart';
-import '../../shared/widgets/destructive_cta.dart';
-import '../../shared/widgets/end_stream_dialog.dart';
-import '../../shared/widgets/live_badge.dart';
-import '../../shared/widgets/screen_insets.dart';
-
-class CameraScreen extends ConsumerStatefulWidget {
- const CameraScreen({super.key, required this.sessionId});
-
- final String sessionId;
-
- @override
- ConsumerState createState() => _CameraScreenState();
-}
-
-class _CameraScreenState extends ConsumerState {
- static const _previewKey = ValueKey('camera_streaming_preview');
-
- Timer? _elapsedTimer;
- Timer? _sessionSyncTimer;
- Timer? _telemetryTimer;
- Timer? _statsTimer;
- Timer? _scorePullTimer;
- int _batteryLevel = 100;
- int? _lastDelta;
- int _prevHomePoints = 0;
- int _prevAwayPoints = 0;
-
- double _bitrateMbps = 0;
- int _fps = 0;
- String _networkType = '4G';
- int _viewerCount = 0;
-
- StreamSubscription