mymuseum-visitapp/lib/Helpers/DatabaseHelper.dart
Thomas Fransolet d5f36865b6 D2 : re-télécharger une ressource périmée, base locale v3 vers v4
Le filtre incrémental testait la présence du fichier, jamais sa version.

La prémisse d'origine était fausse, et ça change ce que le correctif répare :
« une image remplacée dans le CMS ne remonte jamais » n'existe pas, parce que
Upload appelle GenerateHexId() à chaque téléversement — remplacer une image
produit un nouvel id, donc une nouvelle URL, que l'ancien filtre téléchargeait
déjà.

Le vrai cas de péremption vient d'être créé par D3 : les MP3 déjà présents sur
les devices y sont en <id>.unknown, et « le fichier est présent » les déclarait
à jour. Sans D2, D3 ne réparait que les installations neuves. isResourceOutdated
traite donc .unknown comme absent.

Deux pièges tranchés en écrivant. La montée v4 laisse les dates existantes à
NULL et les considère à jour : backfiller à zéro aurait fait re-télécharger
l'intégralité des visites de tous les visiteurs sur leur réseau mobile, au
premier lancement. Et la boucle en masse héritée de D1 écrasait la date que la
boucle de téléchargement venait d'écrire, DatabaseHelper.insert faisant un
UPDATE de la ligne entière quand l'id existe ; pour un téléchargement échoué,
c'est l'ancienne date locale qui est conservée, jamais celle du serveur.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 17:47:16 +02:00

471 lines
17 KiB
Dart

