first commit

This commit is contained in:
Daniel Esteban 2026-03-18 12:47:06 +01:00
commit 34e7cbc382
156 changed files with 10747 additions and 0 deletions

45
.gitignore vendored Normal file
View file

@ -0,0 +1,45 @@
# 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

45
.metadata Normal file
View file

@ -0,0 +1,45 @@
# 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: "8b872868494e429d94fa06dca855c306438b22c0"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: 8b872868494e429d94fa06dca855c306438b22c0
base_revision: 8b872868494e429d94fa06dca855c306438b22c0
- platform: android
create_revision: 8b872868494e429d94fa06dca855c306438b22c0
base_revision: 8b872868494e429d94fa06dca855c306438b22c0
- platform: ios
create_revision: 8b872868494e429d94fa06dca855c306438b22c0
base_revision: 8b872868494e429d94fa06dca855c306438b22c0
- platform: linux
create_revision: 8b872868494e429d94fa06dca855c306438b22c0
base_revision: 8b872868494e429d94fa06dca855c306438b22c0
- platform: macos
create_revision: 8b872868494e429d94fa06dca855c306438b22c0
base_revision: 8b872868494e429d94fa06dca855c306438b22c0
- platform: web
create_revision: 8b872868494e429d94fa06dca855c306438b22c0
base_revision: 8b872868494e429d94fa06dca855c306438b22c0
- platform: windows
create_revision: 8b872868494e429d94fa06dca855c306438b22c0
base_revision: 8b872868494e429d94fa06dca855c306438b22c0
# 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'

64
CLAUDE.md Normal file
View file

@ -0,0 +1,64 @@
# DeporOS — Guía para Claude
## Qué es este proyecto
Port del panel de administración PHP (`sc-admin`) de un club deportivo a Flutter.
**Solo el panel de administración** (uso interno), NO la parte pública.
## Rutas clave
| Recurso | Ruta |
|---|---|
| PHP original (referencia) | `/home/conejo/private_html/sportscenter/sc-admin/` |
| PHP sidebar (menú) | `/home/conejo/private_html/sportscenter/sc-admin/themes/modern/sidebar.php` |
| Flutter (este proyecto) | `/home/conejo/public_html/depor_os/` |
| Estado de tareas | `/home/conejo/public_html/depor_os/TAREAS.md` |
## API backend
- Base URL: `https://reservas.madriguera.me/2.0`
- Auth: `POST /login` con `{"email": "...", "password": "..."}``{"access_token": "..."}`
- Token Bearer, guardado en SharedPreferences. Sin expiración, sin endpoint de logout.
## Stack técnico
- Flutter con Material 3, seed color `#1565C0` (azul)
- `go_router ^14` para navegación (ShellRoute con sidebar)
- `http ^1.2` para llamadas a la API
- `shared_preferences ^2.3` para persistir el token
- Soporte light/dark mode
## Estructura Flutter (`lib/`)
```
core/
constants.dart ← kApiBase
router.dart ← GoRouter + ShellRoute, todas las rutas
models/
nav_item.dart ← modelo NavItem (label, icon, route, children)
navigation/
app_navigation.dart ← árbol completo del sidebar (kAdminNavItems)
services/
auth_service.dart ← login(), logout(), getToken(), isLoggedIn()
widgets/
app_shell.dart ← layout responsive: sidebar fijo ≥800px / drawer <800px
sidebar/
sidebar_widget.dart ← header + scroll nav + footer logout
nav_item_tile.dart ← tiles simples y grupos expandibles (ExpansionTile)
screens/
login/login_screen.dart
dashboard/dashboard_screen.dart ← 4 accesos rápidos en grid
placeholder_screen.dart ← pantalla temporal para módulos pendientes
```
## Cómo retomar trabajo
1. Leer `TAREAS.md` para ver qué está hecho y qué sigue
2. Para cada módulo nuevo: revisar el PHP original en `/home/conejo/private_html/sportscenter/sc-admin/`
3. Preguntar al usuario el endpoint de la API antes de codificar
4. Al terminar una tarea: actualizar `TAREAS.md`
## Plataformas y responsive
- **Objetivo principal:** web + tablet Android landscape
- **Breakpoint sidebar:** 800px (permanente) / <800px (drawer)
- **Grid dashboard:** ≥900px = 4 col / resto = 2 col
- Preparado para móvil aunque no es prioritario
## Convenciones
- UI en español
- No usar Scaffold en pantallas hijas del ShellRoute (ya lo provee AppShell)
- Pantallas aún no implementadas usan `PlaceholderScreen`

16
README.md Normal file
View file

@ -0,0 +1,16 @@
# depor_os
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:
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
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.

138
TAREAS.md Normal file
View file

@ -0,0 +1,138 @@
# DeporOS — Flutter Port (sc-admin)
> Proyecto: portar el panel de administración PHP (`sc-admin`) a Flutter (web + tablet Android, responsive).
> App en español. Backend: nueva API PHP separada (ya existe parcialmente).
> Sesiones de trabajo: incrementales, pantalla a pantalla.
---
## Contexto técnico
- **PHP origen:** `/home/conejo/private_html/sportscenter/sc-admin/`
- **Flutter destino:** `/home/conejo/public_html/depor_os/`
- **API backend:** proyecto separado (no en este repo), URL base `https://reservas.madriguera.me/2.0`
- **Plataformas:** Web + Tablet Android (responsive, preparado para móvil)
- **Auth:** Bearer token guardado en SharedPreferences. Sin logout en API, sin expiración.
- **UI:** Material 3, seed color `#1565C0` (azul), soporte light/dark
- **Dependencias:** `http ^1.2.0`, `shared_preferences ^2.3.0`, `go_router ^14.0.0`
---
## Módulos identificados en sc-admin
| Módulo | Fichero PHP principal | Estado Flutter |
|---|---|---|
| Login / Auth | `login.php`, `gui/login.php` | ✅ Hecho |
| Shell / Sidebar / Routing | — | ✅ Hecho |
| Dashboard | `main.php`, `index.php` | ✅ Hecho |
| Abonados | `members.php` | ⏳ Pendiente |
| Alumnos | `alumni.php` | ⏳ Pendiente |
| Búsqueda | `search.php` | ⏳ Pendiente |
| Reservas pistas | `booking.php` | ⏳ Pendiente |
| Reservas salas | `booking.php?rooms=true` | ⏳ Pendiente |
| Cursos | `courses.php` | ⏳ Pendiente |
| Recibos | `bills.php` | ⏳ Pendiente |
| Caja | `cash.php` | ⏳ Pendiente |
| Remesas SEPA | `remittance.php` | ⏳ Pendiente |
| Listados | `list.php` | ⏳ Pendiente |
| Utilidades | `extra.php` | ⏳ Pendiente |
| Tornos | `tools.php` | ⏳ Pendiente |
| Administradores / Permisos | `admins.php`, `tools.php` | ⏳ Pendiente |
| Emails | `emails.php` | ⏳ Pendiente |
---
## Tareas
### ✅ Completadas (sesión 4 — 2026-03-18)
- [x] **BOOK-01** — Reservas de pistas (maquetación completa)
- Layout responsive con dos breakpoints independientes:
- **910px (contenido):** panel derecho al lado / debajo del grid
- **900px (ventana):** lista de actividades al lado del calendario / dropdown encima
- Lado izquierdo: calendario (`CalendarDatePicker`) + lista o dropdown de actividades + grid horario/pistas
- Lado derecho (≥910px) / debajo (<910px): 6 botones de acción + 3 botones de pago (Efectivo/Tarjeta/Bizum) + ticket preview
- Grid: celdas se expanden al 100% del ancho disponible; scroll horizontal solo si no caben
- Celdas coloreadas: verde=pagada, rojo=impagada, blanco=libre
- Banner de celebraciones del día
- Patrón mock/prod: `BookingRepository` abstracto, `BookingService` (HTTP), `MockBookingService`
- `lib/models/booking.dart`, `lib/services/booking_repository.dart`, `lib/services/booking_service.dart`
- `lib/services/mock/mock_booking_service.dart`, `lib/screens/booking/booking_screen.dart`
### ✅ Completadas (sesión 3 — 2026-03-18)
- [x] **MEMB-01** — Ficha de abonado (solo lectura)
- Barra de navegación: búsqueda autocomplete + botones primero/anterior/siguiente/último
- Tarjeta de información: número, fecha de alta, tipo, estado (activo/baja/retenido)
- Tarjeta de comentarios: nota heredada, nota interna, lista de comentarios
- Tabla de familiares asociados (read-only): avatar, nombre, DNI, móvil, email, fecha nac.
- Tarjeta de ficha: dirección + datos bancarios
- Responsive: 2 columnas ≥700px / 1 columna en el resto
- `lib/models/member.dart`, `lib/services/member_service.dart`, `lib/screens/members/members_screen.dart`
### ✅ Completadas (sesión 2 — 2026-03-18)
- [x] **DASH-02** — Accesos rápidos en dashboard
- 4 tarjetas: Abonados, Reservas, Recibos por abonado, Caja
- Grid responsive: 4 columnas ≥900px / 2 columnas en el resto
- Cada tarjeta usa un color tonal de Material 3 (primary/secondary/tertiary/error container)
- `lib/screens/dashboard/dashboard_screen.dart`
### ✅ Completadas (sesión 1 — 2026-03-17)
- [x] **AUTH-01** — Pantalla de Login
- `POST /login` con `email`+`password` → `access_token` guardado en SharedPreferences
- Validación, spinner, SnackBar de error, responsive, dark mode
- `lib/core/constants.dart`, `lib/services/auth_service.dart`, `lib/screens/login/login_screen.dart`
- [x] **STRUCT-01** — Estructura base: GoRouter + ShellRoute + AppShell responsive
- Breakpoint sidebar permanente: **800px**. Por debajo → Drawer + AppBar
- `lib/core/router.dart`, `lib/widgets/app_shell.dart`
- [x] **STRUCT-02** — Sidebar navegación multinivel (Material 3)
- Árbol completo extraído del PHP (`sidebar.php`), grupos expandibles con `ExpansionTile`
- Resaltado de ruta activa, auto-expansión del grupo activo, logout en pie
- `lib/models/nav_item.dart`, `lib/navigation/app_navigation.dart`, `lib/widgets/sidebar/`
- [x] **DASH-01** — Dashboard (estructura base)
- Pantalla de inicio con placeholder para accesos rápidos
- Todas las rutas del sidebar apuntan a `PlaceholderScreen` hasta que se implementen
- `lib/screens/dashboard/dashboard_screen.dart`, `lib/screens/placeholder_screen.dart`
### 🔄 En progreso
_(ninguna)_
### ⏳ Pendientes (próximas sesiones)
- [ ] **AUTH-02** — Logo/imagen en pantalla de login (pendiente de asset)
- [ ] **MEMB-01** — Listado de abonados
- [ ] **MEMB-02** — Ficha de abonado (detalle + edición)
- [ ] **MEMB-03** — Pre-inscripciones
- [ ] **MEMB-04** — Alta de nuevo abonado
- [ ] **ALUM-01** — Listado de alumnos + altas/bajas
- [ ] **BOOK-01b** — Reservas de pistas (acciones reales: cobrar, devolver, imprimir ticket, QR)
- [ ] **BOOK-02** — Reservas de salas
- [ ] **CAJA-01** — Caja (cobros)
- [ ] **REC-01** — Recibos por abonado
- [ ] **REC-02** — Recibos por actividad
- [ ] **REM-01** — Remesas SEPA
- [ ] **LIST-01** — Listados (varios)
- [ ] **UTIL-01** — Utilidades / parámetros
---
## Decisiones técnicas
- API base: `https://reservas.madriguera.me/2.0`
- Auth: `POST /login``{"access_token": "..."}` Bearer, sin expiración, sin logout en API
- UI: Material 3, seed `#1565C0`, light + dark
- Sidebar breakpoint: 800px (permanente) / <800px (drawer)
- Breakpoints en pantallas: usar `LayoutBuilder` para medir el contenido disponible (ya excluye sidebar). Para breakpoints basados en ventana total, usar `MediaQuery.sizeOf(context).width`. Ejemplo: en BookingScreen, el layout de paneles usa LayoutBuilder (910px contenido), pero el selector actividad usa MediaQuery (900px ventana).
- Patrón mock/prod: `kUseMock` en `constants.dart`, repositorio abstracto en `*_repository.dart`, factory en `service_locator.dart`. Nunca importar mock desde el servicio real ni viceversa.
- Dependencias: `http ^1.2.0`, `shared_preferences ^2.3.0`, `go_router ^14.0.0`
---
## Pendiente de confirmar
- ¿Hay logo para la pantalla de login?

28
analysis_options.yaml Normal file
View file

@ -0,0 +1,28 @@
# 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

14
android/.gitignore vendored Normal file
View file

@ -0,0 +1,14 @@
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

View file

@ -0,0 +1,44 @@
plugins {
id("com.android.application")
id("kotlin-android")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
}
android {
namespace = "com.example.depor_os"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_17.toString()
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.example.depor_os"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
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")
}
}
}
flutter {
source = "../.."
}

View file

@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View file

@ -0,0 +1,45 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="depor_os"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest>

View file

@ -0,0 +1,5 @@
package com.example.depor_os
import io.flutter.embedding.android.FlutterActivity
class MainActivity : FlutterActivity()

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View file

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View file

@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

24
android/build.gradle.kts Normal file
View file

@ -0,0 +1,24 @@
allprojects {
repositories {
google()
mavenCentral()
}
}
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<Delete>("clean") {
delete(rootProject.layout.buildDirectory)
}

View file

@ -0,0 +1,2 @@
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true

View file

@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip

View file

