update app
|
|
@ -0,0 +1,2 @@
|
|||
#Sun Jan 12 11:55:42 COT 2025
|
||||
java.home=/Applications/Android Studio.app/Contents/jbr/Contents/Home
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "search_engine",
|
||||
"request": "launch",
|
||||
"type": "dart"
|
||||
},
|
||||
{
|
||||
"name": "search_engine (profile mode)",
|
||||
"request": "launch",
|
||||
"type": "dart",
|
||||
"flutterMode": "profile"
|
||||
},
|
||||
{
|
||||
"name": "search_engine (release mode)",
|
||||
"request": "launch",
|
||||
"type": "dart",
|
||||
"flutterMode": "release"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
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"
|
||||
}
|
||||
|
||||
def localProperties = new Properties()
|
||||
def localPropertiesFile = rootProject.file("local.properties")
|
||||
if (localPropertiesFile.exists()) {
|
||||
localPropertiesFile.withReader("UTF-8") { reader ->
|
||||
localProperties.load(reader)
|
||||
}
|
||||
}
|
||||
|
||||
def flutterVersionCode = localProperties.getProperty("flutter.versionCode")
|
||||
if (flutterVersionCode == null) {
|
||||
flutterVersionCode = "1"
|
||||
}
|
||||
|
||||
def flutterVersionName = localProperties.getProperty("flutter.versionName")
|
||||
if (flutterVersionName == null) {
|
||||
flutterVersionName = "1.0"
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.carpa.search_engine"
|
||||
compileSdk = flutter.compileSdkVersion
|
||||
ndkVersion = flutter.ndkVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
lintOptions {
|
||||
disable 'Deprecation'
|
||||
}
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||
applicationId "com.carpa.search_engine"
|
||||
// You can update the following values to match your application needs.
|
||||
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
|
||||
minSdk = 24
|
||||
targetSdk = flutter.targetSdkVersion
|
||||
versionCode = flutterVersionCode.toInteger()
|
||||
versionName = flutterVersionName
|
||||
}
|
||||
|
||||
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.debug
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
flutter {
|
||||
source = "../.."
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"project_info": {
|
||||
"project_number": "289926488951",
|
||||
"project_id": "carpa-search-engine",
|
||||
"storage_bucket": "carpa-search-engine.appspot.com"
|
||||
},
|
||||
"client": [
|
||||
{
|
||||
"client_info": {
|
||||
"mobilesdk_app_id": "1:289926488951:android:ecbbaa7efda3d6721ab772",
|
||||
"android_client_info": {
|
||||
"package_name": "com.carpa.search_engine"
|
||||
}
|
||||
},
|
||||
"oauth_client": [],
|
||||
"api_key": [
|
||||
{
|
||||
"current_key": "AIzaSyBwkWdv6iBcKLjAPnoe0WpdpLTO4tBdqSA"
|
||||
}
|
||||
],
|
||||
"services": {
|
||||
"appinvite_service": {
|
||||
"other_platform_oauth_client": []
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"configuration_version": "1"
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package com.carpa.carpa
|
||||
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
|
||||
class MainActivity: FlutterActivity()
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package com.carpa.estudiosbiblicosapp
|
||||
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
|
||||
class MainActivity: FlutterActivity()
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package com.carpa.search_engine
|
||||
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
|
||||
class MainActivity: FlutterActivity()
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.buildDir = "../build"
|
||||
subprojects {
|
||||
project.buildDir = "${rootProject.buildDir}/${project.name}"
|
||||
}
|
||||
subprojects {
|
||||
project.evaluationDependsOn(":app")
|
||||
}
|
||||
|
||||
tasks.register("clean", Delete) {
|
||||
delete rootProject.buildDir
|
||||
}
|
||||
|
||||
ext.kotlin_version = '2.1.10'
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
pluginManagement {
|
||||
def flutterSdkPath = {
|
||||
def properties = new Properties()
|
||||
file("local.properties").withInputStream { properties.load(it) }
|
||||
def flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
|
||||
return 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.1.4" apply false
|
||||
// START: FlutterFire Configuration
|
||||
id "com.google.gms.google-services" version "4.4.2" apply false
|
||||
// END: FlutterFire Configuration
|
||||
id "org.jetbrains.kotlin.android" version "2.1.10" apply false
|
||||
}
|
||||
|
||||
rootProject.name = 'estudiosbiblicosapp'
|
||||
include ":app"
|
||||
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
|
@ -0,0 +1,145 @@
|
|||
{
|
||||
"title": "Bible\nStudies",
|
||||
"version": "Version",
|
||||
"downloading_data": "Downloading data...",
|
||||
"updating_data": "Updating database...",
|
||||
"success_updating_data": "Data updated successfully",
|
||||
"welcome": "Home",
|
||||
"home": "Bible Studies",
|
||||
"search": "Search",
|
||||
"searching": "Searching",
|
||||
"searching_in_progress": "Searching...",
|
||||
"loading_results": "Loading results...",
|
||||
"loading_more": "Loading more...",
|
||||
"try_different_search": "Try a different search",
|
||||
"search_placeholder": "Search...",
|
||||
"image_saved": "Image saved",
|
||||
"image_saved_desc": "Please, check your gallery",
|
||||
"error_saving_image": "Error saving image",
|
||||
"error_saving_image_desc": "Please, try again later",
|
||||
"results": {
|
||||
"one": "Result",
|
||||
"other": "Results"
|
||||
},
|
||||
"results_found": {
|
||||
"one": "Result found",
|
||||
"other": "Results found"
|
||||
},
|
||||
"calendar": "By date",
|
||||
"empty_search_term": "Please enter a search term",
|
||||
"empty_results": "No results found",
|
||||
"library": "Library",
|
||||
"config": "Settings",
|
||||
"downloaded": "Downloaded",
|
||||
"available": "Available",
|
||||
"about": "About",
|
||||
"settings": "Settings",
|
||||
"lang": "Language",
|
||||
"theme": "Theme",
|
||||
"system": "System",
|
||||
"light": "Light",
|
||||
"dark": "Dark",
|
||||
"auto": "Automatic",
|
||||
"hide_thumbnails": "Hide thumbnails",
|
||||
"download_pdf": "Download PDF",
|
||||
"pdf_download": "Automatic PDF download",
|
||||
"pdf_download_desc": "A PDF will be downloaded automatically if it is not available",
|
||||
"hd_thumbnails_desc": "Only downloaded when entering a specific study",
|
||||
"hd_thumbnails": "High-quality thumbnails",
|
||||
"locale": "Language",
|
||||
"confirm_locale_change": "Confirm language change",
|
||||
"confirm_locale_change_desc": "Are you sure you want to change the language?",
|
||||
"no": "No",
|
||||
"yes": "Yes",
|
||||
"cancel": "Cancel",
|
||||
"accept": "Accept",
|
||||
"last_activities": "Recent activities",
|
||||
"recent_text": "Recent texts",
|
||||
"no_text": "No transcription available",
|
||||
"draft": "Draft",
|
||||
"from": "from",
|
||||
"full_screen": "Full screen",
|
||||
"activity": {
|
||||
"one": "Activity",
|
||||
"other": "Activities"
|
||||
},
|
||||
"back": "Back",
|
||||
"favorite": "Favorite",
|
||||
"year": {
|
||||
"one": "Year",
|
||||
"other": "Years"
|
||||
},
|
||||
"month": {
|
||||
"one": "Month",
|
||||
"other": "Months"
|
||||
},
|
||||
"day": {
|
||||
"one": "Day",
|
||||
"other": "Days"
|
||||
},
|
||||
"hour": {
|
||||
"one": "Hour",
|
||||
"other": "Hours"
|
||||
},
|
||||
"minute": {
|
||||
"one": "Minute",
|
||||
"other": "Minutes"
|
||||
},
|
||||
"filter": "Filter",
|
||||
"clear_filters": "Clear filters",
|
||||
"select_year": "Select year",
|
||||
"select_month": "Select month",
|
||||
"select_year_first": "Select a year first",
|
||||
"no_months_available": "No months available",
|
||||
"no_activities_for_period": "No activities for this period",
|
||||
"clear": "Clear",
|
||||
"apply": "Apply",
|
||||
"cache_storage": "Internal storage",
|
||||
"cache_storage_desc": "Internal storage of the application",
|
||||
"cache_storage_clear": "Clear internal storage",
|
||||
"cache_storage_clear_desc": "Clear the internal storage of the application",
|
||||
"cache_storage_clear_confirm": "Are you sure you want to clear the internal storage of the application?",
|
||||
"cache_storage_clear_success": "Internal storage cleared successfully",
|
||||
"cache_storage_clear_error": "Error clearing internal storage",
|
||||
"storage_usage": "Storage Usage",
|
||||
"storage_usage_desc": "Downloaded files are stored on your device for faster access",
|
||||
"storage_used": "Storage Used",
|
||||
"thumbnails": "Thumbnails",
|
||||
"pdfs": "PDF Documents",
|
||||
"other": "Other Files",
|
||||
"clear_cache": "Clear Cache",
|
||||
"clear_cache_desc": "Are you sure you want to delete all downloaded files? Files will be downloaded again when needed.",
|
||||
"connecting_to_server": "Connecting to server...",
|
||||
"server_unreachable": "Server unreachable",
|
||||
"processing_data": "Processing data...",
|
||||
"processed_items": "Processed {} of {} items",
|
||||
"sync_complete": "Sync completed",
|
||||
"sync_error": "Sync error",
|
||||
"sync_title": "Synchronization",
|
||||
"sync_in_progress": "Synchronization in progress",
|
||||
"open_app": "Open app",
|
||||
"no_new_data": "No new data",
|
||||
"internal": "Internal",
|
||||
"internal_desc": "Internal storage of the application",
|
||||
"internal_clear": "Clear internal storage",
|
||||
"internal_clear_desc": "Clear the internal storage of the application",
|
||||
"internal_clear_confirm": "Are you sure you want to clear the internal storage of the application?",
|
||||
"internal_clear_success": "Internal storage cleared successfully",
|
||||
"internal_clear_error": "Error clearing internal storage",
|
||||
"checking_database": "Checking database...",
|
||||
"syncing_search_index": "Syncing search index...",
|
||||
"updating_search_index": "Updating search index...",
|
||||
"search_index_updated": "Search index updated",
|
||||
"download_manager": "Download Manager",
|
||||
"search_files": "Search files",
|
||||
"select_language": "Select language",
|
||||
"all_languages": "All languages",
|
||||
"all": "All",
|
||||
"images": "Images",
|
||||
"files_found": "files found",
|
||||
"no_files_found": "No downloaded files found",
|
||||
"no_pdfs_found": "No PDF documents found",
|
||||
"confirm_delete": "Confirm deletion",
|
||||
"delete_file_confirmation": "Are you sure you want to delete the file {0}?",
|
||||
"delete": "Delete"
|
||||
}
|
||||
|
|
@ -0,0 +1,146 @@
|
|||
{
|
||||
"title": "Estudios\nBíblicos",
|
||||
"version": "Versión",
|
||||
"downloading_data": "Descargando datos...",
|
||||
"updating_data": "Actualizando base de datos...",
|
||||
"success_updating_data": "Datos actualizados correctamente",
|
||||
"welcome": "Inicio",
|
||||
"home": "Estudios Bíblicos",
|
||||
"search": "Búsqueda",
|
||||
"searching": "Buscando",
|
||||
"searching_in_progress": "Buscando...",
|
||||
"loading_results": "Cargando resultados...",
|
||||
"loading_more": "Cargando más...",
|
||||
"try_different_search": "Intenta con otra búsqueda",
|
||||
"search_placeholder": "Buscar...",
|
||||
"image_saved": "Imagen guardada",
|
||||
"image_saved_desc": "Por favor, revisa tu galería",
|
||||
"error_saving_image": "Error al guardar la imagen",
|
||||
"error_saving_image_desc": "Por favor, reintenta más tarde",
|
||||
"results": {
|
||||
"one": "Resultado",
|
||||
"other": "Resultados"
|
||||
},
|
||||
"results_found": {
|
||||
"one": "Resultado encontrado",
|
||||
"other": "Resultados encontrados"
|
||||
},
|
||||
"calendar": "Por fecha",
|
||||
"empty_search_term": "Por favor, introduzca un término de búsqueda",
|
||||
"empty_results": "No se encontraron resultados",
|
||||
"library": "Biblioteca",
|
||||
"config": "Ajustes",
|
||||
"downloaded": "Descargados",
|
||||
"available": "Disponibles",
|
||||
"about": "Acerca de",
|
||||
"settings": "Ajustes",
|
||||
"lang": "Idioma",
|
||||
"theme": "Tema",
|
||||
"system": "Sistema",
|
||||
"light": "Claro",
|
||||
"dark": "Oscuro",
|
||||
"auto": "Automático",
|
||||
"hide_thumbnails": "Ocultar portadas",
|
||||
"download_pdf": "Descargar PDF",
|
||||
"pdf_download": "Descarga automática de PDF",
|
||||
"pdf_download_desc": "Al acceder a un PDF no disponible, se descargará automáticamente",
|
||||
"hd_thumbnails_desc": "Sólo se descargarán al entrar a un estudio específico",
|
||||
"hd_thumbnails": "Portadas en alta calidad",
|
||||
"locale": "Idioma",
|
||||
"confirm_locale_change": "Confirmar cambio de idioma",
|
||||
"confirm_locale_change_desc": "¿Está seguro que desea cambiar de idioma?",
|
||||
"no": "No",
|
||||
"yes": "Sí",
|
||||
"cancel": "Cancelar",
|
||||
"accept": "Aceptar",
|
||||
"last_activities": "Últimas actividades",
|
||||
"recent_text": "Textos recientes",
|
||||
"no_text": "No hay transcripción disponible",
|
||||
"draft": "Borrador",
|
||||
"from": "de",
|
||||
"full_screen": "Pantalla completa",
|
||||
"activity": {
|
||||
"one": "Actividad",
|
||||
"other": "Actividades"
|
||||
},
|
||||
"back": "Volver",
|
||||
"favorite": "Favorito",
|
||||
"year": {
|
||||
"one": "Año",
|
||||
"other": "Años"
|
||||
},
|
||||
"month": {
|
||||
"one": "Mes",
|
||||
"other": "Meses"
|
||||
},
|
||||
"day": {
|
||||
"one": "Día",
|
||||
"other": "Días"
|
||||
},
|
||||
"hour": {
|
||||
"one": "Hora",
|
||||
"other": "Horas"
|
||||
},
|
||||
"minute": {
|
||||
"one": "Minuto",
|
||||
"other": "Minutos"
|
||||
},
|
||||
"filter": "Filtrar",
|
||||
"clear_filters": "Limpiar filtros",
|
||||
"select_year": "Seleccionar año",
|
||||
"select_month": "Seleccionar mes",
|
||||
"select_year_first": "Primero selecciona un año",
|
||||
"no_months_available": "No hay meses disponibles",
|
||||
"no_activities_for_period": "No hay actividades para este período",
|
||||
"clear": "Limpiar",
|
||||
"apply": "Aplicar",
|
||||
"cache_storage": "Almacenamiento interno",
|
||||
"cache_storage_desc": "Almacenamiento interno de la aplicación",
|
||||
"cache_storage_clear": "Limpiar almacenamiento interno",
|
||||
"cache_storage_clear_desc": "Limpiar el almacenamiento interno de la aplicación",
|
||||
"cache_storage_clear_confirm": "¿Está seguro que desea limpiar el almacenamiento interno de la aplicación?",
|
||||
"cache_storage_clear_success": "Almacenamiento interno limpiado correctamente",
|
||||
"cache_storage_clear_error": "Error al limpiar el almacenamiento interno",
|
||||
"storage_usage": "Uso de almacenamiento",
|
||||
"storage_usage_desc": "Los archivos descargados se almacenan en tu dispositivo para un acceso más rápido",
|
||||
"storage_used": "Almacenamiento usado",
|
||||
"thumbnails": "Portadas",
|
||||
"pdfs": "Documentos PDF",
|
||||
"other": "Otros archivos",
|
||||
"clear_cache": "Limpiar caché",
|
||||
"clear_cache_desc": "¿Estás seguro de que deseas eliminar todos los archivos descargados? Los archivos se volverán a descargar cuando sean necesarios.",
|
||||
"connecting_to_server": "Conectando al servidor...",
|
||||
"server_unreachable": "Servidor no accesible",
|
||||
"processing_data": "Procesando datos...",
|
||||
"processed_items": "Procesados {} de {} elementos",
|
||||
"sync_complete": "Sincronización completada",
|
||||
"sync_error": "Error de sincronización",
|
||||
"sync_title": "Sincronización",
|
||||
"sync_in_progress": "Sincronización en progreso",
|
||||
"open_app": "Abrir aplicación",
|
||||
"no_new_data": "No hay datos nuevos",
|
||||
"internal": "Interno",
|
||||
"internal_desc": "Almacenamiento interno de la aplicación",
|
||||
"internal_clear": "Limpiar almacenamiento interno",
|
||||
"internal_clear_desc": "Limpiar el almacenamiento interno de la aplicación",
|
||||
"internal_clear_confirm": "¿Está seguro que desea limpiar el almacenamiento interno de la aplicación?",
|
||||
"internal_clear_success": "Almacenamiento interno limpiado correctamente",
|
||||
"internal_clear_error": "Error al limpiar el almacenamiento interno",
|
||||
"checking_database": "Verificando base de datos...",
|
||||
"syncing_search_index": "Sincronizando índice de búsqueda...",
|
||||
"updating_search_index": "Actualizando índice de búsqueda...",
|
||||
"search_index_updated": "Índice de búsqueda actualizado",
|
||||
"download_manager": "Gestor de descargas",
|
||||
"search_files": "Buscar archivos",
|
||||
"select_language": "Seleccionar idioma",
|
||||
"all_languages": "Todos los idiomas",
|
||||
"all": "Todos",
|
||||
"images": "Imágenes",
|
||||
"files_found": "archivos encontrados",
|
||||
"no_files_found": "No se encontraron archivos descargados",
|
||||
"no_pdfs_found": "No se encontraron documentos PDF",
|
||||
"confirm_delete": "Confirmar eliminación",
|
||||
"delete_file_confirmation": "¿Estás seguro de querer eliminar el archivo {0}?",
|
||||
"delete": "Eliminar",
|
||||
"syncronize": "Sincronizar"
|
||||
}
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
{
|
||||
"title": "Études\nBibliques",
|
||||
"version": "Version",
|
||||
"downloading_data": "Téléchargement des données...",
|
||||
"updating_data": "Mise à jour de la base de données...",
|
||||
"success_updating_data": "Données mises à jour avec succès",
|
||||
"welcome": "Bienvenue",
|
||||
"home": "Études Biblques",
|
||||
"search": "Recherche",
|
||||
"searching": "Recherche",
|
||||
"searching_in_progress": "Recherche en cours...",
|
||||
"loading_results": "Chargement des résultats...",
|
||||
"loading_more": "Chargement...",
|
||||
"try_different_search": "Essayez une recherche différente",
|
||||
"search_placeholder": "Rechercher...",
|
||||
"image_saved": "Image enregistrée",
|
||||
"image_saved_desc": "Veuillez vérifier votre galerie",
|
||||
"error_saving_image": "Erreur lors de l'enregistrement de l'image",
|
||||
"error_saving_image_desc": "Veuillez réessayer plus tard",
|
||||
"results": {
|
||||
"one": "Résultat",
|
||||
"other": "Résultats"
|
||||
},
|
||||
"results_found": {
|
||||
"one": "Résultat trouvé",
|
||||
"other": "Résultats trouvés"
|
||||
},
|
||||
"calendar": "Par date",
|
||||
"empty_search_term": "Veuillez entrer un terme de recherche",
|
||||
"empty_results": "Aucun résultat trouvé",
|
||||
"library": "Bibliothèque",
|
||||
"config": "Paramètres",
|
||||
"downloaded": "Téléchargés",
|
||||
"available": "Disponibles",
|
||||
"about": "À propos",
|
||||
"settings": "Paramètres",
|
||||
"lang": "Langue",
|
||||
"theme": "Thème",
|
||||
"system": "Système",
|
||||
"light": "Clair",
|
||||
"dark": "Sombre",
|
||||
"auto": "Automatique",
|
||||
"hide_thumbnails": "Cacher les vignettes",
|
||||
"download_pdf": "Télécharger le PDF",
|
||||
"pdf_download": "Téléchargement automatique du PDF",
|
||||
"pdf_download_desc": "Lors de l'accès à un PDF non disponible, il sera téléchargé automatiquement",
|
||||
"hd_thumbnails_desc": "Elles ne seront téléchargées que lors de l'accès à une étude spécifique",
|
||||
"hd_thumbnails": "Vignettes en haute qualité",
|
||||
"locale": "Langue",
|
||||
"confirm_locale_change": "Confirmer le changement de langue",
|
||||
"confirm_locale_change_desc": "Êtes-vous sûr de vouloir changer de langue?",
|
||||
"no": "Non",
|
||||
"yes": "Oui",
|
||||
"cancel": "Annuler",
|
||||
"accept": "Accepter",
|
||||
"last_activities": "Dernières activités",
|
||||
"recent_text": "Textes récents",
|
||||
"no_text": "Aucune transcription disponible",
|
||||
"draft": "Brouillon",
|
||||
"from": "de",
|
||||
"full_screen": "Plein écran",
|
||||
"activity": {
|
||||
"one": "Activité",
|
||||
"other": "Activités"
|
||||
},
|
||||
"back": "Retour",
|
||||
"favorite": "Favori",
|
||||
"year": {
|
||||
"one": "An",
|
||||
"other": "Ans"
|
||||
},
|
||||
"month": {
|
||||
"one": "Mois",
|
||||
"other": "Mois"
|
||||
},
|
||||
"day": {
|
||||
"one": "Jour",
|
||||
"other": "Jours"
|
||||
},
|
||||
"hour": {
|
||||
"one": "Heure",
|
||||
"other": "Heures"
|
||||
},
|
||||
"minute": {
|
||||
"one": "Minute",
|
||||
"other": "Minutes"
|
||||
},
|
||||
"checking_database": "Vérification de la base de données...",
|
||||
"syncing_search_index": "Synchronisation de l'index de recherche...",
|
||||
"updating_search_index": "Mise à jour de l'index de recherche...",
|
||||
"search_index_updated": "Index de recherche mis à jour",
|
||||
"download_manager": "Gestionnaire de téléchargements",
|
||||
"search_files": "Rechercher des fichiers",
|
||||
"select_language": "Sélectionner la langue",
|
||||
"all_languages": "Toutes les langues",
|
||||
"all": "Tous",
|
||||
"images": "Images",
|
||||
"files_found": "fichiers trouvés",
|
||||
"no_files_found": "Aucun fichier téléchargé trouvé",
|
||||
"no_pdfs_found": "Aucun document PDF trouvé",
|
||||
"confirm_delete": "Confirmer la suppression",
|
||||
"delete_file_confirmation": "Êtes-vous sûr de vouloir supprimer le fichier {0}?",
|
||||
"delete": "Supprimer"
|
||||
}
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
{
|
||||
"title": "Estudos\nBíblicos",
|
||||
"version": "Versão",
|
||||
"downloading_data": "Descarregando dados...",
|
||||
"updating_data": "Atualizando base de dados...",
|
||||
"success_updating_data": "Dados atualizados com sucesso",
|
||||
"welcome": "Início",
|
||||
"home": "Estudos Bíblicos",
|
||||
"search": "Pesquisa",
|
||||
"searching": "Pesquisando",
|
||||
"searching_in_progress": "Pesquisando...",
|
||||
"loading_results": "Carregando resultados...",
|
||||
"loading_more": "Carregando mais...",
|
||||
"try_different_search": "Tente uma pesquisa diferente",
|
||||
"search_placeholder": "Pesquisar...",
|
||||
"image_saved": "Imagem salva",
|
||||
"image_saved_desc": "Por favor, verifique sua galeria",
|
||||
"error_saving_image": "Erro ao salvar a imagem",
|
||||
"error_saving_image_desc": "Por favor, tente novamente mais tarde",
|
||||
"results": {
|
||||
"one": "Resultado",
|
||||
"other": "Resultados"
|
||||
},
|
||||
"results_found": {
|
||||
"one": "Resultado encontrado",
|
||||
"other": "Resultados encontrados"
|
||||
},
|
||||
"calendar": "Por data",
|
||||
"empty_search_term": "Por favor, introduza un termo de pesquisa",
|
||||
"empty_results": "Não foram encontrados resultados",
|
||||
"library": "Biblioteca",
|
||||
"config": "Configuração",
|
||||
"downloaded": "Baixados",
|
||||
"available": "Disponíveis",
|
||||
"about": "Sobre",
|
||||
"settings": "Ajustes",
|
||||
"lang": "Idioma",
|
||||
"theme": "Tema",
|
||||
"system": "Sistema",
|
||||
"light": "Claro",
|
||||
"dark": "Escuro",
|
||||
"auto": "Automático",
|
||||
"hide_thumbnails": "Ocultar miniaturas",
|
||||
"download_pdf": "Baixar PDF",
|
||||
"pdf_download": "Baixar automáticamente PDF",
|
||||
"pdf_download_desc": "Ao acessar um PDF não disponivel, sera baixado automáticamente",
|
||||
"hd_thumbnails_desc": "Somente serão baixados ao entrar em um estudo específico",
|
||||
"hd_thumbnails": "Miniaturas em alta qualidade",
|
||||
"locale": "Idioma",
|
||||
"confirm_locale_change": "Conferir mudança de idioma",
|
||||
"confirm_locale_change_desc": "Está seguro que deseja mudar o idioma?",
|
||||
"no": "Não",
|
||||
"yes": "Sim",
|
||||
"cancel": "Cancelar",
|
||||
"accept": "Aceitar",
|
||||
"last_activities": "Últimas atividades",
|
||||
"recent_text": "Textos recentes",
|
||||
"draft": "Rascunho",
|
||||
"no_text": "Nenhuma transcrição disponível",
|
||||
"from": "de",
|
||||
"full_screen": "Tela cheia",
|
||||
"activity": {
|
||||
"one": "Atividade",
|
||||
"other": "Atividades"
|
||||
},
|
||||
"back": "Voltar",
|
||||
"favorite": "Favorito",
|
||||
"year": {
|
||||
"one": "Ano",
|
||||
"other": "Anos"
|
||||
},
|
||||
"month": {
|
||||
"one": "Mes",
|
||||
"other": "Meses"
|
||||
},
|
||||
"day": {
|
||||
"one": "Dia",
|
||||
"other": "Dias"
|
||||
},
|
||||
"hour": {
|
||||
"one": "Hora",
|
||||
"other": "Horas"
|
||||
},
|
||||
"minute": {
|
||||
"one": "Minuto",
|
||||
"other": "Minutos"
|
||||
},
|
||||
"checking_database": "Verificando base de dados...",
|
||||
"syncing_search_index": "Sincronizando índice de pesquisa...",
|
||||
"updating_search_index": "Atualizando índice de pesquisa...",
|
||||
"search_index_updated": "Índice de pesquisa atualizado",
|
||||
"download_manager": "Gerenciador de downloads",
|
||||
"search_files": "Pesquisar arquivos",
|
||||
"select_language": "Selecionar idioma",
|
||||
"all_languages": "Todos os idiomas",
|
||||
"all": "Todos",
|
||||
"images": "Imagens",
|
||||
"files_found": "arquivos encontrados",
|
||||
"no_files_found": "Nenhum arquivo baixado encontrado",
|
||||
"no_pdfs_found": "Nenhum documento PDF encontrado",
|
||||
"confirm_delete": "Confirmar exclusão",
|
||||
"delete_file_confirmation": "Tem certeza que deseja excluir o arquivo {0}?",
|
||||
"delete": "Excluir"
|
||||
}
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
{
|
||||
"title": "Amasomo\nYa Bibiliya",
|
||||
"version": "Icyiciro",
|
||||
"downloading_data": "Kurimo gukuramo amakuru...",
|
||||
"updating_data": "Gukurura amakuru mashya...",
|
||||
"success_updating_data": "Amakuru yakuru kugirango",
|
||||
"welcome": "Ahabanza",
|
||||
"home": "Amasomo ya Bibiliya",
|
||||
"search": "Shakisha",
|
||||
"searching": "Kurimo gushakisha",
|
||||
"searching_in_progress": "Kurimo gushakisha...",
|
||||
"loading_results": "Kurimo gutaha ibisubizo...",
|
||||
"loading_more": "Kurimo gutaha ibindi...",
|
||||
"try_different_search": "Gerageza gushakisha mu bundi buryo",
|
||||
"search_placeholder": "Shakisha...",
|
||||
"image_saved": "Imaji yeguruka",
|
||||
"image_saved_desc": "Tafadhali, angalia kwenye galeri",
|
||||
"error_saving_image": "Hitilafu ili kushuka imaji",
|
||||
"error_saving_image_desc": "Tafadhali, jaribu tena baadaye",
|
||||
"results": {
|
||||
"one": "Ibisubizo",
|
||||
"other": "Ibisubizo"
|
||||
},
|
||||
"results_found": {
|
||||
"one": "Ibisubizo byabonetse",
|
||||
"other": "Ibisubizo byabonetse"
|
||||
},
|
||||
"calendar": "Ku itariki",
|
||||
"empty_search_term": "Nyamuneka, shyiramo ijambo ryo gushakisha",
|
||||
"empty_results": "Nta bisubizo byabonetse",
|
||||
"library": "Isomero",
|
||||
"config": "Amashusho",
|
||||
"downloaded": "Kuramo",
|
||||
"available": "Igihe",
|
||||
"about": "Ibyerekeye",
|
||||
"settings": "Amashusho",
|
||||
"lang": "Ururimi",
|
||||
"theme": "Insanganyamatsiko",
|
||||
"system": "Sisitemu",
|
||||
"light": "Urumuri",
|
||||
"dark": "Umwijima",
|
||||
"auto": "Byikora",
|
||||
"hide_thumbnails": "Guhisha uturemereke",
|
||||
"download_pdf": "Kuramo PDF",
|
||||
"pdf_download": "Kuramo PDF byikora",
|
||||
"pdf_download_desc": "Iyo ugeze kuri PDF itaboneka, irahita ikurwamo byikora",
|
||||
"hd_thumbnails_desc": "Bizakururwa gusa igihe winjiye mu isomo ryihariye",
|
||||
"hd_thumbnails": "Uturere two mu bwiza bwo hejuru",
|
||||
"locale": "Ururimi",
|
||||
"confirm_locale_change": "Emeza guhindura ururimi",
|
||||
"confirm_locale_change_desc": "Urashaka guhindura ururimi koko?",
|
||||
"no": "Oya",
|
||||
"yes": "Yego",
|
||||
"cancel": "Gusubika",
|
||||
"accept": "Emera",
|
||||
"last_activities": "Ibikorwa biheruka",
|
||||
"recent_text": "Inyandiko ziheruka",
|
||||
"draft": "Ibyo nkunda",
|
||||
"no_text": "Ibyo nkunda kuri kugirango",
|
||||
"from": "ku",
|
||||
"full_screen": "Ibikorwa byose",
|
||||
"activity": {
|
||||
"one": "Igikorwa",
|
||||
"other": "Ibikorwa"
|
||||
},
|
||||
"back": "Subira inyuma",
|
||||
"favorite": "Ibyo nkunda",
|
||||
"year": {
|
||||
"one": "Umwaka",
|
||||
"other": "Imyaka"
|
||||
},
|
||||
"month": {
|
||||
"one": "Ukwezi",
|
||||
"other": "Amezi"
|
||||
},
|
||||
"day": {
|
||||
"one": "Umunsi",
|
||||
"other": "Iminsi"
|
||||
},
|
||||
"hour": {
|
||||
"one": "Isaha",
|
||||
"other": "Amasaha"
|
||||
},
|
||||
"minute": {
|
||||
"one": "Umunota",
|
||||
"other": "Iminota"
|
||||
},
|
||||
"checking_database": "Kurimo kugenzura ububiko bw'amakuru...",
|
||||
"syncing_search_index": "Kurimo guhuza ibipimo byo gushakisha...",
|
||||
"updating_search_index": "Kurimo kuvugurura ibipimo byo gushakisha...",
|
||||
"search_index_updated": "Ibipimo byo gushakisha byavuguruwe",
|
||||
|
||||
"download_manager": "Umucungamadosiye",
|
||||
"search_files": "Shakisha dosiye",
|
||||
"select_language": "Hitamo ururimi",
|
||||
"all_languages": "Indimi zose",
|
||||
"all": "Byose",
|
||||
"images": "Amashusho",
|
||||
"files_found": "dosiye zabonetse",
|
||||
"no_files_found": "Nta dosiye zakuruwe zabonetse",
|
||||
"no_pdfs_found": "Nta nyandiko za PDF zabonetse",
|
||||
"confirm_delete": "Emeza gukuraho",
|
||||
"delete_file_confirmation": "Uremeza ko ushaka gukuraho dosiye {0}?",
|
||||
"delete": "Kuraho"
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="230px" height="43px" viewBox="0 0 227 43" version="1.1">
|
||||
<g id="surface1">
|
||||
<path style=" stroke:none;fill-rule:evenodd;fill:rgb(92.941176%,10.980392%,14.117647%);fill-opacity:1;" d="M 88.472656 25.492188 C 79.652344 22.367188 63.421875 23.441406 59.117188 25.472656 C 77.457031 13.191406 100.925781 2.789062 128.515625 0.34375 C 134.09375 -0.0976562 141.601562 -0.144531 148.871094 0.402344 C 183.085938 2.96875 210.519531 21.003906 226.96875 43 C 211.910156 29.101562 187.027344 10.214844 147.359375 10.214844 C 116.78125 10.214844 93.78125 22.441406 88.472656 25.492188 Z M 88.472656 25.492188 "/>
|
||||
<path style=" stroke:none;fill-rule:evenodd;fill:rgb(17.647059%,30.588235%,63.529412%);fill-opacity:1;" d="M 29.234375 25.472656 C 20.65625 22.078125 4.144531 23.65625 0.199219 25.417969 C 22.53125 9.53125 54.453125 -1.191406 101.210938 0.511719 C 57.046875 3.617188 32.789062 23.152344 29.234375 25.472656 Z M 29.234375 25.472656 "/>
|
||||
<path style=" stroke:none;fill-rule:evenodd;fill:rgb(100%,100%,100%);fill-opacity:1;" d="M 59.117188 25.535156 C 50.535156 22.140625 33.179688 23.710938 29.234375 25.472656 C 51.570312 9.585938 79.882812 -2.050781 128.960938 0.472656 C 94.257812 2.949219 69.058594 19.316406 59.117188 25.535156 Z M 59.117188 25.535156 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(0%,0%,0%);fill-opacity:1;" d="M 0.03125 27.792969 L 2.445312 27.792969 L 2.445312 35.066406 C 2.445312 35.597656 2.527344 36.039062 2.691406 36.390625 C 2.851562 36.742188 3.074219 37.027344 3.355469 37.242188 C 3.636719 37.457031 3.96875 37.605469 4.34375 37.699219 C 4.726562 37.792969 5.128906 37.835938 5.550781 37.835938 L 8.65625 37.835938 L 8.65625 39.914062 L 5.550781 39.914062 C 4.792969 39.914062 4.078125 39.835938 3.410156 39.671875 C 2.746094 39.511719 2.160156 39.238281 1.660156 38.855469 C 1.160156 38.476562 0.761719 37.976562 0.46875 37.359375 C 0.175781 36.742188 0.03125 35.976562 0.03125 35.066406 Z M 0.03125 27.792969 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(0%,0%,0%);fill-opacity:1;" d="M 12.164062 31.273438 L 14.019531 31.273438 L 17.855469 39.914062 L 15.9375 39.914062 L 15.210938 38.1875 L 10.972656 38.1875 L 10.246094 39.914062 L 8.324219 39.914062 Z M 14.585938 36.707031 L 13.085938 33.175781 L 11.597656 36.707031 Z M 14.585938 36.707031 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(0%,0%,0%);fill-opacity:1;" d="M 66.410156 39.53125 C 65.785156 39.316406 65.234375 38.972656 64.761719 38.5 C 64.289062 38.027344 63.917969 37.40625 63.648438 36.632812 C 63.378906 35.859375 63.246094 34.914062 63.246094 33.789062 C 63.246094 32.671875 63.378906 31.726562 63.648438 30.949219 C 63.921875 30.175781 64.292969 29.550781 64.761719 29.082031 C 65.234375 28.605469 65.78125 28.261719 66.410156 28.050781 C 67.035156 27.835938 67.707031 27.730469 68.417969 27.730469 L 72.90625 27.730469 L 72.90625 29.808594 L 68.765625 29.808594 C 68.339844 29.808594 67.9375 29.871094 67.558594 29.996094 C 67.175781 30.121094 66.847656 30.339844 66.566406 30.644531 C 66.285156 30.949219 66.0625 31.355469 65.902344 31.867188 C 65.742188 32.375 65.660156 33.015625 65.660156 33.789062 C 65.660156 34.558594 65.738281 35.203125 65.902344 35.707031 C 66.0625 36.214844 66.285156 36.621094 66.566406 36.929688 C 66.847656 37.234375 67.179688 37.453125 67.558594 37.578125 C 67.9375 37.707031 68.339844 37.765625 68.765625 37.765625 L 72.90625 37.765625 L 72.90625 39.84375 L 68.417969 39.84375 C 67.707031 39.851562 67.035156 39.746094 66.410156 39.53125 Z M 66.410156 39.53125 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(0%,0%,0%);fill-opacity:1;" d="M 77.234375 30.476562 L 79.25 30.476562 L 83.414062 39.851562 L 81.332031 39.851562 L 80.546875 37.976562 L 75.941406 37.976562 L 75.15625 39.851562 L 73.070312 39.851562 Z M 79.863281 36.367188 L 78.238281 32.539062 L 76.625 36.367188 Z M 79.863281 36.367188 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(0%,0%,0%);fill-opacity:1;" d="M 83.609375 30.476562 L 89.148438 30.476562 C 89.554688 30.476562 89.941406 30.527344 90.308594 30.625 C 90.671875 30.726562 90.992188 30.882812 91.269531 31.101562 C 91.542969 31.320312 91.761719 31.609375 91.917969 31.96875 C 92.074219 32.332031 92.152344 32.769531 92.152344 33.289062 C 92.152344 33.742188 92.097656 34.136719 91.992188 34.472656 C 91.886719 34.808594 91.734375 35.09375 91.542969 35.324219 C 91.351562 35.554688 91.125 35.738281 90.863281 35.871094 C 90.601562 36.007812 90.316406 36.101562 90.015625 36.167969 L 92.136719 39.847656 L 89.976562 39.847656 L 88 36.234375 L 85.480469 36.234375 L 85.480469 39.851562 L 83.609375 39.851562 Z M 89.148438 34.628906 C 89.457031 34.628906 89.726562 34.53125 89.949219 34.339844 C 90.171875 34.148438 90.28125 33.796875 90.28125 33.289062 C 90.28125 32.839844 90.171875 32.527344 89.949219 32.351562 C 89.726562 32.171875 89.460938 32.085938 89.148438 32.085938 L 85.480469 32.085938 L 85.480469 34.632812 L 89.148438 34.632812 Z M 89.148438 34.628906 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(0%,0%,0%);fill-opacity:1;" d="M 93.21875 30.476562 L 98.621094 30.476562 C 99.046875 30.476562 99.449219 30.53125 99.835938 30.640625 C 100.21875 30.746094 100.550781 30.921875 100.835938 31.160156 C 101.117188 31.402344 101.34375 31.71875 101.507812 32.113281 C 101.671875 32.507812 101.753906 32.988281 101.753906 33.558594 C 101.753906 34.175781 101.671875 34.6875 101.507812 35.097656 C 101.34375 35.507812 101.117188 35.839844 100.835938 36.089844 C 100.550781 36.339844 100.214844 36.515625 99.835938 36.617188 C 99.449219 36.722656 99.046875 36.773438 98.621094 36.773438 L 95.082031 36.773438 L 95.082031 39.855469 L 93.214844 39.855469 L 93.214844 30.476562 Z M 98.621094 35.164062 C 98.789062 35.164062 98.953125 35.136719 99.105469 35.082031 C 99.261719 35.027344 99.398438 34.941406 99.511719 34.824219 C 99.625 34.703125 99.71875 34.542969 99.785156 34.335938 C 99.851562 34.128906 99.886719 33.871094 99.886719 33.558594 C 99.886719 33.023438 99.761719 32.644531 99.511719 32.421875 C 99.261719 32.195312 98.964844 32.089844 98.617188 32.089844 L 95.082031 32.089844 L 95.082031 35.167969 L 98.621094 35.167969 Z M 98.621094 35.164062 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(0%,0%,0%);fill-opacity:1;" d="M 104.8125 30.476562 L 106.828125 30.476562 L 110.992188 39.851562 L 108.910156 39.851562 L 108.125 37.976562 L 103.519531 37.976562 L 102.734375 39.851562 L 100.652344 39.851562 Z M 107.441406 36.367188 L 105.816406 32.539062 L 104.203125 36.367188 Z M 107.441406 36.367188 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(0%,0%,0%);fill-opacity:1;" d="M 116.734375 39.636719 C 116.109375 39.421875 115.558594 39.078125 115.085938 38.605469 C 114.613281 38.132812 114.242188 37.511719 113.972656 36.738281 C 113.703125 35.964844 113.570312 35.019531 113.570312 33.898438 C 113.570312 32.777344 113.703125 31.832031 113.972656 31.054688 C 114.246094 30.28125 114.617188 29.65625 115.085938 29.1875 C 115.558594 28.710938 116.105469 28.367188 116.734375 28.15625 C 117.359375 27.941406 118.03125 27.835938 118.746094 27.835938 L 123.230469 27.835938 L 123.230469 29.914062 L 119.089844 29.914062 C 118.664062 29.914062 118.261719 29.976562 117.882812 30.101562 C 117.5 30.230469 117.171875 30.445312 116.886719 30.75 C 116.605469 31.054688 116.382812 31.460938 116.222656 31.972656 C 116.0625 32.480469 115.980469 33.121094 115.980469 33.894531 C 115.980469 34.667969 116.0625 35.308594 116.222656 35.8125 C 116.382812 36.320312 116.605469 36.726562 116.886719 37.035156 C 117.167969 37.339844 117.5 37.558594 117.882812 37.683594 C 118.261719 37.8125 118.664062 37.871094 119.089844 37.871094 L 123.230469 37.871094 L 123.230469 39.949219 L 118.746094 39.949219 C 118.027344 39.957031 117.359375 39.851562 116.734375 39.636719 Z M 116.734375 39.636719 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(0%,0%,0%);fill-opacity:1;" d="M 127.554688 30.585938 L 129.570312 30.585938 L 133.734375 39.957031 L 131.652344 39.957031 L 130.867188 38.082031 L 126.261719 38.082031 L 125.476562 39.957031 L 123.394531 39.957031 Z M 130.183594 36.476562 L 128.558594 32.644531 L 126.945312 36.476562 Z M 130.183594 36.476562 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(0%,0%,0%);fill-opacity:1;" d="M 135.292969 32.191406 L 131.957031 32.191406 L 131.957031 30.585938 L 140.496094 30.585938 L 140.496094 32.191406 L 137.164062 32.191406 L 137.164062 39.957031 L 135.292969 39.957031 Z M 135.292969 32.191406 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(0%,0%,0%);fill-opacity:1;" d="M 142.957031 39.710938 C 142.472656 39.546875 142.046875 39.28125 141.683594 38.910156 C 141.320312 38.546875 141.03125 38.066406 140.824219 37.464844 C 140.617188 36.867188 140.511719 36.132812 140.511719 35.269531 C 140.511719 34.402344 140.617188 33.671875 140.824219 33.074219 C 141.03125 32.472656 141.320312 31.992188 141.683594 31.625 C 142.046875 31.261719 142.472656 30.996094 142.957031 30.832031 C 143.441406 30.664062 143.960938 30.585938 144.511719 30.585938 L 147.984375 30.585938 L 147.984375 32.191406 L 144.78125 32.191406 C 144.488281 32.191406 144.214844 32.230469 143.953125 32.304688 C 143.695312 32.382812 143.460938 32.507812 143.253906 32.679688 C 143.046875 32.855469 142.867188 33.089844 142.722656 33.378906 C 142.574219 33.667969 142.476562 34.03125 142.421875 34.46875 L 147.984375 34.46875 L 147.984375 36.074219 L 142.421875 36.074219 C 142.476562 36.511719 142.574219 36.875 142.722656 37.164062 C 142.867188 37.457031 143.046875 37.6875 143.253906 37.859375 C 143.460938 38.03125 143.695312 38.15625 143.953125 38.234375 C 144.214844 38.3125 144.488281 38.347656 144.78125 38.347656 L 147.984375 38.347656 L 147.984375 39.953125 L 144.511719 39.953125 C 143.960938 39.957031 143.441406 39.875 142.957031 39.710938 Z M 142.957031 39.710938 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(0%,0%,0%);fill-opacity:1;" d="M 149.25 30.585938 L 153.789062 30.585938 C 154.339844 30.585938 154.859375 30.667969 155.34375 30.832031 C 155.828125 30.996094 156.253906 31.261719 156.617188 31.625 C 156.984375 31.992188 157.269531 32.476562 157.476562 33.074219 C 157.6875 33.671875 157.792969 34.402344 157.792969 35.269531 C 157.792969 36.132812 157.6875 36.867188 157.476562 37.464844 C 157.265625 38.066406 156.984375 38.546875 156.617188 38.910156 C 156.253906 39.277344 155.828125 39.542969 155.34375 39.710938 C 154.859375 39.875 154.339844 39.957031 153.789062 39.957031 L 149.25 39.957031 Z M 153.519531 38.351562 C 153.847656 38.351562 154.160156 38.300781 154.453125 38.203125 C 154.746094 38.105469 155.003906 37.9375 155.21875 37.699219 C 155.4375 37.460938 155.609375 37.148438 155.734375 36.757812 C 155.859375 36.367188 155.917969 35.867188 155.917969 35.269531 C 155.917969 34.667969 155.855469 34.175781 155.734375 33.78125 C 155.609375 33.390625 155.4375 33.074219 155.21875 32.839844 C 155.003906 32.601562 154.746094 32.433594 154.453125 32.335938 C 154.160156 32.234375 153.847656 32.1875 153.519531 32.1875 L 151.117188 32.1875 L 151.117188 38.347656 L 153.519531 38.347656 Z M 153.519531 38.351562 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(0%,0%,0%);fill-opacity:1;" d="M 158.65625 30.585938 L 164.195312 30.585938 C 164.601562 30.585938 164.988281 30.632812 165.351562 30.730469 C 165.71875 30.832031 166.035156 30.988281 166.316406 31.207031 C 166.589844 31.425781 166.808594 31.714844 166.964844 32.074219 C 167.121094 32.4375 167.195312 32.875 167.195312 33.394531 C 167.195312 33.847656 167.140625 34.242188 167.035156 34.578125 C 166.929688 34.914062 166.78125 35.199219 166.589844 35.429688 C 166.398438 35.660156 166.171875 35.84375 165.910156 35.976562 C 165.648438 36.113281 165.363281 36.207031 165.0625 36.273438 L 167.183594 39.953125 L 165.019531 39.953125 L 163.046875 36.335938 L 160.527344 36.335938 L 160.527344 39.953125 L 158.65625 39.953125 Z M 164.195312 34.734375 C 164.503906 34.734375 164.773438 34.636719 164.996094 34.445312 C 165.21875 34.253906 165.328125 33.902344 165.328125 33.394531 C 165.328125 32.949219 165.214844 32.632812 164.996094 32.457031 C 164.773438 32.277344 164.507812 32.191406 164.195312 32.191406 L 160.527344 32.191406 L 160.527344 34.738281 L 164.195312 34.738281 Z M 164.195312 34.734375 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(0%,0%,0%);fill-opacity:1;" d="M 171.359375 30.585938 L 173.375 30.585938 L 177.539062 39.957031 L 175.457031 39.957031 L 174.667969 38.082031 L 170.066406 38.082031 L 169.28125 39.957031 L 167.199219 39.957031 Z M 173.984375 36.476562 L 172.359375 32.644531 L 170.746094 36.476562 Z M 173.984375 36.476562 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(0%,0%,0%);fill-opacity:1;" d="M 177.601562 30.585938 L 179.472656 30.585938 L 179.472656 36.207031 C 179.472656 36.617188 179.535156 36.960938 179.660156 37.230469 C 179.785156 37.503906 179.957031 37.722656 180.175781 37.890625 C 180.390625 38.054688 180.648438 38.171875 180.941406 38.246094 C 181.234375 38.316406 181.542969 38.351562 181.875 38.351562 L 184.277344 38.351562 L 184.277344 39.957031 L 181.875 39.957031 C 181.285156 39.957031 180.734375 39.894531 180.21875 39.769531 C 179.699219 39.644531 179.25 39.4375 178.863281 39.140625 C 178.476562 38.847656 178.171875 38.460938 177.945312 37.984375 C 177.71875 37.507812 177.601562 36.917969 177.601562 36.210938 Z M 177.601562 30.585938 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(0%,0%,0%);fill-opacity:1;" d="M 191.242188 38.832031 C 190.949219 38.730469 190.695312 38.570312 190.476562 38.351562 C 190.257812 38.128906 190.085938 37.839844 189.960938 37.476562 C 189.835938 37.121094 189.769531 36.675781 189.769531 36.152344 C 189.769531 35.632812 189.835938 35.191406 189.960938 34.828125 C 190.085938 34.46875 190.257812 34.179688 190.476562 33.957031 C 190.699219 33.734375 190.953125 33.574219 191.242188 33.476562 C 191.535156 33.378906 191.847656 33.324219 192.179688 33.324219 L 194.269531 33.324219 L 194.269531 34.292969 L 192.339844 34.292969 C 192.140625 34.292969 191.953125 34.324219 191.777344 34.382812 C 191.601562 34.441406 191.445312 34.542969 191.3125 34.683594 C 191.183594 34.828125 191.082031 35.019531 191.003906 35.253906 C 190.925781 35.488281 190.890625 35.789062 190.890625 36.148438 C 190.890625 36.507812 190.929688 36.804688 191.003906 37.042969 C 191.078125 37.28125 191.183594 37.46875 191.3125 37.609375 C 191.445312 37.75 191.597656 37.851562 191.777344 37.910156 C 191.953125 37.96875 192.140625 38 192.339844 38 L 194.269531 38 L 194.269531 38.96875 L 192.179688 38.96875 C 191.847656 38.980469 191.539062 38.929688 191.242188 38.832031 Z M 191.242188 38.832031 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(0%,0%,0%);fill-opacity:1;" d="M 194.914062 36.15625 C 194.914062 35.617188 194.984375 35.152344 195.128906 34.777344 C 195.269531 34.402344 195.464844 34.097656 195.710938 33.867188 C 195.957031 33.636719 196.246094 33.46875 196.578125 33.367188 C 196.910156 33.265625 197.269531 33.214844 197.648438 33.214844 C 198.019531 33.214844 198.371094 33.265625 198.703125 33.367188 C 199.035156 33.46875 199.328125 33.636719 199.574219 33.867188 C 199.824219 34.097656 200.023438 34.402344 200.167969 34.777344 C 200.3125 35.152344 200.382812 35.617188 200.382812 36.15625 C 200.382812 36.699219 200.3125 37.160156 200.167969 37.535156 C 200.023438 37.910156 199.824219 38.214844 199.574219 38.445312 C 199.328125 38.675781 199.035156 38.84375 198.703125 38.945312 C 198.371094 39.050781 198.019531 39.101562 197.648438 39.101562 C 197.269531 39.101562 196.910156 39.050781 196.578125 38.945312 C 196.246094 38.84375 195.957031 38.675781 195.710938 38.445312 C 195.464844 38.214844 195.269531 37.910156 195.128906 37.535156 C 194.984375 37.160156 194.914062 36.699219 194.914062 36.15625 Z M 197.648438 38.132812 C 197.871094 38.132812 198.078125 38.101562 198.273438 38.039062 C 198.46875 37.980469 198.636719 37.871094 198.785156 37.722656 C 198.929688 37.570312 199.042969 37.371094 199.128906 37.113281 C 199.214844 36.855469 199.257812 36.539062 199.257812 36.15625 C 199.257812 35.777344 199.214844 35.453125 199.128906 35.203125 C 199.042969 34.949219 198.925781 34.742188 198.785156 34.59375 C 198.640625 34.441406 198.46875 34.335938 198.273438 34.273438 C 198.078125 34.214844 197.871094 34.183594 197.648438 34.183594 C 197.421875 34.183594 197.214844 34.214844 197.015625 34.273438 C 196.820312 34.335938 196.648438 34.441406 196.503906 34.59375 C 196.363281 34.742188 196.246094 34.945312 196.164062 35.203125 C 196.082031 35.457031 196.039062 35.777344 196.039062 36.15625 C 196.039062 36.539062 196.082031 36.859375 196.164062 37.113281 C 196.246094 37.371094 196.363281 37.570312 196.503906 37.722656 C 196.648438 37.871094 196.820312 37.980469 197.015625 38.039062 C 197.210938 38.105469 197.421875 38.132812 197.648438 38.132812 Z M 197.648438 38.132812 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(0%,0%,0%);fill-opacity:1;" d="M 201.105469 33.332031 L 204.441406 33.332031 C 204.6875 33.332031 204.921875 33.359375 205.140625 33.421875 C 205.363281 33.480469 205.554688 33.574219 205.71875 33.707031 C 205.886719 33.839844 206.015625 34.015625 206.109375 34.234375 C 206.203125 34.453125 206.25 34.71875 206.25 35.027344 C 206.25 35.304688 206.21875 35.542969 206.152344 35.742188 C 206.089844 35.945312 206 36.117188 205.886719 36.257812 C 205.769531 36.398438 205.632812 36.507812 205.476562 36.585938 C 205.316406 36.667969 205.148438 36.726562 204.964844 36.761719 L 206.242188 38.980469 L 204.941406 38.980469 L 203.75 36.804688 L 202.230469 36.804688 L 202.230469 38.980469 L 201.105469 38.980469 Z M 204.441406 35.832031 C 204.628906 35.832031 204.789062 35.777344 204.925781 35.660156 C 205.058594 35.542969 205.125 35.332031 205.125 35.023438 C 205.125 34.757812 205.058594 34.566406 204.925781 34.460938 C 204.789062 34.355469 204.628906 34.300781 204.441406 34.300781 L 202.230469 34.300781 L 202.230469 35.832031 Z M 204.441406 35.832031 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(0%,0%,0%);fill-opacity:1;" d="M 207.097656 33.332031 L 210.351562 33.332031 C 210.609375 33.332031 210.851562 33.363281 211.082031 33.429688 C 211.3125 33.492188 211.515625 33.597656 211.6875 33.742188 C 211.859375 33.886719 211.992188 34.078125 212.09375 34.316406 C 212.191406 34.554688 212.242188 34.84375 212.242188 35.1875 C 212.242188 35.5625 212.191406 35.867188 212.09375 36.117188 C 211.992188 36.363281 211.859375 36.5625 211.6875 36.710938 C 211.515625 36.863281 211.3125 36.96875 211.082031 37.03125 C 210.851562 37.089844 210.609375 37.121094 210.351562 37.121094 L 208.222656 37.121094 L 208.222656 38.980469 L 207.097656 38.980469 Z M 210.351562 36.15625 C 210.453125 36.15625 210.554688 36.140625 210.644531 36.109375 C 210.738281 36.078125 210.820312 36.027344 210.890625 35.953125 C 210.960938 35.878906 211.015625 35.78125 211.058594 35.65625 C 211.097656 35.53125 211.117188 35.378906 211.117188 35.1875 C 211.117188 34.863281 211.042969 34.636719 210.890625 34.503906 C 210.742188 34.367188 210.5625 34.300781 210.351562 34.300781 L 208.222656 34.300781 L 208.222656 36.15625 Z M 210.351562 36.15625 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(0%,0%,0%);fill-opacity:1;" d="M 210.5625 38.113281 L 211.84375 38.113281 L 211.84375 39.398438 L 210.5625 39.398438 Z M 210.5625 38.113281 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(0%,0%,0%);fill-opacity:1;" d="M 23.472656 39.613281 C 22.847656 39.398438 22.296875 39.054688 21.824219 38.585938 C 21.351562 38.109375 20.980469 37.488281 20.710938 36.714844 C 20.441406 35.941406 20.304688 34.996094 20.304688 33.875 C 20.304688 32.753906 20.441406 31.808594 20.710938 31.03125 C 20.984375 30.261719 21.355469 29.636719 21.824219 29.164062 C 22.296875 28.6875 22.84375 28.347656 23.472656 28.132812 C 24.097656 27.917969 24.769531 27.8125 25.480469 27.8125 L 31.347656 27.8125 L 31.347656 29.890625 L 25.828125 29.890625 C 25.40625 29.890625 25.003906 29.957031 24.621094 30.082031 C 24.242188 30.207031 23.914062 30.425781 23.632812 30.726562 C 23.351562 31.03125 23.128906 31.441406 22.96875 31.949219 C 22.808594 32.457031 22.722656 33.097656 22.722656 33.871094 C 22.722656 34.644531 22.804688 35.285156 22.96875 35.792969 C 23.128906 36.296875 23.351562 36.707031 23.632812 37.011719 C 23.914062 37.316406 24.246094 37.535156 24.621094 37.660156 C 25.003906 37.789062 25.40625 37.851562 25.828125 37.851562 L 28.933594 37.851562 L 28.933594 34.90625 L 26.519531 34.90625 L 26.519531 32.828125 L 31.351562 32.828125 L 31.351562 39.929688 L 25.484375 39.929688 C 24.769531 39.933594 24.101562 39.824219 23.472656 39.613281 Z M 23.472656 39.613281 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(0%,0%,0%);fill-opacity:1;" d="M 32.730469 30.820312 L 38.113281 30.820312 C 38.511719 30.820312 38.886719 30.871094 39.242188 30.964844 C 39.597656 31.0625 39.90625 31.214844 40.175781 31.425781 C 40.441406 31.640625 40.652344 31.921875 40.804688 32.273438 C 40.953125 32.625 41.03125 33.054688 41.03125 33.558594 C 41.03125 34 40.980469 34.382812 40.875 34.707031 C 40.773438 35.03125 40.625 35.308594 40.441406 35.535156 C 40.253906 35.757812 40.035156 35.9375 39.777344 36.066406 C 39.523438 36.199219 39.25 36.292969 38.953125 36.351562 L 41.019531 39.933594 L 38.914062 39.933594 L 36.996094 36.421875 L 34.546875 36.421875 L 34.546875 39.933594 L 32.730469 39.933594 Z M 38.113281 34.855469 C 38.417969 34.855469 38.675781 34.761719 38.894531 34.578125 C 39.109375 34.390625 39.214844 34.050781 39.214844 33.554688 C 39.214844 33.121094 39.109375 32.816406 38.894531 32.644531 C 38.675781 32.46875 38.417969 32.382812 38.113281 32.382812 L 34.546875 32.382812 L 34.546875 34.855469 Z M 38.113281 34.855469 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(0%,0%,0%);fill-opacity:1;" d="M 45.078125 30.820312 L 47.035156 30.820312 L 51.085938 39.933594 L 49.0625 39.933594 L 48.296875 38.113281 L 43.820312 38.113281 L 43.054688 39.933594 L 41.03125 39.933594 Z M 47.632812 36.546875 L 46.054688 32.824219 L 44.484375 36.546875 Z M 47.632812 36.546875 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(0%,0%,0%);fill-opacity:1;" d="M 51.28125 30.820312 L 53.613281 30.820312 L 57.246094 37.449219 L 57.246094 30.820312 L 59.0625 30.820312 L 59.0625 39.933594 L 56.726562 39.933594 L 53.09375 33.304688 L 53.09375 39.933594 L 51.28125 39.933594 Z M 51.28125 30.820312 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(0%,0%,0%);fill-opacity:1;" d="M 186.085938 41.8125 L 186.085938 40.605469 L 185.371094 40.605469 L 185.371094 39 L 186.90625 39 L 186.90625 40.164062 C 186.90625 40.515625 186.839844 40.824219 186.707031 41.082031 C 186.578125 41.34375 186.371094 41.589844 186.085938 41.8125 Z M 186.085938 41.8125 "/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 22 KiB |
|
|
@ -0,0 +1,3 @@
|
|||
description: This file stores settings for Dart & Flutter DevTools.
|
||||
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
|
||||
extensions:
|
||||
|
|
@ -0,0 +1,144 @@
|
|||
flutter_native_splash:
|
||||
# This package generates native code to customize Flutter's default white native splash screen
|
||||
# with background color and splash image.
|
||||
# Customize the parameters below, and run the following command in the terminal:
|
||||
# dart run flutter_native_splash:create
|
||||
# To restore Flutter's default white splash screen, run the following command in the terminal:
|
||||
# dart run flutter_native_splash:remove
|
||||
|
||||
# IMPORTANT NOTE: These parameter do not affect the configuration of Android 12 and later, which
|
||||
# handle splash screens differently that prior versions of Android. Android 12 and later must be
|
||||
# configured specifically in the android_12 section below.
|
||||
|
||||
# color or background_image is the only required parameter. Use color to set the background
|
||||
# of your splash screen to a solid color. Use background_image to set the background of your
|
||||
# splash screen to a png image. This is useful for gradients. The image will be stretch to the
|
||||
# size of the app. Only one parameter can be used, color and background_image cannot both be set.
|
||||
color: "#AFC289"
|
||||
#background_image: "assets/background.png"
|
||||
|
||||
# Optional parameters are listed below. To enable a parameter, uncomment the line by removing
|
||||
# the leading # character.
|
||||
|
||||
# The image parameter allows you to specify an image used in the splash screen. It must be a
|
||||
# png file and should be sized for 4x pixel density.
|
||||
image: assets/image/logo.png
|
||||
|
||||
# The branding property allows you to specify an image used as branding in the splash screen.
|
||||
# It must be a png file. It is supported for Android, iOS and the Web. For Android 12,
|
||||
# see the Android 12 section below.
|
||||
#branding: assets/dart.png
|
||||
|
||||
# To position the branding image at the bottom of the screen you can use bottom, bottomRight,
|
||||
# and bottomLeft. The default values is bottom if not specified or specified something else.
|
||||
#branding_mode: bottom
|
||||
|
||||
# The color_dark, background_image_dark, image_dark, branding_dark are parameters that set the background
|
||||
# and image when the device is in dark mode. If they are not specified, the app will use the
|
||||
# parameters from above. If the image_dark parameter is specified, color_dark or
|
||||
# background_image_dark must be specified. color_dark and background_image_dark cannot both be
|
||||
# set.
|
||||
#color_dark: "#042a49"
|
||||
#background_image_dark: "assets/dark-background.png"
|
||||
#image_dark: assets/splash-invert.png
|
||||
#branding_dark: assets/dart_dark.png
|
||||
|
||||
# From Android 12 onwards, the splash screen is handled differently than in previous versions.
|
||||
# Please visit https://developer.android.com/guide/topics/ui/splash-screen
|
||||
# Following are specific parameters for Android 12+.
|
||||
android_12:
|
||||
# The image parameter sets the splash screen icon image. If this parameter is not specified,
|
||||
# the app's launcher icon will be used instead.
|
||||
# Please note that the splash screen will be clipped to a circle on the center of the screen.
|
||||
# App icon with an icon background: This should be 960×960 pixels, and fit within a circle
|
||||
# 640 pixels in diameter.
|
||||
# App icon without an icon background: This should be 1152×1152 pixels, and fit within a circle
|
||||
# 768 pixels in diameter.
|
||||
#image: assets/android12splash.png
|
||||
|
||||
# Splash screen background color.
|
||||
color: "#AFC289"
|
||||
|
||||
# App icon background color.
|
||||
#icon_background_color: "#111111"
|
||||
|
||||
# The branding property allows you to specify an image used as branding in the splash screen.
|
||||
#branding: assets/dart.png
|
||||
|
||||
# The image_dark, color_dark, icon_background_color_dark, and branding_dark set values that
|
||||
# apply when the device is in dark mode. If they are not specified, the app will use the
|
||||
# parameters from above.
|
||||
#image_dark: assets/android12splash-invert.png
|
||||
#color_dark: "#042a49"
|
||||
#icon_background_color_dark: "#eeeeee"
|
||||
|
||||
# The android, ios and web parameters can be used to disable generating a splash screen on a given
|
||||
# platform.
|
||||
#android: false
|
||||
#ios: false
|
||||
#web: false
|
||||
|
||||
# Platform specific images can be specified with the following parameters, which will override
|
||||
# the respective parameter. You may specify all, selected, or none of these parameters:
|
||||
#color_android: "#42a5f5"
|
||||
#color_dark_android: "#042a49"
|
||||
#color_ios: "#42a5f5"
|
||||
#color_dark_ios: "#042a49"
|
||||
#color_web: "#42a5f5"
|
||||
#color_dark_web: "#042a49"
|
||||
#image_android: assets/splash-android.png
|
||||
#image_dark_android: assets/splash-invert-android.png
|
||||
#image_ios: assets/splash-ios.png
|
||||
#image_dark_ios: assets/splash-invert-ios.png
|
||||
#image_web: assets/splash-web.gif
|
||||
#image_dark_web: assets/splash-invert-web.gif
|
||||
#background_image_android: "assets/background-android.png"
|
||||
#background_image_dark_android: "assets/dark-background-android.png"
|
||||
#background_image_ios: "assets/background-ios.png"
|
||||
#background_image_dark_ios: "assets/dark-background-ios.png"
|
||||
#background_image_web: "assets/background-web.png"
|
||||
#background_image_dark_web: "assets/dark-background-web.png"
|
||||
#branding_android: assets/brand-android.png
|
||||
#branding_dark_android: assets/dart_dark-android.png
|
||||
#branding_ios: assets/brand-ios.png
|
||||
#branding_dark_ios: assets/dart_dark-ios.png
|
||||
#branding_web: assets/brand-web.gif
|
||||
#branding_dark_web: assets/dart_dark-web.gif
|
||||
|
||||
# The position of the splash image can be set with android_gravity, ios_content_mode, and
|
||||
# web_image_mode parameters. All default to center.
|
||||
#
|
||||
# android_gravity can be one of the following Android Gravity (see
|
||||
# https://developer.android.com/reference/android/view/Gravity): bottom, center,
|
||||
# center_horizontal, center_vertical, clip_horizontal, clip_vertical, end, fill, fill_horizontal,
|
||||
# fill_vertical, left, right, start, or top.
|
||||
#android_gravity: center
|
||||
#
|
||||
# ios_content_mode can be one of the following iOS UIView.ContentMode (see
|
||||
# https://developer.apple.com/documentation/uikit/uiview/contentmode): scaleToFill,
|
||||
# scaleAspectFit, scaleAspectFill, center, top, bottom, left, right, topLeft, topRight,
|
||||
# bottomLeft, or bottomRight.
|
||||
#ios_content_mode: center
|
||||
#
|
||||
# web_image_mode can be one of the following modes: center, contain, stretch, and cover.
|
||||
#web_image_mode: center
|
||||
|
||||
# The screen orientation can be set in Android with the android_screen_orientation parameter.
|
||||
# Valid parameters can be found here:
|
||||
# https://developer.android.com/guide/topics/manifest/activity-element#screen
|
||||
#android_screen_orientation: sensorLandscape
|
||||
|
||||
# To hide the notification bar, use the fullscreen parameter. Has no effect in web since web
|
||||
# has no notification bar. Defaults to false.
|
||||
# NOTE: Unlike Android, iOS will not automatically show the notification bar when the app loads.
|
||||
# To show the notification bar, add the following code to your Flutter app:
|
||||
# WidgetsFlutterBinding.ensureInitialized();
|
||||
# SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: [SystemUiOverlay.bottom, SystemUiOverlay.top], );
|
||||
#fullscreen: true
|
||||
|
||||
# If you have changed the name(s) of your info.plist file(s), you can specify the filename(s)
|
||||
# with the info_plist_files parameter. Remove only the # characters in the three lines below,
|
||||
# do not remove any spaces:
|
||||
#info_plist_files:
|
||||
# - 'ios/Runner/Info-Debug.plist'
|
||||
# - 'ios/Runner/Info-Release.plist'
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
# Uncomment this line to define a global platform for your project
|
||||
platform :ios, '12.0' # Versión mínima para la aplicación principal
|
||||
|
||||
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||
|
||||
project 'Runner', {
|
||||
'Debug' => :debug,
|
||||
'Profile' => :release,
|
||||
'Release' => :release,
|
||||
}
|
||||
|
||||
def flutter_root
|
||||
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
|
||||
unless File.exist?(generated_xcode_build_settings_path)
|
||||
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
|
||||
end
|
||||
|
||||
File.foreach(generated_xcode_build_settings_path) do |line|
|
||||
matches = line.match(/FLUTTER_ROOT\=(.*)/)
|
||||
return matches[1].strip if matches
|
||||
end
|
||||
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
|
||||
end
|
||||
|
||||
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
|
||||
|
||||
flutter_ios_podfile_setup
|
||||
|
||||
target 'Runner' do
|
||||
use_frameworks!
|
||||
use_modular_headers!
|
||||
|
||||
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
|
||||
end
|
||||
|
||||
# Agregar el target de la extensión solo si existe
|
||||
extension_path = Dir.glob("**/*SearchWidgetExtension.xcodeproj").first || Dir.glob("**/SearchWidgetExtension/SearchWidgetExtension.xcodeproj").first
|
||||
if extension_path != nil
|
||||
puts "Encontrada la extensión SearchWidgetExtension, configurando pods..."
|
||||
target 'SearchWidgetExtension' do
|
||||
use_frameworks!
|
||||
use_modular_headers!
|
||||
# Remove SwiftUI pod as it's part of the iOS SDK
|
||||
# pod 'SwiftUI' # Framework básico para SwiftUI
|
||||
|
||||
# Don't share the same pods as the main app to avoid circular dependencies
|
||||
# Add only specific pods needed by the extension
|
||||
end
|
||||
else
|
||||
puts "No se encontró la extensión SearchWidgetExtension, omitiendo configuración específica."
|
||||
end
|
||||
|
||||
post_install do |installer|
|
||||
# Configurar iOS 12.0 como el mínimo por defecto, pero dejar que cada pod use su propio mínimo
|
||||
installer.pods_project.targets.each do |target|
|
||||
target.build_configurations.each do |config|
|
||||
# Asegurarse de que ningún pod tenga deployment target menor que 12.0
|
||||
if config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'].to_f < 12.0
|
||||
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '12.0'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Configurar los targets de Flutter
|
||||
installer.pods_project.targets.each do |target|
|
||||
flutter_additional_ios_build_settings(target)
|
||||
|
||||
target.build_configurations.each do |config|
|
||||
# Configuraciones específicas para cada target
|
||||
if target.name == 'Runner'
|
||||
# Configuraciones para el target principal
|
||||
config.build_settings['EXPANDED_CODE_SIGN_IDENTITY'] = ""
|
||||
config.build_settings['CODE_SIGNING_REQUIRED'] = "NO"
|
||||
config.build_settings['CODE_SIGNING_ALLOWED'] = "NO"
|
||||
end
|
||||
|
||||
# Configuraciones para la extensión si existe
|
||||
if target.name.include?('SearchWidgetExtension')
|
||||
config.build_settings['EXPANDED_CODE_SIGN_IDENTITY'] = ""
|
||||
config.build_settings['CODE_SIGNING_REQUIRED'] = "NO"
|
||||
config.build_settings['CODE_SIGNING_ALLOWED'] = "NO"
|
||||
config.build_settings['SKIP_INSTALL'] = 'YES'
|
||||
# Establecer iOS 16.1 como mínimo solo para la extensión (requisito para Live Activities)
|
||||
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '16.1'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Corregir problemas con el toolchain
|
||||
installer.generated_projects.each do |project|
|
||||
project.targets.each do |target|
|
||||
target.build_configurations.each do |config|
|
||||
config.build_settings['DEVELOPMENT_TEAM'] = ''
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,164 @@
|
|||
PODS:
|
||||
- audio_session (0.0.1):
|
||||
- Flutter
|
||||
- country_codes (0.0.1):
|
||||
- Flutter
|
||||
- device_info_plus (0.0.1):
|
||||
- Flutter
|
||||
- Flutter (1.0.0)
|
||||
- flutter_app_group_directory (0.0.1):
|
||||
- Flutter
|
||||
- flutter_local_notifications (0.0.1):
|
||||
- Flutter
|
||||
- flutter_mimir (0.0.1)
|
||||
- flutter_native_splash (2.4.3):
|
||||
- Flutter
|
||||
- gal (1.0.0):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- just_audio (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- live_activities (0.0.1):
|
||||
- Flutter
|
||||
- package_info_plus (0.4.5):
|
||||
- Flutter
|
||||
- path_provider_foundation (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- shared_preferences_foundation (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- sqflite_darwin (0.0.4):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- sqlite3 (3.49.1):
|
||||
- sqlite3/common (= 3.49.1)
|
||||
- sqlite3/common (3.49.1)
|
||||
- sqlite3/dbstatvtab (3.49.1):
|
||||
- sqlite3/common
|
||||
- sqlite3/fts5 (3.49.1):
|
||||
- sqlite3/common
|
||||
- sqlite3/perf-threadsafe (3.49.1):
|
||||
- sqlite3/common
|
||||
- sqlite3/rtree (3.49.1):
|
||||
- sqlite3/common
|
||||
- sqlite3_flutter_libs (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- sqlite3 (~> 3.49.1)
|
||||
- sqlite3/dbstatvtab
|
||||
- sqlite3/fts5
|
||||
- sqlite3/perf-threadsafe
|
||||
- sqlite3/rtree
|
||||
- syncfusion_flutter_pdfviewer (0.0.1):
|
||||
- Flutter
|
||||
- url_launcher_ios (0.0.1):
|
||||
- Flutter
|
||||
- video_player_avfoundation (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- wakelock_plus (0.0.1):
|
||||
- Flutter
|
||||
- webview_flutter_wkwebview (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
|
||||
DEPENDENCIES:
|
||||
- audio_session (from `.symlinks/plugins/audio_session/ios`)
|
||||
- country_codes (from `.symlinks/plugins/country_codes/ios`)
|
||||
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
|
||||
- Flutter (from `Flutter`)
|
||||
- flutter_app_group_directory (from `.symlinks/plugins/flutter_app_group_directory/ios`)
|
||||
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
|
||||
- flutter_mimir (from `.symlinks/plugins/flutter_mimir/ios`)
|
||||
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
|
||||
- gal (from `.symlinks/plugins/gal/darwin`)
|
||||
- just_audio (from `.symlinks/plugins/just_audio/darwin`)
|
||||
- live_activities (from `.symlinks/plugins/live_activities/ios`)
|
||||
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
||||
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
|
||||
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
|
||||
- sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/darwin`)
|
||||
- syncfusion_flutter_pdfviewer (from `.symlinks/plugins/syncfusion_flutter_pdfviewer/ios`)
|
||||
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
||||
- video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`)
|
||||
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
|
||||
- webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/darwin`)
|
||||
|
||||
SPEC REPOS:
|
||||
trunk:
|
||||
- sqlite3
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
audio_session:
|
||||
:path: ".symlinks/plugins/audio_session/ios"
|
||||
country_codes:
|
||||
:path: ".symlinks/plugins/country_codes/ios"
|
||||
device_info_plus:
|
||||
:path: ".symlinks/plugins/device_info_plus/ios"
|
||||
Flutter:
|
||||
:path: Flutter
|
||||
flutter_app_group_directory:
|
||||
:path: ".symlinks/plugins/flutter_app_group_directory/ios"
|
||||
flutter_local_notifications:
|
||||
:path: ".symlinks/plugins/flutter_local_notifications/ios"
|
||||
flutter_mimir:
|
||||
:path: ".symlinks/plugins/flutter_mimir/ios"
|
||||
flutter_native_splash:
|
||||
:path: ".symlinks/plugins/flutter_native_splash/ios"
|
||||
gal:
|
||||
:path: ".symlinks/plugins/gal/darwin"
|
||||
just_audio:
|
||||
:path: ".symlinks/plugins/just_audio/darwin"
|
||||
live_activities:
|
||||
:path: ".symlinks/plugins/live_activities/ios"
|
||||
package_info_plus:
|
||||
:path: ".symlinks/plugins/package_info_plus/ios"
|
||||
path_provider_foundation:
|
||||
:path: ".symlinks/plugins/path_provider_foundation/darwin"
|
||||
shared_preferences_foundation:
|
||||
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
|
||||
sqflite_darwin:
|
||||
:path: ".symlinks/plugins/sqflite_darwin/darwin"
|
||||
sqlite3_flutter_libs:
|
||||
:path: ".symlinks/plugins/sqlite3_flutter_libs/darwin"
|
||||
syncfusion_flutter_pdfviewer:
|
||||
:path: ".symlinks/plugins/syncfusion_flutter_pdfviewer/ios"
|
||||
url_launcher_ios:
|
||||
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
||||
video_player_avfoundation:
|
||||
:path: ".symlinks/plugins/video_player_avfoundation/darwin"
|
||||
wakelock_plus:
|
||||
:path: ".symlinks/plugins/wakelock_plus/ios"
|
||||
webview_flutter_wkwebview:
|
||||
:path: ".symlinks/plugins/webview_flutter_wkwebview/darwin"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
audio_session: 9bb7f6c970f21241b19f5a3658097ae459681ba0
|
||||
country_codes: b534fe92b5dd4d4cfd31a720f2bfa223373d162c
|
||||
device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe
|
||||
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
||||
flutter_app_group_directory: 55b5362007d1c0cb45dc1dd1e94f67d615f45a6b
|
||||
flutter_local_notifications: ad39620c743ea4c15127860f4b5641649a988100
|
||||
flutter_mimir: abc5575f7deea72a2716fb3a75295984681dfd87
|
||||
flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
|
||||
gal: baecd024ebfd13c441269ca7404792a7152fde89
|
||||
just_audio: 4e391f57b79cad2b0674030a00453ca5ce817eed
|
||||
live_activities: f2e133059358f99655c8d181d65ff54f024a6e93
|
||||
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
|
||||
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
|
||||
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
|
||||
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
|
||||
sqlite3: fc1400008a9b3525f5914ed715a5d1af0b8f4983
|
||||
sqlite3_flutter_libs: f8fc13346870e73fe35ebf6dbb997fbcd156b241
|
||||
syncfusion_flutter_pdfviewer: dfb514751af5b6b71e504c9c04a2e4ddbc1dd895
|
||||
url_launcher_ios: 694010445543906933d732453a59da0a173ae33d
|
||||
video_player_avfoundation: 2cef49524dd1f16c5300b9cd6efd9611ce03639b
|
||||
wakelock_plus: 04623e3f525556020ebd4034310f20fe7fda8b49
|
||||
webview_flutter_wkwebview: 1821ceac936eba6f7984d89a9f3bcb4dea99ebb2
|
||||
|
||||
PODFILE CHECKSUM: c6b83d8cd699251ccc3bbb754fede6c1135cc1f5
|
||||
|
||||
COCOAPODS: 1.16.2
|
||||
|
After Width: | Height: | Size: 9.5 KiB |
|
After Width: | Height: | Size: 550 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 779 B |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 140 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 4.5 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 6.3 KiB |
|
After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 7.6 KiB |
|
|
@ -0,0 +1,10 @@
|
|||
<?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>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.com.lgcc.search</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
//
|
||||
// AppIntent.swift
|
||||
// SearchWidget
|
||||
//
|
||||
// Created by Johann Villegas on 9/04/25.
|
||||
//
|
||||
|
||||
import WidgetKit
|
||||
import AppIntents
|
||||
|
||||
struct ConfigurationAppIntent: WidgetConfigurationIntent {
|
||||
static var title: LocalizedStringResource { "Configuration" }
|
||||
static var description: IntentDescription { "This is an example widget." }
|
||||
|
||||
// An example configurable parameter.
|
||||
@Parameter(title: "Favorite Emoji", default: "😃")
|
||||
var favoriteEmoji: String
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"colors" : [
|
||||
{
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "tinted"
|
||||
}
|
||||
],
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"colors" : [
|
||||
{
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<?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>NSSupportsLiveActivities</key>
|
||||
<true/>
|
||||
<key>NSExtension</key>
|
||||
<dict>
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
<string>com.apple.widgetkit-extension</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
@ -0,0 +1,226 @@
|
|||
//
|
||||
// SearchWidget.swift
|
||||
// SearchWidget
|
||||
//
|
||||
// Created by Johann Villegas on 9/04/25.
|
||||
//
|
||||
|
||||
import WidgetKit
|
||||
import SwiftUI
|
||||
import ActivityKit
|
||||
|
||||
// Shared FlutterLiveActivities definition for use in both app and widget extension
|
||||
struct FlutterLiveActivities: ActivityAttributes, Identifiable {
|
||||
public typealias ContentState = FlutterLiveActivitiesContent
|
||||
|
||||
public struct FlutterLiveActivitiesContent: Codable, Hashable {
|
||||
var title: String
|
||||
var query: String
|
||||
var resultsCount: String
|
||||
var timestamp: String
|
||||
}
|
||||
|
||||
var id = UUID()
|
||||
}
|
||||
|
||||
// Define the lock screen/banner state of Live Activities
|
||||
@available(iOS 16.2, *)
|
||||
struct FlutterLiveActivitiesLiveActivity: Widget {
|
||||
var body: some WidgetConfiguration {
|
||||
ActivityConfiguration(for: FlutterLiveActivities.self) { context in
|
||||
// Lock screen/banner UI goes here
|
||||
LockScreenLiveActivityView(context: context)
|
||||
} dynamicIsland: { context in
|
||||
// Dynamic Island UI goes here
|
||||
DynamicIslandLiveActivityView(context: context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 16.2, *)
|
||||
struct LockScreenLiveActivityView: View {
|
||||
let context: ActivityViewContext<FlutterLiveActivities>
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
Text(context.state.title)
|
||||
.font(.headline)
|
||||
|
||||
HStack {
|
||||
Image(systemName: "magnifyingglass")
|
||||
.foregroundColor(.green)
|
||||
Text(context.state.query)
|
||||
.font(.subheadline)
|
||||
}
|
||||
|
||||
HStack {
|
||||
Image(systemName: "doc.text")
|
||||
.foregroundColor(.blue)
|
||||
Text("Results: \(context.state.resultsCount)")
|
||||
.font(.subheadline)
|
||||
}
|
||||
|
||||
Text("Updated: \(formattedDate(from: context.state.timestamp))")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
|
||||
func formattedDate(from isoString: String) -> String {
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
|
||||
if let date = dateFormatter.date(from: isoString) {
|
||||
dateFormatter.dateFormat = "HH:mm:ss"
|
||||
return dateFormatter.string(from: date)
|
||||
}
|
||||
return isoString
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 16.2, *)
|
||||
struct DynamicIslandLiveActivityView: View {
|
||||
let context: ActivityViewContext<FlutterLiveActivities>
|
||||
|
||||
var body: some View {
|
||||
DynamicIsland {
|
||||
// Expanded state
|
||||
DynamicIslandExpandedRegion(.leading) {
|
||||
HStack {
|
||||
Image(systemName: "magnifyingglass")
|
||||
.foregroundColor(.green)
|
||||
Text(context.state.query)
|
||||
.font(.headline)
|
||||
.lineLimit(1)
|
||||
}
|
||||
}
|
||||
|
||||
DynamicIslandExpandedRegion(.trailing) {
|
||||
HStack {
|
||||
Image(systemName: "doc.text")
|
||||
.foregroundColor(.blue)
|
||||
Text(context.state.resultsCount)
|
||||
.font(.headline)
|
||||
}
|
||||
}
|
||||
|
||||
DynamicIslandExpandedRegion(.bottom) {
|
||||
VStack(alignment: .leading) {
|
||||
Text(context.state.title)
|
||||
.font(.caption)
|
||||
Text("Updated: \(formattedDate(from: context.state.timestamp))")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
} compactLeading: {
|
||||
Image(systemName: "magnifyingglass")
|
||||
.foregroundColor(.green)
|
||||
} compactTrailing: {
|
||||
Text(context.state.resultsCount)
|
||||
.font(.headline)
|
||||
} minimal: {
|
||||
Image(systemName: "magnifyingglass")
|
||||
.foregroundColor(.green)
|
||||
}
|
||||
}
|
||||
|
||||
func formattedDate(from isoString: String) -> String {
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
|
||||
if let date = dateFormatter.date(from: isoString) {
|
||||
dateFormatter.dateFormat = "HH:mm:ss"
|
||||
return dateFormatter.string(from: date)
|
||||
}
|
||||
return isoString
|
||||
}
|
||||
}
|
||||
|
||||
struct SearchWidget: Widget {
|
||||
let kind: String = "SearchWidget"
|
||||
|
||||
var body: some WidgetConfiguration {
|
||||
StaticConfiguration(kind: kind, provider: EmptyTimelineProvider()) { _ in
|
||||
EmptyWidgetView()
|
||||
}
|
||||
.configurationDisplayName("Estudios Bíblicos")
|
||||
.description("Search Widget for Bible Studies App")
|
||||
.supportedFamilies([.systemSmall, .systemMedium])
|
||||
}
|
||||
}
|
||||
|
||||
// Empty widget for devices that don't support Live Activities
|
||||
struct EmptyWidgetView: View {
|
||||
var body: some View {
|
||||
ZStack {
|
||||
Color(red: 0.42, green: 0.55, blue: 0.14) // #6b8e23 color
|
||||
VStack {
|
||||
Image(systemName: "magnifyingglass")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 40, height: 40)
|
||||
.foregroundColor(.white)
|
||||
Text("Estudios Bíblicos")
|
||||
.font(.headline)
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Empty provider for devices that don't support Live Activities
|
||||
struct EmptyTimelineProvider: TimelineProvider {
|
||||
func placeholder(in context: Context) -> EmptyTimelineEntry {
|
||||
EmptyTimelineEntry()
|
||||
}
|
||||
|
||||
func getSnapshot(in context: Context, completion: @escaping (EmptyTimelineEntry) -> ()) {
|
||||
let entry = EmptyTimelineEntry()
|
||||
completion(entry)
|
||||
}
|
||||
|
||||
func getTimeline(in context: Context, completion: @escaping (Timeline<EmptyTimelineEntry>) -> ()) {
|
||||
let entry = EmptyTimelineEntry()
|
||||
let timeline = Timeline(entries: [entry], policy: .never)
|
||||
completion(timeline)
|
||||
}
|
||||
}
|
||||
|
||||
struct EmptyTimelineEntry: TimelineEntry {
|
||||
let date: Date = Date()
|
||||
}
|
||||
|
||||
struct SearchWidget_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
SearchWidget()
|
||||
.previewContext(WidgetPreviewContext(family: .systemSmall))
|
||||
}
|
||||
}
|
||||
|
||||
// Preview for Live Activities
|
||||
@available(iOS 16.2, *)
|
||||
struct FlutterLiveActivitiesLiveActivity_Previews: PreviewProvider {
|
||||
static let attributes = FlutterLiveActivities()
|
||||
static let contentState = FlutterLiveActivities.ContentState(
|
||||
title: "Searching Bible Studies",
|
||||
query: "resurrection",
|
||||
resultsCount: "42",
|
||||
timestamp: ISO8601DateFormatter().string(from: Date())
|
||||
)
|
||||
|
||||
static var previews: some View {
|
||||
attributes
|
||||
.previewContext(contentState, viewKind: .dynamicIsland(.compact))
|
||||
.previewDisplayName("Island Compact")
|
||||
attributes
|
||||
.previewContext(contentState, viewKind: .dynamicIsland(.expanded))
|
||||
.previewDisplayName("Island Expanded")
|
||||
attributes
|
||||
.previewContext(contentState, viewKind: .dynamicIsland(.minimal))
|
||||
.previewDisplayName("Minimal")
|
||||
attributes
|
||||
.previewContext(contentState, viewKind: .content)
|
||||
.previewDisplayName("Notification")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
//
|
||||
// SearchWidgetBundle.swift
|
||||
// SearchWidget
|
||||
//
|
||||
// Created by Johann Villegas on 9/04/25.
|
||||
//
|
||||
|
||||
import WidgetKit
|
||||
import SwiftUI
|
||||
|
||||
@main
|
||||
struct SearchWidgetBundle: WidgetBundle {
|
||||
var body: some Widget {
|
||||
SearchWidget()
|
||||
if #available(iOS 16.2, *) {
|
||||
FlutterLiveActivitiesLiveActivity()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
//
|
||||
// SearchWidgetControl.swift
|
||||
// SearchWidget
|
||||
//
|
||||
// Created by Johann Villegas on 9/04/25.
|
||||
//
|
||||
|
||||
import AppIntents
|
||||
import SwiftUI
|
||||
import WidgetKit
|
||||
|
||||
struct SearchWidgetControl: ControlWidget {
|
||||
static let kind: String = "com.carpa.searchEngine.SearchWidget"
|
||||
|
||||
var body: some ControlWidgetConfiguration {
|
||||
AppIntentControlConfiguration(
|
||||
kind: Self.kind,
|
||||
provider: Provider()
|
||||
) { value in
|
||||
ControlWidgetToggle(
|
||||
"Start Timer",
|
||||
isOn: value.isRunning,
|
||||
action: StartTimerIntent(value.name)
|
||||
) { isRunning in
|
||||
Label(isRunning ? "On" : "Off", systemImage: "timer")
|
||||
}
|
||||
}
|
||||
.displayName("Timer")
|
||||
.description("A an example control that runs a timer.")
|
||||
}
|
||||
}
|
||||
|
||||
extension SearchWidgetControl {
|
||||
struct Value {
|
||||
var isRunning: Bool
|
||||
var name: String
|
||||
}
|
||||
|
||||
struct Provider: AppIntentControlValueProvider {
|
||||
func previewValue(configuration: TimerConfiguration) -> Value {
|
||||
SearchWidgetControl.Value(isRunning: false, name: configuration.timerName)
|
||||
}
|
||||
|
||||
func currentValue(configuration: TimerConfiguration) async throws -> Value {
|
||||
let isRunning = true // Check if the timer is running
|
||||
return SearchWidgetControl.Value(isRunning: isRunning, name: configuration.timerName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct TimerConfiguration: ControlConfigurationIntent {
|
||||
static let title: LocalizedStringResource = "Timer Name Configuration"
|
||||
|
||||
@Parameter(title: "Timer Name", default: "Timer")
|
||||
var timerName: String
|
||||
}
|
||||
|
||||
struct StartTimerIntent: SetValueIntent {
|
||||
static let title: LocalizedStringResource = "Start a timer"
|
||||
|
||||
@Parameter(title: "Timer Name")
|
||||
var name: String
|
||||
|
||||
@Parameter(title: "Timer is running")
|
||||
var value: Bool
|
||||
|
||||
init() {}
|
||||
|
||||
init(_ name: String) {
|
||||
self.name = name
|
||||
}
|
||||
|
||||
func perform() async throws -> some IntentResult {
|
||||
// Start the timer…
|
||||
return .result()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
//
|
||||
// SearchWidgetLiveActivity.swift
|
||||
// SearchWidget
|
||||
//
|
||||
// Created by Johann Villegas on 9/04/25.
|
||||
//
|
||||
|
||||
import ActivityKit
|
||||
import WidgetKit
|
||||
import SwiftUI
|
||||
|
||||
struct SearchWidgetAttributes: ActivityAttributes {
|
||||
public typealias LiveDeliveryData = ContentState
|
||||
|
||||
public struct ContentState: Codable, Hashable {
|
||||
var title: String
|
||||
var body: String
|
||||
var searchTerm: String
|
||||
var totalResults: Int
|
||||
var timestamp: Int64
|
||||
}
|
||||
|
||||
var id = UUID()
|
||||
}
|
||||
|
||||
struct SearchWidgetLiveActivity: Widget {
|
||||
var body: some WidgetConfiguration {
|
||||
ActivityConfiguration(for: SearchWidgetAttributes.self) { context in
|
||||
// Lock Screen/Banner UI
|
||||
VStack {
|
||||
HStack {
|
||||
Text(context.state.title)
|
||||
.font(.headline)
|
||||
Spacer()
|
||||
Text("\(context.state.totalResults)")
|
||||
.font(.caption)
|
||||
}
|
||||
Text(context.state.body)
|
||||
.font(.subheadline)
|
||||
if !context.state.searchTerm.isEmpty {
|
||||
Text("Search: \(context.state.searchTerm)")
|
||||
.font(.caption)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
} dynamicIsland: { context in
|
||||
// Dynamic Island UI
|
||||
DynamicIsland {
|
||||
// Expanded UI
|
||||
DynamicIslandExpandedRegion(.leading) {
|
||||
Text(context.state.title)
|
||||
.font(.headline)
|
||||
}
|
||||
|
||||
DynamicIslandExpandedRegion(.trailing) {
|
||||
Text("\(context.state.totalResults)")
|
||||
.font(.caption)
|
||||
}
|
||||
|
||||
DynamicIslandExpandedRegion(.bottom) {
|
||||
Text(context.state.body)
|
||||
.font(.subheadline)
|
||||
if !context.state.searchTerm.isEmpty {
|
||||
Text("Search: \(context.state.searchTerm)")
|
||||
.font(.caption)
|
||||
}
|
||||
}
|
||||
} compactLeading: {
|
||||
// Compact Leading
|
||||
Text("\(context.state.totalResults)")
|
||||
} compactTrailing: {
|
||||
// Compact Trailing
|
||||
Image(systemName: "magnifyingglass")
|
||||
} minimal: {
|
||||
// Minimal UI
|
||||
Image(systemName: "magnifyingglass")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
<?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>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.com.carpa.searchEngine.widget</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class NotificationController {
|
||||
static final FlutterLocalNotificationsPlugin _notifications =
|
||||
FlutterLocalNotificationsPlugin();
|
||||
|
||||
static Future<void> initialize() async {
|
||||
const androidSettings =
|
||||
AndroidInitializationSettings('@mipmap/ic_launcher');
|
||||
const iosSettings = DarwinInitializationSettings(
|
||||
requestAlertPermission: true,
|
||||
requestBadgePermission: true,
|
||||
requestSoundPermission: true,
|
||||
);
|
||||
|
||||
const initSettings = InitializationSettings(
|
||||
android: androidSettings,
|
||||
iOS: iosSettings,
|
||||
);
|
||||
|
||||
await _notifications.initialize(
|
||||
initSettings,
|
||||
onDidReceiveNotificationResponse: (NotificationResponse response) {
|
||||
if (response.payload != null) {
|
||||
// Handle notification tap
|
||||
// You can navigate or perform actions based on the payload
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
static Future<void> showNotification({
|
||||
required String title,
|
||||
required String body,
|
||||
String? payload,
|
||||
}) async {
|
||||
const androidDetails = AndroidNotificationDetails(
|
||||
'basic_channel',
|
||||
'Basic notifications',
|
||||
channelDescription: 'Notification channel for basic tests',
|
||||
importance: Importance.high,
|
||||
priority: Priority.high,
|
||||
color: Color(0xFF9D50DD),
|
||||
enableLights: true,
|
||||
enableVibration: true,
|
||||
playSound: true,
|
||||
);
|
||||
|
||||
const iosDetails = DarwinNotificationDetails(
|
||||
presentAlert: true,
|
||||
presentBadge: true,
|
||||
presentSound: true,
|
||||
);
|
||||
|
||||
const details = NotificationDetails(
|
||||
android: androidDetails,
|
||||
iOS: iosDetails,
|
||||
);
|
||||
|
||||
await _notifications.show(
|
||||
0,
|
||||
title,
|
||||
body,
|
||||
details,
|
||||
payload: payload,
|
||||
);
|
||||
}
|
||||
|
||||
static Future<void> requestPermissions() async {
|
||||
await _notifications
|
||||
.resolvePlatformSpecificImplementation<
|
||||
AndroidFlutterLocalNotificationsPlugin>()
|
||||
?.requestNotificationsPermission();
|
||||
|
||||
await _notifications
|
||||
.resolvePlatformSpecificImplementation<
|
||||
IOSFlutterLocalNotificationsPlugin>()
|
||||
?.requestPermissions(
|
||||
alert: true,
|
||||
badge: true,
|
||||
sound: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,826 @@
|
|||
import 'dart:io';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:drift/native.dart';
|
||||
import 'package:drift/drift.dart' as drift;
|
||||
import 'package:search_engine/services/config_service.dart';
|
||||
import 'package:search_engine/services/mimir_service.dart';
|
||||
|
||||
part 'database.g.dart';
|
||||
|
||||
class Draft {
|
||||
final String id;
|
||||
final String title;
|
||||
final DateTime date;
|
||||
final int activity;
|
||||
final String thumbnail;
|
||||
final int draft;
|
||||
final String locale;
|
||||
final String country;
|
||||
final String city;
|
||||
final String? body;
|
||||
final String? pdf;
|
||||
final String languagesCode;
|
||||
final Map<String, dynamic>? searchResultData;
|
||||
final int position;
|
||||
final int length;
|
||||
|
||||
Draft({
|
||||
required this.id,
|
||||
required this.title,
|
||||
required this.date,
|
||||
required this.activity,
|
||||
required this.thumbnail,
|
||||
required this.draft,
|
||||
required this.locale,
|
||||
required this.country,
|
||||
required this.city,
|
||||
this.body,
|
||||
this.pdf,
|
||||
required this.languagesCode,
|
||||
this.searchResultData,
|
||||
this.position = 0,
|
||||
this.length = 0,
|
||||
});
|
||||
}
|
||||
|
||||
class Messages extends Table {
|
||||
TextColumn get id => text()();
|
||||
|
||||
TextColumn get country => text()();
|
||||
|
||||
TextColumn get city => text()();
|
||||
|
||||
DateTimeColumn get date => dateTime()();
|
||||
|
||||
IntColumn get activity => integer()();
|
||||
|
||||
IntColumn get draft => integer()();
|
||||
|
||||
TextColumn get thumbnail => text()();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {id};
|
||||
}
|
||||
|
||||
@TableIndex(name: 'title_index', columns: {#title})
|
||||
@TableIndex(name: 'body_index', columns: {#body})
|
||||
@TableIndex(name: 'message_id_index', columns: {#messageId})
|
||||
class Translations extends Table {
|
||||
TextColumn get messageId => text()();
|
||||
|
||||
TextColumn get title => text()();
|
||||
|
||||
TextColumn get body => text()();
|
||||
|
||||
TextColumn get languagesCode => text()();
|
||||
|
||||
TextColumn get pdf => text().nullable()();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {messageId, languagesCode};
|
||||
}
|
||||
|
||||
class Favorites extends Table {
|
||||
TextColumn get id => text()();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {id};
|
||||
}
|
||||
|
||||
@DriftDatabase(tables: [Messages, Translations, Favorites])
|
||||
class AppDatabase extends _$AppDatabase {
|
||||
AppDatabase() : super(_createConnection());
|
||||
|
||||
static DatabaseConnection _createConnection() {
|
||||
return DatabaseConnection.delayed(Future(() async {
|
||||
final dbFolder = await getApplicationDocumentsDirectory();
|
||||
final filePath = p.join(dbFolder.path, 'LGCC_Search/internal/db.sqlite');
|
||||
|
||||
final file = File(filePath);
|
||||
final database = NativeDatabase(file,
|
||||
logStatements: false, cachePreparedStatements: true);
|
||||
|
||||
return DatabaseConnection(database);
|
||||
}));
|
||||
}
|
||||
|
||||
@override
|
||||
int get schemaVersion => 1;
|
||||
|
||||
Future<void> addMessages(List<Draft> drafts) async {
|
||||
if (drafts.isEmpty) {
|
||||
return; // No hay nada que insertar
|
||||
}
|
||||
|
||||
try {
|
||||
// Preparar las listas para mensajes y traducciones
|
||||
List<MessagesCompanion> messagesList = [];
|
||||
List<TranslationsCompanion> translationsList = [];
|
||||
|
||||
// Validar y preparar los datos
|
||||
for (final draft in drafts) {
|
||||
// Validar que el ID no esté vacío
|
||||
if (draft.id.isEmpty) {
|
||||
if (kDebugMode) {
|
||||
print('Saltando mensaje con ID vacío');
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Preparar el mensaje
|
||||
messagesList.add(MessagesCompanion(
|
||||
id: Value(draft.id),
|
||||
country: Value(draft.country),
|
||||
city: Value(draft.city),
|
||||
date: Value(draft.date),
|
||||
activity: Value(draft.activity),
|
||||
thumbnail: Value(draft.thumbnail),
|
||||
draft: Value(draft.draft),
|
||||
));
|
||||
|
||||
// Preparar la traducción
|
||||
translationsList.add(TranslationsCompanion(
|
||||
messageId: Value(draft.id),
|
||||
body: Value(draft.body ?? ''),
|
||||
languagesCode: Value(draft.languagesCode),
|
||||
title: Value(draft.title),
|
||||
pdf: Value(draft.pdf ?? ''),
|
||||
));
|
||||
}
|
||||
|
||||
// Ejecutar las inserciones en una transacción para garantizar la atomicidad
|
||||
await transaction(() async {
|
||||
// Insertar mensajes con manejo de conflictos
|
||||
for (var message in messagesList) {
|
||||
await into(messages).insert(
|
||||
message,
|
||||
onConflict: DoUpdate(
|
||||
(old) => MessagesCompanion(
|
||||
country: message.country,
|
||||
city: message.city,
|
||||
date: message.date,
|
||||
activity: message.activity,
|
||||
thumbnail: message.thumbnail,
|
||||
draft: message.draft,
|
||||
),
|
||||
target: [messages.id],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Insertar traducciones con manejo de conflictos
|
||||
for (var translation in translationsList) {
|
||||
await into(translations).insert(
|
||||
translation,
|
||||
onConflict: DoUpdate(
|
||||
(old) => TranslationsCompanion(
|
||||
title: translation.title,
|
||||
body: translation.body,
|
||||
pdf: translation.pdf,
|
||||
),
|
||||
target: [translations.messageId, translations.languagesCode],
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
print('Error al insertar mensajes en la base de datos: $e');
|
||||
}
|
||||
// Reintento con enfoque más seguro en caso de error
|
||||
try {
|
||||
// Insertar uno por uno para identificar registros problemáticos
|
||||
for (int i = 0; i < drafts.length; i++) {
|
||||
final draft = drafts[i];
|
||||
try {
|
||||
// Insertar mensaje
|
||||
await into(messages).insert(
|
||||
MessagesCompanion(
|
||||
id: Value(draft.id),
|
||||
country: Value(draft.country),
|
||||
city: Value(draft.city),
|
||||
date: Value(draft.date),
|
||||
activity: Value(draft.activity),
|
||||
thumbnail: Value(draft.thumbnail),
|
||||
draft: Value(draft.draft),
|
||||
),
|
||||
onConflict: DoUpdate(
|
||||
(old) => MessagesCompanion(
|
||||
country: Value(draft.country),
|
||||
city: Value(draft.city),
|
||||
date: Value(draft.date),
|
||||
activity: Value(draft.activity),
|
||||
thumbnail: Value(draft.thumbnail),
|
||||
draft: Value(draft.draft),
|
||||
),
|
||||
target: [messages.id],
|
||||
),
|
||||
);
|
||||
|
||||
// Insertar traducción
|
||||
await into(translations).insert(
|
||||
TranslationsCompanion(
|
||||
messageId: Value(draft.id),
|
||||
body: Value(draft.body ?? ''),
|
||||
languagesCode: Value(draft.languagesCode),
|
||||
title: Value(draft.title),
|
||||
pdf: Value(draft.pdf ?? ''),
|
||||
),
|
||||
onConflict: DoUpdate(
|
||||
(old) => TranslationsCompanion(
|
||||
title: Value(draft.title),
|
||||
body: Value(draft.body ?? ''),
|
||||
pdf: Value(draft.pdf ?? ''),
|
||||
),
|
||||
target: [translations.messageId, translations.languagesCode],
|
||||
),
|
||||
);
|
||||
} catch (innerError) {
|
||||
if (kDebugMode) {
|
||||
print(
|
||||
'Error al insertar mensaje #$i (ID: ${draft.id}): $innerError');
|
||||
}
|
||||
// Continuar con el siguiente registro
|
||||
}
|
||||
}
|
||||
} catch (fallbackError) {
|
||||
if (kDebugMode) {
|
||||
print('Error en el proceso de recuperación: $fallbackError');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<Map<String, dynamic>>> getPdfList() async {
|
||||
final locale = await ConfigService.getLocale();
|
||||
try {
|
||||
final queryResult = await customSelect(
|
||||
"SELECT pdf, id, t.title, date, m.country, activity FROM messages LEFT JOIN translations ON messages.id = translations.message_id WHERE pdf <> '' AND locale = '$locale'",
|
||||
readsFrom: {messages}).map((row) {
|
||||
return {
|
||||
'pdf': row.read<String>('pdf'),
|
||||
'title': row.read<String>('title')
|
||||
};
|
||||
}).get();
|
||||
return queryResult;
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
print('Error fetching PDF list: $e');
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<Draft>> getMessages(bool isDraft, [String? localeParam]) async {
|
||||
final locale = localeParam ?? await ConfigService.getLocale();
|
||||
if (locale.isEmpty) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
// Consulta SQL optimizada sin usar INDEXED BY
|
||||
final queryResult = await customSelect(
|
||||
"""
|
||||
SELECT DISTINCT
|
||||
m.id,
|
||||
m.date,
|
||||
m.activity,
|
||||
m.thumbnail,
|
||||
m.draft,
|
||||
m.country,
|
||||
m.city,
|
||||
t.languages_code as locale,
|
||||
t.title,
|
||||
t.pdf,
|
||||
t.languages_code
|
||||
FROM
|
||||
messages m
|
||||
JOIN translations t ON m.id = t.message_id
|
||||
WHERE
|
||||
m.draft = ?
|
||||
AND t.languages_code = ?
|
||||
ORDER BY m.date DESC
|
||||
LIMIT 20
|
||||
""",
|
||||
variables: [
|
||||
Variable<int>(isDraft ? 1 : 0),
|
||||
Variable.withString(locale),
|
||||
],
|
||||
readsFrom: {messages, translations},
|
||||
).map((row) {
|
||||
return Draft(
|
||||
id: row.read<String>('id'),
|
||||
title: row.read<String>('title'),
|
||||
date: row.read<DateTime>('date'),
|
||||
activity: row.read<int>('activity'),
|
||||
thumbnail: row.read<String>('thumbnail'),
|
||||
draft: row.read<int>('draft'),
|
||||
locale: row.read<String>('locale'),
|
||||
country: row.read<String>('country'),
|
||||
city: row.read<String>('city'),
|
||||
// Cargar el cuerpo solo cuando sea necesario para mejorar el rendimiento
|
||||
body: '',
|
||||
pdf: row.read<String?>('pdf') ?? '',
|
||||
languagesCode: row.read<String>('languages_code'),
|
||||
);
|
||||
}).get();
|
||||
|
||||
return queryResult;
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
print('Error fetching messages: $e');
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// Nuevo método para cargar el cuerpo de un mensaje específico cuando sea necesario
|
||||
Future<String> getMessageBody(String messageId, String languageCode) async {
|
||||
try {
|
||||
final result = await customSelect(
|
||||
"""
|
||||
SELECT body
|
||||
FROM translations
|
||||
WHERE message_id = ? AND languages_code = ?
|
||||
""",
|
||||
variables: [
|
||||
Variable.withString(messageId),
|
||||
Variable.withString(languageCode),
|
||||
],
|
||||
).getSingleOrNull();
|
||||
|
||||
return result?.read<String?>('body') ?? '';
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
print('Error fetching message body: $e');
|
||||
}
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<Draft>> getYearActivities(String year, String month) async {
|
||||
final locale = await ConfigService.getLocale();
|
||||
try {
|
||||
final queryResult = await customSelect(
|
||||
"SELECT DISTINCT m.id, t.title, m.date, m.activity, m.thumbnail, m.draft, t.languages_code as locale, m.country, m.city, t.body, t.pdf, t.languages_code, strftime('%Y', datetime(m.date, 'unixepoch')) AS year, strftime('%m', datetime(m.date, 'unixepoch')) AS month "
|
||||
"FROM messages m JOIN translations t ON m.id = t.message_id "
|
||||
"WHERE strftime('%Y', datetime(m.date, 'unixepoch')) = ? AND strftime('%m', datetime(m.date, 'unixepoch')) = ? AND t.languages_code = ? "
|
||||
"ORDER BY m.date DESC, m.activity DESC",
|
||||
variables: [
|
||||
Variable.withString(year),
|
||||
Variable.withString(month),
|
||||
Variable.withString(locale)
|
||||
],
|
||||
readsFrom: {messages, translations},
|
||||
).map((row) {
|
||||
final id = row.read<String>('id');
|
||||
final title = row.read<String>('title');
|
||||
final date = row.read<DateTime>('date');
|
||||
final activity = row.read<int>('activity');
|
||||
final thumbnail = row.read<String>('thumbnail');
|
||||
final draft = row.read<int>('draft');
|
||||
final locale = row.read<String>('locale');
|
||||
final country = row.read<String>('country');
|
||||
final city = row.read<String>('city');
|
||||
final body = row.read<String?>('body') ?? '';
|
||||
final pdf = row.read<String?>('pdf') ?? '';
|
||||
final languagesCode = row.read<String>('languages_code');
|
||||
return Draft(
|
||||
id: id,
|
||||
title: title,
|
||||
date: date,
|
||||
activity: activity,
|
||||
thumbnail: thumbnail,
|
||||
draft: draft,
|
||||
locale: locale,
|
||||
country: country,
|
||||
city: city,
|
||||
body: body,
|
||||
pdf: pdf,
|
||||
languagesCode: languagesCode,
|
||||
);
|
||||
}).get();
|
||||
|
||||
return queryResult;
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
print('Error fetching activities: $e');
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<String>> getMonths(String year) async {
|
||||
try {
|
||||
final queryResult = await customSelect(
|
||||
"SELECT DISTINCT strftime('%m', datetime(date, 'unixepoch')) AS month FROM messages WHERE strftime('%Y', datetime(date, 'unixepoch')) = ? ORDER BY month ASC",
|
||||
variables: [Variable.withString(year)],
|
||||
readsFrom: {messages},
|
||||
).map((row) {
|
||||
return row.read<String>('month');
|
||||
}).get();
|
||||
|
||||
return queryResult;
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
print('Error fetching months: $e');
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> toggleFavorite(String id) async {
|
||||
final result = await customSelect(
|
||||
'SELECT COUNT(*) AS count FROM favorites WHERE id = ?',
|
||||
variables: [Variable.withString(id)],
|
||||
).getSingle();
|
||||
|
||||
final isFavorite = result.read<int>('count') > 0;
|
||||
|
||||
if (isFavorite) {
|
||||
delete(favorites).delete(FavoritesCompanion(id: Value(id)));
|
||||
} else {
|
||||
into(favorites).insert(
|
||||
FavoritesCompanion(id: Value(id)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> checkFavorite(String id) async {
|
||||
final result = await customSelect(
|
||||
'SELECT DISTINCT COUNT(*) AS count FROM favorites WHERE id = ?',
|
||||
variables: [Variable.withString(id)],
|
||||
).getSingle();
|
||||
final isFavorite = result.read<int>('count') > 0;
|
||||
return isFavorite;
|
||||
}
|
||||
|
||||
Future<List<String>> getFavorites() async {
|
||||
final result = await customSelect('SELECT DISTINCT id FROM favorites')
|
||||
.map((row) => row.read<String>('id'))
|
||||
.get();
|
||||
return result;
|
||||
}
|
||||
|
||||
// Método para obtener los años disponibles en la base de datos
|
||||
Future<List<String>> getAvailableYears() async {
|
||||
try {
|
||||
final queryResult = await customSelect(
|
||||
"SELECT DISTINCT strftime('%Y', datetime(date, 'unixepoch')) AS year FROM messages ORDER BY year DESC",
|
||||
readsFrom: {messages},
|
||||
).map((row) {
|
||||
return row.read<String>('year');
|
||||
}).get();
|
||||
|
||||
return queryResult;
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
print('Error fetching available years: $e');
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// Método para obtener mensajes filtrados por año y mes
|
||||
Future<List<Draft>> getFilteredMessages(String year, String month,
|
||||
[String? localeParam]) async {
|
||||
final locale = localeParam ?? await ConfigService.getLocale();
|
||||
if (locale.isEmpty) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
final queryResult = await customSelect(
|
||||
"""
|
||||
SELECT DISTINCT
|
||||
m.id,
|
||||
m.date,
|
||||
m.activity,
|
||||
m.thumbnail,
|
||||
m.draft,
|
||||
m.country,
|
||||
m.city,
|
||||
t.languages_code as locale,
|
||||
t.title,
|
||||
t.pdf,
|
||||
t.languages_code
|
||||
FROM
|
||||
messages m
|
||||
JOIN translations t ON m.id = t.message_id
|
||||
WHERE
|
||||
strftime('%Y', datetime(m.date, 'unixepoch')) = ?
|
||||
AND strftime('%m', datetime(m.date, 'unixepoch')) = ?
|
||||
AND t.languages_code = ?
|
||||
ORDER BY m.date DESC
|
||||
""",
|
||||
variables: [
|
||||
Variable.withString(year),
|
||||
Variable.withString(month),
|
||||
Variable.withString(locale),
|
||||
],
|
||||
readsFrom: {messages, translations},
|
||||
).map((row) {
|
||||
return Draft(
|
||||
id: row.read<String>('id'),
|
||||
title: row.read<String>('title'),
|
||||
date: row.read<DateTime>('date'),
|
||||
activity: row.read<int>('activity'),
|
||||
thumbnail: row.read<String>('thumbnail'),
|
||||
draft: row.read<int>('draft'),
|
||||
locale: row.read<String>('locale'),
|
||||
country: row.read<String>('country'),
|
||||
city: row.read<String>('city'),
|
||||
body: '',
|
||||
pdf: row.read<String?>('pdf') ?? '',
|
||||
languagesCode: row.read<String>('languages_code'),
|
||||
);
|
||||
}).get();
|
||||
|
||||
return queryResult;
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
print('Error fetching filtered messages: $e');
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// Método para obtener todos los mensajes (borradores y no borradores)
|
||||
Future<List<Draft>> getAllMessages([String? localeParam, int? limit]) async {
|
||||
final locale = localeParam ?? await ConfigService.getLocale();
|
||||
if (locale.isEmpty) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
// Consulta SQL optimizada para obtener todos los mensajes
|
||||
var query = """
|
||||
SELECT DISTINCT
|
||||
m.id,
|
||||
m.date,
|
||||
m.activity,
|
||||
m.thumbnail,
|
||||
m.draft,
|
||||
m.country,
|
||||
m.city,
|
||||
t.languages_code as locale,
|
||||
t.title,
|
||||
t.body,
|
||||
t.pdf,
|
||||
t.languages_code
|
||||
FROM
|
||||
messages m
|
||||
JOIN translations t ON m.id = t.message_id
|
||||
WHERE
|
||||
t.languages_code = ?
|
||||
ORDER BY m.date DESC
|
||||
""";
|
||||
|
||||
// Añadir límite si se especifica
|
||||
if (limit != null) {
|
||||
query += " LIMIT $limit";
|
||||
}
|
||||
|
||||
final queryResult = await customSelect(
|
||||
query,
|
||||
variables: [
|
||||
Variable.withString(locale),
|
||||
],
|
||||
readsFrom: {messages, translations},
|
||||
).map((row) {
|
||||
return Draft(
|
||||
id: row.read<String>('id'),
|
||||
title: row.read<String>('title'),
|
||||
date: row.read<DateTime>('date'),
|
||||
activity: row.read<int>('activity'),
|
||||
thumbnail: row.read<String>('thumbnail'),
|
||||
draft: row.read<int>('draft'),
|
||||
locale: row.read<String>('locale'),
|
||||
country: row.read<String>('country'),
|
||||
city: row.read<String>('city'),
|
||||
body: row.read<String?>('body') ?? '',
|
||||
pdf: row.read<String?>('pdf') ?? '',
|
||||
languagesCode: row.read<String>('languages_code'),
|
||||
);
|
||||
}).get();
|
||||
|
||||
return queryResult;
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
print('Error fetching all messages: $e');
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<Draft>> searchMessages(String query, String languagesCode) async {
|
||||
try {
|
||||
final mimirService = MimirService();
|
||||
await mimirService.initialize();
|
||||
|
||||
// Buscar en Mimir para obtener los IDs relevantes y metadatos
|
||||
final searchResult = await mimirService.search(query, languagesCode);
|
||||
|
||||
// Extraer IDs de los resultados
|
||||
List<Map<String, dynamic>> searchResultsData = searchResult['results'];
|
||||
List<String> docIds =
|
||||
searchResultsData.map((doc) => doc['id'] as String).toList();
|
||||
|
||||
if (docIds.isEmpty) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Convertir los IDs en una cadena para la consulta SQL
|
||||
final String idList = docIds.map((id) => "'$id'").join(',');
|
||||
|
||||
// Buscar los documentos completos en la base de datos
|
||||
final queryResult = await customSelect(
|
||||
'''
|
||||
SELECT DISTINCT
|
||||
m.id,
|
||||
m.date,
|
||||
m.activity,
|
||||
m.thumbnail,
|
||||
m.draft,
|
||||
m.country,
|
||||
m.city,
|
||||
t.languages_code as locale,
|
||||
t.title,
|
||||
t.pdf,
|
||||
t.languages_code,
|
||||
t.body
|
||||
FROM
|
||||
messages m
|
||||
JOIN translations t ON m.id = t.message_id
|
||||
WHERE
|
||||
m.id IN ($idList) AND t.languages_code = ?
|
||||
ORDER BY m.date DESC
|
||||
''',
|
||||
variables: [Variable.withString(languagesCode)],
|
||||
readsFrom: {messages, translations},
|
||||
).map((row) {
|
||||
final String id = row.read<String>('id');
|
||||
|
||||
// Buscar los datos del resultado de búsqueda correspondiente
|
||||
Map<String, dynamic>? resultData;
|
||||
for (var data in searchResultsData) {
|
||||
if (data['id'] == id) {
|
||||
resultData = data;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return Draft(
|
||||
id: id,
|
||||
title: row.read<String>('title'),
|
||||
date: row.read<DateTime>('date'),
|
||||
activity: row.read<int>('activity'),
|
||||
thumbnail: row.read<String>('thumbnail'),
|
||||
draft: row.read<int>('draft'),
|
||||
locale: row.read<String>('locale'),
|
||||
country: row.read<String>('country'),
|
||||
city: row.read<String>('city'),
|
||||
body: row.read<String?>('body'),
|
||||
pdf: row.read<String?>('pdf') ?? '',
|
||||
languagesCode: row.read<String>('languages_code'),
|
||||
searchResultData: {
|
||||
...resultData ?? {},
|
||||
'allResultIds': docIds,
|
||||
'mimirSearchResult': searchResult,
|
||||
},
|
||||
);
|
||||
}).get();
|
||||
|
||||
return queryResult;
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
print('Error searching messages: $e');
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// Método para obtener todos los PDFs disponibles
|
||||
Future<List<Draft>> getAvailablePdfs(String locale) async {
|
||||
try {
|
||||
final query = """
|
||||
SELECT DISTINCT
|
||||
m.id,
|
||||
m.date,
|
||||
m.activity,
|
||||
m.thumbnail,
|
||||
m.draft,
|
||||
m.country,
|
||||
m.city,
|
||||
t.languages_code as locale,
|
||||
t.title,
|
||||
t.pdf,
|
||||
t.languages_code
|
||||
FROM
|
||||
messages m
|
||||
JOIN translations t ON m.id = t.message_id
|
||||
WHERE
|
||||
t.languages_code = ?
|
||||
AND t.pdf IS NOT NULL
|
||||
AND t.pdf != ''
|
||||
ORDER BY m.date DESC
|
||||
""";
|
||||
|
||||
final queryResult = await customSelect(
|
||||
query,
|
||||
variables: [
|
||||
Variable.withString(locale),
|
||||
],
|
||||
readsFrom: {messages, translations},
|
||||
).map((row) {
|
||||
return Draft(
|
||||
id: row.read<String>('id'),
|
||||
title: row.read<String>('title'),
|
||||
date: row.read<DateTime>('date'),
|
||||
activity: row.read<int>('activity'),
|
||||
thumbnail: row.read<String>('thumbnail'),
|
||||
draft: row.read<int>('draft'),
|
||||
locale: row.read<String>('locale'),
|
||||
country: row.read<String>('country'),
|
||||
city: row.read<String>('city'),
|
||||
body: '',
|
||||
pdf: row.read<String?>('pdf') ?? '',
|
||||
languagesCode: row.read<String>('languages_code'),
|
||||
);
|
||||
}).get();
|
||||
|
||||
return queryResult;
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
print('Error fetching available PDFs: $e');
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// Método para obtener mensajes por IDs
|
||||
Future<List<Draft>> getMessagesByIds(List<String> ids, String locale) async {
|
||||
if (ids.isEmpty) return [];
|
||||
|
||||
try {
|
||||
// Convertir la lista de IDs a un formato que pueda usarse en la consulta SQL
|
||||
final String idList = ids.map((id) => "'$id'").join(',');
|
||||
|
||||
final queryResult = await customSelect(
|
||||
'''
|
||||
SELECT DISTINCT
|
||||
m.id,
|
||||
m.date,
|
||||
m.activity,
|
||||
m.thumbnail,
|
||||
m.draft,
|
||||
m.country,
|
||||
m.city,
|
||||
t.languages_code as locale,
|
||||
t.title,
|
||||
t.body,
|
||||
t.pdf,
|
||||
t.languages_code
|
||||
FROM
|
||||
messages m
|
||||
JOIN translations t ON m.id = t.message_id
|
||||
WHERE
|
||||
m.id IN ($idList) AND t.languages_code = ?
|
||||
ORDER BY m.date DESC
|
||||
''',
|
||||
variables: [Variable.withString(locale)],
|
||||
readsFrom: {messages, translations},
|
||||
).map((row) {
|
||||
return Draft(
|
||||
id: row.read<String>('id'),
|
||||
title: row.read<String>('title'),
|
||||
date: row.read<DateTime>('date'),
|
||||
activity: row.read<int>('activity'),
|
||||
thumbnail: row.read<String>('thumbnail'),
|
||||
draft: row.read<int>('draft'),
|
||||
locale: row.read<String>('locale'),
|
||||
country: row.read<String>('country'),
|
||||
city: row.read<String>('city'),
|
||||
body: row.read<String?>('body') ?? '',
|
||||
pdf: row.read<String?>('pdf') ?? '',
|
||||
languagesCode: row.read<String>('languages_code'),
|
||||
);
|
||||
}).get();
|
||||
|
||||
if (kDebugMode) {
|
||||
print(
|
||||
'Encontrados ${queryResult.length} mensajes de ${ids.length} IDs');
|
||||
}
|
||||
|
||||
return queryResult;
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
print('Error obteniendo mensajes por IDs: $e');
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,667 @@
|
|||
import 'dart:io';
|
||||
import 'dart:math' as math;
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart';
|
||||
import 'package:search_engine/database.dart';
|
||||
import 'package:search_engine/screens/landing.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:filesize/filesize.dart';
|
||||
import 'package:search_engine/services/config_service.dart';
|
||||
import 'package:drift/drift.dart' as drift;
|
||||
|
||||
// Clase para almacenar información de categorías de almacenamiento
|
||||
class StorageCategory {
|
||||
final String name;
|
||||
final IconData icon;
|
||||
final Color color;
|
||||
int size;
|
||||
double percentage;
|
||||
|
||||
StorageCategory({
|
||||
required this.name,
|
||||
required this.icon,
|
||||
required this.color,
|
||||
this.size = 0,
|
||||
this.percentage = 0,
|
||||
});
|
||||
}
|
||||
|
||||
// Clase para pintar el gráfico circular
|
||||
class StorageChartPainter extends CustomPainter {
|
||||
final List<StorageCategory> categories;
|
||||
|
||||
StorageChartPainter(this.categories);
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final center = Offset(size.width / 2, size.height / 2);
|
||||
final radius = math.min(size.width, size.height) / 2;
|
||||
final rect = Rect.fromCircle(center: center, radius: radius);
|
||||
|
||||
double startAngle = -math.pi / 2;
|
||||
|
||||
for (var category in categories) {
|
||||
if (category.percentage > 0) {
|
||||
final sweepAngle = (category.percentage / 100) * 2 * math.pi;
|
||||
final paint = Paint()
|
||||
..color = category.color
|
||||
..style = PaintingStyle.fill;
|
||||
|
||||
canvas.drawArc(rect, startAngle, sweepAngle, true, paint);
|
||||
startAngle += sweepAngle;
|
||||
}
|
||||
}
|
||||
|
||||
// Dibujar círculo central
|
||||
canvas.drawCircle(
|
||||
center,
|
||||
radius * 0.6,
|
||||
Paint()..color = Colors.white,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
|
||||
}
|
||||
|
||||
class ConfigView extends StatefulWidget {
|
||||
const ConfigView({super.key});
|
||||
|
||||
@override
|
||||
State<ConfigView> createState() => _ConfigViewState();
|
||||
}
|
||||
|
||||
class _ConfigViewState extends State<ConfigView> {
|
||||
bool hdThumbnail = false;
|
||||
bool pdfDownload = false;
|
||||
bool isSavingHdThumbnail = false;
|
||||
bool isSavingPdfDownload = false;
|
||||
bool isCalculatingCache = false;
|
||||
bool isCleaningCache = false;
|
||||
String selectedLocale = 'es';
|
||||
String cacheSize = '0 B';
|
||||
late Future<AppDatabase> _databaseFuture;
|
||||
final String _version = dotenv.env['VERSION'] ?? '1.0';
|
||||
|
||||
// Lista de categorías de almacenamiento
|
||||
final List<StorageCategory> _categories = [
|
||||
StorageCategory(
|
||||
name: 'thumbnails',
|
||||
icon: Icons.image,
|
||||
color: Colors.blue,
|
||||
),
|
||||
StorageCategory(
|
||||
name: 'pdfs',
|
||||
icon: Icons.picture_as_pdf,
|
||||
color: Colors.green,
|
||||
),
|
||||
StorageCategory(
|
||||
name: 'other',
|
||||
icon: Icons.folder,
|
||||
color: Colors.orange,
|
||||
),
|
||||
];
|
||||
|
||||
final Map<String, String> locales = {
|
||||
'es': 'Español',
|
||||
'en': 'English',
|
||||
'pt': 'Português',
|
||||
'fr': 'Français',
|
||||
'rw': 'Kinyarwanda',
|
||||
};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_databaseFuture = Future.value(AppDatabase());
|
||||
_getConfig();
|
||||
_calculateCacheSize();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_databaseFuture.then((database) => database.close());
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _calculateCacheSize() async {
|
||||
setState(() {
|
||||
isCalculatingCache = true;
|
||||
});
|
||||
|
||||
try {
|
||||
final appDir = await getApplicationDocumentsDirectory();
|
||||
final searchDir = Directory('${appDir.path}/LGCC_Search');
|
||||
|
||||
if (!await searchDir.exists()) {
|
||||
setState(() {
|
||||
cacheSize = '0 B';
|
||||
isCalculatingCache = false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
int totalSize = 0;
|
||||
Map<String, int> categorySizes = {
|
||||
'thumbnails': 0,
|
||||
'pdfs': 0,
|
||||
'other': 0,
|
||||
};
|
||||
|
||||
await for (var entity in searchDir.list(recursive: true)) {
|
||||
if (entity is File && !entity.path.contains('/internal/')) {
|
||||
final size = await entity.length();
|
||||
totalSize += size;
|
||||
|
||||
if (entity.path.endsWith('.jpg')) {
|
||||
categorySizes['thumbnails'] =
|
||||
(categorySizes['thumbnails'] ?? 0) + size;
|
||||
} else if (entity.path.endsWith('.pdf')) {
|
||||
categorySizes['pdfs'] = (categorySizes['pdfs'] ?? 0) + size;
|
||||
} else {
|
||||
categorySizes['other'] = (categorySizes['other'] ?? 0) + size;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Actualizar tamaños y porcentajes de categorías
|
||||
for (var category in _categories) {
|
||||
category.size = categorySizes[category.name] ?? 0;
|
||||
category.percentage =
|
||||
totalSize > 0 ? (category.size / totalSize) * 100 : 0;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
cacheSize = filesize(totalSize);
|
||||
isCalculatingCache = false;
|
||||
});
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
cacheSize = 'Error';
|
||||
isCalculatingCache = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _clearCache() async {
|
||||
setState(() {
|
||||
isCleaningCache = true;
|
||||
});
|
||||
|
||||
try {
|
||||
final appDir = await getApplicationDocumentsDirectory();
|
||||
final searchDir = Directory('${appDir.path}/LGCC_Search');
|
||||
|
||||
if (await searchDir.exists()) {
|
||||
await for (var entity in searchDir.list(recursive: true)) {
|
||||
if (entity is File && !entity.path.contains('/internal/')) {
|
||||
await entity.delete();
|
||||
} else if (entity is Directory &&
|
||||
!entity.path.contains('/internal/')) {
|
||||
if (await entity.exists() && entity.listSync().isEmpty) {
|
||||
await entity.delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await _calculateCacheSize();
|
||||
} catch (e) {
|
||||
// Handle error
|
||||
} finally {
|
||||
setState(() {
|
||||
isCleaningCache = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Widget para mostrar el gráfico circular
|
||||
Widget _buildStorageChart() {
|
||||
return SizedBox(
|
||||
height: 200,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
CustomPaint(
|
||||
size: const Size(200, 200),
|
||||
painter: StorageChartPainter(_categories),
|
||||
),
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
cacheSize,
|
||||
style: const TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'storage_used'.tr(),
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Widget para mostrar la lista de categorías
|
||||
Widget _buildCategoryList() {
|
||||
return Column(
|
||||
children: _categories.map((category) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 32,
|
||||
height: 32,
|
||||
decoration: BoxDecoration(
|
||||
color: category.color.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Center(
|
||||
child: Icon(
|
||||
category.icon,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
category.name.tr(),
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
filesize(category.size),
|
||||
style: TextStyle(
|
||||
color: Colors.grey[600],
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
LinearProgressIndicator(
|
||||
value: category.percentage / 100,
|
||||
backgroundColor: category.color.withOpacity(0.1),
|
||||
valueColor:
|
||||
AlwaysStoppedAnimation<Color>(category.color),
|
||||
minHeight: 4,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (_categories.last != category) const SizedBox(height: 8),
|
||||
],
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _getConfig() async {
|
||||
final hd = await ConfigService.getHdThumbnails();
|
||||
setState(() {
|
||||
hdThumbnail = hd;
|
||||
});
|
||||
final pdf = await ConfigService.getPdfDownload();
|
||||
setState(() {
|
||||
pdfDownload = pdf;
|
||||
});
|
||||
final locale = await ConfigService.getLocale();
|
||||
setState(() {
|
||||
selectedLocale = locale;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _updateConfig(String key, dynamic value) async {
|
||||
setState(() {
|
||||
if (key == 'hd_thumbnails') {
|
||||
isSavingHdThumbnail = true;
|
||||
} else if (key == 'pdf_download') {
|
||||
isSavingPdfDownload = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (key == 'hd_thumbnails') {
|
||||
await ConfigService.setHdThumbnails(value);
|
||||
} else if (key == 'pdf_download') {
|
||||
await ConfigService.setPdfDownload(value);
|
||||
} else if (key == 'locale') {
|
||||
await ConfigService.setLocale(value);
|
||||
}
|
||||
|
||||
setState(() {
|
||||
if (key == 'hd_thumbnails') {
|
||||
isSavingHdThumbnail = false;
|
||||
hdThumbnail = value;
|
||||
} else if (key == 'pdf_download') {
|
||||
isSavingPdfDownload = false;
|
||||
pdfDownload = value;
|
||||
} else if (key == 'locale') {
|
||||
selectedLocale = value;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _confirmLocaleChange(String newValue) async {
|
||||
final currentContext = context;
|
||||
final bool? confirm = await showDialog<bool>(
|
||||
context: currentContext,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text('change_language'.tr()),
|
||||
content: Text('change_language_confirm'.tr()),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
child: Text('no'.tr()),
|
||||
onPressed: () => Navigator.of(context).pop(false),
|
||||
),
|
||||
TextButton(
|
||||
child: Text('yes'.tr()),
|
||||
onPressed: () => Navigator.of(context).pop(true),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
if (confirm == true) {
|
||||
// Mostrar diálogo de progreso
|
||||
if (mounted) {
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text('updating_data'.tr()),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const CircularProgressIndicator(),
|
||||
const SizedBox(height: 16),
|
||||
Text('processing_data'.tr()),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
// Actualizar el idioma
|
||||
await _updateConfig('locale', newValue);
|
||||
|
||||
// Obtener la base de datos
|
||||
final database = await _databaseFuture;
|
||||
|
||||
// Eliminar traducciones que no correspondan al nuevo idioma
|
||||
await database.customSelect(
|
||||
'DELETE FROM translations WHERE languages_code != ?',
|
||||
variables: [drift.Variable.withString(newValue)],
|
||||
).get();
|
||||
|
||||
// Reiniciar la fecha para el nuevo idioma
|
||||
await ConfigService.setLastDate(newValue, '0');
|
||||
|
||||
if (mounted) {
|
||||
// Cerrar el diálogo de progreso
|
||||
Navigator.of(context).pop();
|
||||
|
||||
// Reiniciar la aplicación
|
||||
currentContext.setLocale(Locale(newValue)).then((_) {
|
||||
pushReplacementWithoutNavBar(context,
|
||||
MaterialPageRoute(builder: (context) => const LandingPage()));
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
// Cerrar el diálogo de progreso
|
||||
Navigator.of(context).pop();
|
||||
|
||||
// Mostrar error
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('sync_error'.tr()),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _confirmClearCache() {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text('clear_cache'.tr()),
|
||||
content: Text('clear_cache_desc'.tr()),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text('no'.tr()),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
_clearCache();
|
||||
},
|
||||
child: Text('yes'.tr()),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [Color(0xFFffffff), Color(0xFFe3ead6)],
|
||||
begin: Alignment.topRight,
|
||||
end: Alignment.bottomLeft,
|
||||
),
|
||||
),
|
||||
child: Scaffold(
|
||||
backgroundColor: Colors.transparent,
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
title: Text(
|
||||
'config'.tr(),
|
||||
style: const TextStyle(
|
||||
fontSize: 24,
|
||||
fontFamily: 'Outfit',
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
iconTheme: const IconThemeData(color: Colors.black, size: 20),
|
||||
),
|
||||
body: SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'storage_usage'.tr(),
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
cacheSize,
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0XFF6b8b66),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'storage_usage_desc'.tr(),
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
if (isCalculatingCache)
|
||||
const LinearProgressIndicator(
|
||||
backgroundColor: Color(0xFFdce2ca),
|
||||
valueColor:
|
||||
AlwaysStoppedAnimation<Color>(Color(0XFF6b8b66)),
|
||||
)
|
||||
else ...[
|
||||
_buildCategoryList(),
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.high_quality_outlined,
|
||||
size: 20,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'hd_thumbnails'.tr(),
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Switch(
|
||||
value: hdThumbnail,
|
||||
onChanged: isSavingHdThumbnail
|
||||
? null
|
||||
: (val) => _updateConfig('hd_thumbnails', val),
|
||||
activeColor: const Color(0XFF6b8b66),
|
||||
activeTrackColor: const Color(0XFFdce2ca),
|
||||
inactiveThumbColor: Colors.grey,
|
||||
inactiveTrackColor: Colors.grey.shade100,
|
||||
),
|
||||
],
|
||||
),
|
||||
Text(
|
||||
'hd_thumbnails_desc'.tr(),
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: TextButton.icon(
|
||||
onPressed: isCleaningCache ? null : _confirmClearCache,
|
||||
icon: const Icon(Icons.delete_outline),
|
||||
label: Text('clear_cache'.tr()),
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: Colors.red,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 32),
|
||||
DropdownButtonFormField<String>(
|
||||
value: selectedLocale,
|
||||
icon: const Icon(Icons.language, color: Colors.grey),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
dropdownColor: const Color(0xFFf1f4ea),
|
||||
onChanged: (newValue) {
|
||||
if (newValue != null) {
|
||||
_confirmLocaleChange(newValue);
|
||||
}
|
||||
},
|
||||
items: locales.entries.map((entry) {
|
||||
return DropdownMenuItem<String>(
|
||||
value: entry.key,
|
||||
child: Text(entry.value),
|
||||
);
|
||||
}).toList(),
|
||||
decoration: InputDecoration(
|
||||
labelText: 'locale'.tr(),
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 40),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"title".tr(),
|
||||
style: const TextStyle(
|
||||
fontSize: 52,
|
||||
fontWeight: FontWeight.bold,
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'${'version'.tr()} $_version',
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w200,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Center(
|
||||
child: SvgPicture.asset(
|
||||
'assets/svg/logo.svg',
|
||||
height: 30,
|
||||
width: 30,
|
||||
fit: BoxFit.cover,
|
||||
placeholderBuilder: (context) =>
|
||||
const CircularProgressIndicator(),
|
||||
semanticsLabel: 'Logo LGCC',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,226 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||
import 'package:percent_indicator/circular_percent_indicator.dart';
|
||||
import 'package:syncfusion_flutter_pdfviewer/pdfviewer.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'dart:io';
|
||||
|
||||
class FilePdf extends StatefulWidget {
|
||||
final String pdf;
|
||||
final String? searchTerm;
|
||||
final String title;
|
||||
|
||||
const FilePdf({
|
||||
super.key,
|
||||
required this.pdf,
|
||||
required this.title,
|
||||
this.searchTerm,
|
||||
});
|
||||
|
||||
@override
|
||||
// ignore: library_private_types_in_public_api
|
||||
_FilePdfState createState() => _FilePdfState();
|
||||
}
|
||||
|
||||
class _FilePdfState extends State<FilePdf> {
|
||||
late Future<String> _pdfPathFuture;
|
||||
late PdfViewerController _pdfViewerController;
|
||||
late PdfTextSearchResult _pdfTextSearchResult;
|
||||
final _baseUrl = dotenv.env['BASE_URL']!;
|
||||
final _token = dotenv.env['TOKEN']!;
|
||||
bool isDownloading = true;
|
||||
ValueNotifier<int> downloadProgressNotifier = ValueNotifier(0);
|
||||
bool isSearching = false;
|
||||
int downloadProgress = 0;
|
||||
Timer? _debounceTimer;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_pdfPathFuture = _fetchPdf();
|
||||
_pdfViewerController = PdfViewerController();
|
||||
_pdfTextSearchResult = PdfTextSearchResult();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_pdfTextSearchResult.removeListener(() {});
|
||||
_pdfTextSearchResult.dispose();
|
||||
_pdfViewerController.dispose();
|
||||
_debounceTimer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<String> _fetchPdf() async {
|
||||
final String pdfFileName = widget.pdf;
|
||||
final String baseUrl = '$_baseUrl/assets/$pdfFileName?access_token=$_token';
|
||||
|
||||
final appDir = await getApplicationDocumentsDirectory();
|
||||
final pdfDir = Directory(
|
||||
'${appDir.path}/LGCC_Search/${context.locale.toString()}/library/');
|
||||
if (!await pdfDir.exists()) {
|
||||
await pdfDir.create(recursive: true);
|
||||
}
|
||||
final String filePath = '${pdfDir.path}/$pdfFileName.pdf';
|
||||
if (await File(filePath).exists()) {
|
||||
return filePath;
|
||||
} else {
|
||||
try {
|
||||
await Dio().download(baseUrl, filePath,
|
||||
onReceiveProgress: (actualBytes, totalBytes) {
|
||||
final progress = (actualBytes / totalBytes * 100).floor();
|
||||
setState(() {
|
||||
downloadProgress = progress;
|
||||
downloadProgressNotifier.value = progress;
|
||||
});
|
||||
});
|
||||
setState(() {
|
||||
isDownloading = false;
|
||||
});
|
||||
return filePath;
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
print('Error downloading file: $e');
|
||||
}
|
||||
setState(() {
|
||||
isDownloading = false;
|
||||
downloadProgress = 0;
|
||||
downloadProgressNotifier.value = 0;
|
||||
});
|
||||
return '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> saveDoc() async {
|
||||
final List<int> savedBytes = await _pdfViewerController.saveDocument();
|
||||
_saveDocument(savedBytes);
|
||||
}
|
||||
|
||||
void _performSearch(String searchTerm, {bool immediate = false}) {
|
||||
if (mounted && searchTerm.isEmpty) {
|
||||
setState(() {
|
||||
isSearching = false;
|
||||
});
|
||||
}
|
||||
if (immediate) {
|
||||
_performSearchAction(searchTerm);
|
||||
} else {
|
||||
_debounceTimer?.cancel();
|
||||
_debounceTimer = Timer(const Duration(milliseconds: 300), () {
|
||||
_performSearchAction(searchTerm);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _performSearchAction(String searchTerm) {
|
||||
_pdfTextSearchResult = _pdfViewerController.searchText(searchTerm);
|
||||
_pdfTextSearchResult.addListener(() {
|
||||
if (mounted && _pdfTextSearchResult.isSearchCompleted) {
|
||||
setState(() {});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _saveDocument(List<int> dataBytes) async {
|
||||
final appDir = await getApplicationDocumentsDirectory();
|
||||
final directory = Directory(
|
||||
'${appDir.path}/LGCC_Search/${context.locale.toString()}/library/');
|
||||
final String path = directory.path;
|
||||
final File file = File('$path/${widget.pdf}.pdf');
|
||||
await file.writeAsBytes(dataBytes);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: isSearching
|
||||
? TextField(
|
||||
onChanged: (val) => _performSearch(val),
|
||||
)
|
||||
: Text(widget.title),
|
||||
backgroundColor: const Color(0xFFe0e6d1),
|
||||
actions: [
|
||||
Container(
|
||||
margin: const EdgeInsets.only(right: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
if (_pdfTextSearchResult.hasResult &&
|
||||
_pdfTextSearchResult.totalInstanceCount > 1)
|
||||
Text(
|
||||
'${_pdfTextSearchResult.currentInstanceIndex} / ${_pdfTextSearchResult.totalInstanceCount}'),
|
||||
if (_pdfTextSearchResult.hasResult &&
|
||||
_pdfTextSearchResult.totalInstanceCount > 1)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.chevron_left),
|
||||
onPressed: (_pdfTextSearchResult.currentInstanceIndex > 1)
|
||||
? () {
|
||||
_pdfTextSearchResult.previousInstance();
|
||||
}
|
||||
: null,
|
||||
),
|
||||
if (_pdfTextSearchResult.hasResult &&
|
||||
_pdfTextSearchResult.totalInstanceCount > 1)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.chevron_right),
|
||||
onPressed: (_pdfTextSearchResult.currentInstanceIndex <
|
||||
_pdfTextSearchResult.totalInstanceCount)
|
||||
? () {
|
||||
_pdfTextSearchResult.nextInstance();
|
||||
}
|
||||
: null,
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(isSearching ? Icons.close : Icons.search),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
isSearching = !isSearching;
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
]),
|
||||
body: FutureBuilder<String>(
|
||||
future: _pdfPathFuture,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
return Center(
|
||||
child: CircularPercentIndicator(
|
||||
backgroundColor: Colors.grey.shade400,
|
||||
progressColor: const Color(0xFF6b8e23),
|
||||
lineWidth: 3,
|
||||
radius: 32,
|
||||
center: Text(downloadProgress.toString(),
|
||||
style: const TextStyle(fontSize: 14, color: Colors.black)),
|
||||
percent: downloadProgress / 100,
|
||||
),
|
||||
);
|
||||
} else if (snapshot.hasError) {
|
||||
return Center(child: Text('no_results'.tr()));
|
||||
} else if (snapshot.hasData) {
|
||||
return SfPdfViewer.file(
|
||||
File(snapshot.data!),
|
||||
controller: _pdfViewerController,
|
||||
onDocumentLoaded: (PdfDocumentLoadedDetails details) {
|
||||
if (widget.searchTerm != null) {
|
||||
_performSearch(widget.searchTerm!, immediate: true);
|
||||
}
|
||||
},
|
||||
onAnnotationAdded: (annotation) => saveDoc(),
|
||||
);
|
||||
} else {
|
||||
return const Center(child: Text('Error'));
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
import 'dart:io';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class ConfigService {
|
||||
static const String _localeKey = 'locale';
|
||||
static const String _hdThumbnailsKey = 'hd_thumbnails';
|
||||
static const String _pdfDownloadKey = 'pdf_download';
|
||||
static const String _lastDateKey = 'last_date';
|
||||
|
||||
static Future<String> getLocale() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final deviceLocale = Platform.localeName.split('_')[0];
|
||||
return prefs.getString(_localeKey) ?? deviceLocale;
|
||||
}
|
||||
|
||||
static Future<void> setLocale(String locale) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(_localeKey, locale);
|
||||
}
|
||||
|
||||
static Future<bool> getHdThumbnails() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getBool(_hdThumbnailsKey) ?? true;
|
||||
}
|
||||
|
||||
static Future<void> setHdThumbnails(bool value) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool(_hdThumbnailsKey, value);
|
||||
}
|
||||
|
||||
static Future<bool> getPdfDownload() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getBool(_pdfDownloadKey) ?? true;
|
||||
}
|
||||
|
||||
static Future<void> setPdfDownload(bool value) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool(_pdfDownloadKey, value);
|
||||
}
|
||||
|
||||
static Future<String> getLastDate(String locale) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getString('$_lastDateKey-$locale') ?? '0';
|
||||
}
|
||||
|
||||
static Future<void> setLastDate(String locale, String date) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString('$_lastDateKey-$locale', date);
|
||||
}
|
||||
|
||||
static Future<void> initializeConfig() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
|
||||
// Initialize default values if they don't exist
|
||||
if (!prefs.containsKey(_hdThumbnailsKey)) {
|
||||
await setHdThumbnails(true);
|
||||
}
|
||||
if (!prefs.containsKey(_pdfDownloadKey)) {
|
||||
await setPdfDownload(true);
|
||||
}
|
||||
if (!prefs.containsKey(_localeKey)) {
|
||||
await setLocale(Platform.localeName.split('_')[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,381 @@
|
|||
import 'dart:io';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:live_activities/live_activities.dart';
|
||||
|
||||
class LiveActivitiesService {
|
||||
// Use a singleton pattern
|
||||
static final LiveActivitiesService _instance =
|
||||
LiveActivitiesService._internal();
|
||||
|
||||
// LiveActivities plugin instance
|
||||
final LiveActivities _liveActivities = LiveActivities();
|
||||
bool _isInitialized = false;
|
||||
bool _isSupported = false;
|
||||
String? _appGroupId;
|
||||
String? _urlScheme;
|
||||
|
||||
factory LiveActivitiesService() => _instance;
|
||||
LiveActivitiesService._internal();
|
||||
|
||||
/// Initialize the Live Activities service with specific app group ID and URL scheme
|
||||
Future<bool> init(
|
||||
{required String appGroupId, required String urlScheme}) async {
|
||||
debugPrint(
|
||||
'LiveActivitiesService: Initializing with appGroupId: $appGroupId, urlScheme: $urlScheme');
|
||||
|
||||
// Store directly in the instance
|
||||
_appGroupId = appGroupId;
|
||||
_urlScheme = urlScheme;
|
||||
|
||||
// Initialize the plugin with the app group ID
|
||||
try {
|
||||
// Using the init method of the plugin with appGroupId
|
||||
await _liveActivities.init(appGroupId: appGroupId);
|
||||
debugPrint(
|
||||
'LiveActivitiesService: Initialized plugin with appGroupId: $appGroupId');
|
||||
} catch (e) {
|
||||
debugPrint('LiveActivitiesService: Error initializing plugin: $e');
|
||||
}
|
||||
|
||||
final result = await initialize();
|
||||
|
||||
// Debug verification
|
||||
debugPrint(
|
||||
'LiveActivitiesService: After init - appGroupId: $_appGroupId, initialized: $_isInitialized, supported: $_isSupported');
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Initialize the Live Activities service
|
||||
Future<bool> initialize() async {
|
||||
if (_isInitialized) {
|
||||
debugPrint(
|
||||
'LiveActivitiesService: Already initialized, supported: $_isSupported, appGroupId: $_appGroupId');
|
||||
return _isSupported;
|
||||
}
|
||||
|
||||
// Live Activities are only supported on iOS 16.1+
|
||||
if (!Platform.isIOS) {
|
||||
debugPrint(
|
||||
'LiveActivitiesService: Platform is not iOS, Live Activities not supported');
|
||||
_isSupported = false;
|
||||
_isInitialized = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// Request permissions explicitly
|
||||
final permissionGranted = await _requestLiveActivitiesPermission();
|
||||
if (!permissionGranted) {
|
||||
debugPrint('LiveActivitiesService: Permission not granted');
|
||||
_isSupported = false;
|
||||
_isInitialized = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if Live Activities are supported on this device
|
||||
_isSupported = await _liveActivities.areActivitiesEnabled();
|
||||
_isInitialized = true;
|
||||
|
||||
debugPrint(
|
||||
'LiveActivitiesService: Initialized, supported: $_isSupported, appGroupId: $_appGroupId');
|
||||
return _isSupported;
|
||||
} catch (e) {
|
||||
debugPrint('LiveActivitiesService: Error initializing: $e');
|
||||
_isSupported = false;
|
||||
_isInitialized = true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Request permission for Live Activities
|
||||
Future<bool> _requestLiveActivitiesPermission() async {
|
||||
try {
|
||||
// Check if permission is already granted
|
||||
bool alreadyGranted = await _liveActivities.areActivitiesEnabled();
|
||||
if (alreadyGranted) {
|
||||
debugPrint('LiveActivitiesService: Permission already granted');
|
||||
return true;
|
||||
}
|
||||
|
||||
// iOS 16.1+ requires the user to explicitly enable Live Activities
|
||||
// There's no direct permission API, the user must enable it in Settings
|
||||
// We can only check if it's enabled, not request it directly
|
||||
debugPrint(
|
||||
'LiveActivitiesService: Checking Live Activities availability');
|
||||
final isEnabled = await _liveActivities.areActivitiesEnabled();
|
||||
|
||||
if (!isEnabled) {
|
||||
debugPrint(
|
||||
'LiveActivitiesService: Live Activities not enabled. User needs to enable in Settings');
|
||||
}
|
||||
|
||||
return isEnabled;
|
||||
} catch (e) {
|
||||
debugPrint('LiveActivitiesService: Error checking permission: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Get appGroupId safely, ensuring it's available
|
||||
String? _getAppGroupId() {
|
||||
if (_appGroupId == null) {
|
||||
debugPrint(
|
||||
'LiveActivitiesService: Warning - appGroupId is null. Please call init() first.');
|
||||
debugPrint(
|
||||
'LiveActivitiesService: Current state: initialized=$_isInitialized, supported=$_isSupported');
|
||||
}
|
||||
return _appGroupId;
|
||||
}
|
||||
|
||||
/// Helper method to create a search activity
|
||||
Future<String?> createSearchActivity({
|
||||
required String title,
|
||||
required String query,
|
||||
required String count,
|
||||
}) async {
|
||||
if (!_isInitialized) await initialize();
|
||||
if (!_isSupported) return null;
|
||||
|
||||
try {
|
||||
// Use instance appGroupId
|
||||
final String? appGroupId = _getAppGroupId();
|
||||
|
||||
// Verify appGroupId is set
|
||||
if (appGroupId == null) {
|
||||
throw Exception('appGroupId is null. Please call init() first.');
|
||||
}
|
||||
|
||||
// Always reinitialize with the current app group ID to ensure it's set correctly
|
||||
await _liveActivities.init(appGroupId: appGroupId);
|
||||
debugPrint(
|
||||
'LiveActivitiesService: Ensuring plugin initialized with appGroupId: $appGroupId');
|
||||
|
||||
debugPrint(
|
||||
'LiveActivitiesService: Creating activity with appGroupId: $appGroupId');
|
||||
|
||||
// Create data for the activity
|
||||
final Map<String, dynamic> activityData = {
|
||||
'title': title,
|
||||
'query': query,
|
||||
'resultsCount': count,
|
||||
'timestamp': DateTime.now().toIso8601String(),
|
||||
'appGroup': appGroupId,
|
||||
};
|
||||
|
||||
// Add urlScheme if available
|
||||
if (_urlScheme != null) {
|
||||
activityData['urlScheme'] = _urlScheme;
|
||||
}
|
||||
|
||||
debugPrint('LiveActivitiesService: Activity data: $activityData');
|
||||
|
||||
// Create the activity
|
||||
final activityId = await _liveActivities.createActivity(activityData);
|
||||
|
||||
debugPrint(
|
||||
'LiveActivitiesService: Created activity with ID: $activityId');
|
||||
|
||||
// Get current active activities count
|
||||
final activities = await _liveActivities.getAllActivitiesIds();
|
||||
debugPrint(
|
||||
'LiveActivitiesService: Active activities count: ${activities?.length ?? 0}');
|
||||
|
||||
return activityId;
|
||||
} catch (e) {
|
||||
debugPrint('LiveActivitiesService: Error creating activity: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Update an existing search activity
|
||||
Future<bool> updateSearchActivity({
|
||||
required String activityId,
|
||||
required String title,
|
||||
required String query,
|
||||
required String count,
|
||||
}) async {
|
||||
if (!_isInitialized) await initialize();
|
||||
if (!_isSupported) return false;
|
||||
|
||||
try {
|
||||
// Use instance appGroupId
|
||||
final String? appGroupId = _getAppGroupId();
|
||||
|
||||
// Verify appGroupId is set
|
||||
if (appGroupId == null) {
|
||||
throw Exception('appGroupId is null. Please call init() first.');
|
||||
}
|
||||
|
||||
// Always reinitialize with the current app group ID to ensure it's set correctly
|
||||
await _liveActivities.init(appGroupId: appGroupId);
|
||||
debugPrint(
|
||||
'LiveActivitiesService: Ensuring plugin initialized with appGroupId: $appGroupId');
|
||||
|
||||
// Create updated data for the activity
|
||||
final Map<String, dynamic> activityData = {
|
||||
'title': title,
|
||||
'query': query,
|
||||
'resultsCount': count,
|
||||
'timestamp': DateTime.now().toIso8601String(),
|
||||
'appGroup': appGroupId,
|
||||
};
|
||||
|
||||
// Add urlScheme if available
|
||||
if (_urlScheme != null) {
|
||||
activityData['urlScheme'] = _urlScheme;
|
||||
}
|
||||
|
||||
// Update the activity
|
||||
final success =
|
||||
await _liveActivities.updateActivity(activityId, activityData);
|
||||
|
||||
debugPrint(
|
||||
'LiveActivitiesService: Updated activity $activityId: $success');
|
||||
return success ?? false;
|
||||
} catch (e) {
|
||||
debugPrint('LiveActivitiesService: Error updating activity: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// End an existing search activity
|
||||
Future<bool> endSearchActivity(String activityId) async {
|
||||
if (!_isInitialized) await initialize();
|
||||
if (!_isSupported) return false;
|
||||
|
||||
try {
|
||||
// Get the appGroupId
|
||||
final String? appGroupId = _getAppGroupId();
|
||||
|
||||
// Verify appGroupId is set
|
||||
if (appGroupId == null) {
|
||||
throw Exception('appGroupId is null. Please call init() first.');
|
||||
}
|
||||
|
||||
// Always reinitialize with the current app group ID to ensure it's set correctly
|
||||
await _liveActivities.init(appGroupId: appGroupId);
|
||||
debugPrint(
|
||||
'LiveActivitiesService: Ensuring plugin initialized with appGroupId: $appGroupId');
|
||||
|
||||
debugPrint('LiveActivitiesService: Ending activity $activityId');
|
||||
|
||||
// End the activity
|
||||
final success = await _liveActivities.endActivity(activityId);
|
||||
|
||||
debugPrint('LiveActivitiesService: Ended activity $activityId: $success');
|
||||
return success ?? false;
|
||||
} catch (e) {
|
||||
debugPrint('LiveActivitiesService: Error ending activity: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Get all active activities
|
||||
Future<List<String>?> getActiveActivities() async {
|
||||
if (!_isInitialized) await initialize();
|
||||
if (!_isSupported) return null;
|
||||
|
||||
try {
|
||||
// Get the appGroupId
|
||||
final String? appGroupId = _getAppGroupId();
|
||||
|
||||
// Verify appGroupId is set
|
||||
if (appGroupId == null) {
|
||||
throw Exception('appGroupId is null. Please call init() first.');
|
||||
}
|
||||
|
||||
// Always reinitialize with the current app group ID to ensure it's set correctly
|
||||
await _liveActivities.init(appGroupId: appGroupId);
|
||||
debugPrint(
|
||||
'LiveActivitiesService: Ensuring plugin initialized with appGroupId: $appGroupId');
|
||||
|
||||
final activities = await _liveActivities.getAllActivitiesIds();
|
||||
debugPrint(
|
||||
'LiveActivitiesService: Found ${activities?.length ?? 0} active activities');
|
||||
return activities;
|
||||
} catch (e) {
|
||||
debugPrint('LiveActivitiesService: Error getting active activities: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// End all active activities
|
||||
Future<bool> endAllActivities() async {
|
||||
if (!_isInitialized) await initialize();
|
||||
if (!_isSupported) return false;
|
||||
|
||||
try {
|
||||
// Get the appGroupId
|
||||
final String? appGroupId = _getAppGroupId();
|
||||
|
||||
// Verify appGroupId is set
|
||||
if (appGroupId == null) {
|
||||
throw Exception('appGroupId is null. Please call init() first.');
|
||||
}
|
||||
|
||||
// Always reinitialize with the current app group ID to ensure it's set correctly
|
||||
await _liveActivities.init(appGroupId: appGroupId);
|
||||
debugPrint(
|
||||
'LiveActivitiesService: Ensuring plugin initialized with appGroupId: $appGroupId');
|
||||
|
||||
// First try using the native plugin method if available
|
||||
final success = await _liveActivities.endAllActivities();
|
||||
if (success == true) {
|
||||
debugPrint(
|
||||
'LiveActivitiesService: Ended all activities using plugin method');
|
||||
return true;
|
||||
}
|
||||
|
||||
// Fallback to manual method if plugin doesn't support endAllActivities
|
||||
final activities = await getActiveActivities();
|
||||
if (activities == null || activities.isEmpty) {
|
||||
debugPrint('LiveActivitiesService: No active activities to end');
|
||||
return true;
|
||||
}
|
||||
|
||||
bool allEnded = true;
|
||||
for (final activityId in activities) {
|
||||
final ended = await endSearchActivity(activityId);
|
||||
if (!ended) {
|
||||
allEnded = false;
|
||||
debugPrint(
|
||||
'LiveActivitiesService: Failed to end activity $activityId');
|
||||
}
|
||||
}
|
||||
|
||||
debugPrint(
|
||||
'LiveActivitiesService: Ended ${activities.length} activities manually');
|
||||
return allEnded;
|
||||
} catch (e) {
|
||||
debugPrint('LiveActivitiesService: Error ending all activities: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Request notification permission if needed
|
||||
Future<bool> requestNotificationPermission() async {
|
||||
if (!Platform.isIOS) return false;
|
||||
|
||||
try {
|
||||
debugPrint('LiveActivitiesService: Requesting notification permission');
|
||||
// The LiveActivities plugin doesn't have a direct method to request notification permission
|
||||
// This is usually handled by the main app's notification system
|
||||
return true;
|
||||
} catch (e) {
|
||||
debugPrint(
|
||||
'LiveActivitiesService: Error requesting notification permission: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if Live Activities are supported on this device
|
||||
bool get isSupported => _isSupported;
|
||||
|
||||
/// Check if Live Activities are initialized
|
||||
bool get isInitialized => _isInitialized;
|
||||
|
||||
/// Get the current appGroupId
|
||||
String? get appGroupId => _appGroupId;
|
||||
}
|
||||
|
|
@ -0,0 +1,343 @@
|
|||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter_mimir/flutter_mimir.dart';
|
||||
import 'package:search_engine/database.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
typedef ProgressCallback = void Function(String message, double progress);
|
||||
|
||||
class MimirService {
|
||||
static final MimirService _instance = MimirService._internal();
|
||||
factory MimirService() => _instance;
|
||||
MimirService._internal();
|
||||
|
||||
late MimirIndex _index;
|
||||
bool _isInitialized = false;
|
||||
|
||||
Future<void> initialize() async {
|
||||
if (_isInitialized) return;
|
||||
|
||||
final instance = await Mimir.defaultInstance;
|
||||
_index = instance.getIndex('messages');
|
||||
_isInitialized = true;
|
||||
}
|
||||
|
||||
Future<int> getDocumentCount() async {
|
||||
if (!_isInitialized) await initialize();
|
||||
final stats = await _index.getAllDocuments();
|
||||
return stats.length;
|
||||
}
|
||||
|
||||
Future<List<Map<String, dynamic>>> getAllDocuments() async {
|
||||
if (!_isInitialized) await initialize();
|
||||
return await _index.getAllDocuments();
|
||||
}
|
||||
|
||||
Future<void> addDocument(
|
||||
String id, String languagesCode, String content) async {
|
||||
if (!_isInitialized) await initialize();
|
||||
|
||||
await _index.addDocument({
|
||||
'id': id,
|
||||
'languages_code': languagesCode,
|
||||
'content': content,
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> addDocuments(List<Map<String, String>> documents) async {
|
||||
if (!_isInitialized) await initialize();
|
||||
|
||||
await _index.addDocuments(documents);
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> search(String query, String languagesCode,
|
||||
{int limit = 1000, int offset = 0}) async {
|
||||
if (!_isInitialized) await initialize();
|
||||
|
||||
// Execute the search without pagination to get ALL matching results
|
||||
final allResults = await _index.search(
|
||||
query: query,
|
||||
filter: Mimir.where('languages_code', isEqualTo: languagesCode),
|
||||
);
|
||||
|
||||
if (kDebugMode && allResults.isNotEmpty) {
|
||||
print('🔍 Mimir encontró ${allResults.length} resultados totales');
|
||||
}
|
||||
|
||||
// Apply pagination AFTER getting all results
|
||||
final start = offset;
|
||||
final end = offset + limit;
|
||||
final paginatedResults = allResults.sublist(
|
||||
start.clamp(0, allResults.length),
|
||||
end.clamp(0, allResults.length),
|
||||
);
|
||||
|
||||
// Transform results to include both id and content snippet
|
||||
final transformedResults = paginatedResults.map((doc) {
|
||||
final String content = doc['content'] as String;
|
||||
// Extract a snippet of content and position information
|
||||
final Map<String, dynamic> snippetInfo =
|
||||
_extractSnippetWithPosition(content, query);
|
||||
|
||||
return {
|
||||
'id': doc['id'] as String,
|
||||
'content': snippetInfo['snippet'] as String,
|
||||
'position': snippetInfo['position'].toString(),
|
||||
'length': snippetInfo['length'].toString(),
|
||||
};
|
||||
}).toList();
|
||||
|
||||
// Return both the paginated results and the total count
|
||||
return {
|
||||
'results': transformedResults,
|
||||
'total': allResults.length,
|
||||
'allResultIds': allResults.map((doc) => doc['id'] as String).toList(),
|
||||
};
|
||||
}
|
||||
|
||||
// Helper method to extract a relevant snippet from content with position information
|
||||
Map<String, dynamic> _extractSnippetWithPosition(
|
||||
String content, String query) {
|
||||
// Clean HTML tags and entities first
|
||||
String cleanContent = _cleanHtmlContent(content);
|
||||
|
||||
// Default values
|
||||
int snippetPosition = 0;
|
||||
int matchLength = 0;
|
||||
|
||||
// If content is short enough, return it all
|
||||
if (cleanContent.length <= 300) {
|
||||
return {
|
||||
'snippet': cleanContent,
|
||||
'position': 0,
|
||||
'length': cleanContent.length
|
||||
};
|
||||
}
|
||||
|
||||
// Try to find the query in the content
|
||||
final lowerContent = cleanContent.toLowerCase();
|
||||
final lowerQuery = query.toLowerCase();
|
||||
final int position = lowerContent.indexOf(lowerQuery);
|
||||
|
||||
if (position != -1) {
|
||||
// Found the query, extract a snippet around it
|
||||
final int start = (position - 100).clamp(0, cleanContent.length);
|
||||
final int end =
|
||||
(position + query.length + 100).clamp(0, cleanContent.length);
|
||||
|
||||
String snippet = cleanContent.substring(start, end);
|
||||
|
||||
// Add ellipsis if needed
|
||||
if (start > 0) snippet = '...$snippet';
|
||||
if (end < cleanContent.length) snippet = '$snippet...';
|
||||
|
||||
// Calculate relative position in the snippet
|
||||
snippetPosition = position - start;
|
||||
if (start > 0) snippetPosition += 3; // Adjust for ellipsis
|
||||
matchLength = query.length;
|
||||
|
||||
return {
|
||||
'snippet': snippet,
|
||||
'position': snippetPosition,
|
||||
'length': matchLength
|
||||
};
|
||||
} else {
|
||||
// Query not found directly, try with individual words
|
||||
final List<String> queryWords =
|
||||
query.split(' ').where((word) => word.trim().length > 2).toList();
|
||||
|
||||
for (final word in queryWords) {
|
||||
final int wordPos = lowerContent.indexOf(word.toLowerCase());
|
||||
if (wordPos != -1) {
|
||||
final int start = (wordPos - 100).clamp(0, cleanContent.length);
|
||||
final int end =
|
||||
(wordPos + word.length + 100).clamp(0, cleanContent.length);
|
||||
|
||||
String snippet = cleanContent.substring(start, end);
|
||||
|
||||
if (start > 0) snippet = '...$snippet';
|
||||
if (end < cleanContent.length) snippet = '$snippet...';
|
||||
|
||||
// Calculate relative position in the snippet
|
||||
snippetPosition = wordPos - start;
|
||||
if (start > 0) snippetPosition += 3; // Adjust for ellipsis
|
||||
matchLength = word.length;
|
||||
|
||||
return {
|
||||
'snippet': snippet,
|
||||
'position': snippetPosition,
|
||||
'length': matchLength
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// No match found, return the beginning of the content
|
||||
return {
|
||||
'snippet': '${cleanContent.substring(0, 300)}...',
|
||||
'position': 0,
|
||||
'length': 0
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Helper method to extract a snippet without position information (legacy)
|
||||
String _extractSnippet(String content, String query) {
|
||||
return _extractSnippetWithPosition(content, query)['snippet'] as String;
|
||||
}
|
||||
|
||||
// Helper method to clean HTML content using regex
|
||||
String _cleanHtmlContent(String html) {
|
||||
if (html.isEmpty) return '';
|
||||
|
||||
// Step 1: Remove HTML tags
|
||||
String result = html.replaceAll(RegExp(r'<[^>]*>'), ' ');
|
||||
|
||||
// Step 2: Replace common HTML entities
|
||||
final Map<String, String> htmlEntities = {
|
||||
' ': ' ',
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
''': "'",
|
||||
'¢': '¢',
|
||||
'£': '£',
|
||||
'¥': '¥',
|
||||
'€': '€',
|
||||
'©': '©',
|
||||
'®': '®',
|
||||
'á': 'á',
|
||||
'é': 'é',
|
||||
'í': 'í',
|
||||
'ó': 'ó',
|
||||
'ú': 'ú',
|
||||
'ñ': 'ñ',
|
||||
'Á': 'Á',
|
||||
'É': 'É',
|
||||
'Í': 'Í',
|
||||
'Ó': 'Ó',
|
||||
'Ú': 'Ú',
|
||||
'Ñ': 'Ñ',
|
||||
};
|
||||
|
||||
// Replace known HTML entities
|
||||
htmlEntities.forEach((entity, replacement) {
|
||||
result = result.replaceAll(entity, replacement);
|
||||
});
|
||||
|
||||
// Step 3: Replace numeric HTML entities (like {)
|
||||
result = result.replaceAllMapped(RegExp(r'&#(\d+);'), (match) {
|
||||
try {
|
||||
final int charCode = int.parse(match.group(1)!);
|
||||
return String.fromCharCode(charCode);
|
||||
} catch (e) {
|
||||
return '';
|
||||
}
|
||||
});
|
||||
|
||||
// Step 4: Replace any remaining entities with a generic pattern
|
||||
result = result.replaceAll(RegExp(r'&[a-zA-Z0-9]+;'), '');
|
||||
|
||||
// Step 5: Normalize whitespace (replace multiple spaces with a single space)
|
||||
result = result.replaceAll(RegExp(r'\s+'), ' ').trim();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
Future<void> deleteDocument(String id) async {
|
||||
if (!_isInitialized) await initialize();
|
||||
|
||||
await _index.deleteDocument(id);
|
||||
}
|
||||
|
||||
Future<void> clearIndex() async {
|
||||
if (!_isInitialized) await initialize();
|
||||
|
||||
await _index.deleteAllDocuments();
|
||||
}
|
||||
|
||||
Future<void> syncWithDatabase({ProgressCallback? onProgress}) async {
|
||||
if (!_isInitialized) await initialize();
|
||||
|
||||
final database = AppDatabase();
|
||||
try {
|
||||
// Get all documents from Mimir
|
||||
final mimirDocuments = await getAllDocuments();
|
||||
final mimirCount = mimirDocuments.length;
|
||||
|
||||
// Create a set of document IDs that are already in Mimir
|
||||
final mimirIds =
|
||||
Set<String>.from(mimirDocuments.map((doc) => doc['id'] as String));
|
||||
|
||||
// Get all messages from database
|
||||
final messages = await database.getAllMessages();
|
||||
final dbCount = messages.length;
|
||||
|
||||
if (kDebugMode) {
|
||||
print('📊 Documentos en Mimir: $mimirCount');
|
||||
print('📊 Documentos en Base de Datos: $dbCount');
|
||||
}
|
||||
|
||||
// Find documents that need to be added (in DB but not in Mimir)
|
||||
final documentsToAdd = messages.where((draft) {
|
||||
return !mimirIds.contains(draft.id) &&
|
||||
draft.body != null &&
|
||||
draft.body!.isNotEmpty;
|
||||
}).toList();
|
||||
|
||||
if (documentsToAdd.isNotEmpty) {
|
||||
if (kDebugMode) {
|
||||
print('🔄 Documentos a agregar a Mimir: ${documentsToAdd.length}');
|
||||
}
|
||||
|
||||
onProgress?.call('updating_search_index'.tr(), 0.0);
|
||||
|
||||
// Prepare documents for Mimir
|
||||
final documents = documentsToAdd.map((draft) {
|
||||
return {
|
||||
'id': draft.id,
|
||||
'languages_code': draft.languagesCode,
|
||||
'content': draft.body!,
|
||||
};
|
||||
}).toList();
|
||||
|
||||
if (kDebugMode) {
|
||||
print('📝 Documentos válidos para indexar: ${documents.length}');
|
||||
}
|
||||
|
||||
// Add documents to Mimir in batches
|
||||
const batchSize = 50;
|
||||
var indexedCount = 0;
|
||||
for (var i = 0; i < documents.length; i += batchSize) {
|
||||
final end = (i + batchSize < documents.length)
|
||||
? i + batchSize
|
||||
: documents.length;
|
||||
final batch = documents.sublist(i, end);
|
||||
await addDocuments(batch);
|
||||
indexedCount += batch.length;
|
||||
|
||||
// Update progress
|
||||
final progress = indexedCount / documents.length;
|
||||
onProgress?.call('updating_search_index'.tr(), progress);
|
||||
|
||||
if (kDebugMode) {
|
||||
print(
|
||||
'✓ Progreso de indexación: $indexedCount/${documents.length}');
|
||||
}
|
||||
}
|
||||
|
||||
if (kDebugMode) {
|
||||
final finalCount = await getDocumentCount();
|
||||
print('✅ Indexación completada. Documentos en Mimir: $finalCount');
|
||||
}
|
||||
|
||||
onProgress?.call('search_index_updated'.tr(), 1.0);
|
||||
} else {
|
||||
if (kDebugMode) {
|
||||
print('✅ Mimir y Base de Datos están sincronizados');
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
database.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,198 @@
|
|||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
import 'package:search_engine/services/live_activities_service.dart';
|
||||
|
||||
class NotificationService {
|
||||
static final NotificationService _instance = NotificationService._internal();
|
||||
factory NotificationService() => _instance;
|
||||
NotificationService._internal();
|
||||
|
||||
final FlutterLocalNotificationsPlugin _notifications =
|
||||
FlutterLocalNotificationsPlugin();
|
||||
bool _isLiveActivitiesSupported = false;
|
||||
bool _isLiveActivitiesInitialized = false;
|
||||
|
||||
// Variables para manejo condicional de LiveActivities
|
||||
dynamic _liveActivities;
|
||||
|
||||
// Method to create LiveActivities instance
|
||||
dynamic createLiveActivitiesInstance() {
|
||||
if (Platform.isIOS) {
|
||||
return LiveActivitiesService();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<void> initialize() async {
|
||||
// Inicializar notificaciones normales
|
||||
const initializationSettingsAndroid =
|
||||
AndroidInitializationSettings('@mipmap/ic_launcher');
|
||||
const initializationSettingsIOS = DarwinInitializationSettings(
|
||||
requestAlertPermission: true,
|
||||
requestBadgePermission: true,
|
||||
requestSoundPermission: true,
|
||||
);
|
||||
const initializationSettings = InitializationSettings(
|
||||
android: initializationSettingsAndroid,
|
||||
iOS: initializationSettingsIOS,
|
||||
);
|
||||
await _notifications.initialize(initializationSettings);
|
||||
|
||||
// Inicializar Live Activities solo en iOS
|
||||
if (Platform.isIOS) {
|
||||
try {
|
||||
await _initializeLiveActivities();
|
||||
} catch (e) {
|
||||
print('Error al inicializar Live Activities: $e');
|
||||
_isLiveActivitiesSupported = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _initializeLiveActivities() async {
|
||||
try {
|
||||
// Intentamos crear una instancia de LiveActivities de forma segura
|
||||
_liveActivities = createLiveActivitiesInstance();
|
||||
|
||||
if (_liveActivities != null) {
|
||||
try {
|
||||
await _liveActivities.init(
|
||||
appGroupId:
|
||||
'group.com.lgcc.search', // Reemplazar con tu App Group ID
|
||||
urlScheme: 'lgcc' // Reemplazar con tu URL scheme
|
||||
);
|
||||
|
||||
// Verificamos si están habilitadas
|
||||
_isLiveActivitiesSupported =
|
||||
await _liveActivities.areActivitiesEnabled();
|
||||
_isLiveActivitiesInitialized = true;
|
||||
|
||||
print('LiveActivities inicializadas: $_isLiveActivitiesSupported');
|
||||
} catch (e) {
|
||||
print('Error al inicializar LiveActivities: $e');
|
||||
_isLiveActivitiesSupported = false;
|
||||
_isLiveActivitiesInitialized = false;
|
||||
}
|
||||
} else {
|
||||
print('LiveActivities no disponible en este dispositivo');
|
||||
_isLiveActivitiesSupported = false;
|
||||
_isLiveActivitiesInitialized = false;
|
||||
}
|
||||
} catch (e) {
|
||||
print('Error al crear instancia de LiveActivities: $e');
|
||||
_isLiveActivitiesSupported = false;
|
||||
_isLiveActivitiesInitialized = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> showSearchNotification({
|
||||
required String title,
|
||||
required String body,
|
||||
String? searchTerm,
|
||||
int? totalResults,
|
||||
}) async {
|
||||
if (_isLiveActivitiesSupported &&
|
||||
_isLiveActivitiesInitialized &&
|
||||
Platform.isIOS &&
|
||||
_liveActivities != null) {
|
||||
// Usar Live Activities en iOS 16.1+
|
||||
try {
|
||||
final Map<String, dynamic> activityModel = {
|
||||
'title': title,
|
||||
'body': body,
|
||||
'searchTerm': searchTerm ?? '',
|
||||
'totalResults': totalResults ?? 0,
|
||||
'timestamp': DateTime.now().millisecondsSinceEpoch,
|
||||
};
|
||||
|
||||
final activityId = await _liveActivities.createActivity(activityModel);
|
||||
print('LiveActivity creada con éxito: $activityId');
|
||||
return;
|
||||
} catch (e) {
|
||||
print('Error al crear LiveActivity: $e');
|
||||
// Si falla, usamos notificación estándar como fallback
|
||||
}
|
||||
}
|
||||
|
||||
// Usar notificación normal en Android o iOS < 16.1 o si falló Live Activities
|
||||
await _showNormalNotification(title: title, body: body);
|
||||
}
|
||||
|
||||
Future<void> _showNormalNotification({
|
||||
required String title,
|
||||
required String body,
|
||||
}) async {
|
||||
const androidDetails = AndroidNotificationDetails(
|
||||
'search_channel',
|
||||
'Search Notifications',
|
||||
channelDescription: 'Notifications for search results',
|
||||
importance: Importance.high,
|
||||
priority: Priority.high,
|
||||
);
|
||||
|
||||
const iosDetails = DarwinNotificationDetails(
|
||||
presentAlert: true,
|
||||
presentBadge: true,
|
||||
presentSound: true,
|
||||
);
|
||||
|
||||
const details = NotificationDetails(
|
||||
android: androidDetails,
|
||||
iOS: iosDetails,
|
||||
);
|
||||
|
||||
await _notifications.show(
|
||||
DateTime.now().millisecond,
|
||||
title,
|
||||
body,
|
||||
details,
|
||||
);
|
||||
|
||||
print('Notificación estándar mostrada');
|
||||
}
|
||||
|
||||
Future<void> updateSearchActivity({
|
||||
required String activityId,
|
||||
required Map<String, dynamic> data,
|
||||
}) async {
|
||||
if (_isLiveActivitiesSupported &&
|
||||
_isLiveActivitiesInitialized &&
|
||||
Platform.isIOS &&
|
||||
_liveActivities != null) {
|
||||
try {
|
||||
await _liveActivities.updateActivity(activityId, data);
|
||||
print('LiveActivity actualizada: $activityId');
|
||||
} catch (e) {
|
||||
print('Error al actualizar LiveActivity: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> endSearchActivity(String activityId) async {
|
||||
if (_isLiveActivitiesSupported &&
|
||||
_isLiveActivitiesInitialized &&
|
||||
Platform.isIOS &&
|
||||
_liveActivities != null) {
|
||||
try {
|
||||
await _liveActivities.endActivity(activityId);
|
||||
print('LiveActivity finalizada: $activityId');
|
||||
} catch (e) {
|
||||
print('Error al finalizar LiveActivity: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void dispose() async {
|
||||
if (_isLiveActivitiesSupported &&
|
||||
_isLiveActivitiesInitialized &&
|
||||
Platform.isIOS &&
|
||||
_liveActivities != null) {
|
||||
try {
|
||||
await _liveActivities.dispose();
|
||||
} catch (e) {
|
||||
print('Error al liberar LiveActivities: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
import 'package:easy_localization/easy_localization.dart';
|
||||
|
||||
String formatDate(DateTime date, String locale) {
|
||||
final formatter =
|
||||
DateFormat('EEEE, d \'de\' MMMM \'del\' y', locale);
|
||||
String formattedDate = formatter.format(date);
|
||||
formattedDate =
|
||||
formattedDate.replaceRange(0, 1, formattedDate[0].toUpperCase());
|
||||
formattedDate = formattedDate.replaceRange(
|
||||
formattedDate.indexOf(' de ') + 4,
|
||||
formattedDate.indexOf(' de ') + 5,
|
||||
formattedDate[formattedDate.indexOf(' de ') + 4].toUpperCase());
|
||||
|
||||
return formattedDate;
|
||||
}
|
||||
|
|
@ -0,0 +1,315 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:search_engine/screens/config.dart';
|
||||
import 'package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart'
|
||||
as nav;
|
||||
|
||||
class BaseScreen extends StatelessWidget {
|
||||
final Widget child;
|
||||
final String? title;
|
||||
final bool showSearchBar;
|
||||
final bool showSettingsButton;
|
||||
final bool showUserAvatar;
|
||||
final TextEditingController? searchController;
|
||||
final Function(String)? onSearchChanged;
|
||||
final Function(String)? onSearchSubmitted;
|
||||
final String? searchHintText;
|
||||
final Widget? searchSuffixIcon;
|
||||
final EdgeInsetsGeometry contentPadding;
|
||||
final Widget? drawer;
|
||||
final bool returnButton;
|
||||
|
||||
const BaseScreen({
|
||||
super.key,
|
||||
required this.child,
|
||||
this.title,
|
||||
this.showSearchBar = false,
|
||||
this.showSettingsButton = false,
|
||||
this.showUserAvatar = false,
|
||||
this.searchController,
|
||||
this.onSearchChanged,
|
||||
this.onSearchSubmitted,
|
||||
this.searchHintText,
|
||||
this.searchSuffixIcon,
|
||||
this.contentPadding = const EdgeInsets.all(20.0),
|
||||
this.drawer,
|
||||
this.returnButton = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isLandscape =
|
||||
MediaQuery.of(context).orientation == Orientation.landscape;
|
||||
|
||||
return Scaffold(
|
||||
drawer: drawer,
|
||||
drawerEnableOpenDragGesture: true,
|
||||
drawerEdgeDragWidth: 50,
|
||||
onDrawerChanged: (isOpened) {},
|
||||
body: SafeArea(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Layout adaptativo según la orientación
|
||||
if (isLandscape)
|
||||
_buildLandscapeHeader(context)
|
||||
else
|
||||
_buildPortraitHeader(context),
|
||||
|
||||
// Content area
|
||||
Expanded(
|
||||
child: child,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Layout para orientación horizontal (landscape)
|
||||
Widget _buildLandscapeHeader(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 4.0),
|
||||
child: Row(
|
||||
children: [
|
||||
// Navigation icon (back button or drawer)
|
||||
if (returnButton)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.arrow_back, size: 20),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
visualDensity: VisualDensity.compact,
|
||||
)
|
||||
else if (drawer != null)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.menu, size: 20),
|
||||
onPressed: () => Scaffold.of(context).openDrawer(),
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
visualDensity: VisualDensity.compact,
|
||||
),
|
||||
|
||||
// Title
|
||||
if (title != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
child: Text(
|
||||
title!,
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(width: 8),
|
||||
|
||||
// Search bar inline
|
||||
if (showSearchBar && searchController != null)
|
||||
Expanded(
|
||||
child: SizedBox(
|
||||
height: 34,
|
||||
child: TextFormField(
|
||||
decoration: InputDecoration(
|
||||
fillColor: const Color(0xFFEBEBEB),
|
||||
filled: true,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
prefixIcon:
|
||||
const Icon(Icons.search, color: Colors.grey, size: 18),
|
||||
hintText: searchHintText ?? 'search_placeholder'.tr(),
|
||||
hintStyle: const TextStyle(
|
||||
color: Colors.grey,
|
||||
fontSize: 14,
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
isDense: true,
|
||||
suffixIcon: searchController?.text.isNotEmpty ?? false
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.clear, size: 18),
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
onPressed: () {
|
||||
searchController?.clear();
|
||||
onSearchChanged?.call('');
|
||||
},
|
||||
)
|
||||
: null,
|
||||
),
|
||||
controller: searchController,
|
||||
onChanged: onSearchChanged,
|
||||
onFieldSubmitted: onSearchSubmitted,
|
||||
autofocus: false,
|
||||
autocorrect: true,
|
||||
style: const TextStyle(fontSize: 14),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Settings button
|
||||
if (showSettingsButton)
|
||||
IconButton(
|
||||
onPressed: () =>
|
||||
nav.pushScreenWithoutNavBar(context, const ConfigView()),
|
||||
icon: const Icon(Icons.settings, size: 20),
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
visualDensity: VisualDensity.compact,
|
||||
),
|
||||
|
||||
// User avatar
|
||||
if (showUserAvatar)
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(left: 4),
|
||||
child: CircleAvatar(
|
||||
backgroundColor: Colors.grey,
|
||||
radius: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Layout para orientación vertical (portrait)
|
||||
Widget _buildPortraitHeader(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
// Header row with title and buttons
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 12.0,
|
||||
right: 12.0,
|
||||
top: 6.0,
|
||||
bottom: 4.0,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// Navigation icon (back button or drawer)
|
||||
if (returnButton)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
padding: const EdgeInsets.all(4),
|
||||
constraints: const BoxConstraints(),
|
||||
iconSize: 20,
|
||||
)
|
||||
else if (drawer != null)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.menu),
|
||||
onPressed: () => Scaffold.of(context).openDrawer(),
|
||||
padding: const EdgeInsets.all(4),
|
||||
constraints: const BoxConstraints(),
|
||||
iconSize: 20,
|
||||
),
|
||||
|
||||
// Title
|
||||
if (title != null)
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
child: Text(
|
||||
title!,
|
||||
style: const TextStyle(
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Settings button
|
||||
if (showSettingsButton)
|
||||
IconButton(
|
||||
onPressed: () =>
|
||||
nav.pushScreenWithoutNavBar(context, const ConfigView()),
|
||||
icon: const Icon(Icons.settings, size: 20),
|
||||
padding: const EdgeInsets.all(4),
|
||||
constraints: const BoxConstraints(),
|
||||
visualDensity: VisualDensity.compact,
|
||||
),
|
||||
|
||||
// User avatar
|
||||
if (showUserAvatar)
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(left: 4),
|
||||
child: CircleAvatar(
|
||||
backgroundColor: Colors.grey,
|
||||
radius: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Search bar in a separate row
|
||||
if (showSearchBar && searchController != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 12.0,
|
||||
right: 12.0,
|
||||
bottom: 6.0,
|
||||
),
|
||||
child: SizedBox(
|
||||
height: 34,
|
||||
child: TextFormField(
|
||||
decoration: InputDecoration(
|
||||
fillColor: const Color(0xFFEBEBEB),
|
||||
filled: true,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
prefixIcon:
|
||||
const Icon(Icons.search, color: Colors.grey, size: 18),
|
||||
hintText: searchHintText ?? 'search_placeholder'.tr(),
|
||||
hintStyle: const TextStyle(
|
||||
color: Colors.grey,
|
||||
fontSize: 14,
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
isDense: true,
|
||||
suffixIcon: searchController?.text.isNotEmpty ?? false
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.clear, size: 18),
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
onPressed: () {
|
||||
searchController?.clear();
|
||||
onSearchChanged?.call('');
|
||||
},
|
||||
)
|
||||
: null,
|
||||
),
|
||||
controller: searchController,
|
||||
onChanged: onSearchChanged,
|
||||
onFieldSubmitted: onSearchSubmitted,
|
||||
autofocus: false,
|
||||
autocorrect: true,
|
||||
style: const TextStyle(fontSize: 14),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:search_engine/screens/home.dart';
|
||||
import 'package:search_engine/screens/search.dart';
|
||||
import 'package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart'
|
||||
as nav;
|
||||
|
||||
class GlobalNavigator extends StatefulWidget {
|
||||
const GlobalNavigator({super.key});
|
||||
|
||||
@override
|
||||
_GlobalNavigatorState createState() => _GlobalNavigatorState();
|
||||
|
||||
static void navigateToIndex(BuildContext context, int index) {
|
||||
final navigatorState =
|
||||
context.findAncestorStateOfType<_GlobalNavigatorState>();
|
||||
navigatorState?._navigateToIndex(index);
|
||||
}
|
||||
}
|
||||
|
||||
class _GlobalNavigatorState extends State<GlobalNavigator> {
|
||||
late nav.PersistentTabController _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = nav.PersistentTabController(initialIndex: 0);
|
||||
}
|
||||
|
||||
List<nav.PersistentTabConfig> _navBarsItems() {
|
||||
return [
|
||||
nav.PersistentTabConfig(
|
||||
screen: const SearchPage(),
|
||||
item: nav.ItemConfig(
|
||||
icon: const Icon(Icons.search),
|
||||
title: "search".tr(),
|
||||
activeForegroundColor: const Color(0xFF871818),
|
||||
inactiveForegroundColor: Colors.grey,
|
||||
)),
|
||||
nav.PersistentTabConfig(
|
||||
screen: const HomePage(),
|
||||
item: nav.ItemConfig(
|
||||
icon: const Icon(Icons.book),
|
||||
title: "title".tr(),
|
||||
activeForegroundColor: const Color(0XFF6b8e23),
|
||||
inactiveForegroundColor: Colors.grey,
|
||||
))
|
||||
];
|
||||
}
|
||||
|
||||
void _navigateToIndex(int index) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_controller.jumpToTab(index);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return nav.PersistentTabView(
|
||||
navBarBuilder: (navBarConfig) => nav.Style2BottomNavBar(
|
||||
navBarConfig: navBarConfig,
|
||||
navBarDecoration: nav.NavBarDecoration(color: Colors.transparent),
|
||||
),
|
||||
controller: _controller,
|
||||
tabs: _navBarsItems(),
|
||||
backgroundColor: Colors.white,
|
||||
handleAndroidBackButtonPress: true,
|
||||
resizeToAvoidBottomInset: true,
|
||||
gestureNavigationEnabled: true,
|
||||
stateManagement: true,
|
||||
screenTransitionAnimation: nav.ScreenTransitionAnimation(
|
||||
curve: Easing.emphasizedDecelerate,
|
||||
duration: Duration(milliseconds: 150)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -0,0 +1,124 @@
|
|||
#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)
|
||||
|
||||
// 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, "Estudios bíblicos");
|
||||
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, "Estudios bíblicos");
|
||||
}
|
||||
|
||||
gtk_window_set_default_size(window, 1280, 720);
|
||||
gtk_widget_show(GTK_WIDGET(window));
|
||||
|
||||
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);
|
||||
gtk_widget_show(GTK_WIDGET(view));
|
||||
gtk_container_add(GTK_CONTAINER(window), 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() {
|
||||
return MY_APPLICATION(g_object_new(my_application_get_type(),
|
||||
"application-id", APPLICATION_ID,
|
||||
"flags", G_APPLICATION_NON_UNIQUE,
|
||||
nullptr));
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
#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_
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
platform :osx, '11.14'
|
||||
|
||||
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||
|
||||
project 'Runner', {
|
||||
'Debug' => :debug,
|
||||
'Profile' => :release,
|
||||
'Release' => :release,
|
||||
}
|
||||
|
||||
def flutter_root
|
||||
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__)
|
||||
unless File.exist?(generated_xcode_build_settings_path)
|
||||
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first"
|
||||
end
|
||||
|
||||
File.foreach(generated_xcode_build_settings_path) do |line|
|
||||
matches = line.match(/FLUTTER_ROOT\=(.*)/)
|
||||
return matches[1].strip if matches
|
||||
end
|
||||
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\""
|
||||
end
|
||||
|
||||
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
|
||||
|
||||
flutter_macos_podfile_setup
|
||||
|
||||
target 'Runner' do
|
||||
use_frameworks!
|
||||
use_modular_headers!
|
||||
|
||||
flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__))
|
||||
target 'RunnerTests' do
|
||||
inherit! :search_paths
|
||||
end
|
||||
end
|
||||
|
||||
post_install do |installer|
|
||||
installer.pods_project.targets.each do |target|
|
||||
flutter_additional_macos_build_settings(target)
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,146 @@
|
|||
PODS:
|
||||
- audio_session (0.0.1):
|
||||
- FlutterMacOS
|
||||
- country_codes (0.0.1):
|
||||
- FlutterMacOS
|
||||
- device_info_plus (0.0.1):
|
||||
- FlutterMacOS
|
||||
- flutter_local_notifications (0.0.1):
|
||||
- FlutterMacOS
|
||||
- flutter_mimir (0.0.1)
|
||||
- FlutterMacOS (1.0.0)
|
||||
- gal (1.0.0):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- just_audio (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- package_info_plus (0.0.1):
|
||||
- FlutterMacOS
|
||||
- path_provider_foundation (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- shared_preferences_foundation (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- sqflite_darwin (0.0.4):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- sqlite3 (3.49.1):
|
||||
- sqlite3/common (= 3.49.1)
|
||||
- sqlite3/common (3.49.1)
|
||||
- sqlite3/dbstatvtab (3.49.1):
|
||||
- sqlite3/common
|
||||
- sqlite3/fts5 (3.49.1):
|
||||
- sqlite3/common
|
||||
- sqlite3/perf-threadsafe (3.49.1):
|
||||
- sqlite3/common
|
||||
- sqlite3/rtree (3.49.1):
|
||||
- sqlite3/common
|
||||
- sqlite3_flutter_libs (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- sqlite3 (~> 3.49.1)
|
||||
- sqlite3/dbstatvtab
|
||||
- sqlite3/fts5
|
||||
- sqlite3/perf-threadsafe
|
||||
- sqlite3/rtree
|
||||
- syncfusion_pdfviewer_macos (0.0.1):
|
||||
- FlutterMacOS
|
||||
- url_launcher_macos (0.0.1):
|
||||
- FlutterMacOS
|
||||
- video_player_avfoundation (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- wakelock_plus (0.0.1):
|
||||
- FlutterMacOS
|
||||
- webview_flutter_wkwebview (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
|
||||
DEPENDENCIES:
|
||||
- audio_session (from `Flutter/ephemeral/.symlinks/plugins/audio_session/macos`)
|
||||
- country_codes (from `Flutter/ephemeral/.symlinks/plugins/country_codes/macos`)
|
||||
- device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`)
|
||||
- flutter_local_notifications (from `Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos`)
|
||||
- flutter_mimir (from `Flutter/ephemeral/.symlinks/plugins/flutter_mimir/macos`)
|
||||
- FlutterMacOS (from `Flutter/ephemeral`)
|
||||
- gal (from `Flutter/ephemeral/.symlinks/plugins/gal/darwin`)
|
||||
- just_audio (from `Flutter/ephemeral/.symlinks/plugins/just_audio/darwin`)
|
||||
- package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`)
|
||||
- path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`)
|
||||
- shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||
- sqflite_darwin (from `Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin`)
|
||||
- sqlite3_flutter_libs (from `Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/darwin`)
|
||||
- syncfusion_pdfviewer_macos (from `Flutter/ephemeral/.symlinks/plugins/syncfusion_pdfviewer_macos/macos`)
|
||||
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
|
||||
- video_player_avfoundation (from `Flutter/ephemeral/.symlinks/plugins/video_player_avfoundation/darwin`)
|
||||
- wakelock_plus (from `Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos`)
|
||||
- webview_flutter_wkwebview (from `Flutter/ephemeral/.symlinks/plugins/webview_flutter_wkwebview/darwin`)
|
||||
|
||||
SPEC REPOS:
|
||||
trunk:
|
||||
- sqlite3
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
audio_session:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/audio_session/macos
|
||||
country_codes:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/country_codes/macos
|
||||
device_info_plus:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos
|
||||
flutter_local_notifications:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos
|
||||
flutter_mimir:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/flutter_mimir/macos
|
||||
FlutterMacOS:
|
||||
:path: Flutter/ephemeral
|
||||
gal:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/gal/darwin
|
||||
just_audio:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/just_audio/darwin
|
||||
package_info_plus:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos
|
||||
path_provider_foundation:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin
|
||||
shared_preferences_foundation:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin
|
||||
sqflite_darwin:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin
|
||||
sqlite3_flutter_libs:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/darwin
|
||||
syncfusion_pdfviewer_macos:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/syncfusion_pdfviewer_macos/macos
|
||||
url_launcher_macos:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos
|
||||
video_player_avfoundation:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/video_player_avfoundation/darwin
|
||||
wakelock_plus:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos
|
||||
webview_flutter_wkwebview:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/webview_flutter_wkwebview/darwin
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
audio_session: eaca2512cf2b39212d724f35d11f46180ad3a33e
|
||||
country_codes: 5dfbeb7300cc873e3125c47806a3cc8f4b04009d
|
||||
device_info_plus: 4fb280989f669696856f8b129e4a5e3cd6c48f76
|
||||
flutter_local_notifications: 13862b132e32eb858dea558a86d45d08daeacfe7
|
||||
flutter_mimir: abc5575f7deea72a2716fb3a75295984681dfd87
|
||||
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
|
||||
gal: baecd024ebfd13c441269ca7404792a7152fde89
|
||||
just_audio: 4e391f57b79cad2b0674030a00453ca5ce817eed
|
||||
package_info_plus: f0052d280d17aa382b932f399edf32507174e870
|
||||
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
|
||||
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
|
||||
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
|
||||
sqlite3: fc1400008a9b3525f5914ed715a5d1af0b8f4983
|
||||
sqlite3_flutter_libs: f8fc13346870e73fe35ebf6dbb997fbcd156b241
|
||||
syncfusion_pdfviewer_macos: 94d2eafcb3475d32309d4ba282ef02dc75fd682f
|
||||
url_launcher_macos: 0fba8ddabfc33ce0a9afe7c5fef5aab3d8d2d673
|
||||
video_player_avfoundation: 2cef49524dd1f16c5300b9cd6efd9611ce03639b
|
||||
wakelock_plus: 21ddc249ac4b8d018838dbdabd65c5976c308497
|
||||
webview_flutter_wkwebview: 1821ceac936eba6f7984d89a9f3bcb4dea99ebb2
|
||||
|
||||
PODFILE CHECKSUM: ebd4484585a0d7bc022aaecd743fb4a450f70847
|
||||
|
||||
COCOAPODS: 1.16.2
|
||||
|
|
@ -0,0 +1 @@
|
|||
curl -X POST "https://gotify-production-34a7.up.railway.app/message?token=AUsFSONcW5Y2UHS" -F "title=Hola" -F "message=Este es un mensaje de prueba" -F "priority=5"
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
// This is a basic Flutter widget test.
|
||||
//
|
||||
// To perform an interaction with a widget in your test, use the WidgetTester
|
||||
// utility in the flutter_test package. For example, you can send tap and scroll
|
||||
// gestures. You can also use WidgetTester to find child widgets in the widget
|
||||
// tree, read text, and verify that the values of widget properties are correct.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'package:search_engine/main.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
|
||||
// Build our app and trigger a frame.
|
||||
await tester.pumpWidget(const MyApp());
|
||||
|
||||
// Verify that our counter starts at 0.
|
||||
expect(find.text('0'), findsOneWidget);
|
||||
expect(find.text('1'), findsNothing);
|
||||
|
||||
// Tap the '+' icon and trigger a frame.
|
||||
await tester.tap(find.byIcon(Icons.add));
|
||||
await tester.pump();
|
||||
|
||||
// Verify that our counter has incremented.
|
||||
expect(find.text('0'), findsNothing);
|
||||
expect(find.text('1'), findsOneWidget);
|
||||
});
|
||||
}
|
||||