Compare commits

..

3 Commits

13 changed files with 69 additions and 166 deletions
@@ -23,6 +23,6 @@ class ImmichApp : Application() {
// as the previous start might have been killed without unlocking. // as the previous start might have been killed without unlocking.
if (BackgroundEngineLock.connectEngines > 0) return@postDelayed if (BackgroundEngineLock.connectEngines > 0) return@postDelayed
BackgroundWorkerApiImpl.enqueueBackgroundWorker(this) BackgroundWorkerApiImpl.enqueueBackgroundWorker(this)
}, 5000) }, 15000)
} }
} }
@@ -15,6 +15,7 @@ import androidx.work.ListenableWorker
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import app.alextran.immich.MainActivity import app.alextran.immich.MainActivity
import app.alextran.immich.R import app.alextran.immich.R
import com.google.common.util.concurrent.Futures
import com.google.common.util.concurrent.ListenableFuture import com.google.common.util.concurrent.ListenableFuture
import com.google.common.util.concurrent.SettableFuture import com.google.common.util.concurrent.SettableFuture
import io.flutter.FlutterInjector import io.flutter.FlutterInjector
@@ -61,6 +62,11 @@ class BackgroundWorker(context: Context, params: WorkerParameters) :
} }
override fun startWork(): ListenableFuture<Result> { override fun startWork(): ListenableFuture<Result> {
if (BackgroundWorkerPreferences(ctx).isLocked() && BackgroundEngineLock.connectEngines > 0) {
Log.i(TAG, "Foreground engine active, skipping background worker")
return Futures.immediateFuture(Result.success())
}
Log.i(TAG, "Starting background upload worker") Log.i(TAG, "Starting background upload worker")
if (!loader.initialized()) { if (!loader.initialized()) {
@@ -77,6 +83,10 @@ class BackgroundWorker(context: Context, params: WorkerParameters) :
showNotification(notificationConfig.first, notificationConfig.second) showNotification(notificationConfig.first, notificationConfig.second)
loader.ensureInitializationCompleteAsync(ctx, null, Handler(Looper.getMainLooper())) { loader.ensureInitializationCompleteAsync(ctx, null, Handler(Looper.getMainLooper())) {
if (isStopped || isComplete) {
return@ensureInitializationCompleteAsync
}
engine = FlutterEngine(ctx) engine = FlutterEngine(ctx)
FlutterEngineCache.getInstance().put(BackgroundWorkerApiImpl.ENGINE_CACHE_KEY, engine!!) FlutterEngineCache.getInstance().put(BackgroundWorkerApiImpl.ENGINE_CACHE_KEY, engine!!)
@@ -143,11 +153,17 @@ class BackgroundWorker(context: Context, params: WorkerParameters) :
return return
} }
val api = flutterApi
if (api == null) {
Handler(Looper.getMainLooper()).postAtFrontOfQueue {
complete(Result.failure())
}
return
}
Handler(Looper.getMainLooper()).postAtFrontOfQueue { Handler(Looper.getMainLooper()).postAtFrontOfQueue {
if (flutterApi != null) { api.cancel {
flutterApi?.cancel { complete(Result.failure())
complete(Result.failure())
}
} }
} }
@@ -138,9 +138,7 @@ class LocalSyncService {
final Stopwatch stopwatch = Stopwatch()..start(); final Stopwatch stopwatch = Stopwatch()..start();
final deviceAlbums = await _nativeSyncApi.getAlbums(); final deviceAlbums = await _nativeSyncApi.getAlbums();
final getAlbumsTime = stopwatch.elapsedMilliseconds;
final dbAlbums = await _localAlbumRepository.getAll(sortBy: {SortLocalAlbumsBy.id}); final dbAlbums = await _localAlbumRepository.getAll(sortBy: {SortLocalAlbumsBy.id});
final getAllTime = stopwatch.elapsedMilliseconds;
await diffSortedLists( await diffSortedLists(
dbAlbums, dbAlbums,
@@ -150,15 +148,10 @@ class LocalSyncService {
onlyFirst: removeAlbum, onlyFirst: removeAlbum,
onlySecond: addAlbum, onlySecond: addAlbum,
); );
final diffTime = stopwatch.elapsedMilliseconds;
await _nativeSyncApi.checkpointSync(); await _nativeSyncApi.checkpointSync();
stopwatch.stop(); stopwatch.stop();
_log.info( _log.info("Full device sync took - ${stopwatch.elapsedMilliseconds}ms");
"Full device sync took - ${stopwatch.elapsedMilliseconds}ms "
"(getAlbums=${getAlbumsTime}ms, getAll=${getAllTime - getAlbumsTime}ms, "
"diff=${diffTime - getAllTime}ms, checkpoint=${stopwatch.elapsedMilliseconds - diffTime}ms)",
);
} on PlatformException catch (e, s) { } on PlatformException catch (e, s) {
if (e.code == _kSyncCancelledCode) { if (e.code == _kSyncCancelledCode) {
_log.warning("Full device sync cancelled"); _log.warning("Full device sync cancelled");
@@ -10,8 +10,6 @@ import 'package:immich_mobile/domain/utils/event_stream.dart';
import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/timeline.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/timeline.repository.dart';
import 'package:immich_mobile/utils/async_mutex.dart'; import 'package:immich_mobile/utils/async_mutex.dart';
import 'package:logging/logging.dart';
import 'package:stream_transform/stream_transform.dart';
typedef TimelineAssetSource = Future<List<BaseAsset>> Function(int index, int count); typedef TimelineAssetSource = Future<List<BaseAsset>> Function(int index, int count);
@@ -92,7 +90,6 @@ class TimelineFactory {
} }
class TimelineService { class TimelineService {
static final Logger _log = Logger('TimelineService');
final TimelineAssetSource _assetSource; final TimelineAssetSource _assetSource;
final TimelineBucketSource _bucketSource; final TimelineBucketSource _bucketSource;
final TimelineOrigin origin; final TimelineOrigin origin;
@@ -108,53 +105,37 @@ class TimelineService {
: this._(assetSource: query.assetSource, bucketSource: query.bucketSource, origin: query.origin); : this._(assetSource: query.assetSource, bucketSource: query.bucketSource, origin: query.origin);
TimelineService._({required this._assetSource, required this._bucketSource, required this.origin}) { TimelineService._({required this._assetSource, required this._bucketSource, required this.origin}) {
_bucketSubscription = watchBuckets().listen( _bucketSubscription = _bucketSource().listen((buckets) {
(buckets) { _mutex.run(() async {
_mutex.run(() async { final totalAssets = buckets.fold<int>(0, (acc, bucket) => acc + bucket.assetCount);
try {
final totalAssets = buckets.fold<int>(0, (acc, bucket) => acc + bucket.assetCount);
_log.info( if (totalAssets == 0) {
'[$origin] bucket emission: ${buckets.length} buckets / $totalAssets assets ' _bufferOffset = 0;
'(current _totalAssets=$_totalAssets, _bufferOffset=$_bufferOffset, _buffer=${_buffer.length})', _buffer = [];
); } else {
final int offset;
if (totalAssets == 0) { final int count;
_bufferOffset = 0; // When the buffer is empty or the old bufferOffset is greater than the new total assets,
_buffer = []; // we need to reset the buffer and load the first batch of assets.
} else { if (_bufferOffset >= totalAssets || _buffer.isEmpty) {
final int offset; offset = 0;
final int count; count = kTimelineAssetLoadBatchSize;
// When the buffer is empty or the old bufferOffset is greater than the new total assets, } else {
// we need to reset the buffer and load the first batch of assets. offset = _bufferOffset;
if (_bufferOffset >= totalAssets || _buffer.isEmpty) { count = math.min(_buffer.length, totalAssets - _bufferOffset);
offset = 0;
count = kTimelineAssetLoadBatchSize;
} else {
offset = _bufferOffset;
count = math.min(_buffer.length, totalAssets - _bufferOffset);
}
_buffer = await _assetSource(offset, count);
_bufferOffset = offset;
_log.info('[$origin] buffer reloaded: offset=$offset requested=$count got=${_buffer.length}');
}
_totalAssets = totalAssets;
EventStream.shared.emit(const TimelineReloadEvent());
} catch (error, stack) {
_log.severe('[$origin] bucket reload FAILED — _totalAssets stuck at $_totalAssets', error, stack);
rethrow;
} }
}); _buffer = await _assetSource(offset, count);
}, _bufferOffset = offset;
onError: (Object error, StackTrace stack) { }
_log.severe('[$origin] bucket stream errored', error, stack);
}, // change the state's total assets count only after the buffer is reloaded
); _totalAssets = totalAssets;
EventStream.shared.emit(const TimelineReloadEvent());
});
});
} }
Stream<List<Bucket>> Function() get watchBuckets => Stream<List<Bucket>> Function() get watchBuckets => _bucketSource;
() => _bucketSource().throttle(const Duration(milliseconds: 500), trailing: true);
Future<List<BaseAsset>> loadAssets(int index, int count) => _mutex.run(() => _loadAssets(index, count)); Future<List<BaseAsset>> loadAssets(int index, int count) => _mutex.run(() => _loadAssets(index, count));
@@ -183,13 +164,6 @@ class TimelineService {
_buffer = await _assetSource(start, len); _buffer = await _assetSource(start, len);
_bufferOffset = start; _bufferOffset = start;
if (!hasRange(index, count)) {
_log.warning(
'[$origin] _loadAssets($index, $count): buffer loaded (offset=$start, got=${_buffer.length}) but still '
'out of range — _totalAssets=$_totalAssets. getAssets is about to throw RangeError.',
);
}
return getAssets(index, count); return getAssets(index, count);
} }
@@ -23,7 +23,6 @@ import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.da
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/routing/router.dart';
import 'package:logging/logging.dart';
class FixedSegment extends Segment { class FixedSegment extends Segment {
final double tileHeight; final double tileHeight;
@@ -91,7 +90,6 @@ class FixedSegment extends Segment {
} }
class _FixedSegmentRow extends ConsumerWidget { class _FixedSegmentRow extends ConsumerWidget {
static final Logger _log = Logger('TimelineRow');
final int assetIndex; final int assetIndex;
final int assetCount; final int assetCount;
final double tileHeight; final double tileHeight;
@@ -111,20 +109,8 @@ class _FixedSegmentRow extends ConsumerWidget {
final isScrubbing = ref.watch(timelineStateProvider.select((s) => s.isScrubbing)); final isScrubbing = ref.watch(timelineStateProvider.select((s) => s.isScrubbing));
final timelineService = ref.read(timelineServiceProvider); final timelineService = ref.read(timelineServiceProvider);
final isDynamicLayout = columnCount <= (context.isMobile ? 2 : 3); final isDynamicLayout = columnCount <= (context.isMobile ? 2 : 3);
final inRange = timelineService.hasRange(assetIndex, assetCount);
if (assetIndex == 0) { if (timelineService.hasRange(assetIndex, assetCount)) {
_log.info(
'row[0] inRange=$inRange isScrubbing=$isScrubbing totalAssets=${timelineService.totalAssets} '
'branch=${inRange
? "assets"
: isScrubbing
? "placeholder(scrubbing)"
: "future(load)"}',
);
}
if (inRange) {
return _buildAssetRow( return _buildAssetRow(
context, context,
timelineService.getAssets(assetIndex, assetCount), timelineService.getAssets(assetIndex, assetCount),
@@ -143,13 +129,6 @@ class _FixedSegmentRow extends ConsumerWidget {
if (snapshot.connectionState != ConnectionState.done) { if (snapshot.connectionState != ConnectionState.done) {
return _buildPlaceholder(context); return _buildPlaceholder(context);
} }
if (snapshot.hasError) {
_log.warning(
'render row loadAssets($assetIndex, $assetCount) failed (totalAssets=${timelineService.totalAssets})',
snapshot.error,
snapshot.stackTrace,
);
}
return _buildAssetRow(context, snapshot.requireData, timelineService, isDynamicLayout); return _buildAssetRow(context, snapshot.requireData, timelineService, isDynamicLayout);
}, },
); );
@@ -13,7 +13,6 @@ import 'package:immich_mobile/presentation/widgets/timeline/timeline.state.dart'
import 'package:immich_mobile/providers/haptic_feedback.provider.dart'; import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
import 'package:immich_mobile/utils/debounce.dart'; import 'package:immich_mobile/utils/debounce.dart';
import 'package:intl/intl.dart' hide TextDirection; import 'package:intl/intl.dart' hide TextDirection;
import 'package:logging/logging.dart';
/// A widget that will display a BoxScrollView with a ScrollThumb that can be dragged /// A widget that will display a BoxScrollView with a ScrollThumb that can be dragged
/// for quick navigation of the BoxScrollView. /// for quick navigation of the BoxScrollView.
@@ -85,7 +84,6 @@ List<_Segment> _buildSegments({required List<Segment> layoutSegments, required d
} }
class ScrubberState extends ConsumerState<Scrubber> with TickerProviderStateMixin { class ScrubberState extends ConsumerState<Scrubber> with TickerProviderStateMixin {
static final Logger _log = Logger('Scrubber');
String? _lastLabel; String? _lastLabel;
double _thumbTopOffset = 0.0; double _thumbTopOffset = 0.0;
bool _isDragging = false; bool _isDragging = false;
@@ -116,7 +114,6 @@ class ScrubberState extends ConsumerState<Scrubber> with TickerProviderStateMixi
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_log.info('Scrubber initState');
_isDragging = false; _isDragging = false;
_segments = _buildSegments(layoutSegments: widget.layoutSegments, timelineHeight: _scrubberHeight); _segments = _buildSegments(layoutSegments: widget.layoutSegments, timelineHeight: _scrubberHeight);
_thumbAnimationController = AnimationController(vsync: this, duration: kTimelineScrubberFadeInDuration); _thumbAnimationController = AnimationController(vsync: this, duration: kTimelineScrubberFadeInDuration);
@@ -137,10 +134,7 @@ class ScrubberState extends ConsumerState<Scrubber> with TickerProviderStateMixi
void didUpdateWidget(covariant Scrubber oldWidget) { void didUpdateWidget(covariant Scrubber oldWidget) {
super.didUpdateWidget(oldWidget); super.didUpdateWidget(oldWidget);
final oldEnd = oldWidget.layoutSegments.lastOrNull?.endOffset; if (oldWidget.layoutSegments.lastOrNull?.endOffset != widget.layoutSegments.lastOrNull?.endOffset) {
final newEnd = widget.layoutSegments.lastOrNull?.endOffset;
if (oldEnd != newEnd) {
_log.info('Scrubber layoutSegments endOffset $oldEnd -> $newEnd (isDragging=$_isDragging)');
_segments = _buildSegments(layoutSegments: widget.layoutSegments, timelineHeight: _scrubberHeight); _segments = _buildSegments(layoutSegments: widget.layoutSegments, timelineHeight: _scrubberHeight);
_monthCount = getMonthCount(); _monthCount = getMonthCount();
} }
@@ -148,15 +142,6 @@ class ScrubberState extends ConsumerState<Scrubber> with TickerProviderStateMixi
@override @override
void dispose() { void dispose() {
if (_isDragging || _currentScrubberDate != null || _scrubberDebouncer != null) {
_log.warning(
'Scrubber dispose mid-scrub '
'(isDragging=$_isDragging, pendingDate=$_currentScrubberDate, '
'debouncerPending=${_scrubberDebouncer != null}) — scrubbing reset may be orphaned',
);
} else {
_log.info('Scrubber dispose');
}
_thumbAnimationController.dispose(); _thumbAnimationController.dispose();
_labelAnimationController.dispose(); _labelAnimationController.dispose();
_fadeOutTimer?.cancel(); _fadeOutTimer?.cancel();
@@ -223,7 +208,6 @@ class ScrubberState extends ConsumerState<Scrubber> with TickerProviderStateMixi
} }
void _onDragStart(DragStartDetails _) { void _onDragStart(DragStartDetails _) {
_log.info('scrub dragStart');
setState(() { setState(() {
_isDragging = true; _isDragging = true;
_labelAnimationController.forward(); _labelAnimationController.forward();
@@ -238,15 +222,9 @@ class ScrubberState extends ConsumerState<Scrubber> with TickerProviderStateMixi
} }
if (_scrubberHeight <= 0) { if (_scrubberHeight <= 0) {
_log.warning('drag ignored: scrubberHeight=$_scrubberHeight <= 0');
return; return;
} }
final maxScrollExtent = _scrollController.hasClients ? _scrollController.position.maxScrollExtent : -1;
if (maxScrollExtent <= 0) {
_log.warning('drag ineffective: hasClients=${_scrollController.hasClients} maxScrollExtent=$maxScrollExtent');
}
if (_thumbAnimationController.status != AnimationStatus.forward) { if (_thumbAnimationController.status != AnimationStatus.forward) {
_thumbAnimationController.forward(); _thumbAnimationController.forward();
} }
@@ -366,7 +344,6 @@ class ScrubberState extends ConsumerState<Scrubber> with TickerProviderStateMixi
} }
void _onDragEnd(DragEndDetails _) { void _onDragEnd(DragEndDetails _) {
_log.info('scrub dragEnd -> setScrubbing(false)');
_labelAnimationController.reverse(); _labelAnimationController.reverse();
setState(() { setState(() {
_isDragging = false; _isDragging = false;
@@ -7,7 +7,6 @@ import 'package:immich_mobile/presentation/widgets/timeline/fixed/segment_builde
import 'package:immich_mobile/presentation/widgets/timeline/segment.model.dart'; import 'package:immich_mobile/presentation/widgets/timeline/segment.model.dart';
import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/providers/infrastructure/settings.provider.dart';
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
import 'package:logging/logging.dart';
class TimelineArgs { class TimelineArgs {
final double maxWidth; final double maxWidth;
@@ -72,27 +71,14 @@ class TimelineState {
} }
class TimelineStateNotifier extends Notifier<TimelineState> { class TimelineStateNotifier extends Notifier<TimelineState> {
static final Logger _log = Logger('TimelineState');
void setScrubbing(bool isScrubbing) { void setScrubbing(bool isScrubbing) {
if (state.isScrubbing != isScrubbing) {
_log.info('isScrubbing ${state.isScrubbing} -> $isScrubbing (from ${_callSite()})');
}
state = state.copyWith(isScrubbing: isScrubbing); state = state.copyWith(isScrubbing: isScrubbing);
} }
void setScrolling(bool isScrolling) { void setScrolling(bool isScrolling) {
if (state.isScrolling != isScrolling) {
_log.info('isScrolling ${state.isScrolling} -> $isScrolling (from ${_callSite()})');
}
state = state.copyWith(isScrolling: isScrolling); state = state.copyWith(isScrolling: isScrolling);
} }
static String _callSite() {
final frames = StackTrace.current.toString().split('\n');
return frames.length > 2 ? frames[2].trim() : 'unknown';
}
@override @override
TimelineState build() => const TimelineState(isScrubbing: false, isScrolling: false); TimelineState build() => const TimelineState(isScrubbing: false, isScrolling: false);
} }
@@ -110,11 +96,6 @@ final timelineSegmentProvider = StreamProvider.autoDispose<List<Segment>>((ref)
final timelineService = ref.watch(timelineServiceProvider); final timelineService = ref.watch(timelineServiceProvider);
yield* timelineService.watchBuckets().map((buckets) { yield* timelineService.watchBuckets().map((buckets) {
final layoutTotal = buckets.fold<int>(0, (acc, bucket) => acc + bucket.assetCount);
Logger('TimelineService').info(
'[${timelineService.origin}] segment layout: '
'${buckets.length} buckets / $layoutTotal assets (service.totalAssets=${timelineService.totalAssets})',
);
return FixedSegmentBuilder( return FixedSegmentBuilder(
buckets: buckets, buckets: buckets,
tileHeight: tileExtent, tileHeight: tileExtent,
@@ -28,7 +28,6 @@ import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
import 'package:immich_mobile/widgets/common/immich_sliver_app_bar.dart'; import 'package:immich_mobile/widgets/common/immich_sliver_app_bar.dart';
import 'package:immich_mobile/widgets/common/mesmerizing_sliver_app_bar.dart'; import 'package:immich_mobile/widgets/common/mesmerizing_sliver_app_bar.dart';
import 'package:immich_mobile/widgets/common/selection_sliver_app_bar.dart'; import 'package:immich_mobile/widgets/common/selection_sliver_app_bar.dart';
import 'package:logging/logging.dart';
class Timeline extends StatelessWidget { class Timeline extends StatelessWidget {
const Timeline({ const Timeline({
@@ -137,7 +136,6 @@ class _SliverTimeline extends ConsumerStatefulWidget {
} }
class _SliverTimelineState extends ConsumerState<_SliverTimeline> { class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
static final Logger _log = Logger('Timeline');
late final ScrollController _scrollController; late final ScrollController _scrollController;
StreamSubscription? _eventSubscription; StreamSubscription? _eventSubscription;
@@ -155,7 +153,6 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_log.info('SliverTimeline initState');
_scrollController = ScrollController(onAttach: _restoreAssetPosition); _scrollController = ScrollController(onAttach: _restoreAssetPosition);
_eventSubscription = EventStream.shared.listen(_onEvent); _eventSubscription = EventStream.shared.listen(_onEvent);
@@ -182,7 +179,6 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
} }
void _onEvent(Event event) { void _onEvent(Event event) {
_log.info('event ${event.runtimeType}');
switch (event) { switch (event) {
case ScrollToTopEvent(): case ScrollToTopEvent():
{ {
@@ -190,10 +186,7 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
timelineState.setScrubbing(true); timelineState.setScrubbing(true);
_scrollController _scrollController
.animateTo(0, duration: const Duration(milliseconds: 250), curve: Curves.easeInOut) .animateTo(0, duration: const Duration(milliseconds: 250), curve: Curves.easeInOut)
.whenComplete(() { .whenComplete(() => timelineState.setScrubbing(false));
_log.info('ScrollToTop animation done -> setScrubbing(false)');
timelineState.setScrubbing(false);
});
} }
case ScrollToDateEvent scrollToDateEvent: case ScrollToDateEvent scrollToDateEvent:
@@ -253,7 +246,6 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
@override @override
void dispose() { void dispose() {
_log.info('SliverTimeline dispose');
_scrollController.dispose(); _scrollController.dispose();
_eventSubscription?.cancel(); _eventSubscription?.cancel();
super.dispose(); super.dispose();
@@ -294,12 +286,8 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
duration: const Duration(milliseconds: 500), duration: const Duration(milliseconds: 500),
curve: Curves.easeInOut, curve: Curves.easeInOut,
) )
.whenComplete(() { .whenComplete(() => timelineState.setScrubbing(false));
_log.info('ScrollToDate animation done -> setScrubbing(false)');
timelineState.setScrubbing(false);
});
} else { } else {
_log.info('ScrollToDate: no matching segment for $date -> setScrubbing(false)');
timelineState.setScrubbing(false); timelineState.setScrubbing(false);
} }
}); });
@@ -5,9 +5,6 @@ import 'package:immich_mobile/presentation/widgets/timeline/timeline.state.dart'
import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/providers/infrastructure/settings.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart';
import 'package:logging/logging.dart';
final _log = Logger('TimelineProvider');
final timelineRepositoryProvider = Provider<DriftTimelineRepository>( final timelineRepositoryProvider = Provider<DriftTimelineRepository>(
(ref) => DriftTimelineRepository(ref.watch(driftProvider)), (ref) => DriftTimelineRepository(ref.watch(driftProvider)),
@@ -21,11 +18,7 @@ final timelineServiceProvider = Provider<TimelineService>(
(ref) { (ref) {
final timelineUsers = ref.watch(timelineUsersProvider).valueOrNull ?? []; final timelineUsers = ref.watch(timelineUsersProvider).valueOrNull ?? [];
final timelineService = ref.watch(timelineFactoryProvider).main(timelineUsers); final timelineService = ref.watch(timelineFactoryProvider).main(timelineUsers);
_log.info('main TimelineService built users=$timelineUsers'); ref.onDispose(timelineService.dispose);
ref.onDispose(() {
_log.info('main TimelineService disposed');
timelineService.dispose();
});
return timelineService; return timelineService;
}, },
// Empty dependencies to inform the framework that this provider // Empty dependencies to inform the framework that this provider
@@ -43,12 +36,8 @@ final timelineFactoryProvider = Provider<TimelineFactory>(
final timelineUsersProvider = StreamProvider<List<String>>((ref) { final timelineUsersProvider = StreamProvider<List<String>>((ref) {
final currentUserId = ref.watch(currentUserProvider.select((u) => u?.id)); final currentUserId = ref.watch(currentUserProvider.select((u) => u?.id));
if (currentUserId == null) { if (currentUserId == null) {
_log.info('timelineUsers: currentUserId=null -> []');
return Stream.value([]); return Stream.value([]);
} }
return ref.watch(timelineRepositoryProvider).watchTimelineUserIds(currentUserId).map((users) { return ref.watch(timelineRepositoryProvider).watchTimelineUserIds(currentUserId);
_log.info('timelineUsers emission: $users');
return users;
});
}); });
@@ -44,7 +44,7 @@
brokenAssetClass?: ClassValue; brokenAssetClass?: ClassValue;
dimmed?: boolean; dimmed?: boolean;
albumUsers?: UserResponseDto[]; albumUsers?: UserResponseDto[];
onClick?: (asset: TimelineAsset) => void; onClick?: (asset: TimelineAsset, event?: MouseEvent) => void;
onPreview?: (asset: TimelineAsset) => void; onPreview?: (asset: TimelineAsset) => void;
onSelect?: (asset: TimelineAsset) => void; onSelect?: (asset: TimelineAsset) => void;
onMouseEvent?: (event: { isMouseOver: boolean; selectedGroupIndex: number }) => void; onMouseEvent?: (event: { isMouseOver: boolean; selectedGroupIndex: number }) => void;
@@ -93,12 +93,12 @@
} }
}; };
const callClickHandlers = () => { const callClickHandlers = (e?: MouseEvent) => {
if (selected) { if (selected) {
onIconClickedHandler(); onIconClickedHandler(e);
return; return;
} }
onClick?.($state.snapshot(asset)); onClick?.($state.snapshot(asset), e);
}; };
const handleClick = (e: MouseEvent) => { const handleClick = (e: MouseEvent) => {
@@ -109,7 +109,7 @@
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
callClickHandlers(); callClickHandlers(e);
}; };
const onMouseEnter = () => { const onMouseEnter = () => {
@@ -59,6 +59,7 @@
groupTitle: string, groupTitle: string,
asset: TimelineAsset, asset: TimelineAsset,
) => void, ) => void,
event?: MouseEvent,
) => void; ) => void;
} }
@@ -685,9 +686,9 @@
{asset} {asset}
{albumUsers} {albumUsers}
{groupIndex} {groupIndex}
onClick={(asset) => { onClick={(asset, event) => {
if (typeof onThumbnailClick === 'function') { if (typeof onThumbnailClick === 'function') {
onThumbnailClick(asset, timelineManager, timelineDay, _onClick); onThumbnailClick(asset, timelineManager, timelineDay, _onClick, event);
} else { } else {
_onClick(timelineManager, timelineDay.getAssets(), timelineDay.groupTitle, asset); _onClick(timelineManager, timelineDay.getAssets(), timelineDay.groupTitle, asset);
} }
@@ -118,7 +118,12 @@
groupTitle: string, groupTitle: string,
asset: TimelineAsset, asset: TimelineAsset,
) => void, ) => void,
event?: MouseEvent,
) => { ) => {
if (event?.shiftKey) {
onClick(timelineManager, timelineDay.getAssets(), timelineDay.groupTitle, asset);
return;
}
if (hasGps(asset)) { if (hasGps(asset)) {
locationUpdated = true; locationUpdated = true;
setTimeout(() => { setTimeout(() => {
+1 -1
View File
@@ -16,7 +16,7 @@ const config = {
preprocess: vitePreprocess(), preprocess: vitePreprocess(),
kit: { kit: {
version: { version: {
name: process.env.IMMICH_BUILD || Date.now().toString(), name: process.env.IMMICH_BUILD || process.env.npm_package_version || 'local',
}, },
paths: { paths: {
relative: false, relative: false,