@ -0,0 +1,26 @@
pluginManagement {
val flutterSdkPath =
run {
val properties = java.util.Properties()
file("local.properties").inputStream().use { properties.load(it) }
val flutterSdkPath = properties.getProperty("flutter.sdk")
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
flutterSdkPath
}
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.11.1" apply false
id("org.jetbrains.kotlin.android") version "2.2.20" apply false
}
include(":app")

BIN
claude/abonados.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

BIN
claude/reservas.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

1643
docs/api-v2.md Normal file

File diff suppressed because it is too large Load diff

34
ios/.gitignore vendored Normal file
View file

@ -0,0 +1,34 @@
**/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

View file

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>App</string>
<key>CFBundleIdentifier</key>
<string>io.flutter.flutter.app</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>App</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>13.0</string>
</dict>
</plist>

View file

@ -0,0 +1 @@
#include "Generated.xcconfig"

View file

@ -0,0 +1 @@
#include "Generated.xcconfig"

View file

@ -0,0 +1,616 @@
// !$*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 */; };
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 = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
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 = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
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 = "<group>"; };
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
331C8082294A63A400263BE5 /* RunnerTests */ = {
isa = PBXGroup;
children = (
331C807B294A618700263BE5 /* RunnerTests.swift */,
);
path = RunnerTests;
sourceTree = "<group>";
};
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
9740EEB31CF90195004384FC /* Generated.xcconfig */,
);
name = Flutter;
sourceTree = "<group>";
};
97C146E51CF9000F007C117D = {
isa = PBXGroup;
children = (
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */,
);
sourceTree = "<group>";
};
97C146EF1CF9000F007C117D /* Products */ = {
isa = PBXGroup;
children = (
97C146EE1CF9000F007C117D /* Runner.app */,
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
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 */,
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
);
path = Runner;
sourceTree = "<group>";
};
/* 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;
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;
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 */,
);
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 = "<group>";
};
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C147001CF9000F007C117D /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* 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.example.deporOs;
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.example.deporOs.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.example.deporOs.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.example.deporOs.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.example.deporOs;
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.example.deporOs;
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 */
};
rootObject = 97C146E61CF9000F007C117D /* Project object */;
}

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>

View file

@ -0,0 +1,101 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1510"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "331C8080294A63A400263BE5"
BuildableName = "RunnerTests.xctest"
BlueprintName = "RunnerTests"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
enableGPUValidationMode = "1"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Profile"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
</Workspace>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>

View file

@ -0,0 +1,13 @@
import Flutter
import UIKit
@main
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}

View file

@ -0,0 +1,122 @@
{
"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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 462 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 704 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 762 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -0,0 +1,23 @@
{
"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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

View file

@ -0,0 +1,5 @@
# 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.

View file

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
<viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
</imageView>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
</constraints>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
<resources>
<image name="LaunchImage" width="168" height="185"/>
</resources>
</document>

View file

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
</dependencies>
<scenes>
<!--Flutter View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
</scene>
</scenes>
</document>

49
ios/Runner/Info.plist Normal file
View file

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Depor Os</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>depor_os</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
</dict>
</plist>

View file

@ -0,0 +1 @@
#import "GeneratedPluginRegistrant.h"

View file

@ -0,0 +1,12 @@
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.
}
}

4
lib/core/constants.dart Normal file
View file

@ -0,0 +1,4 @@
const String kApiBase = 'https://reservas.madriguera.me/2.0';
/// Cambiar a false para usar la API real (necesita token).
const bool kUseMock = true;

102
lib/core/router.dart Normal file
View file

@ -0,0 +1,102 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import '../screens/booking/booking_screen.dart';
import '../screens/login/login_screen.dart';
import '../screens/dashboard/dashboard_screen.dart';
import '../screens/members/members_screen.dart';
import '../screens/placeholder_screen.dart';
import '../widgets/app_shell.dart';
final appRouter = GoRouter(
initialLocation: '/dashboard', // TODO: restaurar '/login' antes de producción
routes: [
GoRoute(
path: '/login',
builder: (context, state) => const LoginScreen(),
),
ShellRoute(
builder: (context, state, child) => AppShell(child: child),
routes: [
GoRoute(path: '/dashboard', builder: (context, state) => const DashboardScreen()),
// Abonados
GoRoute(path: '/abonados', builder: (_, __) => const MembersScreen()),
GoRoute(path: '/abonados/pre', builder: (_, s) => PlaceholderScreen(title: 'Pre-inscripciones', route: s.matchedLocation)),
GoRoute(path: '/abonados/nuevo', builder: (_, s) => PlaceholderScreen(title: 'Añadir abonado', route: s.matchedLocation)),
// Cursos
GoRoute(path: '/cursos', builder: (_, s) => PlaceholderScreen(title: 'Cursos', route: s.matchedLocation)),
// Alumnos
GoRoute(path: '/alumnos', builder: (_, s) => PlaceholderScreen(title: 'Alumnos', route: s.matchedLocation)),
GoRoute(path: '/alumnos/altas', builder: (_, s) => PlaceholderScreen(title: 'Altas', route: s.matchedLocation)),
GoRoute(path: '/alumnos/bajas', builder: (_, s) => PlaceholderScreen(title: 'Bajas', route: s.matchedLocation)),
// Reservas
GoRoute(path: '/reservas', builder: (_, __) => const BookingScreen()),
GoRoute(path: '/reservas-salas', builder: (_, s) => PlaceholderScreen(title: 'Reservas Salas', route: s.matchedLocation)),
// Consultas
GoRoute(path: '/consultas', builder: (_, s) => PlaceholderScreen(title: 'Consultas', route: s.matchedLocation)),
// Caja
GoRoute(path: '/caja', builder: (_, s) => PlaceholderScreen(title: 'Caja', route: s.matchedLocation)),
// Recibos
GoRoute(path: '/recibos/abonado', builder: (_, s) => PlaceholderScreen(title: 'Recibos por abonado', route: s.matchedLocation)),
GoRoute(path: '/recibos/actividad', builder: (_, s) => PlaceholderScreen(title: 'Recibos por actividad', route: s.matchedLocation)),
// Remesas
GoRoute(path: '/remesas', builder: (_, s) => PlaceholderScreen(title: 'Administrar remesas', route: s.matchedLocation)),
GoRoute(path: '/remesas/especial', builder: (_, s) => PlaceholderScreen(title: 'Remesa especial', route: s.matchedLocation)),
// Listados
GoRoute(path: '/listados/mensual', builder: (_, s) => PlaceholderScreen(title: 'Listado mensual', route: s.matchedLocation)),
GoRoute(path: '/listados/cobros-cursos', builder: (_, s) => PlaceholderScreen(title: 'Cobros por cursos', route: s.matchedLocation)),
GoRoute(path: '/listados/totales-curso', builder: (_, s) => PlaceholderScreen(title: 'Totales por curso', route: s.matchedLocation)),
GoRoute(path: '/listados/cobros-empleado', builder: (_, s) => PlaceholderScreen(title: 'Cobros por empleado', route: s.matchedLocation)),
GoRoute(path: '/listados/cobros', builder: (_, s) => PlaceholderScreen(title: 'Cobros', route: s.matchedLocation)),
GoRoute(path: '/listados/devueltos', builder: (_, s) => PlaceholderScreen(title: 'Recibos devueltos', route: s.matchedLocation)),
GoRoute(path: '/listados/bajas', builder: (_, s) => PlaceholderScreen(title: 'Listado de bajas', route: s.matchedLocation)),
GoRoute(path: '/listados/altas', builder: (_, s) => PlaceholderScreen(title: 'Listado de altas', route: s.matchedLocation)),
GoRoute(path: '/listados/altas-bajas', builder: (_, s) => PlaceholderScreen(title: 'Altas-bajas por mes', route: s.matchedLocation)),
GoRoute(path: '/listados/csv', builder: (_, s) => PlaceholderScreen(title: 'Ficheros CSV', route: s.matchedLocation)),
GoRoute(path: '/listados/impagadas', builder: (_, s) => PlaceholderScreen(title: 'Impagadas', route: s.matchedLocation)),
GoRoute(path: '/listados/impagadas-total', builder: (_, s) => PlaceholderScreen(title: 'Impagadas totales', route: s.matchedLocation)),
GoRoute(path: '/listados/retenidos', builder: (_, s) => PlaceholderScreen(title: 'Retenidos', route: s.matchedLocation)),
GoRoute(path: '/listados/sin-cuenta', builder: (_, s) => PlaceholderScreen(title: 'Sin cuenta', route: s.matchedLocation)),
GoRoute(path: '/listados/cursos-completos', builder: (_, s) => PlaceholderScreen(title: 'Cursos completos', route: s.matchedLocation)),
GoRoute(path: '/listados/registros-empleado', builder: (_, s) => PlaceholderScreen(title: 'Registros por empleado', route: s.matchedLocation)),
// Utilidades
GoRoute(path: '/utilidades/movimientos', builder: (_, s) => PlaceholderScreen(title: 'Movimientos de Caja', route: s.matchedLocation)),
GoRoute(path: '/utilidades/cerrar-caja', builder: (_, s) => PlaceholderScreen(title: 'Cerrar caja', route: s.matchedLocation)),
GoRoute(path: '/utilidades/tickets', builder: (_, s) => PlaceholderScreen(title: 'Últimos tickets', route: s.matchedLocation)),
GoRoute(path: '/utilidades/parametros', builder: (_, s) => PlaceholderScreen(title: 'Parámetros', route: s.matchedLocation)),
GoRoute(path: '/utilidades/cuotas', builder: (_, s) => PlaceholderScreen(title: 'Modificar Cuotas', route: s.matchedLocation)),
GoRoute(path: '/utilidades/elim-caja', builder: (_, s) => PlaceholderScreen(title: 'Eliminación R. de Caja', route: s.matchedLocation)),
GoRoute(path: '/utilidades/festivos', builder: (_, s) => PlaceholderScreen(title: 'Festivos', route: s.matchedLocation)),
GoRoute(path: '/utilidades/celebraciones', builder: (_, s) => PlaceholderScreen(title: 'Celebraciones', route: s.matchedLocation)),
GoRoute(path: '/utilidades/horarios', builder: (_, s) => PlaceholderScreen(title: 'Horarios pistas', route: s.matchedLocation)),
GoRoute(path: '/utilidades/contadores', builder: (_, s) => PlaceholderScreen(title: 'Contadores', route: s.matchedLocation)),
GoRoute(path: '/utilidades/actividades', builder: (_, s) => PlaceholderScreen(title: 'Actividades', route: s.matchedLocation)),
GoRoute(path: '/utilidades/tablas', builder: (_, s) => PlaceholderScreen(title: 'Tablas', route: s.matchedLocation)),
GoRoute(path: '/utilidades/impresoras', builder: (_, s) => PlaceholderScreen(title: 'Impresoras', route: s.matchedLocation)),
GoRoute(path: '/utilidades/carnet-printer', builder: (_, s) => PlaceholderScreen(title: 'Impresora CARNETS', route: s.matchedLocation)),
GoRoute(path: '/utilidades/sorteo', builder: (_, s) => PlaceholderScreen(title: 'Sorteo abonados', route: s.matchedLocation)),
// Admin / sistema
GoRoute(path: '/usuarios', builder: (_, s) => PlaceholderScreen(title: 'Usuarios', route: s.matchedLocation)),
GoRoute(path: '/tornos', builder: (_, s) => PlaceholderScreen(title: 'Tornos', route: s.matchedLocation)),
GoRoute(path: '/tornos/top', builder: (_, s) => PlaceholderScreen(title: 'Top usuarios', route: s.matchedLocation)),
GoRoute(path: '/tornos/gym', builder: (_, s) => PlaceholderScreen(title: 'Tornos (gym)', route: s.matchedLocation)),
GoRoute(path: '/tornos/activos', builder: (_, s) => PlaceholderScreen(title: 'Usuarios en sala', route: s.matchedLocation)),
GoRoute(path: '/registro', builder: (_, s) => PlaceholderScreen(title: 'Registro', route: s.matchedLocation)),
GoRoute(path: '/permisos', builder: (_, s) => PlaceholderScreen(title: 'Permisos', route: s.matchedLocation)),
],
),
],
errorBuilder: (_, state) => Scaffold(
body: Center(child: Text('Ruta no encontrada: ${state.uri}')),
),
);

View file

@ -0,0 +1,15 @@
import '../services/booking_repository.dart';
import '../services/booking_service.dart';
import '../services/member_repository.dart';
import '../services/member_service.dart';
import '../services/mock/mock_booking_service.dart';
import '../services/mock/mock_member_service.dart';
import 'constants.dart';
/// Devuelve la implementación correcta de cada repositorio según [kUseMock].
/// Cambiar kUseMock en constants.dart para alternar entre mock y producción.
MemberRepository memberRepository() =>
kUseMock ? MockMemberService() : MemberService();
BookingRepository bookingRepository() =>
kUseMock ? MockBookingService() : BookingService();

35
lib/main.dart Normal file
View file

@ -0,0 +1,35 @@
import 'package:flutter/material.dart';
import 'core/router.dart';
void main() {
runApp(const DeporOsApp());
}
class DeporOsApp extends StatelessWidget {
const DeporOsApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp.router(
title: 'DeporOS',
debugShowCheckedModeBanner: false,
routerConfig: appRouter,
theme: ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0xFF1565C0),
brightness: Brightness.light,
),
inputDecorationTheme: const InputDecorationTheme(filled: true),
),
darkTheme: ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0xFF1565C0),
brightness: Brightness.dark,
),
inputDecorationTheme: const InputDecorationTheme(filled: true),
),
);
}
}

173
lib/models/booking.dart Normal file
View file

