221 lines
6.8 KiB
Dart
221 lines
6.8 KiB
Dart
import 'dart:async';
|
||
|
||
import 'package:flutter/foundation.dart';
|
||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||
|
||
import 'package:aves/model/source/collection_source.dart';
|
||
import 'package:aves/remote/remote_settings.dart';
|
||
import 'package:aves/remote/remote_sync_bus.dart';
|
||
import 'package:aves/remote/remote_repository.dart';
|
||
import 'package:aves/services/common/services.dart';
|
||
import 'package:aves/remote/remote_client.dart';
|
||
import 'package:aves/remote/auth_client.dart';
|
||
import 'package:aves/remote/collection_source_remote_ext.dart';
|
||
|
||
class RemoteController {
|
||
RemoteController._();
|
||
static final RemoteController instance = RemoteController._();
|
||
|
||
static const _kBootstrapDone = 'remote_bootstrap_done';
|
||
bool _syncInFlight = false;
|
||
|
||
Future<bool> bootstrapDone() async {
|
||
final storage = FlutterSecureStorage();
|
||
return (await storage.read(key: _kBootstrapDone)) == '1';
|
||
}
|
||
|
||
Future<void> _setBootstrapDone() async {
|
||
final storage = FlutterSecureStorage();
|
||
await storage.write(key: _kBootstrapDone, value: '1');
|
||
}
|
||
|
||
/// Chiamare all’avvio: imposta lo stato icona (grigio/verde) coerente con settings.
|
||
Future<void> initBusFromSettings() async {
|
||
final s = await RemoteSettings.load();
|
||
if (!s.enabled) {
|
||
RemoteSyncBus.instance.setDisabled();
|
||
} else {
|
||
// enabled: stato iniziale "upToDate" (poi la sync può cambiare)
|
||
final opId = RemoteSyncBus.instance.start(total: 0, showOverlay: false);
|
||
RemoteSyncBus.instance.finishUpToDate(opId: opId);
|
||
}
|
||
}
|
||
|
||
/// Logica d’avvio app:
|
||
/// - se remote OFF -> nascondi remoti dalla UI (memoria only) e stop
|
||
/// - se remote ON:
|
||
/// - se bootstrap done -> append DB immediato + sync silenzioso
|
||
/// - se bootstrap NOT done -> opzionale resume bootstrap
|
||
Future<void> onAppStart({
|
||
required CollectionSource source,
|
||
bool resumeBootstrapIfEnabled = true,
|
||
}) async {
|
||
final s = await RemoteSettings.load();
|
||
|
||
if (!s.enabled) {
|
||
RemoteSyncBus.instance.setDisabled();
|
||
final remotesInMemory = source.allEntries.where((e) => e.origin == 1).toSet();
|
||
if (remotesInMemory.isNotEmpty) {
|
||
source.removeEntriesFromMemory(remotesInMemory);
|
||
}
|
||
return;
|
||
}
|
||
|
||
final done = await bootstrapDone();
|
||
if (done) {
|
||
await source.appendRemoteEntriesFromDb();
|
||
// sync in background (solo icona)
|
||
unawaited(fullSync(source: source, showOverlay: false));
|
||
} else {
|
||
if (resumeBootstrapIfEnabled) {
|
||
unawaited(fullSync(
|
||
source: source,
|
||
showOverlay: true,
|
||
markBootstrapDoneOnSuccess: true,
|
||
));
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Toggle da icona (tap)
|
||
Future<void> toggleRemote({required CollectionSource source}) async {
|
||
final s = await RemoteSettings.load();
|
||
|
||
if (s.enabled) {
|
||
// TURN OFF
|
||
final upd = RemoteSettings(
|
||
enabled: false,
|
||
baseUrl: s.baseUrl,
|
||
indexPath: s.indexPath,
|
||
email: s.email,
|
||
password: s.password,
|
||
);
|
||
await upd.save();
|
||
|
||
debugPrint('[remote] toggle -> enabled=false (OFF)');
|
||
|
||
// nascondi remoti (memoria only)
|
||
final remotesInMemory = source.allEntries.where((e) => e.origin == 1).toSet();
|
||
if (remotesInMemory.isNotEmpty) {
|
||
source.removeEntriesFromMemory(remotesInMemory);
|
||
}
|
||
|
||
// invalida sync in corso e icona grigia
|
||
RemoteSyncBus.instance.setDisabled();
|
||
debugPrint('[remote] toggled OFF -> removed remotes from memory=${remotesInMemory.length}');
|
||
return;
|
||
}
|
||
|
||
// TURN ON
|
||
final upd = RemoteSettings(
|
||
enabled: true,
|
||
baseUrl: s.baseUrl,
|
||
indexPath: s.indexPath,
|
||
email: s.email,
|
||
password: s.password,
|
||
);
|
||
await upd.save();
|
||
|
||
debugPrint('[remote] toggle -> enabled=true (ON)');
|
||
|
||
final first = !(await bootstrapDone());
|
||
if (first) {
|
||
debugPrint('[remote] first enable -> FULL sync with overlay');
|
||
await fullSync(
|
||
source: source,
|
||
showOverlay: true,
|
||
markBootstrapDoneOnSuccess: true,
|
||
);
|
||
return;
|
||
}
|
||
|
||
debugPrint('[remote] enable -> append DB then background sync');
|
||
await source.appendRemoteEntriesFromDb();
|
||
unawaited(fullSync(source: source, showOverlay: false));
|
||
}
|
||
|
||
/// Full sync remoto:
|
||
/// - showOverlay=true solo bootstrap
|
||
/// - showOverlay=false -> solo icona
|
||
Future<void> fullSync({
|
||
required CollectionSource source,
|
||
required bool showOverlay,
|
||
bool markBootstrapDoneOnSuccess = false,
|
||
}) async {
|
||
if (_syncInFlight) {
|
||
debugPrint('[remote] sync skipped (already in flight)');
|
||
return;
|
||
}
|
||
_syncInFlight = true;
|
||
|
||
final s = await RemoteSettings.load();
|
||
if (!s.enabled) {
|
||
RemoteSyncBus.instance.setDisabled();
|
||
_syncInFlight = false;
|
||
return;
|
||
}
|
||
|
||
// Start: token opId (protezione anti-race)
|
||
final opId = RemoteSyncBus.instance.start(total: 0, showOverlay: showOverlay);
|
||
|
||
try {
|
||
// base URL vuota -> server down
|
||
if (s.baseUrl.trim().isEmpty) {
|
||
debugPrint('[remote] serverDown (empty baseUrl)');
|
||
RemoteSyncBus.instance.failServerDown(opId: opId);
|
||
return;
|
||
}
|
||
|
||
RemoteAuth? auth;
|
||
if (s.email.isNotEmpty && s.password.isNotEmpty) {
|
||
auth = RemoteAuth(baseUrl: s.baseUrl, email: s.email, password: s.password);
|
||
}
|
||
|
||
final client = RemoteJsonClient(s.baseUrl, s.indexPath, auth: auth);
|
||
|
||
// fetch full list
|
||
final items = await client.fetchAll().timeout(const Duration(seconds: 30));
|
||
final total = items.length;
|
||
|
||
debugPrint('[remote] sync start overlay=$showOverlay total=$total');
|
||
|
||
// aggiorna total corretto
|
||
RemoteSyncBus.instance.update(opId: opId, done: 0, total: total);
|
||
|
||
final repo = RemoteRepository(localMediaDb.rawDb);
|
||
await repo.deleteAllRemotes();
|
||
|
||
const chunkSize = 200;
|
||
int done = 0;
|
||
final serverIds = items.map((e) => e.id).where((v) => v.isNotEmpty).toSet();
|
||
|
||
for (var offset = 0; offset < total; offset += chunkSize) {
|
||
final end = (offset + chunkSize < total) ? offset + chunkSize : total;
|
||
await repo.upsertAll(items.sublist(offset, end), chunkSize: chunkSize);
|
||
done = end;
|
||
RemoteSyncBus.instance.update(opId: opId, done: done, total: total);
|
||
}
|
||
|
||
await repo.pruneMissingRemotes(serverIds);
|
||
|
||
// mostra remoti in UI (dopo sync)
|
||
await source.appendRemoteEntriesFromDb();
|
||
|
||
debugPrint('[remote] sync done');
|
||
|
||
if (markBootstrapDoneOnSuccess) {
|
||
await _setBootstrapDone();
|
||
}
|
||
|
||
RemoteSyncBus.instance.finishUpToDate(opId: opId);
|
||
} on TimeoutException {
|
||
debugPrint('[remote] serverDown (timeout)');
|
||
RemoteSyncBus.instance.failServerDown(opId: opId);
|
||
} catch (e) {
|
||
debugPrint('[remote] serverDown (error=$e)');
|
||
RemoteSyncBus.instance.failServerDown(opId: opId);
|
||
} finally {
|
||
_syncInFlight = false;
|
||
}
|
||
}
|
||
}
|