mirror of
https://github.com/immich-app/immich.git
synced 2026-07-01 18:45:05 -07:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 39fe991451 | |||
| cbe34d7931 | |||
| 06c8d5a183 |
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-4
@@ -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(() => {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user