@ -0,0 +1,173 @@
// Activity
class BookingActivity {
final int id;
final String name;
const BookingActivity({required this.id, required this.name});
factory BookingActivity.fromJson(Map<String, dynamic> json) =>
BookingActivity(
id: json['pk_i_id'] ?? 0,
name: json['s_name'] ?? '',
);
}
// Grid
class BookingCell {
final String raw;
const BookingCell(this.raw);
bool get isEmpty => raw.isEmpty;
bool get isPaid => raw.isNotEmpty && raw.endsWith('+');
bool get isBooked => raw.isNotEmpty;
int? get memberId {
final m = RegExp(r'^(\d+)\.').firstMatch(raw);
return m != null ? int.tryParse(m.group(1)!) : null;
}
int? get familyId {
final m = RegExp(r'\.(\d+)[+\-]?$').firstMatch(raw);
return m != null ? int.tryParse(m.group(1)!) : null;
}
}
class BookingRow {
final int slotIndex;
final String timeLabel;
final List<BookingCell> cells; // one per court
const BookingRow({
required this.slotIndex,
required this.timeLabel,
required this.cells,
});
}
class BookingGrid {
final List<BookingRow> rows;
final List<String> courtTitles; // ["01", "02", ...]
final String activityName;
final int light;
final int extra;
final String? celebrations;
const BookingGrid({
required this.rows,
required this.courtTitles,
required this.activityName,
required this.light,
required this.extra,
this.celebrations,
});
factory BookingGrid.fromJson(Map<String, dynamic> json) {
final aoColumns = json['aoColumns'] as List<dynamic>? ?? [];
final aaData = json['aaData'] as List<dynamic>? ?? [];
final courtTitles = aoColumns
.skip(1)
.map((c) => (c as Map<String, dynamic>)['sTitle'] as String? ?? '')
.toList();
final rows = aaData.map((rowData) {
final row = rowData as List<dynamic>;
final firstCell = row[0] as String? ?? '';
final idMatch = RegExp(r'id="(\d+)"').firstMatch(firstCell);
final slotIndex = idMatch != null ? int.parse(idMatch.group(1)!) : 0;
final timeMatch = RegExp(r'>([^<]+)<').firstMatch(firstCell);
final timeLabel = timeMatch?.group(1) ?? firstCell;
final cells = row
.skip(1)
.map((c) => BookingCell(c as String? ?? ''))
.toList();
return BookingRow(slotIndex: slotIndex, timeLabel: timeLabel, cells: cells);
}).toList();
final ac = json['ac'] as Map<String, dynamic>? ?? {};
return BookingGrid(
rows: rows,
courtTitles: courtTitles,
activityName: ac['s_name'] as String? ?? '',
light: (json['light'] as num?)?.toInt() ?? 0,
extra: (json['extra'] as num?)?.toInt() ?? 0,
celebrations: json['celebrations'] as String?,
);
}
}
// Booking detail (single cell)
class TicketLine {
final String concepto;
final double precio;
const TicketLine({required this.concepto, required this.precio});
factory TicketLine.fromJson(Map<String, dynamic> json) => TicketLine(
concepto: json['concepto'] ?? '',
precio: (json['precio'] as num?)?.toDouble() ?? 0,
);
}
class TicketPreview {
final String concepto;
final double base;
final double iva;
final double total;
final List<TicketLine> lines;
const TicketPreview({
required this.concepto,
required this.base,
required this.iva,
required this.total,
required this.lines,
});
factory TicketPreview.fromJson(Map<String, dynamic> json) => TicketPreview(
concepto: json['concepto'] ?? '',
base: (json['base'] as num?)?.toDouble() ?? 0,
iva: (json['iva'] as num?)?.toDouble() ?? 0,
total: (json['total'] as num?)?.toDouble() ?? 0,
lines: (json['lines'] as List<dynamic>? ?? [])
.map((l) => TicketLine.fromJson(l as Map<String, dynamic>))
.toList(),
);
}
class BookingDetail {
final int id;
final int memberId;
final int familyId;
final String date;
final bool paid;
final TicketPreview? ticket;
const BookingDetail({
required this.id,
required this.memberId,
required this.familyId,
required this.date,
required this.paid,
this.ticket,
});
factory BookingDetail.fromJson(Map<String, dynamic> json) {
final book = json['book'] as Map<String, dynamic>? ?? {};
final ticketJson = json['ticket'] as Map<String, dynamic>?;
return BookingDetail(
id: book['pk_i_id'] ?? 0,
memberId: book['fk_i_member_id'] ?? 0,
familyId: book['fk_i_family'] ?? 1,
date: book['d_date'] ?? '',
paid: (book['b_paid'] ?? 0) == 1,
ticket: ticketJson != null ? TicketPreview.fromJson(ticketJson) : null,
);
}
}

164
lib/models/member.dart Normal file
View file

@ -0,0 +1,164 @@
class MemberComment {
final int id;
final String comment;
final String createdAt;
const MemberComment({
required this.id,
required this.comment,
required this.createdAt,
});
factory MemberComment.fromJson(Map<String, dynamic> json) => MemberComment(
id: json['id'] ?? 0,
comment: json['comment'] ?? '',
createdAt: json['created_at'] ?? '',
);
}
class Person {
final int memberId;
final int family;
final String firstName;
final String lastName;
final String dni;
final String birthDate;
final String phone;
final String email;
final bool lopd;
final bool newsletter;
final bool? bHealth;
final bool authorized;
final bool authorized2;
final String? photoPath;
const Person({
required this.memberId,
required this.family,
required this.firstName,
required this.lastName,
required this.dni,
required this.birthDate,
required this.phone,
required this.email,
required this.lopd,
required this.newsletter,
this.bHealth,
required this.authorized,
required this.authorized2,
this.photoPath,
});
factory Person.fromJson(Map<String, dynamic> json) => Person(
memberId: json['fk_i_member_id'] ?? 0,
family: json['i_family'] ?? 0,
firstName: json['s_first_name'] ?? '',
lastName: json['s_last_name'] ?? '',
dni: json['s_dni'] ?? '',
birthDate: json['d_birth_date'] ?? '',
phone: json['s_phone'] ?? '',
email: json['s_email'] ?? '',
lopd: (json['b_lopd'] ?? 0) == 1,
newsletter: (json['b_newsletter'] ?? 0) == 1,
bHealth: json['b_health'] != null ? (json['b_health'] as int) == 1 : null,
authorized: (json['b_authorized'] ?? 0) == 1,
authorized2: (json['b_authorized2'] ?? 0) == 1,
photoPath: json['s_photo_path'],
);
String get fullName => '$firstName $lastName'.trim();
String get initials {
final parts = fullName.split(' ');
if (parts.length >= 2) return '${parts[0][0]}${parts[1][0]}'.toUpperCase();
if (parts.isNotEmpty && parts[0].isNotEmpty) return parts[0][0].toUpperCase();
return '?';
}
}
class Member {
final int id;
final String regDate;
final String address;
final String city;
final String zip;
final String email;
final String phone;
final String bank;
final String bankOffice;
final String bankSecCode;
final String bankAccount;
final String bankName;
final String mandate;
final String type;
final String comment;
final String comment2;
final bool paid;
final bool remSpecial;
final bool retenido;
final String? unregDate;
final String unregReason;
final double? fee;
final List<Person> people;
final List<MemberComment> comments;
const Member({
required this.id,
required this.regDate,
required this.address,
required this.city,
required this.zip,
required this.email,
required this.phone,
required this.bank,
required this.bankOffice,
required this.bankSecCode,
required this.bankAccount,
required this.bankName,
required this.mandate,
required this.type,
required this.comment,
required this.comment2,
required this.paid,
required this.remSpecial,
required this.retenido,
required this.unregDate,
required this.unregReason,
this.fee,
required this.people,
required this.comments,
});
factory Member.fromJson(Map<String, dynamic> json) => Member(
id: json['pk_i_id'] ?? 0,
regDate: json['d_reg_date'] ?? '',
address: json['s_address'] ?? '',
city: json['s_city'] ?? '',
zip: json['s_zip'] ?? '',
email: json['s_email'] ?? '',
phone: json['s_phone'] ?? '',
bank: json['s_bank'] ?? '',
bankOffice: json['s_bank_office'] ?? '',
bankSecCode: json['s_bank_sec_code'] ?? '',
bankAccount: json['s_bank_account'] ?? '',
bankName: json['s_bank_name'] ?? '',
mandate: json['s_mandate'] ?? '',
type: json['s_type'] ?? '',
comment: json['s_comment'] ?? '',
comment2: json['s_comment2'] ?? '',
paid: (json['b_paid'] ?? 0) == 1,
remSpecial: (json['b_rem_special'] ?? 0) == 1,
retenido: (json['b_retenido'] ?? 0) == 1,
unregDate: json['d_unreg_date'],
unregReason: json['s_unreg_reason'] ?? '',
fee: (json['f_fee'] as num?)?.toDouble(),
people: (json['people'] as List<dynamic>? ?? [])
.map((p) => Person.fromJson(p as Map<String, dynamic>))
.toList(),
comments: (json['comments'] as List<dynamic>? ?? [])
.map((c) => MemberComment.fromJson(c as Map<String, dynamic>))
.toList(),
);
String get formattedId => id.toString().padLeft(5, '0');
bool get isActive => unregDate == null;
}

23
lib/models/nav_item.dart Normal file
View file

@ -0,0 +1,23 @@
import 'package:flutter/material.dart';
class NavItem {
final String label;
final IconData icon;
final String? route;
final List<NavItem> children;
const NavItem({
required this.label,
required this.icon,
this.route,
this.children = const [],
});
bool get hasChildren => children.isNotEmpty;
/// Devuelve true si la ruta actual coincide con este item o algún hijo.
bool isActiveOrAncestor(String currentRoute) {
if (route != null && currentRoute.startsWith(route!)) return true;
return children.any((c) => c.isActiveOrAncestor(currentRoute));
}
}

View file

