md-app/lib/database.dart

827 lines
24 KiB
Dart

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