Files
immich/mobile/lib/presentation/widgets/timeline/timeline.state.dart
2026-05-04 21:28:56 +07:00

144 lines
4.8 KiB
Dart

import 'dart:math' as math;
import 'package:collection/collection.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/setting.model.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';
import 'package:immich_mobile/providers/infrastructure/setting.provider.dart';
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
class TimelineArgs {
final double maxWidth;
final double maxHeight;
final double spacing;
final int columnCount;
final bool showStorageIndicator;
final bool withStack;
final GroupAssetsBy? groupBy;
const TimelineArgs({
required this.maxWidth,
required this.maxHeight,
this.spacing = kTimelineSpacing,
this.columnCount = kTimelineColumnCount,
this.showStorageIndicator = false,
this.withStack = false,
this.groupBy,
});
@override
bool operator ==(covariant TimelineArgs other) {
return spacing == other.spacing &&
maxWidth == other.maxWidth &&
maxHeight == other.maxHeight &&
columnCount == other.columnCount &&
showStorageIndicator == other.showStorageIndicator &&
withStack == other.withStack &&
groupBy == other.groupBy;
}
@override
int get hashCode =>
maxWidth.hashCode ^
maxHeight.hashCode ^
spacing.hashCode ^
columnCount.hashCode ^
showStorageIndicator.hashCode ^
withStack.hashCode ^
groupBy.hashCode;
TimelineArgs copyWith({
double? maxWidth,
double? maxHeight,
double? spacing,
int? columnCount,
bool? showStorageIndicator,
bool? withStack,
GroupAssetsBy? groupBy,
}) {
return TimelineArgs(
maxWidth: maxWidth ?? this.maxWidth,
maxHeight: maxHeight ?? this.maxHeight,
spacing: spacing ?? this.spacing,
columnCount: columnCount ?? this.columnCount,
showStorageIndicator: showStorageIndicator ?? this.showStorageIndicator,
withStack: withStack ?? this.withStack,
groupBy: groupBy ?? this.groupBy,
);
}
}
class TimelineState {
final bool isScrubbing;
final bool isScrolling;
const TimelineState({this.isScrubbing = false, this.isScrolling = false});
bool get isInteracting => isScrubbing || isScrolling;
@override
bool operator ==(covariant TimelineState other) {
return isScrubbing == other.isScrubbing && isScrolling == other.isScrolling;
}
@override
int get hashCode => isScrubbing.hashCode ^ isScrolling.hashCode;
TimelineState copyWith({bool? isScrubbing, bool? isScrolling}) {
return TimelineState(isScrubbing: isScrubbing ?? this.isScrubbing, isScrolling: isScrolling ?? this.isScrolling);
}
}
class TimelineStateNotifier extends Notifier<TimelineState> {
void setScrubbing(bool isScrubbing) {
state = state.copyWith(isScrubbing: isScrubbing);
}
void setScrolling(bool isScrolling) {
state = state.copyWith(isScrolling: isScrolling);
}
@override
TimelineState build() => const TimelineState(isScrubbing: false, isScrolling: false);
}
// 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
final timelineSegmentProvider = StreamNotifierProvider<_TimelineSegmentNotifier, List<Segment>>(
_TimelineSegmentNotifier.new,
dependencies: [timelineServiceProvider, timelineArgsProvider],
);
class _TimelineSegmentNotifier extends StreamNotifier<List<Segment>> {
@override
Stream<List<Segment>> build() async* {
final args = ref.watch(timelineArgsProvider);
final columnCount = args.columnCount;
final spacing = args.spacing;
final availableTileWidth = args.maxWidth - (spacing * (columnCount - 1));
final tileExtent = math.max(0, availableTileWidth) / columnCount;
final groupBy = args.groupBy ?? GroupAssetsBy.values[ref.watch(settingsProvider).get(Setting.groupAssetsBy)];
final timelineService = ref.watch(timelineServiceProvider);
yield* timelineService.watchBuckets().map((buckets) {
return FixedSegmentBuilder(
buckets: buckets,
tileHeight: tileExtent,
columnCount: columnCount,
spacing: spacing,
groupBy: groupBy,
).generate();
});
}
@override
bool updateShouldNotify(AsyncValue<List<Segment>> previous, AsyncValue<List<Segment>> next) {
final listEquals = const DeepCollectionEquality().equals;
return !listEquals(previous.value, next.value);
}
}
final timelineStateProvider = NotifierProvider<TimelineStateNotifier, TimelineState>(TimelineStateNotifier.new);