@ -0,0 +1,114 @@
import 'package:flutter/material.dart';
import '../models/nav_item.dart';
/// Estructura de navegación completa del panel de administración.
/// Refleja el sidebar de sc-admin/themes/modern/sidebar.php (perfil admin completo).
const List<NavItem> kAdminNavItems = [
NavItem(label: 'Inicio', icon: Icons.home_outlined, route: '/dashboard'),
NavItem(
label: 'Abonados',
icon: Icons.people_outlined,
children: [
NavItem(label: 'Gestionar abonados', icon: Icons.manage_accounts_outlined, route: '/abonados'),
NavItem(label: 'Pre-inscripciones', icon: Icons.pending_actions_outlined, route: '/abonados/pre'),
NavItem(label: 'Añadir abonado', icon: Icons.person_add_outlined, route: '/abonados/nuevo'),
],
),
NavItem(label: 'Cursos', icon: Icons.school_outlined, route: '/cursos'),
NavItem(
label: 'Alumnos',
icon: Icons.group_outlined,
children: [
NavItem(label: 'Alumnos', icon: Icons.group_outlined, route: '/alumnos'),
NavItem(label: 'Altas', icon: Icons.add_circle_outline, route: '/alumnos/altas'),
NavItem(label: 'Bajas', icon: Icons.remove_circle_outline, route: '/alumnos/bajas'),
],
),
NavItem(label: 'Reservas', icon: Icons.sports_tennis_outlined, route: '/reservas'),
NavItem(label: 'Reservas Salas', icon: Icons.meeting_room_outlined, route: '/reservas-salas'),
NavItem(label: 'Consultas', icon: Icons.search_outlined, route: '/consultas'),
NavItem(label: 'Caja', icon: Icons.point_of_sale_outlined, route: '/caja'),
NavItem(
label: 'Recibos',
icon: Icons.receipt_long_outlined,
children: [
NavItem(label: 'Recibos por abonado', icon: Icons.receipt_outlined, route: '/recibos/abonado'),
NavItem(label: 'Recibos por actividad', icon: Icons.receipt_long_outlined, route: '/recibos/actividad'),
],
),
NavItem(
label: 'Remesas',
icon: Icons.account_balance_outlined,
children: [
NavItem(label: 'Remesa especial', icon: Icons.star_border_outlined, route: '/remesas/especial'),
NavItem(label: 'Administrar remesas', icon: Icons.manage_history_outlined, route: '/remesas'),
],
),
NavItem(
label: 'Listados',
icon: Icons.list_alt_outlined,
children: [
NavItem(label: 'Listado mensual', icon: Icons.calendar_month_outlined, route: '/listados/mensual'),
NavItem(label: 'Cobros por cursos', icon: Icons.payments_outlined, route: '/listados/cobros-cursos'),
NavItem(label: 'Totales por curso', icon: Icons.summarize_outlined, route: '/listados/totales-curso'),
NavItem(label: 'Cobros por empleado', icon: Icons.badge_outlined, route: '/listados/cobros-empleado'),
NavItem(label: 'Cobros', icon: Icons.euro_outlined, route: '/listados/cobros'),
NavItem(label: 'Recibos devueltos', icon: Icons.undo_outlined, route: '/listados/devueltos'),
NavItem(label: 'Bajas', icon: Icons.person_remove_outlined, route: '/listados/bajas'),
NavItem(label: 'Altas', icon: Icons.person_add_outlined, route: '/listados/altas'),
NavItem(label: 'Altas-bajas por mes', icon: Icons.swap_vert_outlined, route: '/listados/altas-bajas'),
NavItem(label: 'Ficheros CSV', icon: Icons.file_download_outlined, route: '/listados/csv'),
NavItem(label: 'Impagadas', icon: Icons.money_off_outlined, route: '/listados/impagadas'),
NavItem(label: 'Impagadas totales', icon: Icons.money_off_csred_outlined, route: '/listados/impagadas-total'),
NavItem(label: 'Retenidos', icon: Icons.pause_circle_outline, route: '/listados/retenidos'),
NavItem(label: 'Sin cuenta', icon: Icons.no_accounts_outlined, route: '/listados/sin-cuenta'),
NavItem(label: 'Cursos completos', icon: Icons.check_circle_outline, route: '/listados/cursos-completos'),
NavItem(label: 'Registros por empleado', icon: Icons.assignment_ind_outlined, route: '/listados/registros-empleado'),
],
),
NavItem(
label: 'Utilidades',
icon: Icons.build_outlined,
children: [
NavItem(label: 'Movimientos de Caja', icon: Icons.swap_horiz_outlined, route: '/utilidades/movimientos'),
NavItem(label: 'Cerrar caja', icon: Icons.lock_clock_outlined, route: '/utilidades/cerrar-caja'),
NavItem(label: 'Últimos tickets', icon: Icons.confirmation_number_outlined, route: '/utilidades/tickets'),
NavItem(label: 'Parámetros', icon: Icons.tune_outlined, route: '/utilidades/parametros'),
NavItem(label: 'Modificar Cuotas', icon: Icons.price_change_outlined, route: '/utilidades/cuotas'),
NavItem(label: 'Eliminación R. de Caja', icon: Icons.delete_outline, route: '/utilidades/elim-caja'),
NavItem(label: 'Festivos', icon: Icons.beach_access_outlined, route: '/utilidades/festivos'),
NavItem(label: 'Celebraciones', icon: Icons.celebration_outlined, route: '/utilidades/celebraciones'),
NavItem(label: 'Horarios pistas', icon: Icons.schedule_outlined, route: '/utilidades/horarios'),
NavItem(label: 'Contadores', icon: Icons.numbers_outlined, route: '/utilidades/contadores'),
NavItem(label: 'Actividades', icon: Icons.sports_outlined, route: '/utilidades/actividades'),
NavItem(label: 'Tablas', icon: Icons.table_chart_outlined, route: '/utilidades/tablas'),
NavItem(label: 'Impresoras', icon: Icons.print_outlined, route: '/utilidades/impresoras'),
NavItem(label: 'Impresora CARNETS', icon: Icons.credit_card_outlined, route: '/utilidades/carnet-printer'),
NavItem(label: 'Sorteo abonados', icon: Icons.casino_outlined, route: '/utilidades/sorteo'),
],
),
NavItem(label: 'Usuarios', icon: Icons.admin_panel_settings_outlined, route: '/usuarios'),
NavItem(
label: 'Tornos',
icon: Icons.sensor_door_outlined,
children: [
NavItem(label: 'Tornos', icon: Icons.sensor_door_outlined, route: '/tornos'),
NavItem(label: 'Top usuarios', icon: Icons.leaderboard_outlined, route: '/tornos/top'),
NavItem(label: 'Tornos (gym)', icon: Icons.fitness_center_outlined, route: '/tornos/gym'),
NavItem(label: 'Usuarios en sala', icon: Icons.people_alt_outlined, route: '/tornos/activos'),
],
),
NavItem(label: 'Registro', icon: Icons.history_outlined, route: '/registro'),
NavItem(label: 'Permisos', icon: Icons.security_outlined, route: '/permisos'),
];

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,186 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
class DashboardScreen extends StatelessWidget {
const DashboardScreen({super.key});
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
final tt = Theme.of(context).textTheme;
return SingleChildScrollView(
padding: const EdgeInsets.all(32),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Inicio',
style: tt.headlineMedium?.copyWith(fontWeight: FontWeight.bold),
),
const SizedBox(height: 4),
Text(
'Accesos rápidos',
style: tt.bodyLarge?.copyWith(color: cs.onSurfaceVariant),
),
const SizedBox(height: 32),
_QuickAccessGrid(),
],
),
);
}
}
// Grid de accesos rápidos
class _QuickAccessGrid extends StatelessWidget {
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
final columns = constraints.maxWidth >= 900 ? 4 : 2;
final spacing = 16.0;
final cardWidth =
(constraints.maxWidth - spacing * (columns - 1)) / columns;
return Wrap(
spacing: spacing,
runSpacing: spacing,
children: _kShortcuts
.map((s) => SizedBox(
width: cardWidth,
child: _ShortcutCard(shortcut: s),
))
.toList(),
);
},
);
}
}
// Tarjeta individual
class _ShortcutCard extends StatelessWidget {
final _Shortcut shortcut;
const _ShortcutCard({required this.shortcut});
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
final tt = Theme.of(context).textTheme;
final bgColor = shortcut.color.resolve(cs);
final fgColor = shortcut.foreground.resolve(cs);
return Card(
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
side: BorderSide(color: cs.outlineVariant),
),
clipBehavior: Clip.antiAlias,
child: InkWell(
onTap: () => context.go(shortcut.route),
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 52,
height: 52,
decoration: BoxDecoration(
color: bgColor,
borderRadius: BorderRadius.circular(14),
),
child: Icon(shortcut.icon, color: fgColor, size: 26),
),
const SizedBox(height: 20),
Text(
shortcut.title,
style: tt.titleMedium?.copyWith(fontWeight: FontWeight.w600),
),
const SizedBox(height: 4),
Text(
shortcut.subtitle,
style: tt.bodySmall?.copyWith(color: cs.onSurfaceVariant),
),
],
),
),
),
);
}
}
// Datos
class _Shortcut {
final String title;
final String subtitle;
final IconData icon;
final String route;
final _SchemeColor color;
final _SchemeColor foreground;
const _Shortcut({
required this.title,
required this.subtitle,
required this.icon,
required this.route,
required this.color,
required this.foreground,
});
}
/// Referencia indirecta a un color del ColorScheme para evitar context en const.
class _SchemeColor {
final Color Function(ColorScheme) resolve;
const _SchemeColor(this.resolve);
}
const _kShortcuts = [
_Shortcut(
title: 'Abonados',
subtitle: 'Gestionar abonados del club',
icon: Icons.people_outlined,
route: '/abonados',
color: _SchemeColor(_primary),
foreground: _SchemeColor(_onPrimary),
),
_Shortcut(
title: 'Reservas',
subtitle: 'Reservas de pistas',
icon: Icons.sports_tennis_outlined,
route: '/reservas',
color: _SchemeColor(_secondary),
foreground: _SchemeColor(_onSecondary),
),
_Shortcut(
title: 'Recibos',
subtitle: 'Recibos por abonado',
icon: Icons.receipt_long_outlined,
route: '/recibos/abonado',
color: _SchemeColor(_tertiary),
foreground: _SchemeColor(_onTertiary),
),
_Shortcut(
title: 'Caja',
subtitle: 'Movimientos y cobros',
icon: Icons.point_of_sale_outlined,
route: '/caja',
color: _SchemeColor(_error),
foreground: _SchemeColor(_onError),
),
];
// Funciones top-level para poder usarlas en const
Color _primary(ColorScheme cs) => cs.primaryContainer;
Color _onPrimary(ColorScheme cs) => cs.onPrimaryContainer;
Color _secondary(ColorScheme cs) => cs.secondaryContainer;
Color _onSecondary(ColorScheme cs) => cs.onSecondaryContainer;
Color _tertiary(ColorScheme cs) => cs.tertiaryContainer;
Color _onTertiary(ColorScheme cs) => cs.onTertiaryContainer;
Color _error(ColorScheme cs) => cs.errorContainer;
Color _onError(ColorScheme cs) => cs.onErrorContainer;

View file

@ -0,0 +1,197 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import '../../services/auth_service.dart';
class LoginScreen extends StatefulWidget {
const LoginScreen({super.key});
@override
State<LoginScreen> createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen> {
final _formKey = GlobalKey<FormState>();
final _emailCtrl = TextEditingController();
final _passCtrl = TextEditingController();
final _authService = AuthService();
bool _loading = false;
bool _obscurePass = true;
@override
void dispose() {
_emailCtrl.dispose();
_passCtrl.dispose();
super.dispose();
}
Future<void> _submit() async {
if (!_formKey.currentState!.validate()) return;
setState(() => _loading = true);
final error = await _authService.login(
_emailCtrl.text.trim(),
_passCtrl.text,
);
if (!mounted) return;
setState(() => _loading = false);
if (error != null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(error),
behavior: SnackBarBehavior.floating,
backgroundColor: Theme.of(context).colorScheme.error,
),
);
} else {
context.go('/dashboard');
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Theme.of(context).colorScheme.surfaceContainerLowest,
body: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 420),
child: Card(
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(24),
side: BorderSide(
color: Theme.of(context).colorScheme.outlineVariant,
),
),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 36,
vertical: 48,
),
child: Form(
key: _formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Logo / título
Icon(
Icons.sports,
size: 56,
color: Theme.of(context).colorScheme.primary,
),
const SizedBox(height: 16),
Text(
'DeporOS',
textAlign: TextAlign.center,
style: Theme.of(context)
.textTheme
.headlineMedium
?.copyWith(
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.primary,
),
),
const SizedBox(height: 8),
Text(
'Panel de administración',
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context)
.colorScheme
.onSurfaceVariant,
),
),
const SizedBox(height: 40),
// Email
TextFormField(
controller: _emailCtrl,
keyboardType: TextInputType.emailAddress,
textInputAction: TextInputAction.next,
decoration: const InputDecoration(
labelText: 'Email',
prefixIcon: Icon(Icons.email_outlined),
border: OutlineInputBorder(),
),
validator: (v) {
if (v == null || v.trim().isEmpty) {
return 'Introduce tu email';
}
if (!v.contains('@')) return 'Email no válido';
return null;
},
),
const SizedBox(height: 16),
// Contraseña
TextFormField(
controller: _passCtrl,
obscureText: _obscurePass,
textInputAction: TextInputAction.done,
onFieldSubmitted: (_) => _submit(),
decoration: InputDecoration(
labelText: 'Contraseña',
prefixIcon: const Icon(Icons.lock_outlined),
border: const OutlineInputBorder(),
suffixIcon: IconButton(
icon: Icon(
_obscurePass
? Icons.visibility_outlined
: Icons.visibility_off_outlined,
),
onPressed: () =>
setState(() => _obscurePass = !_obscurePass),
),
),
validator: (v) {
if (v == null || v.isEmpty) {
return 'Introduce tu contraseña';
}
return null;
},
),
const SizedBox(height: 32),
// Botón entrar
FilledButton(
onPressed: _loading ? null : _submit,
style: FilledButton.styleFrom(
minimumSize: const Size.fromHeight(52),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: _loading
? const SizedBox(
height: 22,
width: 22,
child: CircularProgressIndicator(
strokeWidth: 2.5,
color: Colors.white,
),
)
: const Text(
'Entrar',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
],
),
),
),
),
),
),
),
);
}
}

View file

