diff --git a/mobile/lib/domain/services/device_sync.service.dart b/mobile/lib/domain/services/device_sync.service.dart index 38e2bfbe03..9e3e2d7292 100644 --- a/mobile/lib/domain/services/device_sync.service.dart +++ b/mobile/lib/domain/services/device_sync.service.dart @@ -9,6 +9,7 @@ import 'package:immich_mobile/domain/models/local_album.model.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/services/store.service.dart'; import 'package:immich_mobile/platform/native_sync_api.g.dart'; +import 'package:immich_mobile/presentation/pages/dev/dev_logger.dart'; import 'package:immich_mobile/utils/diff.dart'; import 'package:logging/logging.dart'; import 'package:platform/platform.dart'; @@ -41,15 +42,20 @@ class DeviceSyncService { try { if (await _nativeSyncApi.shouldFullSync()) { _log.fine("Cannot use partial sync. Performing full sync"); + DLog.log("Cannot use partial sync. Performing full sync"); return await fullSync(); } final delta = await _nativeSyncApi.getMediaChanges(); if (!delta.hasChanges) { _log.fine("No media changes detected. Skipping sync"); + DLog.log("No media changes detected. Skipping sync"); return; } + DLog.log("Delta updated: ${delta.updates.length}"); + DLog.log("Delta deleted: ${delta.deletes.length}"); + final deviceAlbums = await _nativeSyncApi.getAlbums(); await _localAlbumRepository.updateAll(deviceAlbums.toLocalAlbums()); await _localAlbumRepository.processDelta(delta); diff --git a/mobile/lib/presentation/pages/dev/dev_logger.dart b/mobile/lib/presentation/pages/dev/dev_logger.dart new file mode 100644 index 0000000000..6d179241a4 --- /dev/null +++ b/mobile/lib/presentation/pages/dev/dev_logger.dart @@ -0,0 +1,68 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:immich_mobile/domain/models/log.model.dart'; +import 'package:immich_mobile/infrastructure/entities/log.entity.dart'; +import 'package:immich_mobile/infrastructure/repositories/log.repository.dart'; +// ignore: import_rule_isar +import 'package:isar/isar.dart'; + +const kDevLoggerTag = 'DEV'; + +abstract final class DLog { + const DLog(); + + static Stream> watchLog() { + final db = Isar.getInstance(); + if (db == null) { + debugPrint('Isar is not initialized'); + return const Stream.empty(); + } + + return db.loggerMessages + .filter() + .context1EqualTo(kDevLoggerTag) + .sortByCreatedAtDesc() + .watch(fireImmediately: true) + .map((logs) => logs.map((log) => log.toDto()).toList()); + } + + static void clearLog() { + final db = Isar.getInstance(); + if (db == null) { + debugPrint('Isar is not initialized'); + return; + } + + db.writeTxnSync(() { + db.loggerMessages.filter().context1EqualTo(kDevLoggerTag).deleteAllSync(); + }); + } + + static void log(String message, [Object? error, StackTrace? stackTrace]) { + debugPrint('[$kDevLoggerTag] [${DateTime.now()}] $message'); + if (error != null) { + debugPrint('Error: $error'); + } + if (stackTrace != null) { + debugPrint('StackTrace: $stackTrace'); + } + + final isar = Isar.getInstance(); + if (isar == null) { + debugPrint('Isar is not initialized'); + return; + } + + final record = LogMessage( + message: message, + level: LogLevel.info, + createdAt: DateTime.now(), + logger: kDevLoggerTag, + error: error?.toString(), + stack: stackTrace?.toString(), + ); + + unawaited(IsarLogRepository(isar).insert(record)); + } +} diff --git a/mobile/lib/presentation/pages/dev/feat_in_development.page.dart b/mobile/lib/presentation/pages/dev/feat_in_development.page.dart index 9988ae7e85..896d4fdf95 100644 --- a/mobile/lib/presentation/pages/dev/feat_in_development.page.dart +++ b/mobile/lib/presentation/pages/dev/feat_in_development.page.dart @@ -1,9 +1,15 @@ +// ignore_for_file: avoid-local-functions + import 'dart:async'; import 'package:auto_route/auto_route.dart'; -import 'package:drift/drift.dart'; +import 'package:drift/drift.dart' hide Column; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/theme_extensions.dart'; +import 'package:immich_mobile/presentation/pages/dev/dev_logger.dart'; import 'package:immich_mobile/providers/background_sync.provider.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; @@ -60,18 +66,26 @@ class FeatInDevPage extends StatelessWidget { title: const Text('Features in Development'), centerTitle: true, ), - body: ListView.builder( - itemBuilder: (_, index) { - final feat = _features[index]; - return Consumer( - builder: (ctx, ref, _) => ListTile( - title: Text(feat.name), - trailing: Icon(feat.icon), - onTap: () => unawaited(feat.onTap(ctx, ref)), + body: Column( + children: [ + Flexible( + child: ListView.builder( + itemBuilder: (_, index) { + final feat = _features[index]; + return Consumer( + builder: (ctx, ref, _) => ListTile( + title: Text(feat.name), + trailing: Icon(feat.icon), + onTap: () => unawaited(feat.onTap(ctx, ref)), + ), + ); + }, + itemCount: _features.length, ), - ); - }, - itemCount: _features.length, + ), + const Divider(height: 0), + const Flexible(child: _DevLogs()), + ], ), ); } @@ -88,3 +102,66 @@ class _Feature { final IconData icon; final Future Function(BuildContext, WidgetRef _) onTap; } + +// ignore: prefer-single-widget-per-file +class _DevLogs extends StatelessWidget { + const _DevLogs(); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + automaticallyImplyLeading: false, + actions: [ + IconButton( + onPressed: DLog.clearLog, + icon: Icon( + Icons.delete_outline_rounded, + size: 20.0, + color: context.primaryColor, + semanticLabel: "Clear logs", + ), + ), + ], + centerTitle: true, + ), + body: StreamBuilder( + initialData: [], + stream: DLog.watchLog(), + builder: (_, logMessages) { + return ListView.separated( + itemBuilder: (ctx, index) { + // ignore: avoid-unsafe-collection-methods + final logMessage = logMessages.data![index]; + return ListTile( + title: Text( + logMessage.message, + style: TextStyle( + color: ctx.colorScheme.onSurface, + fontSize: 14.0, + overflow: TextOverflow.ellipsis, + ), + ), + subtitle: Text( + "at ${DateFormat("HH:mm:ss.SSS").format(logMessage.createdAt)}", + style: TextStyle( + color: ctx.colorScheme.onSurfaceSecondary, + fontSize: 12.0, + ), + ), + dense: true, + visualDensity: VisualDensity.compact, + tileColor: Colors.transparent, + minLeadingWidth: 10, + ); + }, + separatorBuilder: (_, index) { + return const Divider(height: 0); + }, + itemCount: logMessages.data?.length ?? 0, + ); + }, + ), + ); + } +}