update app

This commit is contained in:
Esteban Paz 2025-08-06 16:22:21 -05:00
parent e9f834c267
commit 695b152d85
99 changed files with 14817 additions and 0 deletions

View File

@ -0,0 +1,2 @@
#Sun Jan 12 11:55:42 COT 2025
java.home=/Applications/Android Studio.app/Contents/jbr/Contents/Home

25
.vscode/launch.json vendored Normal file
View File

@ -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"
}
]
}

64
android/app/build.gradle Normal file
View File

@ -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 = "../.."
}

View File

@ -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"
}

View File

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

View File

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

View File

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

20
android/build.gradle Normal file
View File

@ -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'

29
android/settings.gradle Normal file
View File

@ -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"

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
assets/image/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

145
assets/lang/en.json Normal file
View File

@ -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"
}

146
assets/lang/es.json Normal file
View File

@ -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"
}

104
assets/lang/fr.json Normal file
View File

@ -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"
}

104
assets/lang/pt.json Normal file
View File

@ -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"
}

105
assets/lang/rw.json Normal file
View File

@ -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"
}

33
assets/svg/logo.svg Normal file
View File

@ -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

3
devtools_options.yaml Normal file
View File

@ -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:

144
flutter_native_splash.yaml Normal file
View File

@ -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'

98
ios/Podfile Normal file
View File

@ -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

164
ios/Podfile.lock Normal file
View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 550 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 779 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

View File

@ -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>

View File

@ -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
}

View File

@ -0,0 +1,11 @@
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -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
}
}

View File

@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,11 @@
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -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>

View File

@ -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")
}
}

View File

@ -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()
}
}
}

View File

@ -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()
}
}

View File

@ -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")
}
}
}
}

View File

@ -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>

View File

@ -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,
);
}
}

826
lib/database.dart Normal file
View File

@ -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 [];
}
}
}

1387
lib/database.g.dart Normal file

File diff suppressed because it is too large Load Diff

667
lib/screens/config.dart Normal file
View File

@ -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',
),
),
],
),
],
),
),
),
),
);
}
}

2452
lib/screens/content.dart Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

1546
lib/screens/home.dart Normal file

File diff suppressed because it is too large Load Diff

1207
lib/screens/landing.dart Normal file

File diff suppressed because it is too large Load Diff

226
lib/screens/pdf.dart Normal file
View File

@ -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'));
}
},
),
);
}
}

1578
lib/screens/search.dart Normal file

File diff suppressed because it is too large Load Diff

View File

@ -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]);
}
}
}

View File

@ -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;
}

View File

@ -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 = {
'&nbsp;': ' ',
'&amp;': '&',
'&lt;': '<',
'&gt;': '>',
'&quot;': '"',
'&apos;': "'",
'&cent;': '¢',
'&pound;': '£',
'&yen;': '¥',
'&euro;': '',
'&copy;': '©',
'&reg;': '®',
'&aacute;': 'á',
'&eacute;': 'é',
'&iacute;': 'í',
'&oacute;': 'ó',
'&uacute;': 'ú',
'&ntilde;': 'ñ',
'&Aacute;': 'Á',
'&Eacute;': 'É',
'&Iacute;': 'Í',
'&Oacute;': 'Ó',
'&Uacute;': 'Ú',
'&Ntilde;': 'Ñ',
};
// Replace known HTML entities
htmlEntities.forEach((entity, replacement) {
result = result.replaceAll(entity, replacement);
});
// Step 3: Replace numeric HTML entities (like &#123;)
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();
}
}
}

View File

@ -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');
}
}
}
}

15
lib/utils.dart Normal file
View File

@ -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;
}

315
lib/widgets/base.dart Normal file
View File

@ -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),
),
),
),
],
);
}
}

View File

@ -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)),
);
}
}

6
linux/main.cc Normal file
View File

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

124
linux/my_application.cc Normal file
View File

@ -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));
}

18
linux/my_application.h Normal file
View File

@ -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_

43
macos/Podfile Normal file
View File

@ -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

146
macos/Podfile.lock Normal file
View File

@ -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

1
rest-gotify-jville.txt Normal file
View File

@ -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"

30
test/widget_test.dart Normal file
View File

@ -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);
});
}