@ -0,0 +1,970 @@
import 'package:flutter/material.dart';
import '../../core/service_locator.dart';
import '../../models/member.dart';
import '../../services/member_repository.dart';
class MembersScreen extends StatefulWidget {
const MembersScreen({super.key});
@override
State<MembersScreen> createState() => _MembersScreenState();
}
class _MembersScreenState extends State<MembersScreen> {
final _service = memberRepository();
Member? _member;
bool _loading = true;
String? _error;
@override
void initState() {
super.initState();
_load(_service.first());
}
void _handleAction(String action) {
final labels = {
'receipts': 'Recibos anteriores — pendiente de implementar',
'edit': 'Editar abonado — pendiente de implementar',
'add_family': 'Añadir familiar — pendiente de implementar',
'retain': 'Retener abonado — pendiente de implementar',
'unregister': 'Dar de baja — pendiente de implementar',
};
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(labels[action] ?? action), duration: const Duration(seconds: 2)),
);
}
Future<void> _load(Future<Member> future) async {
setState(() {
_loading = true;
_error = null;
});
try {
final m = await future;
setState(() {
_member = m;
_loading = false;
});
} catch (e) {
setState(() {
_error = e.toString();
_loading = false;
});
}
}
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_NavBar(
member: _member,
loading: _loading,
service: _service,
onFirst: () => _load(_service.first()),
onPrev: () {
if (_member != null) _load(_service.prev(_member!.id));
},
onNext: () {
if (_member != null) _load(_service.next(_member!.id));
},
onLast: () => _load(_service.last()),
onSearch: (id) => _load(_service.show(id)),
onAction: _handleAction,
),
const Divider(height: 1),
Expanded(
child: _loading
? const Center(child: CircularProgressIndicator())
: _error != null
? _ErrorView(error: _error!)
: _member == null
? const Center(child: Text('Sin datos'))
: _MemberBody(member: _member!),
),
],
);
}
}
// Nav bar
class _NavBar extends StatelessWidget {
final Member? member;
final bool loading;
final MemberRepository service;
final VoidCallback onFirst, onPrev, onNext, onLast;
final void Function(int id) onSearch;
final void Function(String action) onAction;
const _NavBar({
required this.member,
required this.loading,
required this.service,
required this.onFirst,
required this.onPrev,
required this.onNext,
required this.onLast,
required this.onSearch,
required this.onAction,
});
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
final disabled = loading || member == null;
return Container(
color: cs.surfaceContainerLow,
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
child: Row(
children: [
Expanded(
child: _MemberSearch(service: service, onSelected: onSearch),
),
const SizedBox(width: 4),
IconButton(
icon: const Icon(Icons.first_page),
onPressed: loading ? null : onFirst,
tooltip: 'Primero',
),
IconButton(
icon: const Icon(Icons.chevron_left),
onPressed: loading ? null : onPrev,
tooltip: 'Anterior',
),
IconButton(
icon: const Icon(Icons.chevron_right),
onPressed: loading ? null : onNext,
tooltip: 'Siguiente',
),
IconButton(
icon: const Icon(Icons.last_page),
onPressed: loading ? null : onLast,
tooltip: 'Último',
),
const VerticalDivider(width: 16, indent: 8, endIndent: 8),
PopupMenuButton<String>(
icon: const Icon(Icons.more_vert),
tooltip: 'Acciones',
enabled: !disabled,
onSelected: onAction,
itemBuilder: (context) => [
const PopupMenuItem(
value: 'receipts',
child: ListTile(
leading: Icon(Icons.receipt_long_outlined),
title: Text('Recibos anteriores'),
dense: true,
),
),
const PopupMenuItem(
value: 'edit',
child: ListTile(
leading: Icon(Icons.edit_outlined),
title: Text('Editar abonado'),
dense: true,
),
),
const PopupMenuItem(
value: 'add_family',
child: ListTile(
leading: Icon(Icons.person_add_outlined),
title: Text('Añadir familiar'),
dense: true,
),
),
const PopupMenuItem(
value: 'retain',
child: ListTile(
leading: Icon(Icons.pause_circle_outline),
title: Text('Retener abonado'),
dense: true,
),
),
const PopupMenuDivider(),
PopupMenuItem(
value: 'unregister',
child: ListTile(
leading: Icon(Icons.person_remove_outlined,
color: cs.error),
title: Text('Dar de baja',
style: TextStyle(color: cs.error)),
dense: true,
),
),
],
),
],
),
);
}
}
class _MemberSearch extends StatefulWidget {
final MemberRepository service;
final void Function(int id) onSelected;
const _MemberSearch({required this.service, required this.onSelected});
@override
State<_MemberSearch> createState() => _MemberSearchState();
}
class _MemberSearchState extends State<_MemberSearch> {
@override
Widget build(BuildContext context) {
return Autocomplete<MapEntry<String, String>>(
displayStringForOption: (entry) => entry.value,
optionsBuilder: (textValue) async {
if (textValue.text.length < 2) return const [];
final results = await widget.service.search(textValue.text);
return results.entries.toList();
},
onSelected: (entry) {
// key format: 5-digit member_id + 2-digit family index
final memberId = int.parse(entry.key.substring(0, entry.key.length - 2));
widget.onSelected(memberId);
},
fieldViewBuilder: (context, controller, focusNode, onSubmit) {
return TextField(
controller: controller,
focusNode: focusNode,
decoration: const InputDecoration(
hintText: 'Buscar abonado...',
prefixIcon: Icon(Icons.search),
isDense: true,
border: OutlineInputBorder(),
contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 10),
),
);
},
optionsViewBuilder: (context, onSelected, options) {
return Align(
alignment: Alignment.topLeft,
child: Material(
elevation: 4,
borderRadius: BorderRadius.circular(8),
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 400, maxHeight: 280),
child: ListView.builder(
shrinkWrap: true,
itemCount: options.length,
itemBuilder: (context, i) {
final entry = options.elementAt(i);
return ListTile(
dense: true,
title: Text(entry.value),
onTap: () => onSelected(entry),
);
},
),
),
),
);
},
);
}
}
// Error
class _ErrorView extends StatelessWidget {
final String error;
const _ErrorView({required this.error});
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.error_outline,
size: 48, color: Theme.of(context).colorScheme.error),
const SizedBox(height: 8),
Text(error,
style: TextStyle(color: Theme.of(context).colorScheme.error)),
],
),
);
}
}
// Member body
class _MemberBody extends StatelessWidget {
final Member member;
const _MemberBody({required this.member});
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
LayoutBuilder(builder: (context, constraints) {
if (constraints.maxWidth >= 700) {
return IntrinsicHeight(
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(flex: 2, child: _InfoCard(member: member)),
const SizedBox(width: 12),
Expanded(flex: 3, child: _CommentsCard(member: member)),
],
),
);
}
return Column(
children: [
_InfoCard(member: member),
const SizedBox(height: 12),
_CommentsCard(member: member),
],
);
}),
const SizedBox(height: 12),
_FamiliarsCard(people: member.people),
const SizedBox(height: 12),
_FichaCard(member: member),
],
),
);
}
}
// Info card
class _InfoCard extends StatelessWidget {
final Member member;
const _InfoCard({required this.member});
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
return Card.outlined(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Información', style: Theme.of(context).textTheme.titleMedium),
const Divider(height: 20),
// Fila principal: Número | Fecha alta | Tipo+Estado | Cuota
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Número
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Número',
style: Theme.of(context)
.textTheme
.labelSmall
?.copyWith(color: cs.outline)),
Text(member.formattedId,
style: Theme.of(context).textTheme.headlineSmall),
],
),
const SizedBox(width: 20),
// Fecha de alta
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Fecha de alta',
style: Theme.of(context)
.textTheme
.labelSmall
?.copyWith(color: cs.outline)),
Text(_fmtDate(member.regDate),
style: Theme.of(context).textTheme.bodyLarge),
],
),
const SizedBox(width: 20),
// Tipo + Estado
Expanded(
child: Wrap(
spacing: 6,
runSpacing: 6,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
if (member.type.isNotEmpty) _TypeChip(label: member.type),
member.isActive
? _StatusChip(
label: 'Activo',
color: Colors.green,
icon: Icons.check_circle_outline)
: _StatusChip(
label: 'Baja',
color: cs.error,
icon: Icons.cancel_outlined),
if (member.retenido)
_StatusChip(
label: 'Retenido',
color: Colors.orange,
icon: Icons.pause_circle_outline),
if (member.remSpecial)
_StatusChip(
label: 'Remesa especial',
color: cs.primary,
icon: Icons.star_outline),
],
),
),
// Cuota
if (member.fee != null) ...[
const SizedBox(width: 20),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text('Cuota',
style: Theme.of(context)
.textTheme
.labelSmall
?.copyWith(color: cs.outline)),
Text(
'${member.fee!.toStringAsFixed(2)}',
style: Theme.of(context)
.textTheme
.titleMedium
?.copyWith(color: cs.primary),
),
],
),
],
],
),
const SizedBox(height: 14),
// Franjas de años
_YearStrips(),
// Info de baja si está dado de baja
if (!member.isActive && member.unregDate != null) ...[
const SizedBox(height: 12),
_InfoRow(label: 'Fecha de baja', value: _fmtDate(member.unregDate)),
if (member.unregReason.isNotEmpty)
_InfoRow(label: 'Motivo', value: member.unregReason),
],
],
),
),
);
}
}
// Year strips
enum _MonthStatus { unknown, paid, unpaid }
class _YearStrips extends StatelessWidget {
/// year month(112) status. Si está vacío todos los meses son amarillo.
final Map<int, Map<int, _MonthStatus>> data;
const _YearStrips({this.data = const {}});
static const _months = [
'Ene', 'Feb', 'Mar', 'Abr', 'May', 'Jun',
'Jul', 'Ago', 'Sep', 'Oct', 'Nov', 'Dic',
];
@override
Widget build(BuildContext context) {
final now = DateTime.now();
final years = [now.year - 1, now.year, now.year + 1, now.year + 2];
return Column(
children: years.map((year) {
final yearData = data[year] ?? {};
return Padding(
padding: const EdgeInsets.only(bottom: 3),
child: Row(
children: [
SizedBox(
width: 34,
child: Text('$year',
style: TextStyle(
fontSize: 10,
color: Theme.of(context).colorScheme.outline)),
),
...List.generate(12, (i) {
final status = yearData[i + 1] ?? _MonthStatus.unknown;
return Expanded(child: _MonthCell(month: _months[i], status: status));
}),
],
),
);
}).toList(),
);
}
}
class _MonthCell extends StatelessWidget {
final String month;
final _MonthStatus status;
const _MonthCell({required this.month, required this.status});
Color _bg(BuildContext context) => switch (status) {
_MonthStatus.paid => Colors.green.shade400,
_MonthStatus.unpaid => Theme.of(context).colorScheme.error,
_MonthStatus.unknown => Colors.amber.shade400,
};
@override
Widget build(BuildContext context) {
return Container(
height: 20,
margin: const EdgeInsets.only(right: 2),
decoration: BoxDecoration(
color: _bg(context),
borderRadius: BorderRadius.circular(3),
),
alignment: Alignment.center,
child: Text(
month,
style: const TextStyle(
fontSize: 8,
color: Colors.white,
fontWeight: FontWeight.w700,
height: 1),
),
);
}
}
class _TypeChip extends StatelessWidget {
final String label;
const _TypeChip({required this.label});
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
return Chip(
label: Text(label,
style: TextStyle(
color: cs.onPrimaryContainer,
fontWeight: FontWeight.bold,
fontSize: 12)),
backgroundColor: cs.primaryContainer,
padding: EdgeInsets.zero,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
);
}
}
class _StatusChip extends StatelessWidget {
final String label;
final Color color;
final IconData icon;
const _StatusChip(
{required this.label, required this.color, required this.icon});
@override
Widget build(BuildContext context) {
return Chip(
avatar: Icon(icon, size: 16, color: color),
label: Text(label,
style: TextStyle(fontSize: 12, color: color)),
side: BorderSide(color: color.withValues(alpha: 0.4)),
backgroundColor: color.withValues(alpha: 0.08),
padding: EdgeInsets.zero,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
);
}
}
// Comments card
class _CommentsCard extends StatelessWidget {
final Member member;
const _CommentsCard({required this.member});
bool get _hasContent =>
member.comment.isNotEmpty ||
member.comment2.isNotEmpty ||
member.comments.isNotEmpty;
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
return Card.outlined(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Comentarios',
style: Theme.of(context).textTheme.titleMedium),
const Divider(height: 20),
if (!_hasContent)
Text('Sin comentarios',
style: TextStyle(color: cs.outline, fontSize: 13))
else ...[
if (member.comment.isNotEmpty)
_CommentBubble(
text: member.comment, label: 'Nota heredada', legacy: true),
if (member.comment2.isNotEmpty)
_CommentBubble(text: member.comment2, label: 'Nota interna'),
...member.comments.map((c) => _CommentBubble(
text: c.comment,
label: _fmtDate(c.createdAt),
)),
],
],
),
),
);
}
}
class _CommentBubble extends StatelessWidget {
final String text;
final String label;
final bool legacy;
const _CommentBubble(
{required this.text, required this.label, this.legacy = false});
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
return Container(
margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: legacy
? cs.errorContainer.withValues(alpha: 0.4)
: cs.surfaceContainerHigh,
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label,
style: TextStyle(
fontSize: 11,
color: cs.outline,
fontWeight: FontWeight.w500)),
const SizedBox(height: 4),
Text(text, style: const TextStyle(fontSize: 13)),
],
),
);
}
}
// Familiars card
class _FamiliarsCard extends StatelessWidget {
final List<Person> people;
const _FamiliarsCard({required this.people});
@override
Widget build(BuildContext context) {
return Card.outlined(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Familiares asociados',
style: Theme.of(context).textTheme.titleMedium),
const Divider(height: 20),
if (people.isEmpty)
Text('Sin familiares',
style: TextStyle(
color: Theme.of(context).colorScheme.outline,
fontSize: 13))
else
LayoutBuilder(builder: (context, constraints) {
return SizedBox(
width: constraints.maxWidth,
child: DataTable(
headingRowHeight: 36,
dataRowMinHeight: 52,
dataRowMaxHeight: 60,
columnSpacing: 12,
columns: const [
DataColumn(label: Text('Foto')),
DataColumn(label: Text('FF')),
DataColumn(label: Text('Nombre')),
DataColumn(label: Text('DNI')),
DataColumn(label: Text('Móvil')),
DataColumn(label: Text('Email')),
DataColumn(label: Text('Fecha nac.')),
DataColumn(label: Tooltip(message: 'Acepta LOPD', child: Icon(Icons.privacy_tip_outlined, size: 16))),
DataColumn(label: Tooltip(message: 'Recibir noticias', child: Icon(Icons.mail_outline, size: 16))),
DataColumn(label: Tooltip(message: 'Cuestionario de salud', child: Icon(Icons.health_and_safety_outlined, size: 16))),
DataColumn(label: Tooltip(message: 'Autorización 1', child: Icon(Icons.verified_user_outlined, size: 16))),
DataColumn(label: Tooltip(message: 'Autorización 2', child: Icon(Icons.how_to_reg_outlined, size: 16))),
DataColumn(label: Text('')),
],
rows: people.map((p) => _personRow(context, p)).toList(),
),
);
}),
],
),
),
);
}
DataRow _personRow(BuildContext context, Person p) {
final cs = Theme.of(context).colorScheme;
final isPrincipal = p.family == 1;
return DataRow(cells: [
// Foto
DataCell(
p.photoPath != null
? CircleAvatar(
radius: 18,
backgroundImage: NetworkImage(p.photoPath!),
)
: CircleAvatar(
radius: 18,
backgroundColor:
isPrincipal ? cs.primary : cs.surfaceContainerHigh,
child: Text(p.initials,
style: TextStyle(
fontSize: 12,
color: isPrincipal ? cs.onPrimary : cs.onSurface)),
),
),
// FF número de familiar
DataCell(
Tooltip(
message: isPrincipal ? 'Familiar principal' : 'Familiar ${p.family}',
child: Text(
p.family.toString(),
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.bold,
color: isPrincipal ? cs.primary : null,
),
),
),
),
DataCell(Text(p.fullName, style: const TextStyle(fontSize: 13))),
DataCell(Text(p.dni.isEmpty ? '' : p.dni, style: const TextStyle(fontSize: 13))),
DataCell(Text(p.phone.isEmpty ? '' : p.phone, style: const TextStyle(fontSize: 13))),
DataCell(Text(p.email.isEmpty ? '' : p.email, style: const TextStyle(fontSize: 13))),
DataCell(Text(_fmtDate(p.birthDate.isEmpty ? null : p.birthDate), style: const TextStyle(fontSize: 13))),
DataCell(_BoolIcon(p.lopd, 'Acepta LOPD')),
DataCell(_BoolIcon(p.newsletter, 'Recibir noticias')),
DataCell(_BoolIcon(p.bHealth, 'Cuestionario de salud')),
DataCell(_BoolIcon(p.authorized, 'Autorización 1')),
DataCell(_BoolIcon(p.authorized2, 'Autorización 2')),
// Acciones
DataCell(
PopupMenuButton<String>(
icon: const Icon(Icons.more_vert, size: 18),
tooltip: 'Acciones',
padding: EdgeInsets.zero,
onSelected: (value) {
// TODO: implementar acciones de familiar
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('$value — pendiente de implementar'),
duration: const Duration(seconds: 2),
),
);
},
itemBuilder: (context) => [
const PopupMenuItem(
value: 'edit',
child: ListTile(leading: Icon(Icons.edit_outlined), title: Text('Editar familiar'), dense: true),
),
const PopupMenuItem(
value: 'photo',
child: ListTile(leading: Icon(Icons.camera_alt_outlined), title: Text('Tomar foto'), dense: true),
),
const PopupMenuItem(
value: 'card',
child: ListTile(leading: Icon(Icons.badge_outlined), title: Text('Ver carnet'), dense: true),
),
const PopupMenuItem(
value: 'password',
child: ListTile(leading: Icon(Icons.lock_reset_outlined), title: Text('Resetear contraseña'), dense: true),
),
const PopupMenuDivider(),
PopupMenuItem(
value: 'delete',
child: ListTile(
leading: Icon(Icons.delete_outline, color: cs.error),
title: Text('Borrar familiar', style: TextStyle(color: cs.error)),
dense: true,
),
),
],
),
),
]);
}
}
class _BoolIcon extends StatelessWidget {
final bool? value;
final String tooltip;
const _BoolIcon(this.value, this.tooltip);
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
if (value == null) {
return Tooltip(
message: '$tooltip: desconocido',
child: Icon(Icons.help_outline, size: 18, color: cs.outlineVariant),
);
}
return Tooltip(
message: '$tooltip: ${value! ? '' : 'No'}',
child: Icon(
value! ? Icons.check_circle : Icons.cancel,
size: 18,
color: value! ? Colors.green.shade600 : cs.error,
),
);
}
}
// Ficha card
class _FichaCard extends StatelessWidget {
final Member member;
const _FichaCard({required this.member});
@override
Widget build(BuildContext context) {
final hasBank = member.bank.isNotEmpty || member.bankAccount.isNotEmpty;
return Card.outlined(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Ficha', style: Theme.of(context).textTheme.titleMedium),
const Divider(height: 20),
_SectionLabel('Contacto'),
const SizedBox(height: 8),
Wrap(
spacing: 32,
runSpacing: 8,
children: [
_InfoRow(
label: 'Dirección',
value: member.address.isEmpty ? '' : member.address),
_InfoRow(
label: 'Población',
value: member.city.isEmpty ? '' : member.city),
_InfoRow(
label: 'C.P.',
value: member.zip.isEmpty ? '' : member.zip),
_InfoRow(
label: 'Teléfono',
value: member.phone.isEmpty ? '' : member.phone),
_InfoRow(
label: 'Email',
value: member.email.isEmpty ? '' : member.email),
],
),
if (hasBank) ...[
const SizedBox(height: 16),
_SectionLabel('Datos bancarios'),
const SizedBox(height: 8),
Wrap(
spacing: 32,
runSpacing: 8,
children: [
_InfoRow(
label: 'Entidad',
value: member.bank.isEmpty ? '' : member.bank),
_InfoRow(
label: 'Sucursal',
value: member.bankOffice.isEmpty
? ''
: member.bankOffice),
_InfoRow(
label: 'D.C.',
value: member.bankSecCode.isEmpty
? ''
: member.bankSecCode),
_InfoRow(
label: 'Cuenta',
value: member.bankAccount.isEmpty
? ''
: member.bankAccount),
if (member.bankName.isNotEmpty)
_InfoRow(label: 'Nombre', value: member.bankName),
if (member.mandate.isNotEmpty)
_InfoRow(label: 'Mandato SEPA', value: member.mandate),
],
),
],
],
),
),
);
}
}
class _SectionLabel extends StatelessWidget {
final String text;
const _SectionLabel(this.text);
@override
Widget build(BuildContext context) {
return Text(text,
style: Theme.of(context).textTheme.labelMedium?.copyWith(
color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.bold,
));
}
}
// Shared helpers
class _InfoRow extends StatelessWidget {
final String label;
final String value;
const _InfoRow({required this.label, required this.value});
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label,
style: TextStyle(
fontSize: 11,
color: cs.outline,
fontWeight: FontWeight.w500)),
const SizedBox(height: 2),
Text(value, style: const TextStyle(fontSize: 14)),
],
);
}
}
String _fmtDate(String? date) {
if (date == null || date.isEmpty) return '';
try {
final parts = date.split('-');
if (parts.length == 3) return '${parts[2]}/${parts[1]}/${parts[0]}';
} catch (_) {}
return date;
}

