mirror of
https://github.com/immich-app/immich.git
synced 2025-06-23 15:30:51 -04:00
timeline monster
This commit is contained in:
parent
6311ecadd4
commit
3a4e9a0129
@ -1,13 +1,20 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:immich_mobile/domain/models/asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/render_list.model.dart';
|
||||
import 'package:immich_mobile/domain/models/render_list_element.model.dart';
|
||||
import 'package:immich_mobile/i18n/strings.g.dart';
|
||||
import 'package:immich_mobile/presentation/components/common/page_empty.widget.dart';
|
||||
import 'package:immich_mobile/presentation/components/grid/asset_grid.state.dart';
|
||||
import 'package:immich_mobile/presentation/components/grid/asset_render_grid.widget.dart';
|
||||
import 'package:immich_mobile/presentation/components/grid/draggable_scrollbar.dart';
|
||||
import 'package:immich_mobile/presentation/components/image/immich_image.widget.dart';
|
||||
import 'package:immich_mobile/presentation/components/image/immich_thumbnail.widget.dart';
|
||||
import 'package:immich_mobile/utils/constants/size_constants.dart';
|
||||
import 'package:immich_mobile/utils/extensions/async_snapshot.extension.dart';
|
||||
import 'package:immich_mobile/utils/extensions/build_context.extension.dart';
|
||||
import 'package:immich_mobile/utils/extensions/color.extension.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
@ -16,6 +23,249 @@ import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
||||
|
||||
part 'asset_grid_header.widget.dart';
|
||||
|
||||
class ImmichAssetGridView extends StatefulWidget {
|
||||
const ImmichAssetGridView({super.key});
|
||||
|
||||
@override
|
||||
createState() {
|
||||
return ImmichAssetGridViewState();
|
||||
}
|
||||
}
|
||||
|
||||
class ImmichAssetGridViewState extends State<ImmichAssetGridView> {
|
||||
final ItemScrollController _itemScrollController = ItemScrollController();
|
||||
final ScrollOffsetController _scrollOffsetController =
|
||||
ScrollOffsetController();
|
||||
final ItemPositionsListener _itemPositionsListener =
|
||||
ItemPositionsListener.create();
|
||||
bool _scrolling = false;
|
||||
|
||||
Widget _itemBuilder(BuildContext c, int position) {
|
||||
int index = position;
|
||||
|
||||
return BlocSelector<AssetGridCubit, AssetGridState, RenderList>(
|
||||
selector: (state) => state.renderList,
|
||||
builder: (_, renderList) {
|
||||
final section = renderList.elements.elementAtOrNull(index);
|
||||
|
||||
if (renderList.totalCount == 0 || section == null) {
|
||||
return const _ImGridEmpty();
|
||||
}
|
||||
|
||||
return _Section(sectionIndex: index);
|
||||
}, // no.of elements are not equal or is modified
|
||||
);
|
||||
}
|
||||
|
||||
Text? _labelBuilder(List<RenderListElement> elements, int currentPosition) {
|
||||
final element = elements.elementAtOrNull(currentPosition);
|
||||
if (element == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Text(
|
||||
DateFormat.yMMMM().format(element.date),
|
||||
style: TextStyle(
|
||||
color: context.colorScheme.onSurface,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAssetGrid() {
|
||||
final useDragScrolling = true;
|
||||
|
||||
// ignore: avoid-local-functions
|
||||
void dragScrolling(bool active) {
|
||||
if (active != _scrolling) {
|
||||
setState(() {
|
||||
_scrolling = active;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ignore: avoid-local-functions
|
||||
bool appBarOffset() => true;
|
||||
|
||||
return BlocSelector<AssetGridCubit, AssetGridState, RenderList>(
|
||||
selector: (state) => state.renderList,
|
||||
builder: (_, renderList) {
|
||||
final listWidget = ScrollablePositionedList.builder(
|
||||
itemCount: renderList.elements.length,
|
||||
itemBuilder: _itemBuilder,
|
||||
itemScrollController: _itemScrollController,
|
||||
itemPositionsListener: _itemPositionsListener,
|
||||
scrollOffsetController: _scrollOffsetController,
|
||||
padding: EdgeInsets.only(top: appBarOffset() ? 60 : 0, bottom: 220),
|
||||
addRepaintBoundaries: true,
|
||||
);
|
||||
|
||||
return (useDragScrolling && ModalRoute.of(context) != null)
|
||||
? DraggableScrollbar.semicircle(
|
||||
controller: _itemScrollController,
|
||||
itemPositionsListener: _itemPositionsListener,
|
||||
scrollStateListener: dragScrolling,
|
||||
backgroundColor: context.colorScheme.surfaceContainerHighest,
|
||||
foregroundColor: context.colorScheme.onSurface,
|
||||
padding: appBarOffset()
|
||||
? const EdgeInsets.only(top: 120)
|
||||
: const EdgeInsets.only(),
|
||||
heightOffset: appBarOffset() ? 60 : 0,
|
||||
scrollbarAnimationDuration: const Duration(milliseconds: 300),
|
||||
scrollbarTimeToFade: const Duration(milliseconds: 1000),
|
||||
labelTextBuilder: (pos) =>
|
||||
_labelBuilder(renderList.elements, pos),
|
||||
labelConstraints: const BoxConstraints(maxHeight: 28),
|
||||
child: listWidget,
|
||||
)
|
||||
: listWidget;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _buildAssetGrid();
|
||||
}
|
||||
}
|
||||
|
||||
/// A single row of all placeholder widgets
|
||||
class _PlaceholderRow extends StatelessWidget {
|
||||
final int number;
|
||||
final double width;
|
||||
final double height;
|
||||
|
||||
const _PlaceholderRow({
|
||||
super.key,
|
||||
required this.number,
|
||||
required this.width,
|
||||
required this.height,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
for (int i = 0; i < 4; i++)
|
||||
ImImagePlaceholder(
|
||||
key: ValueKey(i),
|
||||
width: width,
|
||||
height: height,
|
||||
margin: 1,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A section for the render grid
|
||||
class _Section extends StatelessWidget {
|
||||
final int sectionIndex;
|
||||
|
||||
const _Section({required this.sectionIndex});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<AssetGridCubit, AssetGridState>(
|
||||
builder: (_, state) => LayoutBuilder(
|
||||
builder: (_, constraints) {
|
||||
// ignore: avoid-unsafe-collection-methods
|
||||
final section = state.renderList.elements[sectionIndex];
|
||||
|
||||
if (section is RenderListMonthHeaderElement) {
|
||||
return _MonthHeader(text: section.header);
|
||||
}
|
||||
|
||||
if (section is RenderListDayHeaderElement) {
|
||||
return Text(section.header);
|
||||
}
|
||||
|
||||
if (section is! RenderListAssetElement) {
|
||||
return const SizedBox();
|
||||
}
|
||||
|
||||
final scrolling = state.isDragScrolling;
|
||||
final assetsPerRow = 4;
|
||||
final margin = 1.0;
|
||||
|
||||
final width = constraints.maxWidth / 4 - (4 - 1) * margin / 4;
|
||||
final rows = (section.assetCount + 4 - 1) ~/ 4;
|
||||
final Future<List<Asset>> assetsToRender = scrolling
|
||||
? Future.value([])
|
||||
: context
|
||||
.read<AssetGridCubit>()
|
||||
.loadAssets(section.assetCount, section.assetCount);
|
||||
return FutureBuilder(
|
||||
future: assetsToRender,
|
||||
builder: (_, snap) => Column(
|
||||
key: ValueKey(section.assetCount),
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
for (int i = 0; i < rows; i++)
|
||||
scrolling || snap.isWaiting
|
||||
? _PlaceholderRow(
|
||||
key: ValueKey(i),
|
||||
number: assetsPerRow,
|
||||
width: width - 1.5,
|
||||
height: width,
|
||||
)
|
||||
: _AssetRow(
|
||||
key: ValueKey(i),
|
||||
assets: snap.data!.nestedSlice(
|
||||
i * assetsPerRow,
|
||||
min((i + 1) * assetsPerRow, section.assetCount),
|
||||
),
|
||||
width: width,
|
||||
margin: margin,
|
||||
assetsPerRow: assetsPerRow,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
// no.of elements are not equal or is modified
|
||||
buildWhen: (previous, current) =>
|
||||
(previous.renderList.elements.length !=
|
||||
current.renderList.elements.length) ||
|
||||
!previous.renderList.modifiedTime
|
||||
.isAtSameMomentAs(current.renderList.modifiedTime),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// The row of assets
|
||||
class _AssetRow extends StatelessWidget {
|
||||
final List<Asset> assets;
|
||||
final double width;
|
||||
final double margin;
|
||||
final int assetsPerRow;
|
||||
|
||||
const _AssetRow({
|
||||
super.key,
|
||||
required this.assets,
|
||||
required this.width,
|
||||
required this.margin,
|
||||
required this.assetsPerRow,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
key: key,
|
||||
children: assets.mapIndexed((int index, Asset asset) {
|
||||
final bool last = index + 1 == assetsPerRow;
|
||||
return Container(
|
||||
width: width,
|
||||
height: width,
|
||||
margin: EdgeInsets.only(right: last ? 0.0 : margin, bottom: margin),
|
||||
child: ImThumbnail(asset),
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ImAssetGrid extends StatefulWidget {
|
||||
/// The padding for the grid
|
||||
final double? topPadding;
|
||||
@ -142,3 +392,33 @@ class _ImGridEmpty extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension ListExtension<E> on List<E> {
|
||||
List<E> uniqueConsecutive({
|
||||
int Function(E a, E b)? compare,
|
||||
void Function(E a, E b)? onDuplicate,
|
||||
}) {
|
||||
compare ??= (E a, E b) => a == b ? 0 : 1;
|
||||
int i = 1, j = 1;
|
||||
for (; i < length; i++) {
|
||||
if (compare(this[i - 1], this[i]) != 0) {
|
||||
if (i != j) {
|
||||
this[j] = this[i];
|
||||
}
|
||||
j++;
|
||||
} else if (onDuplicate != null) {
|
||||
onDuplicate(this[i - 1], this[i]);
|
||||
}
|
||||
}
|
||||
length = length == 0 ? 0 : j;
|
||||
return this;
|
||||
}
|
||||
|
||||
ListSlice<E> nestedSlice(int start, int end) {
|
||||
if (this is ListSlice) {
|
||||
final ListSlice<E> self = this as ListSlice<E>;
|
||||
return ListSlice<E>(self.source, self.start + start, self.start + end);
|
||||
}
|
||||
return ListSlice<E>(this, start, end);
|
||||
}
|
||||
}
|
||||
|
@ -36,7 +36,7 @@ class ImStaticGrid extends StatelessWidget {
|
||||
),
|
||||
itemBuilder: (_, i) {
|
||||
if (isDragging) {
|
||||
return const ImImagePlaceholder();
|
||||
return const ImImagePlaceholder(width: 200, height: 200);
|
||||
}
|
||||
|
||||
final asset = assetsSnap.isWaiting || assets == null
|
||||
@ -46,7 +46,7 @@ class ImStaticGrid extends StatelessWidget {
|
||||
dimension: 200,
|
||||
// Show Placeholder when drag scrolled
|
||||
child: asset == null
|
||||
? const ImImagePlaceholder()
|
||||
? const ImImagePlaceholder(width: 200, height: 200)
|
||||
: ImThumbnail(asset),
|
||||
);
|
||||
},
|
||||
|
@ -7,14 +7,23 @@ import 'package:immich_mobile/utils/extensions/build_context.extension.dart';
|
||||
import 'package:octo_image/octo_image.dart';
|
||||
|
||||
class ImImagePlaceholder extends StatelessWidget {
|
||||
const ImImagePlaceholder({super.key});
|
||||
final double width;
|
||||
final double height;
|
||||
final double? margin;
|
||||
const ImImagePlaceholder({
|
||||
super.key,
|
||||
required this.width,
|
||||
required this.height,
|
||||
this.margin,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
color: context.colorScheme.surfaceContainerHighest,
|
||||
width: 200,
|
||||
height: 200,
|
||||
width: width,
|
||||
height: height,
|
||||
margin: EdgeInsets.all(margin ?? 0),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -30,7 +39,7 @@ class ImImage extends StatefulWidget {
|
||||
this.asset, {
|
||||
this.width,
|
||||
this.height,
|
||||
this.placeholder = const ImImagePlaceholder(),
|
||||
this.placeholder = const ImImagePlaceholder(width: 200, height: 200),
|
||||
super.key,
|
||||
});
|
||||
|
||||
|
@ -26,19 +26,13 @@ class _HomePageState extends State<HomePage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final gridHasPadding = !context.isTablet && _showAppBar.value;
|
||||
|
||||
return Scaffold(
|
||||
body: BlocProvider(
|
||||
create: (_) => AssetGridCubit(
|
||||
renderListProvider: RenderListProvider.mainTimeline(),
|
||||
),
|
||||
child: Stack(children: [
|
||||
ImAssetGrid(
|
||||
topPadding: gridHasPadding
|
||||
? kToolbarHeight + context.mediaQueryPadding.top - 8
|
||||
: null,
|
||||
),
|
||||
ImmichAssetGridView(),
|
||||
if (!context.isTablet)
|
||||
ValueListenableBuilder(
|
||||
valueListenable: _showAppBar,
|
||||
|
Loading…
x
Reference in New Issue
Block a user