Co-authored-by: MENDOZA BALLARDO GAEL RICARDO <gael-meb123@users.noreply.github.com>

Co-authored-by: Azareth-Tr <Azareth-Tr@users.noreply.github.com>
Co-authored-by: eddgranados12 <eddgranados12@users.noreply.github.com>

primeras vistas para el frontend, configuracion para firebase
This commit is contained in:
shinra32
2026-05-22 17:45:54 -06:00
parent ba5e5ea12c
commit 9e6bd04755
26 changed files with 322 additions and 108 deletions

BIN
animations/blink crazy1.mp4 Normal file

Binary file not shown.

BIN
animations/condrix.mp4 Normal file

Binary file not shown.

View File

@@ -1,49 +1,43 @@
import os
from typing import Dict
import firebase_admin
from firebase_admin import credentials, messaging
_initialized = False
_use_mock = True
_firebase_initialized = False
def init_firebase(credentials_path: str = None):
global _initialized, _use_mock
try:
import firebase_admin
from firebase_admin import credentials, messaging
except Exception:
_use_mock = True
return
def init_firebase(cred_path: str):
"""Inicializa la conexión con Firebase usando el Service Account (JSON)."""
global _firebase_initialized
if credentials_path is None:
credentials_path = os.environ.get('FIREBASE_CREDENTIALS_PATH', 'backend/secrets/firebase-adminsdk.json')
if os.path.exists(cred_path):
try:
cred = credentials.Certificate(cred_path)
firebase_admin.initialize_app(cred)
_firebase_initialized = True
print(f"Firebase Admin SDK inicializado correctamente desde: {cred_path}")
except ValueError:
# Si el entorno se recarga, Firebase podría quejarse de que ya se inicializó
_firebase_initialized = True
except Exception as e:
print(f"Error al inicializar Firebase: {e}")
else:
print(f"ADVERTENCIA: Credenciales no encontradas en '{cred_path}'.")
print("Las notificaciones se ejecutarán en modo SIMULADO (solo consola).")
if not os.path.exists(credentials_path):
_use_mock = True
def send_to_topic(topic: str, title: str, body: str):
"""Envía una notificación push a todos los dispositivos suscritos a un topic (ej. RUTA-01)."""
if not _firebase_initialized:
print(f"[MOCK PUSH] -> Topic: {topic} | Título: '{title}' | Mensaje: '{body}'")
return
try:
cred = credentials.Certificate(credentials_path)
firebase_admin.initialize_app(cred)
_initialized = True
_use_mock = False
except Exception:
_use_mock = True
def send_to_topic(topic: str, payload: Dict):
"""Sends a push to an FCM topic. Falls back to mock (prints) if not configured."""
global _use_mock
if _use_mock:
print(f"[MOCK PUSH] topic={topic} payload={payload}")
return {"mock": True, "topic": topic, "payload": payload}
try:
from firebase_admin import messaging
message = messaging.Message(
notification=messaging.Notification(title=payload.get('title'), body=payload.get('body')),
notification=messaging.Notification(
title=title,
body=body,
),
topic=topic,
)
resp = messaging.send(message)
return {"result": resp}
response = messaging.send(message)
print(f"Push enviado al topic '{topic}' exitosamente. MessageID: {response}")
except Exception as e:
print(f"[PUSH ERROR] {e}")
return {"error": str(e)}
print(f"Error al enviar push al topic '{topic}': {e}")

View File