View file

@ -0,0 +1,43 @@
import 'package:flutter/material.dart';
/// Pantalla temporal para rutas pendientes de implementar.
class PlaceholderScreen extends StatelessWidget {
final String title;
final String route;
const PlaceholderScreen({
super.key,
required this.title,
required this.route,
});
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
final tt = Theme.of(context).textTheme;
return Padding(
padding: const EdgeInsets.all(32),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: tt.headlineMedium?.copyWith(fontWeight: FontWeight.bold)),
const SizedBox(height: 32),
Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.construction_outlined, size: 64, color: cs.outline),
const SizedBox(height: 16),
Text(
'Pendiente de implementar',
style: tt.bodyMedium?.copyWith(color: cs.onSurfaceVariant),
),
],
),
),
],
),
);
}
}

View file

@ -0,0 +1,52 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:shared_preferences/shared_preferences.dart';
import '../core/constants.dart';
class AuthService {
static const _tokenKey = 'access_token';
/// Intenta hacer login. Devuelve null si ok, o un mensaje de error.
Future<String?> login(String email, String password) async {
try {
final response = await http.post(
Uri.parse('$kApiBase/login'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({'email': email, 'password': password}),
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body) as Map<String, dynamic>;
final token = data['access_token'] as String?;
if (token != null) {
await _saveToken(token);
return null; // éxito
}
return 'Respuesta inesperada del servidor';
} else if (response.statusCode == 401) {
return 'Email o contraseña incorrectos';
} else {
return 'Error del servidor (${response.statusCode})';
}
} catch (_) {
return 'No se pudo conectar con el servidor';
}
}
Future<void> logout() async {
final prefs = await SharedPreferences.getInstance();
await prefs.remove(_tokenKey);
}
Future<String?> getToken() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getString(_tokenKey);
}
Future<bool> isLoggedIn() async => (await getToken()) != null;
Future<void> _saveToken(String token) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_tokenKey, token);
}
}

View file

@ -0,0 +1,18 @@
import '../models/booking.dart';
abstract class BookingRepository {
Future<List<BookingActivity>> activities();
Future<BookingGrid> grid({
required int activityId,
required String date, // dd/mm/yyyy
});
/// Devuelve null si la celda está vacía.
Future<BookingDetail?> singleBooking({
required int activityId,
required String date,
required int slotIndex,
required int courtIndex, // 1-based
});
}

View file

@ -0,0 +1,73 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
import '../core/constants.dart';
import '../models/booking.dart';
import 'auth_service.dart';
import 'booking_repository.dart';
export 'booking_repository.dart';
class BookingService implements BookingRepository {
final _auth = AuthService();
Future<Map<String, String>> _headers() async {
final token = await _auth.getToken();
return {
if (token != null) 'Authorization': 'Bearer $token',
'Accept': 'application/json',
};
}
@override
Future<List<BookingActivity>> activities() async {
final res = await http.get(
Uri.parse('$kApiBase/activities/book'),
headers: await _headers(),
);
if (res.statusCode == 200) {
final list = jsonDecode(res.body) as List<dynamic>;
return list
.map((e) => BookingActivity.fromJson(e as Map<String, dynamic>))
.toList();
}
throw Exception('Error ${res.statusCode}');
}
@override
Future<BookingGrid> grid({required int activityId, required String date}) async {
final uri = Uri.parse('$kApiBase/booking-admin/grid').replace(
queryParameters: {
'fk_i_activity_id': activityId.toString(),
'd_date': date,
},
);
final res = await http.get(uri, headers: await _headers());
if (res.statusCode == 200) {
return BookingGrid.fromJson(jsonDecode(res.body) as Map<String, dynamic>);
}
throw Exception('Error ${res.statusCode}');
}
@override
Future<BookingDetail?> singleBooking({
required int activityId,
required String date,
required int slotIndex,
required int courtIndex,
}) async {
final uri =
Uri.parse('$kApiBase/booking-admin/single').replace(queryParameters: {
'fk_i_activity_id': activityId.toString(),
'd_date': date,
'hour': slotIndex.toString(),
'aPos[1]': courtIndex.toString(),
});
final res = await http.get(uri, headers: await _headers());
if (res.statusCode == 200) {
final data = jsonDecode(res.body) as Map<String, dynamic>;
if (data.isEmpty) return null;
return BookingDetail.fromJson(data);
}
return null;
}
}

View file

@ -0,0 +1,10 @@
import '../models/member.dart';
abstract class MemberRepository {
Future<Member> first();
Future<Member> last();
Future<Member> show(int id);
Future<Member> prev(int id);
Future<Member> next(int id);
Future<Map<String, String>> search(String term);
}

View file

@ -0,0 +1,46 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
import '../core/constants.dart';
import '../models/member.dart';
import 'auth_service.dart';
import 'member_repository.dart';
export 'member_repository.dart';
class MemberService implements MemberRepository {
final _auth = AuthService();
Future<Map<String, String>> _headers() async {
final token = await _auth.getToken();
return {
if (token != null) 'Authorization': 'Bearer $token',
'Accept': 'application/json',
};
}
Future<Member> _fetch(String url) async {
final res = await http.get(Uri.parse(url), headers: await _headers());
if (res.statusCode == 200) {
return Member.fromJson(jsonDecode(res.body) as Map<String, dynamic>);
}
throw Exception('Error ${res.statusCode}');
}
@override Future<Member> first() => _fetch('$kApiBase/members/first');
@override Future<Member> last() => _fetch('$kApiBase/members/last');
@override Future<Member> show(int id) => _fetch('$kApiBase/members/$id');
@override Future<Member> prev(int id) => _fetch('$kApiBase/members/$id/prev');
@override Future<Member> next(int id) => _fetch('$kApiBase/members/$id/next');
@override
Future<Map<String, String>> search(String term) async {
final uri = Uri.parse('$kApiBase/members/search')
.replace(queryParameters: {'term': term});
final res = await http.get(uri, headers: await _headers());
if (res.statusCode == 200) {
final data = jsonDecode(res.body) as Map<String, dynamic>;
return data.map((k, v) => MapEntry(k, v.toString()));
}
return {};
}
}

View file

@ -0,0 +1,139 @@
import '../../models/booking.dart';
import '../booking_repository.dart';
class MockBookingService implements BookingRepository {
static final _activities = [
const BookingActivity(id: 1, name: 'TENIS'),
const BookingActivity(id: 2, name: 'PÁDEL'),
const BookingActivity(id: 3, name: 'BALONCESTO'),
];
// Grid mock por actividad: activityId aaData
static const _grids = {
1: _tenisMock,
2: _padelMock,
3: _baloncestoMock,
};
static const _courtsByActivity = {1: 4, 2: 3, 3: 2};
@override
Future<List<BookingActivity>> activities() async => _activities;
@override
Future<BookingGrid> grid({
required int activityId,
required String date,
}) async {
final activity =
_activities.firstWhere((a) => a.id == activityId, orElse: () => _activities.first);
final courts = _courtsByActivity[activityId] ?? 4;
final courtTitles =
List.generate(courts, (i) => (i + 1).toString().padLeft(2, '0'));
final rawRows = _grids[activityId] ?? _tenisMock;
final rows = rawRows.asMap().entries.map((e) {
final i = e.key;
final r = e.value;
return BookingRow(
slotIndex: i,
timeLabel: r[0],
cells: r.skip(1).map((c) => BookingCell(c)).toList(),
);
}).toList();
return BookingGrid(
rows: rows,
courtTitles: courtTitles,
activityName: activity.name,
light: 100,
extra: 150,
celebrations: date.startsWith('18') ? 'Miguel Ángel' : null,
);
}
@override
Future<BookingDetail?> singleBooking({
required int activityId,
required String date,
required int slotIndex,
required int courtIndex,
}) async {
final rawRows = _grids[activityId] ?? _tenisMock;
if (slotIndex >= rawRows.length) return null;
final row = rawRows[slotIndex];
// courtIndex is 1-based, row[0] is the time label
final cellIndex = courtIndex; // row[0]=time, row[1]=court1, ...
if (cellIndex >= row.length) return null;
final cellRaw = row[cellIndex];
if (cellRaw.isEmpty) return null;
final cell = BookingCell(cellRaw);
final courtStr = courtIndex.toString().padLeft(2, '0');
final timeLabel = row[0];
final actName =
_activities.firstWhere((a) => a.id == activityId).name;
return BookingDetail(
id: 1000 + slotIndex * 10 + courtIndex,
memberId: cell.memberId ?? 7,
familyId: cell.familyId ?? 1,
date: date,
paid: cell.isPaid,
ticket: TicketPreview(
concepto: '$actName — Pista $courtStr$timeLabel',
base: 8.26,
iva: 1.74,
total: 10.00,
lines: [
TicketLine(concepto: '1h $actName Pista $courtStr', precio: 10.00),
if (!cell.isPaid)
const TicketLine(concepto: 'Pendiente de cobro', precio: 0),
],
),
);
}
}
// Mock data
// Formato: [timeLabel, court1, court2, ...]
// Celda: "" libre | "MMMMM.F+" pagada (verde) | "MMMMM.F-" impagada (roja)
const _tenisMock = [
['0800-0900', '', '', '', ''],
['0900-1000', '00007.1+', '', '00042.1+', ''],
['1000-1100', '00085.1-', '', '00007.1+', '00042.1+'],
['1100-1200', '', '00042.1+', '', ''],
['1200-1300', '', '', '', ''],
['1300-1400', '00007.2+', '', '', '00085.1-'],
['1400-1500', '', '', '', ''],
['1500-1600', '', '', '', ''],
['1600-1700', '00007.1+', '00007.1+', '', ''],
['1700-1800', '00007.1+', '00007.1+', '00042.1-', ''],
['1800-1900', '00007.1+', '00007.1+', '00042.1-', '00085.1-'],
['1900-2000', '00042.1+', '', '', ''],
['2000-2100', '00042.1+', '', '', ''],
['2100-2200', '', '', '', ''],
];
const _padelMock = [
['0900-1000', '', '00007.1+', ''],
['1000-1100', '00042.1+', '', '00085.1-'],
['1100-1200', '', '', ''],
['1200-1300', '00007.1+', '', ''],
['1300-1400', '', '', ''],
['1600-1700', '00042.1-', '', ''],
['1700-1800', '00042.1-', '00007.1+', ''],
['1800-1900', '', '00007.1+', '00042.1+'],
['1900-2000', '', '', '00042.1+'],
['2000-2100', '', '', ''],
];
const _baloncestoMock = [
['1000-1200', '00007.1+', ''],
['1600-1800', '', '00042.1-'],
['1800-2000', '00085.1+', '00085.1+'],
['2000-2200', '', ''],
];