import 'dart:convert';
import 'package:manager_api_new/api.dart';
import 'package:mymuseum_visitapp/Models/articleRead.dart';
import 'package:mymuseum_visitapp/Models/beaconSection.dart';
import 'package:mymuseum_visitapp/Models/resourceModel.dart';
import 'package:mymuseum_visitapp/Models/visitContext.dart';
import 'package:path/path.dart';
import 'package:sqflite/sqflite.dart';
enum DatabaseTableType {
main,
configurations,
sections,
resources,
articleRead,
beaconSection
}
class DatabaseHelper {
static const _databaseName = "visit_database.db";
static const _databaseVersion = 4;
static const mainTable = 'visitAppContext';
static const columnLanguage = 'language';
static const columnLabel = 'label';
static const columnId = 'id';
static const columnInstanceId = 'instanceId';
static const columnPath = 'path';
static const columnData = 'data';
static const columnType = 'type';
static const columnDateCreation = 'dateCreation';
// columnIsMobile/columnIsTablet removed: now in AppConfigurationLink
static const columnIsOffline = 'isOffline';
static const configurationsTable = 'configurations';
static const columnLanguages = 'languages';
static const columnPrimaryColor = 'primaryColor';
static const columnSecondaryColor = 'secondaryColor';
static const columnConfigOrder = 'configOrder';
static const columnGridColSpan = 'gridColSpan';
static const columnGridRowSpan = 'gridRowSpan';
static const sectionsTable = 'sections';
static const columnTitle = 'title';
static const columnDescription = 'description';
static const columnImageId = 'imageId';
static const columnImageSource = 'imageSource';
static const columnConfigurationId = 'configurationId';
static const columnIsSubSection = 'isSubSection';
static const columnParentId = 'parentId';
static const columnOrderOfElement = 'orderOfElement';
static const resourcesTable = 'resources';
static const columnSource = 'source';
static const columnDateUpdate = 'dateUpdate';
static const articleReadTable = 'articleRead';
static const columnLastTimeOpen = 'readTime';
static const beaconSectionTable = 'beaconSection';
static const columnMinorBeaconId = 'minorBeaconId';
static const columnOrderInConfig = 'orderInConfig';
static const columnSectionId = 'sectionId';
static const columnSectionType = 'sectionType';
static const columnIsAdmin = 'isAdmin';
static const columnIsAllLanguages = 'isAllLanguages';
static const columnApiKey = 'apiKey';
static const columnNotificationsEnabled = 'notificationsEnabled';
DatabaseHelper._privateConstructor();
static final DatabaseHelper instance = DatabaseHelper._privateConstructor();
static Database? _database;
Future<Database> get database async =>
_database ??= await _initDatabase();
Future<Database> _initDatabase() async {
String path = join(await getDatabasesPath(), _databaseName);
return await openDatabase(path,
version: _databaseVersion, onCreate: _onCreate, onUpgrade: _onUpgrade);
}
Future _onUpgrade(Database db, int oldVersion, int newVersion) async {
if (oldVersion < 2) {
await db.execute('ALTER TABLE $configurationsTable ADD COLUMN $columnConfigOrder INT');
// v2 avait une colonne 'weightMasonryGrid' (poids 1D) — abandonnée en v3
// au profit des spans bento. On ne la supprime pas (SQLite ne le permet pas
// simplement), elle reste inutilisée.
await db.execute('ALTER TABLE $configurationsTable ADD COLUMN weightMasonryGrid REAL');
}
if (oldVersion < 3) {
await db.execute('ALTER TABLE $configurationsTable ADD COLUMN $columnGridColSpan INT');
await db.execute('ALTER TABLE $configurationsTable ADD COLUMN $columnGridRowSpan INT');
}
if (oldVersion < 4) {
// Les lignes existantes restent à NULL : un fichier déjà téléchargé sans date
// connue est considéré à jour, sinon la montée de version déclencherait un
// re-téléchargement intégral de toutes les visites sur le réseau du visiteur.
await db.execute('ALTER TABLE $resourcesTable ADD COLUMN $columnDateUpdate TEXT');
}
}
// SQL code to create the database table
Future _onCreate(Database db, int version) async {
await createTable(db, DatabaseTableType.main);
await createTable(db, DatabaseTableType.configurations);
await createTable(db, DatabaseTableType.sections);
await createTable(db, DatabaseTableType.resources);
await createTable(db, DatabaseTableType.articleRead);
await createTable(db, DatabaseTableType.beaconSection);
}
Future<int> insert(DatabaseTableType type, Map<String, Object?> info) async {
Database db = await instance.database;
if(info.containsKey("languages")) {
info.update("languages", (value) => jsonEncode(value));
//print("languages BEFORE");
//print(info["languages"]);
}
//var test = await instance.getData(type);
// Test if table exist
var isExistResult = await isExist(type);
if(!isExistResult) {
print("TABLE DOESNT EXIST");
await createTable(db, type);
}
var test = await DatabaseHelper.instance.queryWithColumnId(type, info[columnId].toString());
if((type == DatabaseTableType.main && test.isNotEmpty) || (type != DatabaseTableType.main && test.isNotEmpty)) { //test!.where((t) => info[columnId] == t.id).isNotEmpty
if(type != DatabaseTableType.main) {
//print("UPDATE $type - length before ${test.length}");
print("UPDATE $type");
} else {
print("UPDATE $type - main table");
}
var res = await db.update(
_getTableName(type),
info,
where: "$columnId = ?",
whereArgs: [info[columnId]],
);
return res;
} else {
if(type != DatabaseTableType.main) {
//print("INSERT $type - length before ${test.length}");
print("INSERT $type");
} else {
print("INSERT $type - main table");
}
var res = await db.insert(_getTableName(type), info);
return res;
}
}
Future<void> update(DatabaseTableType type, Map<String, Object?> info, String id) async {
// Get a reference to the database.
final db = await instance.database;
await db.update(
_getTableName(type),
info,
where: "$columnId = ?",
whereArgs: [id],
);
}
Future<void> createTable(Database db, DatabaseTableType type) async {
print("CREATE TABLE $type");
switch(type) {
case DatabaseTableType.main :
db.execute('''
CREATE TABLE $mainTable (
$columnId TEXT NOT NULL PRIMARY KEY,
$columnLanguage TEXT NOT NULL,
$columnInstanceId TEXT NOT NULL,
$columnIsAdmin BOOLEAN NOT NULL CHECK ($columnIsAdmin IN (0,1)),
$columnIsAllLanguages BOOLEAN CHECK ($columnIsAllLanguages IN (0,1)),
$columnApiKey TEXT,
$columnNotificationsEnabled BOOLEAN CHECK ($columnNotificationsEnabled IN (0,1))
)
''');
break;
case DatabaseTableType.configurations :
db.execute('''
CREATE TABLE $configurationsTable (
$columnId TEXT NOT NULL PRIMARY KEY,
$columnInstanceId TEXT NOT NULL,
$columnLabel TEXT NOT NULL,
$columnTitle TEXT NOT NULL,
$columnImageId TEXT,
$columnImageSource TEXT,
$columnLanguages TEXT NOT NULL,
$columnDateCreation TEXT NOT NULL,
$columnPrimaryColor TEXT,
$columnSecondaryColor TEXT,
$columnConfigOrder INT,
$columnGridColSpan INT,
$columnGridRowSpan INT,
$columnIsOffline BOOLEAN NOT NULL CHECK ($columnIsOffline IN (0,1))
)
''');
break;
case DatabaseTableType.sections :
db.execute('''
CREATE TABLE $sectionsTable (
$columnId TEXT NOT NULL PRIMARY KEY,
$columnInstanceId TEXT NOT NULL,
$columnLabel TEXT NOT NULL,
$columnTitle TEXT NOT NULL,
$columnDescription TEXT,
$columnImageId TEXT,
$columnImageSource TEXT,
$columnConfigurationId TEXT NOT NULL,
$columnIsSubSection BOOLEAN NOT NULL CHECK ($columnIsSubSection IN (0,1)),
$columnParentId TEXT,
$columnType INT NOT NULL,
$columnData TEXT NOT NULL,
$columnDateCreation TEXT NOT NULL,
$columnOrderOfElement TEXT NOT NULL
)
''');
break;
case DatabaseTableType.resources:
db.execute('''
CREATE TABLE $resourcesTable (
$columnId TEXT NOT NULL PRIMARY KEY,
$columnPath TEXT NOT NULL,
$columnSource TEXT NOT NULL,
$columnType INT NOT NULL,
$columnDateUpdate TEXT
)
''');
break;
case DatabaseTableType.articleRead:
db.execute('''
CREATE TABLE $articleReadTable (
$columnId TEXT NOT NULL PRIMARY KEY,
$columnLastTimeOpen INT NOT NULL
)
''');
break;
case DatabaseTableType.beaconSection:
db.execute('''
CREATE TABLE $beaconSectionTable (
$columnId TEXT NOT NULL PRIMARY KEY,
$columnMinorBeaconId INT NOT NULL,
$columnOrderInConfig INT NOT NULL,
$columnConfigurationId TEXT NOT NULL,
$columnSectionId TEXT NOT NULL,
$columnSectionType INT NOT NULL
)
''');
break;
}
}
void updateTableMain(DatabaseTableType type, VisitAppContext visitAppContext) async {
try {
Database db = await instance.database;
var nameOfTable = _getTableName(type);
// CHECK IF EXIST
String checkQuery = "PRAGMA table_info($nameOfTable)";
var test = await db.rawQuery(checkQuery);
if(test.where((e) => e.toString().contains(columnIsAdmin)).isEmpty) {
print("update .. columnIsAdmin");
await db.rawQuery("ALTER TABLE $nameOfTable Add $columnIsAdmin BOOLEAN CHECK ($columnIsAdmin IN (0,1))");
} else {
print("IN columnIsAdmin");
}
if(test.where((e) => e.toString().contains(columnIsAllLanguages)).isEmpty) {
print("update .. columnIsAllLanguages");
await db.rawQuery("ALTER TABLE $nameOfTable Add $columnIsAllLanguages BOOLEAN CHECK ($columnIsAllLanguages IN (0,1))");
} else {
print("IN columnIsAllLanguages");
}
if(test.where((e) => e.toString().contains(columnApiKey)).isEmpty) {
await db.rawQuery("ALTER TABLE $nameOfTable ADD $columnApiKey TEXT");
}
if(test.where((e) => e.toString().contains(columnNotificationsEnabled)).isEmpty) {
await db.rawQuery("ALTER TABLE $nameOfTable ADD $columnNotificationsEnabled BOOLEAN CHECK ($columnNotificationsEnabled IN (0,1))");
}
DatabaseHelper.instance.insert(DatabaseTableType.main, visitAppContext.toMap());
} catch (e) {
print("ERROR IN updateTableMain");
print(e);
}
}
Future<bool> isExist(DatabaseTableType type) async {
Database db = await instance.database;
var nameOfTable = _getTableName(type);
var test = await db.rawQuery("SELECT name FROM sqlite_master WHERE type='table' AND name='$nameOfTable'");
return test.isNotEmpty;
}
Future<List<Map<String, dynamic>>> queryAllRows(DatabaseTableType type) async {
Database db = await instance.database;
var res = await db.query(_getTableName(type), orderBy: "$columnId DESC");
return res;
}
Future<List<Map<String, dynamic>>> queryWithColumnId(DatabaseTableType type, String id) async {
Database db = await instance.database;
var res = await db.query(_getTableName(type), where: '$columnId = ?', whereArgs: [id]);
return res;
}
Future<List<SectionDTO>> queryWithConfigurationId(DatabaseTableType type, String id) async {
List<SectionDTO> sections = [];
Database db = await instance.database;
var res = await db.query(_getTableName(type), where: '$columnConfigurationId = ?', whereArgs: [id]);
res.forEach((element) {
sections.add(getSectionFromDB(element));
});
return sections;
}
Future<int> delete(String id, DatabaseTableType type) async {
Database db = await instance.database;
return await db.delete(_getTableName(type), where: '$columnId = ?', whereArgs: [id]);
}
Future<List<Map<String, Object?>>> clearTable(DatabaseTableType type) async {
Database db = await instance.database;
String tableName = _getTableName(type);
return await db.rawQuery("DELETE FROM $tableName");
}
Future<dynamic> getData(DatabaseTableType type) async {
dynamic dataToReturn;
if(type != DatabaseTableType.main)
{
dataToReturn = <dynamic>[];
}
await DatabaseHelper.instance.queryAllRows(type).then((value) {
//print("DB - getData - CONTEXT --- $type --> ");
value.forEach((element) {
switch(type) {
case DatabaseTableType.main:
dataToReturn = VisitAppContext(
id: element["id"],
instanceId: element["instanceId"],
language: element["language"],
isAdmin: element["isAdmin"] == 1 ? true : false,
isAllLanguages: element["isAllLanguages"] == 1 ? true : false,
apiKey: element["apiKey"] as String?,
notificationsEnabled: element["notificationsEnabled"] == null || element["notificationsEnabled"] == 1,
);
break;
case DatabaseTableType.configurations:
dataToReturn.add(getConfigurationFromDB(element));
//print("DB - dataToReturn --- $test --> ");
break;
case DatabaseTableType.sections:
dataToReturn.add(getSectionFromDB(element));
//print("DB - dataToReturn --- $test --> ");
break;
case DatabaseTableType.resources:
dataToReturn.add(getResourceFromDB(element));
//print("DB - dataToReturn --- $test --> ");
break;
case DatabaseTableType.articleRead:
dataToReturn.add(getArticleReadFromDB(element));
//print("DB - dataToReturn --- $test --> ");
break;
case DatabaseTableType.beaconSection:
dataToReturn.add(getBeaconSectionFromDB(element));
//print("DB - dataToReturn --- $test --> ");
break;
}
});
}).catchError((error) {
print(error);
});
return dataToReturn;
}
String _getTableName(DatabaseTableType type) {
switch(type) {
case DatabaseTableType.main:
return mainTable;
case DatabaseTableType.configurations:
return configurationsTable;
case DatabaseTableType.sections:
return sectionsTable;
case DatabaseTableType.resources:
return resourcesTable;
case DatabaseTableType.articleRead:
return articleReadTable;
case DatabaseTableType.beaconSection:
return beaconSectionTable;
}
}
ConfigurationDTO getConfigurationFromDB(dynamic element) {
var titles = List<TranslationDTO>.from(json.decode(element['title']).map<dynamic>((q) => TranslationDTO.fromJson(q)).toList());
return ConfigurationDTO(
id: element["id"],
instanceId: element["instanceId"],
label: element["label"],
title: titles,
imageId: element["imageId"],
imageSource: element["imageSource"],
primaryColor: element["primaryColor"],
secondaryColor: element["secondaryColor"],
languages: List<String>.from(jsonDecode(element["languages"])),
dateCreation: DateTime.tryParse(element["dateCreation"]),
isOffline: element["isOffline"] == 1 ? true : false
);
}
SectionDTO getSectionFromDB(dynamic element) {
var titles = List<TranslationDTO>.from(json.decode(element['title']).map<dynamic>((q) => TranslationDTO.fromJson(q)).toList());
var descriptions = List<TranslationDTO>.from(json.decode(element['description']).map<dynamic>((q) => TranslationDTO.fromJson(q)).toList());
return SectionDTO(
id: element["id"],
instanceId: element["instanceId"],
label: element["label"],
title: titles,
description: descriptions,
imageId: element["imageId"],
imageSource: element["imageSource"],
configurationId: element["configurationId"],
type: SectionType.values[element["type"]],
// data: element["data"], // TODO section data
dateCreation: DateTime.tryParse(element["dateCreation"]),
order: int.parse(element["orderOfElement"]),
);
}
ResourceModel getResourceFromDB(dynamic element) {
// Here retrieve file from path ?
return ResourceModel(
id: element["id"],
path: element["path"],
source: element["source"],
type: ResourceType.values[element["type"]],
dateUpdate: element["dateUpdate"] == null ? null : DateTime.tryParse(element["dateUpdate"])
);
}
SectionRead getArticleReadFromDB(dynamic element) {
return SectionRead(
id: element["id"],
readTime: element["readTime"]
);
}
BeaconSection getBeaconSectionFromDB(dynamic element) {
return BeaconSection(
minorBeaconId: element["minorBeaconId"],
orderInConfig: element["orderInConfig"],
configurationId: element["configurationId"],
sectionId: element["sectionId"],
sectionType: SectionType.values[element["sectionType"]]
);
}
}