@@ -1,20 +1,39 @@
from contextlib import asynccontextmanager
from fastapi import FastAPI
import os
from apscheduler.schedulers.asyncio import AsyncIOScheduler
# Aquí se importarán los routers en el futuro
# from app.api.routers import auth, addresses, routes, eta
from app.api.eta import router as eta_router
from app.services import simulation, notifications
scheduler = AsyncIOScheduler()
@asynccontextmanager
async def lifespan(app: FastAPI):
"""
Maneja el ciclo de vida de la aplicación.
Ideal para arrancar el cron job de simulación (APScheduler).
"""
print("Iniciando aplicación: Backend Sistema de Recolección...")
# TODO: Inicializar APScheduler aquí para avanzar current_position_id (1-8)
# 1. Cargar datos de simulación
simulation.load_data()
simulation.start_simulation_state()
# 2. Inicializar Firebase (o Mock si no hay credenciales)
# Ruta relativa correcta cuando se ejecuta desde la carpeta /backend
cred_path = os.environ.get("FIREBASE_CREDENTIALS_PATH", "secrets/firebase-adminsdk.json")
notifications.init_firebase(cred_path)
# 3. Arrancar el scheduler de simulación
tick_seconds = int(os.environ.get("SIMULATION_TICK_SECONDS", 15))
scheduler.add_job(simulation.tick, 'interval', seconds=tick_seconds, id='simulation_tick')
scheduler.start()
print(f"Simulador de rutas iniciado. Avanzando cada {tick_seconds} segundos.")
yield
print("Apagando aplicación y deteniendo simulador...")
# TODO: Apagar APScheduler
scheduler.shutdown()
app = FastAPI(
title="API - Recolección Inteligente y Privada",
@@ -23,6 +42,9 @@ app = FastAPI(
lifespan=lifespan
)
# Incluir routers de la API
app.include_router(eta_router)
# Endpoints de prueba base
@app.get("/")
def read_root():

View File

@@ -1,5 +1,8 @@
plugins {
id("com.android.application")
// START: FlutterFire Configuration
id("com.google.gms.google-services")
// END: FlutterFire Configuration
id("kotlin-android")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")

View File

@@ -20,6 +20,9 @@ pluginManagement {
plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.11.1" apply false
// START: FlutterFire Configuration
id("com.google.gms.google-services") version("4.3.15") apply false
// END: FlutterFire Configuration
id("org.jetbrains.kotlin.android") version "2.2.20" apply false
}

View File

@@ -0,0 +1 @@
{"flutter":{"platforms":{"android":{"default":{"projectId":"recoleccion-app","appId":"1:446089041715:android:561dccabff253d1f879046","fileOutput":"android/app/google-services.json"}},"dart":{"lib/firebase_options.dart":{"projectId":"recoleccion-app","configurations":{"android":"1:446089041715:android:561dccabff253d1f879046","ios":"1:446089041715:ios:6edb76038f517454879046","macos":"1:446089041715:ios:6edb76038f517454879046","web":"1:446089041715:web:4675e76c702e083e879046","windows":"1:446089041715:web:d0f612e0a6749eea879046"}}}}}}

View File

@@ -1,14 +1,88 @@
// File generated by FlutterFire CLI.
// ignore_for_file: type=lint
import 'package:firebase_core/firebase_core.dart' show FirebaseOptions;
import 'package:flutter/foundation.dart'
show defaultTargetPlatform, kIsWeb, TargetPlatform;
// Placeholder file for Firebase configuration.
// Run `flutterfire configure` to generate a proper `firebase_options.dart` file
// and replace the implementation below. For local dev you can also rely on
// `google-services.json` (Android) and `GoogleService-Info.plist` (iOS) instead
// of providing explicit FirebaseOptions.
/// Default [FirebaseOptions] for use with your Firebase apps.
///
/// Example:
/// ```dart
/// import 'firebase_options.dart';
/// // ...
/// await Firebase.initializeApp(
/// options: DefaultFirebaseOptions.currentPlatform,
/// );
/// ```
class DefaultFirebaseOptions {
static FirebaseOptions? get currentPlatform {
// TODO: Replace with generated FirebaseOptions from `flutterfire configure`.
return null;
static FirebaseOptions get currentPlatform {
if (kIsWeb) {
return web;
}
switch (defaultTargetPlatform) {
case TargetPlatform.android:
return android;
case TargetPlatform.iOS:
return ios;
case TargetPlatform.macOS:
return macos;
case TargetPlatform.windows:
return windows;
case TargetPlatform.linux:
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for linux - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
default:
throw UnsupportedError(
'DefaultFirebaseOptions are not supported for this platform.',
);
}
}
static const FirebaseOptions web = FirebaseOptions(
apiKey: 'AIzaSyCFklMyOhE7smpHESbiId9K4v6AIdvb_Vw',
appId: '1:446089041715:web:4675e76c702e083e879046',
messagingSenderId: '446089041715',
projectId: 'recoleccion-app',
authDomain: 'recoleccion-app.firebaseapp.com',
storageBucket: 'recoleccion-app.firebasestorage.app',
measurementId: 'G-9SDC6YCM6M',
);
static const FirebaseOptions android = FirebaseOptions(
apiKey: 'AIzaSyDV0rzoXy6lKOXFaN4vDRnjMrUoTgtqh8E',
appId: '1:446089041715:android:561dccabff253d1f879046',
messagingSenderId: '446089041715',
projectId: 'recoleccion-app',
storageBucket: 'recoleccion-app.firebasestorage.app',
);
static const FirebaseOptions ios = FirebaseOptions(
apiKey: 'AIzaSyA7BewYK-cbZdtUkigxcU2di9en4VOHTyg',
appId: '1:446089041715:ios:6edb76038f517454879046',
messagingSenderId: '446089041715',
projectId: 'recoleccion-app',
storageBucket: 'recoleccion-app.firebasestorage.app',
iosBundleId: 'com.equipo.recolecta.recolectaApp',
);
static const FirebaseOptions macos = FirebaseOptions(
apiKey: 'AIzaSyA7BewYK-cbZdtUkigxcU2di9en4VOHTyg',
appId: '1:446089041715:ios:6edb76038f517454879046',
messagingSenderId: '446089041715',
projectId: 'recoleccion-app',
storageBucket: 'recoleccion-app.firebasestorage.app',
iosBundleId: 'com.equipo.recolecta.recolectaApp',
);
static const FirebaseOptions windows = FirebaseOptions(
apiKey: 'AIzaSyCFklMyOhE7smpHESbiId9K4v6AIdvb_Vw',
appId: '1:446089041715:web:d0f612e0a6749eea879046',
messagingSenderId: '446089041715',
projectId: 'recoleccion-app',
authDomain: 'recoleccion-app.firebaseapp.com',
storageBucket: 'recoleccion-app.firebasestorage.app',
measurementId: 'G-5ZEJB09QEL',
);
}

45
views/.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

30
views/.metadata Normal file
View File

@@ -0,0 +1,30 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: "559ffa3f75e7402d65a8def9c28389a9b2e6fe42"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42
base_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42
- platform: windows
create_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42
base_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'

3
views/README.md Normal file
View File

@@ -0,0 +1,3 @@
# rutaverde
A new Flutter project.

View File

@@ -0,0 +1 @@
include: package:flutter_lints/flutter.yaml

View File

@@ -33,7 +33,7 @@ class _MapScreenState extends State<MapScreen> {
enServicio: true,
);
final HouseModel _casa = const HouseModel(
final HouseModel _casa = HouseModel(
id: 'casa-01',
calle: 'Av. Insurgentes 245',
colonia: 'Centro',
@@ -45,7 +45,6 @@ class _MapScreenState extends State<MapScreen> {
Set<Marker> _markers = {};
Set<Circle> _circles = {};
bool _mapLoaded = false;
Timer? _refreshTimer;
// Distancia simulada (metros)
@@ -136,7 +135,6 @@ class _MapScreenState extends State<MapScreen> {
mapType: MapType.normal,
onMapCreated: (c) {
_mapController.complete(c);
setState(() => _mapLoaded = true);
},
),

View File

@@ -5,10 +5,10 @@ packages:
dependency: transitive
description:
name: _flutterfire_internals
sha256: "37a42d06068e2fe3deddb2da079a8c4d105f241225ba27b7122b37e9865fd8f7"
sha256: "8f89e371e2883de35cdc78f648e725fa4da5f3b6c927269f00fa68f1ea92b598"
url: "https://pub.dev"
source: hosted
version: "1.3.35"
version: "1.3.71"
args:
dependency: transitive
description:
@@ -117,50 +117,50 @@ packages:
dependency: "direct main"
description:
name: firebase_core
sha256: "26de145bb9688a90962faec6f838247377b0b0d32cc0abecd9a4e43525fc856c"
sha256: "93a5bde9775fd5adcc937f39dfa04ae0bc89c4d79bea6abc49de3f7b049d9ff6"
url: "https://pub.dev"
source: hosted
version: "2.32.0"
version: "4.9.0"
firebase_core_platform_interface:
dependency: transitive
description:
name: firebase_core_platform_interface
sha256: "8bcfad6d7033f5ea951d15b867622a824b13812178bfec0c779b9d81de011bbb"
sha256: "4a120366dbf7d5a8ee9438978530b664b855728fb8dcc3a201017660817e555b"
url: "https://pub.dev"
source: hosted
version: "5.4.2"
version: "7.0.1"
firebase_core_web:
dependency: transitive
description:
name: firebase_core_web
sha256: eb3afccfc452b2b2075acbe0c4b27de62dd596802b4e5e19869c1e926cbb20b3
sha256: "7c98f10b8c8e5adedc0b810b66a877120696675e2c22d9ca9caca092da0d9e57"
url: "https://pub.dev"
source: hosted
version: "2.24.0"
version: "3.7.0"
firebase_messaging:
dependency: "direct main"
description:
name: firebase_messaging
sha256: "980259425fa5e2afc03e533f33723335731d21a56fd255611083bceebf4373a8"
sha256: "8d0dc81a31cd030170508dc3e89bfd14355b20a1b991340af5f018e37daab5d7"
url: "https://pub.dev"
source: hosted
version: "14.7.10"
version: "16.2.2"
firebase_messaging_platform_interface:
dependency: transitive
description:
name: firebase_messaging_platform_interface
sha256: "87c4a922cb6f811cfb7a889bdbb3622702443c52a0271636cbc90d813ceac147"
sha256: "37abb0b0535c5497605ee94c12470e1ebbbe47e71a22d0c20bffcc912311f8cb"
url: "https://pub.dev"
source: hosted
version: "4.5.37"
version: "4.7.11"
firebase_messaging_web:
dependency: transitive
description:
name: firebase_messaging_web
sha256: "90dc7ed885e90a24bb0e56d661d4d2b5f84429697fd2cbb9e5890a0ca370e6f4"
sha256: "54e22b43e2c26a2728a3f68c188de0f9011993ae19ae959a06d476dad935c776"
url: "https://pub.dev"
source: hosted
version: "3.5.18"
version: "4.1.7"
fixnum:
dependency: transitive
description:
@@ -178,34 +178,34 @@ packages:
dependency: "direct dev"
description:
name: flutter_lints
sha256: "9e8c3858111da373efc5aa341de011d9bd23e2c5c5e0c62bccf32438e192d7b1"
sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
version: "6.0.0"
flutter_local_notifications:
dependency: "direct main"
description:
name: flutter_local_notifications
sha256: "674173fd3c9eda9d4c8528da2ce0ea69f161577495a9cc835a2a4ecd7eadeb35"
sha256: ef41ae901e7529e52934feba19ed82827b11baa67336829564aeab3129460610
url: "https://pub.dev"
source: hosted
version: "17.2.4"
version: "18.0.1"
flutter_local_notifications_linux:
dependency: transitive
description:
name: flutter_local_notifications_linux
sha256: c49bd06165cad9beeb79090b18cd1eb0296f4bf4b23b84426e37dd7c027fc3af
sha256: "8f685642876742c941b29c32030f6f4f6dacd0e4eaecb3efbb187d6a3812ca01"
url: "https://pub.dev"
source: hosted
version: "4.0.1"
version: "5.0.0"
flutter_local_notifications_platform_interface:
dependency: transitive
description:
name: flutter_local_notifications_platform_interface
sha256: "85f8d07fe708c1bdcf45037f2c0109753b26ae077e9d9e899d55971711a4ea66"
sha256: "6c5b83c86bf819cdb177a9247a3722067dd8cc6313827ce7c77a4b238a26fd52"
url: "https://pub.dev"
source: hosted
version: "7.2.0"
version: "8.0.0"
flutter_plugin_android_lifecycle:
dependency: transitive
description:
@@ -224,22 +224,30 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
geoclue:
dependency: transitive
description:
name: geoclue
sha256: c2a998c77474fc57aa00c6baa2928e58f4b267649057a1c76738656e9dbd2a7f
url: "https://pub.dev"
source: hosted
version: "0.1.1"
geolocator:
dependency: "direct main"
description:
name: geolocator
sha256: "6cb9fb6e5928b58b9a84bdf85012d757fd07aab8215c5205337021c4999bad27"
sha256: "79939537046c9025be47ec645f35c8090ecadb6fe98eba146a0d25e8c1357516"
url: "https://pub.dev"
source: hosted
version: "11.1.0"
version: "14.0.2"
geolocator_android:
dependency: transitive
description:
name: geolocator_android
sha256: fcb1760a50d7500deca37c9a666785c047139b5f9ee15aa5469fae7dbbe3170d
sha256: "179c3cb66dfa674fc9ccbf2be872a02658724d1c067634e2c427cf6df7df901a"
url: "https://pub.dev"
source: hosted
version: "4.6.2"
version: "5.0.2"
geolocator_apple:
dependency: transitive
description:
@@ -248,6 +256,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.3.13"
geolocator_linux:
dependency: transitive
description:
name: geolocator_linux
sha256: d64112a205931926f4363bb6bd48f14cb38e7326833041d170615586cd143797
url: "https://pub.dev"
source: hosted
version: "0.2.4"
geolocator_platform_interface:
dependency: transitive
description:
@@ -260,10 +276,10 @@ packages:
dependency: transitive
description:
name: geolocator_web
sha256: "49d8f846ebeb5e2b6641fe477a7e97e5dd73f03cbfef3fd5c42177b7300fb0ed"
sha256: b1ae9bdfd90f861fde8fd4f209c37b953d65e92823cb73c7dee1fa021b06f172
url: "https://pub.dev"
source: hosted
version: "3.0.0"
version: "4.1.3"
geolocator_windows:
dependency: transitive
description:
@@ -320,6 +336,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.6.2+1"
gsettings:
dependency: transitive
description:
name: gsettings
sha256: "1b0ce661f5436d2db1e51f3c4295a49849f03d304003a7ba177d01e3a858249c"
url: "https://pub.dev"
source: hosted
version: "0.2.8"
html:
dependency: transitive
description:
@@ -348,18 +372,10 @@ packages:
dependency: "direct main"
description:
name: intl
sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf
sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
url: "https://pub.dev"
source: hosted
version: "0.19.0"
js:
dependency: transitive
description:
name: js
sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3
url: "https://pub.dev"
source: hosted
version: "0.6.7"
version: "0.20.2"
leak_tracker:
dependency: transitive
description:
@@ -388,10 +404,10 @@ packages:
dependency: transitive
description:
name: lints
sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290
sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df"
url: "https://pub.dev"
source: hosted
version: "3.0.0"
version: "6.1.0"
matcher:
dependency: transitive
description:
@@ -424,6 +440,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.0"
package_info_plus:
dependency: transitive
description:
name: package_info_plus
sha256: "468c26b4254ab01979fa5e4a98cb343ea3631b9acee6f21028997419a80e1a20"
url: "https://pub.dev"
source: hosted
version: "9.0.1"
package_info_plus_platform_interface:
dependency: transitive
description:
name: package_info_plus_platform_interface
sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086"
url: "https://pub.dev"
source: hosted
version: "3.2.1"
path:
dependency: transitive
description:
@@ -460,18 +492,18 @@ packages:
dependency: "direct main"
description:
name: permission_handler
sha256: "59adad729136f01ea9e35a48f5d1395e25cba6cea552249ddbe9cf950f5d7849"
sha256: bc917da36261b00137bbc8896bf1482169cd76f866282368948f032c8c1caae1
url: "https://pub.dev"
source: hosted
version: "11.4.0"
version: "12.0.1"
permission_handler_android:
dependency: transitive
description:
name: permission_handler_android
sha256: d3971dcdd76182a0c198c096b5db2f0884b0d4196723d21a866fc4cdea057ebc
sha256: "1e3bc410ca1bf84662104b100eb126e066cb55791b7451307f9708d4007350e6"
url: "https://pub.dev"
source: hosted
version: "12.1.0"
version: "13.0.1"
permission_handler_apple:
dependency: transitive
description:
@@ -665,10 +697,10 @@ packages:
dependency: transitive
description:
name: timezone
sha256: "2236ec079a174ce07434e89fcd3fcda430025eb7692244139a9cf54fdcf1fc7d"
sha256: dd14a3b83cfd7cb19e7888f1cbc20f258b8d71b54c06f79ac585f14093a287d1
url: "https://pub.dev"
source: hosted
version: "0.9.4"
version: "0.10.1"
typed_data:
dependency: transitive
description:
@@ -709,6 +741,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.1.1"
win32:
dependency: transitive
description:
name: win32
sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e
url: "https://pub.dev"
source: hosted
version: "5.15.0"
xdg_directories:
dependency: transitive
description:

View File

@@ -12,20 +12,20 @@ dependencies:
sdk: flutter
cupertino_icons: ^1.0.6
google_maps_flutter: ^2.5.0
geolocator: ^11.0.0
flutter_local_notifications: ^17.0.0
firebase_core: ^2.24.0
firebase_messaging: ^14.7.0
geolocator: ^14.0.2
flutter_local_notifications: ^18.0.1
firebase_core: ^4.9.0
firebase_messaging: ^16.2.2
provider: ^6.1.1
shared_preferences: ^2.2.2
http: ^1.1.0
intl: ^0.19.0
permission_handler: ^11.1.0
intl: ^0.20.2
permission_handler: ^12.0.1
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^3.0.0
flutter_lints: ^6.0.0
flutter:
uses-material-design: true