View file

@ -0,0 +1,204 @@
import '../../models/member.dart';
import '../member_repository.dart';
class MockMemberService implements MemberRepository {
static final _members = [_member7, _member42, _member85];
int _indexOf(int id) {
final i = _members.indexWhere((m) => m.id == id);
return i == -1 ? 0 : i;
}
@override Future<Member> first() async => _members.first;
@override Future<Member> last() async => _members.last;
@override Future<Member> show(int id) async =>
_members.firstWhere((m) => m.id == id, orElse: () => _members.first);
@override
Future<Member> prev(int id) async {
final i = _indexOf(id);
return _members[i == 0 ? _members.length - 1 : i - 1];
}
@override
Future<Member> next(int id) async {
final i = _indexOf(id);
return _members[(i + 1) % _members.length];
}
@override
Future<Map<String, String>> search(String term) async {
final t = term.toLowerCase();
final results = <String, String>{};
for (final m in _members) {
for (final p in m.people) {
if (p.fullName.toLowerCase().contains(t) || m.formattedId.contains(t)) {
final key = '${m.formattedId}${p.family.toString().padLeft(2, '0')}';
results[key] =
'(${m.formattedId}-${p.family.toString().padLeft(2, '0')}) ${p.lastName}, ${p.firstName}';
}
}
}
return results;
}
}
// Datos mock
final _member7 = Member(
id: 7,
regDate: '1993-01-01',
address: 'CALLE SALVADOR ESCUDERO, Nº3-2ºA',
city: 'CARTAGENA',
zip: '30205',
email: 'RICARDO.LEGAZ@GMAIL.COM',
phone: '648101900',
bank: '0182',
bankOffice: '6855',
bankSecCode: '87',
bankAccount: '0201674767',
bankName: 'CAIXABANK',
mandate: 'MNDT-00007',
type: 'FAMILIAR',
fee: 33.80,
comment: '',
comment2: '',
paid: false,
remSpecial: false,
retenido: false,
unregDate: null,
unregReason: '',
people: [
Person(
memberId: 7, family: 1,
firstName: 'ANTONIO', lastName: 'PUENTE CARBALLES',
dni: '', birthDate: '1953-01-02',
phone: '633313501', email: 'apcarballes@gmail.com',
lopd: true, newsletter: true, bHealth: true, authorized: true, authorized2: false,
),
Person(
memberId: 7, family: 2,
firstName: 'POLONIA', lastName: 'COLMENAR SÁNCHEZ',
dni: '', birthDate: '1952-08-09',
phone: '670243473', email: '',
lopd: true, newsletter: true, authorized: true, authorized2: false,
),
Person(
memberId: 7, family: 3,
firstName: 'RICARDO', lastName: 'LEGAZ RÍOS',
dni: '23032266C', birthDate: '1961-04-21',
phone: '648101900', email: 'RICARDO.LEGAZ@GMAIL.COM',
lopd: true, newsletter: true, authorized: true, authorized2: true,
),
Person(
memberId: 7, family: 4,
firstName: 'CRISTINA', lastName: 'PUENTE COLMENAR',
dni: '23019853TX', birthDate: '1978-10-15',
phone: '690728311', email: 'CPUENTE.SPANIA@GMAIL.COM',
lopd: true, newsletter: true, authorized: true, authorized2: true,
),
Person(
memberId: 7, family: 5,
firstName: 'MATILDA', lastName: 'LEGAZ PUENTE',
dni: '', birthDate: '2013-04-28',
phone: '', email: '',
lopd: false, newsletter: false, authorized: false, authorized2: false,
),
],
comments: [
MemberComment(
id: 1,
comment:
'11.03.23 chema: viene a darse de baja pero va a intentar traspasar el abono a su hija. 12.03.23. Cristina: Su hija está interesada, en el caso de traspaso, toma nota. 13.03.25: LOS TITULARES 3 Y 4 HARÁN USO ACTIVO DE CLUB.',
createdAt: '2023-03-11',
),
],
);
final _member42 = Member(
id: 42,
regDate: '2018-03-15',
address: 'Calle Mayor 12',
city: 'Madrid',
zip: '28001',
email: 'socio@example.com',
phone: '612345678',
bank: '0049',
bankOffice: '1500',
bankSecCode: '42',
bankAccount: '0123456789',
bankName: 'Santander',
mandate: 'MNDT-00042',
type: 'INDIVIDUAL',
fee: 25.50,
comment: 'Nota antigua importada del sistema anterior.',
comment2: 'Revisado en auditoría 2024.',
paid: false,
remSpecial: false,
retenido: false,
unregDate: null,
unregReason: '',
people: [
Person(
memberId: 42, family: 1,
firstName: 'María', lastName: 'García López',
dni: '12345678A', birthDate: '1985-03-15',
phone: '612345678', email: 'maria@example.com',
lopd: true, newsletter: true, authorized: true, authorized2: false,
),
Person(
memberId: 42, family: 2,
firstName: 'Carlos', lastName: 'García López',
dni: '87654321B', birthDate: '1988-07-22',
phone: '699887766', email: 'carlos@example.com',
lopd: true, newsletter: false, authorized: true, authorized2: true,
),
],
comments: [
MemberComment(
id: 10,
comment: 'Solicitó cambio de domiciliación bancaria el 05/01/2024.',
createdAt: '2024-01-05',
),
MemberComment(
id: 11,
comment: 'Renovó cuota anual sin incidencias.',
createdAt: '2024-09-01',
),
],
);
final _member85 = Member(
id: 85,
regDate: '2010-06-20',
address: 'Av. de la Constitución 45, 3B',
city: 'Murcia',
zip: '30008',
email: 'jlopez@correo.es',
phone: '968112233',
bank: '2100',
bankOffice: '0001',
bankSecCode: '12',
bankAccount: '9876543210',
bankName: 'CaixaBank',
mandate: 'MNDT-00085',
type: 'JUVENIL',
fee: 18.00,
comment: '',
comment2: '',
paid: false,
remSpecial: true,
retenido: true,
unregDate: null,
unregReason: '',
people: [
Person(
memberId: 85, family: 1,
firstName: 'Jorge', lastName: 'López Martínez',
dni: '22334455C', birthDate: '2005-11-30',
phone: '968112233', email: 'jlopez@correo.es',
lopd: true, newsletter: true, authorized: true, authorized2: false,
),
],
comments: [],
);

View file

@ -0,0 +1,63 @@
import 'package:flutter/material.dart';
import 'sidebar/sidebar_widget.dart';
/// Breakpoint a partir del cual el sidebar se muestra permanente.
const double kSidebarBreakpoint = 800;
class AppShell extends StatelessWidget {
final Widget child;
const AppShell({super.key, required this.child});
@override
Widget build(BuildContext context) {
final width = MediaQuery.sizeOf(context).width;
if (width >= kSidebarBreakpoint) {
return _WideLayout(child: child);
}
return _NarrowLayout(child: child);
}
}
// Wide: sidebar permanente
class _WideLayout extends StatelessWidget {
final Widget child;
const _WideLayout({required this.child});
@override
Widget build(BuildContext context) {
return Scaffold(
body: Row(
children: [
const SidebarWidget(),
VerticalDivider(
width: 1,
color: Theme.of(context).colorScheme.outlineVariant,
),
Expanded(child: child),
],
),
);
}
}
// Narrow: drawer + AppBar
class _NarrowLayout extends StatelessWidget {
final Widget child;
const _NarrowLayout({required this.child});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('DeporOS'),
centerTitle: false,
),
drawer: const Drawer(child: SidebarWidget()),
body: child,
);
}
}

View file

@ -0,0 +1,149 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import '../../models/nav_item.dart';
class NavItemTile extends StatelessWidget {
final NavItem item;
final String currentRoute;
const NavItemTile({
super.key,
required this.item,
required this.currentRoute,
});
@override
Widget build(BuildContext context) {
if (item.hasChildren) {
return _GroupTile(item: item, currentRoute: currentRoute);
}
return _LeafTile(item: item, currentRoute: currentRoute);
}
}
// Leaf (item sin hijos)
class _LeafTile extends StatelessWidget {
final NavItem item;
final String currentRoute;
const _LeafTile({required this.item, required this.currentRoute});
bool get _isActive =>
item.route != null && currentRoute == item.route;
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
final tt = Theme.of(context).textTheme;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 1),
child: Material(
color: _isActive ? cs.secondaryContainer : Colors.transparent,
borderRadius: BorderRadius.circular(12),
child: InkWell(
borderRadius: BorderRadius.circular(12),
onTap: () => context.go(item.route!),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
child: Row(
children: [
Icon(
item.icon,
size: 20,
color: _isActive ? cs.onSecondaryContainer : cs.onSurfaceVariant,
),
const SizedBox(width: 12),
Expanded(
child: Text(
item.label,
style: tt.bodyMedium?.copyWith(
color: _isActive ? cs.onSecondaryContainer : cs.onSurface,
fontWeight: _isActive ? FontWeight.w600 : FontWeight.normal,
),
),
),
],
),
),
),
),
);
}
}
// Group (item con hijos, expansible)
class _GroupTile extends StatefulWidget {
final NavItem item;
final String currentRoute;
const _GroupTile({required this.item, required this.currentRoute});
@override
State<_GroupTile> createState() => _GroupTileState();
}
class _GroupTileState extends State<_GroupTile> {
late bool _expanded;
@override
void initState() {
super.initState();
_expanded = widget.item.isActiveOrAncestor(widget.currentRoute);
}
@override
void didUpdateWidget(_GroupTile old) {
super.didUpdateWidget(old);
if (old.currentRoute != widget.currentRoute) {
if (widget.item.isActiveOrAncestor(widget.currentRoute)) {
_expanded = true;
}
}
}
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
final tt = Theme.of(context).textTheme;
final isAncestor = widget.item.isActiveOrAncestor(widget.currentRoute);
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 1),
child: Theme(
// Quita el separador por defecto del ExpansionTile
data: Theme.of(context).copyWith(dividerColor: Colors.transparent),
child: ExpansionTile(
initiallyExpanded: _expanded,
onExpansionChanged: (v) => setState(() => _expanded = v),
tilePadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 0),
childrenPadding: const EdgeInsets.only(left: 16),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
collapsedShape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
backgroundColor: isAncestor ? cs.surfaceContainerHigh : Colors.transparent,
collapsedBackgroundColor: Colors.transparent,
leading: Icon(
widget.item.icon,
size: 20,
color: isAncestor ? cs.primary : cs.onSurfaceVariant,
),
title: Text(
widget.item.label,
style: tt.bodyMedium?.copyWith(
color: isAncestor ? cs.primary : cs.onSurface,
fontWeight: isAncestor ? FontWeight.w600 : FontWeight.normal,
),
),
children: widget.item.children
.map((child) => NavItemTile(
item: child,
currentRoute: widget.currentRoute,
))
.toList(),
),
),
);
}
}

View file

@ -0,0 +1,125 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import '../../navigation/app_navigation.dart';
import '../../services/auth_service.dart';
import 'nav_item_tile.dart';
class SidebarWidget extends StatelessWidget {
const SidebarWidget({super.key});
static const double _width = 260;
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
final currentRoute = GoRouterState.of(context).matchedLocation;
return SizedBox(
width: _width,
child: Material(
color: cs.surfaceContainerLow,
child: Column(
children: [
_SidebarHeader(),
const Divider(height: 1),
Expanded(
child: ListView(
padding: const EdgeInsets.symmetric(vertical: 8),
children: kAdminNavItems
.map((item) => NavItemTile(
item: item,
currentRoute: currentRoute,
))
.toList(),
),
),
const Divider(height: 1),
_SidebarFooter(),
],
),
),
);
}
}
class _SidebarHeader extends StatelessWidget {
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 20),
child: Row(
children: [
CircleAvatar(
radius: 20,
backgroundColor: cs.primaryContainer,
child: Icon(Icons.sports, color: cs.onPrimaryContainer, size: 22),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'DeporOS',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
color: cs.onSurface,
),
),
Text(
'Panel de administración',
style: Theme.of(context).textTheme.labelSmall?.copyWith(
color: cs.onSurfaceVariant,
),
),
],
),
),
],
),
);
}
}
class _SidebarFooter extends StatelessWidget {
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
return Padding(
padding: const EdgeInsets.all(8),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 1),
child: Material(
color: Colors.transparent,
borderRadius: BorderRadius.circular(12),
child: InkWell(
borderRadius: BorderRadius.circular(12),
onTap: () => _logout(context),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Row(
children: [
Icon(Icons.logout, size: 20, color: cs.error),
const SizedBox(width: 12),
Text(
'Salir',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: cs.error,
fontWeight: FontWeight.w500,
),
),
],
),
),
),
),
),
);
}
Future<void> _logout(BuildContext context) async {
await AuthService().logout();
if (context.mounted) context.go('/login');
}
}

