2025-06-16 20:37:45 +05:30
|
|
|
import 'dart:math' as math;
|
|
|
|
|
|
2026-08-05 12:00:49 -07:00
|
|
|
import 'package:flutter/foundation.dart';
|
|
|
|
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
2025-06-16 20:37:45 +05:30
|
|
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
|
|
|
import 'package:immich_mobile/domain/models/timeline.model.dart';
|
|
|
|
|
import 'package:immich_mobile/presentation/widgets/timeline/constants.dart';
|
|
|
|
|
import 'package:immich_mobile/presentation/widgets/timeline/fixed/segment_builder.dart';
|
|
|
|
|
import 'package:immich_mobile/presentation/widgets/timeline/segment.model.dart';
|
2026-05-30 20:57:55 +05:30
|
|
|
import 'package:immich_mobile/providers/infrastructure/settings.provider.dart';
|
2025-06-16 20:37:45 +05:30
|
|
|
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
|
|
|
|
|
2026-08-05 12:00:49 -07:00
|
|
|
part 'timeline.state.freezed.dart';
|
|
|
|
|
|
2025-06-16 20:37:45 +05:30
|
|
|
class TimelineArgs {
|
|
|
|
|
final double maxWidth;
|
|
|
|
|
final double maxHeight;
|
|
|
|
|
final double spacing;
|
|
|
|
|
final int columnCount;
|
2025-10-03 19:37:16 +05:30
|
|
|
final bool showStorageIndicator;
|
2025-07-18 10:01:04 +05:30
|
|
|
final bool withStack;
|
2025-07-18 04:25:25 +08:00
|
|
|
final GroupAssetsBy? groupBy;
|
2025-06-16 20:37:45 +05:30
|
|
|
|
|
|
|
|
const TimelineArgs({
|
|
|
|
|
required this.maxWidth,
|
|
|
|
|
required this.maxHeight,
|
|
|
|
|
this.spacing = kTimelineSpacing,
|
|
|
|
|
this.columnCount = kTimelineColumnCount,
|
2025-10-03 19:37:16 +05:30
|
|
|
this.showStorageIndicator = false,
|
2025-07-18 10:01:04 +05:30
|
|
|
this.withStack = false,
|
2025-07-18 04:25:25 +08:00
|
|
|
this.groupBy,
|
2025-06-16 20:37:45 +05:30
|
|
|
});
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
bool operator ==(covariant TimelineArgs other) {
|
|
|
|
|
return spacing == other.spacing &&
|
|
|
|
|
maxWidth == other.maxWidth &&
|
|
|
|
|
maxHeight == other.maxHeight &&
|
2025-07-11 00:59:52 +08:00
|
|
|
columnCount == other.columnCount &&
|
2025-07-18 04:25:25 +08:00
|
|
|
showStorageIndicator == other.showStorageIndicator &&
|
2025-07-18 10:01:04 +05:30
|
|
|
withStack == other.withStack &&
|
2025-07-18 04:25:25 +08:00
|
|
|
groupBy == other.groupBy;
|
2025-06-16 20:37:45 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
int get hashCode =>
|
|
|
|
|
maxWidth.hashCode ^
|
|
|
|
|
maxHeight.hashCode ^
|
|
|
|
|
spacing.hashCode ^
|
2025-07-11 00:59:52 +08:00
|
|
|
columnCount.hashCode ^
|
2025-07-18 04:25:25 +08:00
|
|
|
showStorageIndicator.hashCode ^
|
2025-07-18 10:01:04 +05:30
|
|
|
withStack.hashCode ^
|
2025-07-18 04:25:25 +08:00
|
|
|
groupBy.hashCode;
|
2025-06-16 20:37:45 +05:30
|
|
|
}
|
|
|
|
|
|
2026-08-05 12:00:49 -07:00
|
|
|
@freezed
|
|
|
|
|
abstract class TimelineState with _$TimelineState {
|
|
|
|
|
const TimelineState._();
|
2025-06-16 20:37:45 +05:30
|
|
|
|
2026-08-05 12:00:49 -07:00
|
|
|
const factory TimelineState({@Default(false) bool isScrubbing, @Default(false) bool isScrolling}) = _TimelineState;
|
2025-06-25 11:08:02 -05:00
|
|
|
|
|
|
|
|
bool get isInteracting => isScrubbing || isScrolling;
|
2025-06-16 20:37:45 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class TimelineStateNotifier extends Notifier<TimelineState> {
|
|
|
|
|
void setScrubbing(bool isScrubbing) {
|
|
|
|
|
state = state.copyWith(isScrubbing: isScrubbing);
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-25 11:08:02 -05:00
|
|
|
void setScrolling(bool isScrolling) {
|
|
|
|
|
state = state.copyWith(isScrolling: isScrolling);
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-16 20:37:45 +05:30
|
|
|
@override
|
2025-07-29 00:34:03 +05:30
|
|
|
TimelineState build() => const TimelineState(isScrubbing: false, isScrolling: false);
|
2025-06-16 20:37:45 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// This provider watches the buckets from the timeline service & args and serves the segments.
|
|
|
|
|
// It should be used only after the timeline service and timeline args provider is overridden
|
2025-07-29 00:34:03 +05:30
|
|
|
final timelineSegmentProvider = StreamProvider.autoDispose<List<Segment>>((ref) async* {
|
2026-07-09 01:13:32 +06:00
|
|
|
// maxHeight is left out on purpose, a height-only change must not restart the bucket stream
|
|
|
|
|
final (maxWidth, columnCount, spacing, groupByArg) = ref.watch(
|
|
|
|
|
timelineArgsProvider.select((args) => (args.maxWidth, args.columnCount, args.spacing, args.groupBy)),
|
|
|
|
|
);
|
|
|
|
|
final availableTileWidth = maxWidth - (spacing * (columnCount - 1));
|
2025-07-29 00:34:03 +05:30
|
|
|
final tileExtent = math.max(0, availableTileWidth) / columnCount;
|
|
|
|
|
|
2026-07-09 01:13:32 +06:00
|
|
|
final groupBy = groupByArg ?? ref.watch(appConfigProvider.select((config) => config.timeline.groupAssetsBy));
|
2025-07-29 00:34:03 +05:30
|
|
|
|
|
|
|
|
final timelineService = ref.watch(timelineServiceProvider);
|
|
|
|
|
yield* timelineService.watchBuckets().map((buckets) {
|
|
|
|
|
return FixedSegmentBuilder(
|
|
|
|
|
buckets: buckets,
|
|
|
|
|
tileHeight: tileExtent,
|
|
|
|
|
columnCount: columnCount,
|
|
|
|
|
spacing: spacing,
|
2026-05-13 02:53:25 +07:00
|
|
|
groupBy: groupBy!,
|
2025-07-29 00:34:03 +05:30
|
|
|
).generate();
|
|
|
|
|
});
|
|
|
|
|
}, dependencies: [timelineServiceProvider, timelineArgsProvider]);
|
|
|
|
|
|
|
|
|
|
final timelineStateProvider = NotifierProvider<TimelineStateNotifier, TimelineState>(TimelineStateNotifier.new);
|