1
linux/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
flutter/ephemeral

128
linux/CMakeLists.txt Normal file
View file

@ -0,0 +1,128 @@
# Project-level configuration.
cmake_minimum_required(VERSION 3.13)
project(runner LANGUAGES CXX)
# The name of the executable created for the application. Change this to change
# the on-disk name of your application.
set(BINARY_NAME "depor_os")
# The unique GTK application identifier for this application. See:
# https://wiki.gnome.org/HowDoI/ChooseApplicationID
set(APPLICATION_ID "com.example.depor_os")
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
# versions of CMake.
cmake_policy(SET CMP0063 NEW)
# Load bundled libraries from the lib/ directory relative to the binary.
set(CMAKE_INSTALL_RPATH "$ORIGIN/lib")
# Root filesystem for cross-building.
if(FLUTTER_TARGET_PLATFORM_SYSROOT)
set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT})
set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT})
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
endif()
# Define build configuration options.
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
set(CMAKE_BUILD_TYPE "Debug" CACHE
STRING "Flutter build mode" FORCE)
set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
"Debug" "Profile" "Release")
endif()
# Compilation settings that should be applied to most targets.
#
# Be cautious about adding new options here, as plugins use this function by
# default. In most cases, you should add new options to specific targets instead
# of modifying this function.
function(APPLY_STANDARD_SETTINGS TARGET)
target_compile_features(${TARGET} PUBLIC cxx_std_14)
target_compile_options(${TARGET} PRIVATE -Wall -Werror)
target_compile_options(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:-O3>")
target_compile_definitions(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:NDEBUG>")
endfunction()
# Flutter library and tool build rules.
set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
add_subdirectory(${FLUTTER_MANAGED_DIR})
# System-level dependencies.
find_package(PkgConfig REQUIRED)
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
# Application build; see runner/CMakeLists.txt.
add_subdirectory("runner")
# Run the Flutter tool portions of the build. This must not be removed.
add_dependencies(${BINARY_NAME} flutter_assemble)
# Only the install-generated bundle's copy of the executable will launch
# correctly, since the resources must in the right relative locations. To avoid
# people trying to run the unbundled copy, put it in a subdirectory instead of
# the default top-level location.
set_target_properties(${BINARY_NAME}
PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run"
)
# Generated plugin build rules, which manage building the plugins and adding
# them to the application.
include(flutter/generated_plugins.cmake)
# === Installation ===
# By default, "installing" just makes a relocatable bundle in the build
# directory.
set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle")
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
endif()
# Start with a clean build bundle directory every time.
install(CODE "
file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\")
" COMPONENT Runtime)
set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib")
install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
COMPONENT Runtime)
install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
COMPONENT Runtime)
install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES})
install(FILES "${bundled_library}"
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
endforeach(bundled_library)
# Copy the native assets provided by the build.dart from all packages.
set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/")
install(DIRECTORY "${NATIVE_ASSETS_DIR}"
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
# Fully re-copy the assets directory on each build to avoid having stale files
# from a previous install.
set(FLUTTER_ASSET_DIR_NAME "flutter_assets")
install(CODE "
file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\")
" COMPONENT Runtime)
install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}"
DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime)
# Install the AOT library on non-Debug builds only.
if(NOT CMAKE_BUILD_TYPE MATCHES "Debug")
install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
endif()

View file

@ -0,0 +1,88 @@
# This file controls Flutter-level build steps. It should not be edited.
cmake_minimum_required(VERSION 3.10)
set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral")
# Configuration provided via flutter tool.
include(${EPHEMERAL_DIR}/generated_config.cmake)
# TODO: Move the rest of this into files in ephemeral. See
# https://github.com/flutter/flutter/issues/57146.
# Serves the same purpose as list(TRANSFORM ... PREPEND ...),
# which isn't available in 3.10.
function(list_prepend LIST_NAME PREFIX)
set(NEW_LIST "")
foreach(element ${${LIST_NAME}})
list(APPEND NEW_LIST "${PREFIX}${element}")
endforeach(element)
set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE)
endfunction()
# === Flutter Library ===
# System-level dependencies.
find_package(PkgConfig REQUIRED)
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0)
pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0)
set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so")
# Published to parent scope for install step.
set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)
set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE)
set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE)
set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE)
list(APPEND FLUTTER_LIBRARY_HEADERS
"fl_basic_message_channel.h"
"fl_binary_codec.h"
"fl_binary_messenger.h"
"fl_dart_project.h"
"fl_engine.h"
"fl_json_message_codec.h"
"fl_json_method_codec.h"
"fl_message_codec.h"
"fl_method_call.h"
"fl_method_channel.h"
"fl_method_codec.h"
"fl_method_response.h"
"fl_plugin_registrar.h"
"fl_plugin_registry.h"
"fl_standard_message_codec.h"
"fl_standard_method_codec.h"
"fl_string_codec.h"
"fl_value.h"
"fl_view.h"
"flutter_linux.h"
)
list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/")
add_library(flutter INTERFACE)
target_include_directories(flutter INTERFACE
"${EPHEMERAL_DIR}"
)
target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}")
target_link_libraries(flutter INTERFACE
PkgConfig::GTK
PkgConfig::GLIB
PkgConfig::GIO
)
add_dependencies(flutter flutter_assemble)
# === Flutter tool backend ===
# _phony_ is a non-existent file to force this command to run every time,
# since currently there's no way to get a full input/output list from the
# flutter tool.
add_custom_command(
OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}
${CMAKE_CURRENT_BINARY_DIR}/_phony_
COMMAND ${CMAKE_COMMAND} -E env
${FLUTTER_TOOL_ENVIRONMENT}
"${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh"
${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE}
VERBATIM
)
add_custom_target(flutter_assemble DEPENDS
"${FLUTTER_LIBRARY}"
${FLUTTER_LIBRARY_HEADERS}
)

View file

@ -0,0 +1,11 @@
//
// Generated file. Do not edit.
//
// clang-format off
#include "generated_plugin_registrant.h"
void fl_register_plugins(FlPluginRegistry* registry) {
}

View file

@ -0,0 +1,15 @@
//
// Generated file. Do not edit.
//
// clang-format off
#ifndef GENERATED_PLUGIN_REGISTRANT_
#define GENERATED_PLUGIN_REGISTRANT_
#include <flutter_linux/flutter_linux.h>
// Registers Flutter plugins.
void fl_register_plugins(FlPluginRegistry* registry);
#endif // GENERATED_PLUGIN_REGISTRANT_

View file

@ -0,0 +1,23 @@
#
# Generated file, do not edit.
#
list(APPEND FLUTTER_PLUGIN_LIST
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
)
set(PLUGIN_BUNDLED_LIBRARIES)
foreach(plugin ${FLUTTER_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin})
target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
endforeach(plugin)
foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin})
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
endforeach(ffi_plugin)

View file

@ -0,0 +1,26 @@
cmake_minimum_required(VERSION 3.13)
project(runner LANGUAGES CXX)
# Define the application target. To change its name, change BINARY_NAME in the
# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer
# work.
#
# Any new source files that you add to the application should be added here.
add_executable(${BINARY_NAME}
"main.cc"
"my_application.cc"
"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
)
# Apply the standard set of build settings. This can be removed for applications
# that need different build settings.
apply_standard_settings(${BINARY_NAME})
# Add preprocessor definitions for the application ID.
add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}")
# Add dependency libraries. Add any application-specific dependencies here.
target_link_libraries(${BINARY_NAME} PRIVATE flutter)
target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK)
target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}")

6
linux/runner/main.cc Normal file
View file

@ -0,0 +1,6 @@
#include "my_application.h"
int main(int argc, char** argv) {
g_autoptr(MyApplication) app = my_application_new();
return g_application_run(G_APPLICATION(app), argc, argv);
}

View file

@ -0,0 +1,148 @@
#include "my_application.h"
#include <flutter_linux/flutter_linux.h>
#ifdef GDK_WINDOWING_X11
#include <gdk/gdkx.h>
#endif
#include "flutter/generated_plugin_registrant.h"
struct _MyApplication {
GtkApplication parent_instance;
char** dart_entrypoint_arguments;
};
G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION)
// Called when first Flutter frame received.
static void first_frame_cb(MyApplication* self, FlView* view) {
gtk_widget_show(gtk_widget_get_toplevel(GTK_WIDGET(view)));
}
// Implements GApplication::activate.
static void my_application_activate(GApplication* application) {
MyApplication* self = MY_APPLICATION(application);
GtkWindow* window =
GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application)));
// Use a header bar when running in GNOME as this is the common style used
// by applications and is the setup most users will be using (e.g. Ubuntu
// desktop).
// If running on X and not using GNOME then just use a traditional title bar
// in case the window manager does more exotic layout, e.g. tiling.
// If running on Wayland assume the header bar will work (may need changing
// if future cases occur).
gboolean use_header_bar = TRUE;
#ifdef GDK_WINDOWING_X11
GdkScreen* screen = gtk_window_get_screen(window);
if (GDK_IS_X11_SCREEN(screen)) {
const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen);
if (g_strcmp0(wm_name, "GNOME Shell") != 0) {
use_header_bar = FALSE;
}
}
#endif
if (use_header_bar) {
GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
gtk_widget_show(GTK_WIDGET(header_bar));
gtk_header_bar_set_title(header_bar, "depor_os");
gtk_header_bar_set_show_close_button(header_bar, TRUE);
gtk_window_set_titlebar(window, GTK_WIDGET(header_bar));
} else {
gtk_window_set_title(window, "depor_os");
}
gtk_window_set_default_size(window, 1280, 720);
g_autoptr(FlDartProject) project = fl_dart_project_new();
fl_dart_project_set_dart_entrypoint_arguments(
project, self->dart_entrypoint_arguments);
FlView* view = fl_view_new(project);
GdkRGBA background_color;
// Background defaults to black, override it here if necessary, e.g. #00000000
// for transparent.
gdk_rgba_parse(&background_color, "#000000");
fl_view_set_background_color(view, &background_color);
gtk_widget_show(GTK_WIDGET(view));
gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view));
// Show the window when Flutter renders.
// Requires the view to be realized so we can start rendering.
g_signal_connect_swapped(view, "first-frame", G_CALLBACK(first_frame_cb),
self);
gtk_widget_realize(GTK_WIDGET(view));
fl_register_plugins(FL_PLUGIN_REGISTRY(view));
gtk_widget_grab_focus(GTK_WIDGET(view));
}
// Implements GApplication::local_command_line.
static gboolean my_application_local_command_line(GApplication* application,
gchar*** arguments,
int* exit_status) {
MyApplication* self = MY_APPLICATION(application);
// Strip out the first argument as it is the binary name.
self->dart_entrypoint_arguments = g_strdupv(*arguments + 1);
g_autoptr(GError) error = nullptr;
if (!g_application_register(application, nullptr, &error)) {
g_warning("Failed to register: %s", error->message);
*exit_status = 1;
return TRUE;
}
g_application_activate(application);
*exit_status = 0;
return TRUE;
}
// Implements GApplication::startup.
static void my_application_startup(GApplication* application) {
// MyApplication* self = MY_APPLICATION(object);
// Perform any actions required at application startup.
G_APPLICATION_CLASS(my_application_parent_class)->startup(application);
}
// Implements GApplication::shutdown.
static void my_application_shutdown(GApplication* application) {
// MyApplication* self = MY_APPLICATION(object);
// Perform any actions required at application shutdown.
G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application);
}
// Implements GObject::dispose.
static void my_application_dispose(GObject* object) {
MyApplication* self = MY_APPLICATION(object);
g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev);
G_OBJECT_CLASS(my_application_parent_class)->dispose(object);
}
static void my_application_class_init(MyApplicationClass* klass) {
G_APPLICATION_CLASS(klass)->activate = my_application_activate;
G_APPLICATION_CLASS(klass)->local_command_line =
my_application_local_command_line;
G_APPLICATION_CLASS(klass)->startup = my_application_startup;
G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown;
G_OBJECT_CLASS(klass)->dispose = my_application_dispose;
}
static void my_application_init(MyApplication* self) {}
MyApplication* my_application_new() {
// Set the program name to the application ID, which helps various systems
// like GTK and desktop environments map this running application to its
// corresponding .desktop file. This ensures better integration by allowing
// the application to be recognized beyond its binary name.
g_set_prgname(APPLICATION_ID);
return MY_APPLICATION(g_object_new(my_application_get_type(),
"application-id", APPLICATION_ID, "flags",
G_APPLICATION_NON_UNIQUE, nullptr));
}

View file

@ -0,0 +1,21 @@
#ifndef FLUTTER_MY_APPLICATION_H_
#define FLUTTER_MY_APPLICATION_H_
#include <gtk/gtk.h>
G_DECLARE_FINAL_TYPE(MyApplication,
my_application,
MY,
APPLICATION,
GtkApplication)
/**
* my_application_new:
*
* Creates a new Flutter-based application.
*
* Returns: a new #MyApplication.
*/
MyApplication* my_application_new();
#endif // FLUTTER_MY_APPLICATION_H_

Some files were not shown because too many files have changed in this diff Show more