Compare commits
6 Commits
refactor/t
...
fix/shared
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e468d91bc | ||
|
|
43173ce208 | ||
|
|
0157e3cec3 | ||
|
|
0abb5fad30 | ||
|
|
34eca2bd17 | ||
|
|
d61569faf6 |
@@ -1037,6 +1037,7 @@
|
||||
"exif_bottom_sheet_description_error": "Error updating description",
|
||||
"exif_bottom_sheet_details": "DETAILS",
|
||||
"exif_bottom_sheet_location": "LOCATION",
|
||||
"exif_bottom_sheet_no_description": "No description",
|
||||
"exif_bottom_sheet_people": "PEOPLE",
|
||||
"exif_bottom_sheet_person_add_person": "Add name",
|
||||
"exit_slideshow": "Exit Slideshow",
|
||||
|
||||
@@ -120,6 +120,10 @@ class RemoteAlbumService {
|
||||
return _repository.getSharedUsers(albumId);
|
||||
}
|
||||
|
||||
Future<AlbumUserRole?> getUserRole(String albumId, String userId) {
|
||||
return _repository.getUserRole(albumId, userId);
|
||||
}
|
||||
|
||||
Future<List<RemoteAsset>> getAssets(String albumId) {
|
||||
return _repository.getAssets(albumId);
|
||||
}
|
||||
|
||||
@@ -221,6 +221,15 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
|
||||
.get();
|
||||
}
|
||||
|
||||
Future<AlbumUserRole?> getUserRole(String albumId, String userId) async {
|
||||
final query = _db.remoteAlbumUserEntity.select()
|
||||
..where((row) => row.albumId.equals(albumId) & row.userId.equals(userId))
|
||||
..limit(1);
|
||||
|
||||
final result = await query.getSingleOrNull();
|
||||
return result?.role;
|
||||
}
|
||||
|
||||
Future<List<RemoteAsset>> getAssets(String albumId) {
|
||||
final query = _db.remoteAlbumAssetEntity.select().join([
|
||||
innerJoin(_db.remoteAssetEntity, _db.remoteAssetEntity.id.equalsExp(_db.remoteAlbumAssetEntity.assetId)),
|
||||
|
||||
@@ -169,9 +169,11 @@ class _RemoteAlbumPageState extends ConsumerState<RemoteAlbumPage> {
|
||||
context.pushRoute(const DriftActivitiesRoute());
|
||||
}
|
||||
|
||||
void showOptionSheet(BuildContext context) {
|
||||
Future<void> showOptionSheet(BuildContext context) async {
|
||||
final user = ref.watch(currentUserProvider);
|
||||
final isOwner = user != null ? user.id == _album.ownerId : false;
|
||||
final canAddPhotos =
|
||||
await ref.read(remoteAlbumServiceProvider).getUserRole(_album.id, user?.id ?? '') == AlbumUserRole.editor;
|
||||
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
@@ -193,22 +195,30 @@ class _RemoteAlbumPageState extends ConsumerState<RemoteAlbumPage> {
|
||||
context.pop();
|
||||
}
|
||||
: null,
|
||||
onAddPhotos: () async {
|
||||
await addAssets(context);
|
||||
context.pop();
|
||||
},
|
||||
onToggleAlbumOrder: () async {
|
||||
await toggleAlbumOrder();
|
||||
context.pop();
|
||||
},
|
||||
onEditAlbum: () async {
|
||||
context.pop();
|
||||
await showEditTitleAndDescription(context);
|
||||
},
|
||||
onCreateSharedLink: () async {
|
||||
context.pop();
|
||||
context.pushRoute(SharedLinkEditRoute(albumId: _album.id));
|
||||
},
|
||||
onAddPhotos: isOwner || canAddPhotos
|
||||
? () async {
|
||||
await addAssets(context);
|
||||
context.pop();
|
||||
}
|
||||
: null,
|
||||
onToggleAlbumOrder: isOwner
|
||||
? () async {
|
||||
await toggleAlbumOrder();
|
||||
context.pop();
|
||||
}
|
||||
: null,
|
||||
onEditAlbum: isOwner
|
||||
? () async {
|
||||
context.pop();
|
||||
await showEditTitleAndDescription(context);
|
||||
}
|
||||
: null,
|
||||
onCreateSharedLink: isOwner
|
||||
? () async {
|
||||
context.pop();
|
||||
context.pushRoute(SharedLinkEditRoute(albumId: _album.id));
|
||||
}
|
||||
: null,
|
||||
onShowOptions: () {
|
||||
context.pop();
|
||||
context.pushRoute(const DriftAlbumOptionsRoute());
|
||||
@@ -220,6 +230,9 @@ class _RemoteAlbumPageState extends ConsumerState<RemoteAlbumPage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final user = ref.watch(currentUserProvider);
|
||||
final isOwner = user != null ? user.id == _album.ownerId : false;
|
||||
|
||||
return PopScope(
|
||||
onPopInvokedWithResult: (didPop, _) {
|
||||
if (didPop) {
|
||||
@@ -243,8 +256,8 @@ class _RemoteAlbumPageState extends ConsumerState<RemoteAlbumPage> {
|
||||
appBar: RemoteAlbumSliverAppBar(
|
||||
icon: Icons.photo_album_outlined,
|
||||
onShowOptions: () => showOptionSheet(context),
|
||||
onToggleAlbumOrder: () => toggleAlbumOrder(),
|
||||
onEditTitle: () => showEditTitleAndDescription(context),
|
||||
onToggleAlbumOrder: isOwner ? () => toggleAlbumOrder() : null,
|
||||
onEditTitle: isOwner ? () => showEditTitleAndDescription(context) : null,
|
||||
onActivity: () => showActivity(context),
|
||||
),
|
||||
bottomSheet: RemoteAlbumBottomSheet(album: _album),
|
||||
|
||||
@@ -43,7 +43,7 @@ class ViewerBottomBar extends ConsumerWidget {
|
||||
final actions = <Widget>[
|
||||
const ShareActionButton(source: ActionSource.viewer),
|
||||
if (asset.isLocalOnly) const UploadActionButton(source: ActionSource.viewer),
|
||||
if (asset.type == AssetType.image) const EditImageActionButton(),
|
||||
if (asset.type == AssetType.image && isOwner) const EditImageActionButton(),
|
||||
if (isOwner) ...[
|
||||
if (asset.hasRemote && isOwner && isArchived)
|
||||
const UnArchiveActionButton(source: ActionSource.viewer)
|
||||
|
||||
@@ -140,6 +140,7 @@ class _AssetDetailBottomSheet extends ConsumerWidget {
|
||||
|
||||
final exifInfo = ref.watch(currentAssetExifProvider).valueOrNull;
|
||||
final cameraTitle = _getCameraInfoTitle(exifInfo);
|
||||
final isOwner = ref.watch(currentUserProvider)?.id == (asset is RemoteAsset ? asset.ownerId : null);
|
||||
|
||||
return SliverList.list(
|
||||
children: [
|
||||
@@ -147,10 +148,10 @@ class _AssetDetailBottomSheet extends ConsumerWidget {
|
||||
_SheetTile(
|
||||
title: _getDateTime(context, asset),
|
||||
titleStyle: context.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w600),
|
||||
trailing: asset.hasRemote ? const Icon(Icons.edit, size: 18) : null,
|
||||
onTap: asset.hasRemote ? () async => await _editDateTime(context, ref) : null,
|
||||
trailing: asset.hasRemote && isOwner ? const Icon(Icons.edit, size: 18) : null,
|
||||
onTap: asset.hasRemote && isOwner ? () async => await _editDateTime(context, ref) : null,
|
||||
),
|
||||
if (exifInfo != null) _SheetAssetDescription(exif: exifInfo),
|
||||
if (exifInfo != null) _SheetAssetDescription(exif: exifInfo, isEditable: isOwner),
|
||||
const SheetPeopleDetails(),
|
||||
const SheetLocationDetails(),
|
||||
// Details header
|
||||
@@ -264,8 +265,9 @@ class _SheetTile extends ConsumerWidget {
|
||||
|
||||
class _SheetAssetDescription extends ConsumerStatefulWidget {
|
||||
final ExifInfo exif;
|
||||
final bool isEditable;
|
||||
|
||||
const _SheetAssetDescription({required this.exif});
|
||||
const _SheetAssetDescription({required this.exif, this.isEditable = true, super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<_SheetAssetDescription> createState() => _SheetAssetDescriptionState();
|
||||
@@ -311,27 +313,33 @@ class _SheetAssetDescriptionState extends ConsumerState<_SheetAssetDescription>
|
||||
|
||||
// Update controller text when EXIF data changes
|
||||
final currentDescription = currentExifInfo?.description ?? '';
|
||||
final hintText = (widget.isEditable ? 'exif_bottom_sheet_description' : 'exif_bottom_sheet_no_description').t(
|
||||
context: context,
|
||||
);
|
||||
if (_controller.text != currentDescription && !_descriptionFocus.hasFocus) {
|
||||
_controller.text = currentDescription;
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8),
|
||||
child: TextField(
|
||||
controller: _controller,
|
||||
keyboardType: TextInputType.multiline,
|
||||
focusNode: _descriptionFocus,
|
||||
maxLines: null, // makes it grow as text is added
|
||||
decoration: InputDecoration(
|
||||
hintText: 'exif_bottom_sheet_description'.t(context: context),
|
||||
border: InputBorder.none,
|
||||
enabledBorder: InputBorder.none,
|
||||
focusedBorder: InputBorder.none,
|
||||
disabledBorder: InputBorder.none,
|
||||
errorBorder: InputBorder.none,
|
||||
focusedErrorBorder: InputBorder.none,
|
||||
child: IgnorePointer(
|
||||
ignoring: !widget.isEditable,
|
||||
child: TextField(
|
||||
controller: _controller,
|
||||
keyboardType: TextInputType.multiline,
|
||||
focusNode: _descriptionFocus,
|
||||
maxLines: null, // makes it grow as text is added
|
||||
decoration: InputDecoration(
|
||||
hintText: hintText,
|
||||
border: InputBorder.none,
|
||||
enabledBorder: InputBorder.none,
|
||||
focusedBorder: InputBorder.none,
|
||||
disabledBorder: InputBorder.none,
|
||||
errorBorder: InputBorder.none,
|
||||
focusedErrorBorder: InputBorder.none,
|
||||
),
|
||||
onTapOutside: (_) => saveDescription(currentExifInfo?.description),
|
||||
),
|
||||
onTapOutside: (_) => saveDescription(currentExifInfo?.description),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -44,7 +44,8 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
|
||||
final showViewInTimelineButton =
|
||||
(previousRouteName != TabShellRoute.name || tabRoute == TabEnum.search) &&
|
||||
previousRouteName != AssetViewerRoute.name &&
|
||||
previousRouteName != null;
|
||||
previousRouteName != null &&
|
||||
isOwner;
|
||||
|
||||
final isShowingSheet = ref.watch(assetViewerProvider.select((state) => state.showingBottomSheet));
|
||||
int opacity = ref.watch(assetViewerProvider.select((state) => state.backgroundOpacity));
|
||||
|
||||
@@ -24,6 +24,7 @@ import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_shee
|
||||
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||
import 'package:immich_mobile/providers/user.provider.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||
|
||||
class RemoteAlbumBottomSheet extends ConsumerStatefulWidget {
|
||||
@@ -53,6 +54,7 @@ class _RemoteAlbumBottomSheetState extends ConsumerState<RemoteAlbumBottomSheet>
|
||||
Widget build(BuildContext context) {
|
||||
final multiselect = ref.watch(multiSelectProvider);
|
||||
final isTrashEnable = ref.watch(serverInfoProvider.select((state) => state.serverFeatures.trash));
|
||||
final ownsAlbum = ref.watch(currentUserProvider)?.id == widget.album.ownerId;
|
||||
|
||||
Future<void> addAssetsToAlbum(RemoteAlbum album) async {
|
||||
final selectedAssets = multiselect.selectedAssets;
|
||||
@@ -93,28 +95,35 @@ class _RemoteAlbumBottomSheetState extends ConsumerState<RemoteAlbumBottomSheet>
|
||||
const ShareActionButton(source: ActionSource.timeline),
|
||||
if (multiselect.hasRemote) ...[
|
||||
const ShareLinkActionButton(source: ActionSource.timeline),
|
||||
const ArchiveActionButton(source: ActionSource.timeline),
|
||||
const FavoriteActionButton(source: ActionSource.timeline),
|
||||
|
||||
if (ownsAlbum) ...[
|
||||
const ArchiveActionButton(source: ActionSource.timeline),
|
||||
const FavoriteActionButton(source: ActionSource.timeline),
|
||||
],
|
||||
const DownloadActionButton(source: ActionSource.timeline),
|
||||
isTrashEnable
|
||||
? const TrashActionButton(source: ActionSource.timeline)
|
||||
: const DeletePermanentActionButton(source: ActionSource.timeline),
|
||||
const EditDateTimeActionButton(source: ActionSource.timeline),
|
||||
const EditLocationActionButton(source: ActionSource.timeline),
|
||||
const MoveToLockFolderActionButton(source: ActionSource.timeline),
|
||||
if (multiselect.selectedAssets.length > 1) const StackActionButton(source: ActionSource.timeline),
|
||||
if (multiselect.hasStacked) const UnStackActionButton(source: ActionSource.timeline),
|
||||
if (ownsAlbum) ...[
|
||||
isTrashEnable
|
||||
? const TrashActionButton(source: ActionSource.timeline)
|
||||
: const DeletePermanentActionButton(source: ActionSource.timeline),
|
||||
const EditDateTimeActionButton(source: ActionSource.timeline),
|
||||
const EditLocationActionButton(source: ActionSource.timeline),
|
||||
const MoveToLockFolderActionButton(source: ActionSource.timeline),
|
||||
if (multiselect.selectedAssets.length > 1) const StackActionButton(source: ActionSource.timeline),
|
||||
if (multiselect.hasStacked) const UnStackActionButton(source: ActionSource.timeline),
|
||||
],
|
||||
],
|
||||
if (multiselect.hasLocal) ...[
|
||||
const DeleteLocalActionButton(source: ActionSource.timeline),
|
||||
const UploadActionButton(source: ActionSource.timeline),
|
||||
],
|
||||
RemoveFromAlbumActionButton(source: ActionSource.timeline, albumId: widget.album.id),
|
||||
],
|
||||
slivers: [
|
||||
const AddToAlbumHeader(),
|
||||
AlbumSelector(onAlbumSelected: addAssetsToAlbum, onKeyboardExpanded: onKeyboardExpand),
|
||||
if (ownsAlbum) RemoveFromAlbumActionButton(source: ActionSource.timeline, albumId: widget.album.id),
|
||||
],
|
||||
slivers: ownsAlbum
|
||||
? [
|
||||
const AddToAlbumHeader(),
|
||||
AlbumSelector(onAlbumSelected: addAssetsToAlbum, onKeyboardExpanded: onKeyboardExpand),
|
||||
]
|
||||
: null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import type { ScrubberMonth } from '$lib/managers/timeline-manager/types';
|
||||
import { mobileDevice } from '$lib/stores/mobile-device.svelte';
|
||||
import { getTabbable } from '$lib/utils/focus-util';
|
||||
import { type ScrubberListener, type TimelineYearMonth } from '$lib/utils/timeline-util';
|
||||
import { type ScrubberListener } from '$lib/utils/timeline-util';
|
||||
import { Icon } from '@immich/ui';
|
||||
import { mdiPlay } from '@mdi/js';
|
||||
import { clamp } from 'lodash-es';
|
||||
@@ -11,31 +11,18 @@
|
||||
import { fade, fly } from 'svelte/transition';
|
||||
|
||||
interface Props {
|
||||
/** Offset from the top of the timeline (e.g., for headers) */
|
||||
timelineTopOffset?: number;
|
||||
/** Offset from the bottom of the timeline (e.g., for footers) */
|
||||
timelineBottomOffset?: number;
|
||||
/** Total height of the scrubber component */
|
||||
height?: number;
|
||||
/** Timeline manager instance that controls the timeline state */
|
||||
timelineManager: TimelineManager;
|
||||
/** Overall scroll percentage through the entire timeline (0-1), used when no specific month is targeted */
|
||||
timelineScrollPercent?: number;
|
||||
/** The percentage of scroll through the month that is currently intersecting the top boundary of the viewport */
|
||||
viewportTopMonthScrollPercent?: number;
|
||||
/** The year/month of the timeline month at the top of the viewport */
|
||||
viewportTopMonth?: TimelineYearMonth;
|
||||
/** Indicates whether the viewport is currently in the lead-out section (after all months) */
|
||||
isInLeadOutSection?: boolean;
|
||||
/** Width of the scrubber component in pixels (bindable for parent component margin adjustments) */
|
||||
scrubOverallPercent?: number;
|
||||
scrubberMonthPercent?: number;
|
||||
scrubberMonth?: { year: number; month: number };
|
||||
leadout?: boolean;
|
||||
scrubberWidth?: number;
|
||||
/** Callback fired when user interacts with the scrubber to navigate */
|
||||
onScrub?: ScrubberListener;
|
||||
/** Callback fired when keyboard events occur on the scrubber */
|
||||
onScrubKeyDown?: (event: KeyboardEvent, element: HTMLElement) => void;
|
||||
/** Callback fired when scrubbing starts */
|
||||
startScrub?: ScrubberListener;
|
||||
/** Callback fired when scrubbing stops */
|
||||
stopScrub?: ScrubberListener;
|
||||
}
|
||||
|
||||
@@ -44,10 +31,10 @@
|
||||
timelineBottomOffset = 0,
|
||||
height = 0,
|
||||
timelineManager,
|
||||
timelineScrollPercent = 0,
|
||||
viewportTopMonthScrollPercent = 0,
|
||||
viewportTopMonth = undefined,
|
||||
isInLeadOutSection = false,
|
||||
scrubOverallPercent = 0,
|
||||
scrubberMonthPercent = 0,
|
||||
scrubberMonth = undefined,
|
||||
leadout = false,
|
||||
onScrub = undefined,
|
||||
onScrubKeyDown = undefined,
|
||||
startScrub = undefined,
|
||||
@@ -113,7 +100,7 @@
|
||||
offset += scrubberMonthPercent * relativeBottomOffset;
|
||||
}
|
||||
return offset;
|
||||
} else if (isInLeadOutSection) {
|
||||
} else if (leadout) {
|
||||
let offset = relativeTopOffset;
|
||||
for (const segment of segments) {
|
||||
offset += segment.height;
|
||||
@@ -124,9 +111,7 @@
|
||||
return scrubOverallPercent * (height - (PADDING_TOP + PADDING_BOTTOM));
|
||||
}
|
||||
};
|
||||
let scrollY = $derived(
|
||||
toScrollFromMonthGroupPercentage(viewportTopMonth, viewportTopMonthScrollPercent, timelineScrollPercent),
|
||||
);
|
||||
let scrollY = $derived(toScrollFromMonthGroupPercentage(scrubberMonth, scrubberMonthPercent, scrubOverallPercent));
|
||||
let timelineFullHeight = $derived(timelineManager.scrubberTimelineHeight + timelineTopOffset + timelineBottomOffset);
|
||||
let relativeTopOffset = $derived(toScrollY(timelineTopOffset / timelineFullHeight));
|
||||
let relativeBottomOffset = $derived(toScrollY(timelineBottomOffset / timelineFullHeight));
|
||||
@@ -310,24 +295,12 @@
|
||||
|
||||
const scrollPercent = toTimelineY(hoverY);
|
||||
if (wasDragging === false && isDragging) {
|
||||
void startScrub?.({
|
||||
scrubberMonth: segmentDate!,
|
||||
overallScrollPercent: scrollPercent,
|
||||
scrubberMonthScrollPercent: monthGroupPercentY,
|
||||
});
|
||||
void onScrub?.({
|
||||
scrubberMonth: segmentDate!,
|
||||
overallScrollPercent: scrollPercent,
|
||||
scrubberMonthScrollPercent: monthGroupPercentY,
|
||||
});
|
||||
void startScrub?.(segmentDate!, scrollPercent, monthGroupPercentY);
|
||||
void onScrub?.(segmentDate!, scrollPercent, monthGroupPercentY);
|
||||
}
|
||||
|
||||
if (wasDragging && !isDragging) {
|
||||
void stopScrub?.({
|
||||
scrubberMonth: segmentDate!,
|
||||
overallScrollPercent: scrollPercent,
|
||||
scrubberMonthScrollPercent: monthGroupPercentY,
|
||||
});
|
||||
void stopScrub?.(segmentDate!, scrollPercent, monthGroupPercentY);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -335,11 +308,7 @@
|
||||
return;
|
||||
}
|
||||
|
||||
void onScrub?.({
|
||||
scrubberMonth: segmentDate!,
|
||||
overallScrollPercent: scrollPercent,
|
||||
scrubberMonthScrollPercent: monthGroupPercentY,
|
||||
});
|
||||
void onScrub?.(segmentDate!, scrollPercent, monthGroupPercentY);
|
||||
};
|
||||
/* eslint-disable tscompat/tscompat */
|
||||
const getTouch = (event: TouchEvent) => {
|
||||
@@ -443,11 +412,7 @@
|
||||
}
|
||||
if (next) {
|
||||
event.preventDefault();
|
||||
void onScrub?.({
|
||||
scrubberMonth: { year: next.year, month: next.month },
|
||||
overallScrollPercent: -1,
|
||||
scrubberMonthScrollPercent: 0,
|
||||
});
|
||||
void onScrub?.({ year: next.year, month: next.month }, -1, 0);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -457,11 +422,7 @@
|
||||
const next = segments[idx + 1];
|
||||
if (next) {
|
||||
event.preventDefault();
|
||||
void onScrub?.({
|
||||
scrubberMonth: { year: next.year, month: next.month },
|
||||
overallScrollPercent: -1,
|
||||
scrubberMonthScrollPercent: 0,
|
||||
});
|
||||
void onScrub?.({ year: next.year, month: next.month }, -1, 0);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,19 +110,14 @@
|
||||
let timelineElement: HTMLElement | undefined = $state();
|
||||
let showSkeleton = $state(true);
|
||||
let isShowSelectDate = $state(false);
|
||||
// The percentage of scroll through the month that is currently intersecting the top boundary of the viewport.
|
||||
// Note: There may be multiple months visible within the viewport at any given time.
|
||||
let viewportTopMonthScrollPercent = $state(0);
|
||||
// The timeline month intersecting the top position of the viewport
|
||||
let viewportTopMonth: { year: number; month: number } | undefined = $state(undefined);
|
||||
// Overall scroll percentage through the entire timeline (0-1)
|
||||
let timelineScrollPercent: number = $state(0);
|
||||
let scrubberMonthPercent = $state(0);
|
||||
let scrubberMonth: { year: number; month: number } | undefined = $state(undefined);
|
||||
let scrubOverallPercent: number = $state(0);
|
||||
let scrubberWidth = $state(0);
|
||||
|
||||
// 60 is the bottom spacer element at 60px
|
||||
let bottomSectionHeight = 60;
|
||||
// Indicates whether the viewport is currently in the lead-out section (after all months)
|
||||
let isInLeadOutSection = $state(false);
|
||||
let leadout = $state(false);
|
||||
|
||||
const maxMd = $derived(mobileDevice.maxMd);
|
||||
const usingMobileDevice = $derived(mobileDevice.pointerCoarse);
|
||||
@@ -306,19 +301,20 @@
|
||||
scrollTop(scrollToTop);
|
||||
};
|
||||
|
||||
// note: don't throttle, debounce, or otherwise make this function async - it causes flicker
|
||||
// this function scrolls the timeline to the specified month group and offset, based on scrubber interaction
|
||||
const onScrub: ScrubberListener = (scrubberData) => {
|
||||
const { scrubberMonth, overallScrollPercent, scrubberMonthScrollPercent } = scrubberData;
|
||||
|
||||
if (!scrubberMonth || timelineManager.timelineHeight < timelineManager.viewportHeight * 2) {
|
||||
// note: don't throttle, debounch, or otherwise make this function async - it causes flicker
|
||||
const onScrub: ScrubberListener = (
|
||||
scrubMonth: { year: number; month: number },
|
||||
overallScrollPercent: number,
|
||||
scrubberMonthScrollPercent: number,
|
||||
) => {
|
||||
if (!scrubMonth || timelineManager.timelineHeight < timelineManager.viewportHeight * 2) {
|
||||
// edge case - scroll limited due to size of content, must adjust - use use the overall percent instead
|
||||
const maxScroll = getMaxScroll();
|
||||
const offset = maxScroll * overallScrollPercent;
|
||||
scrollTop(offset);
|
||||
} else {
|
||||
const monthGroup = timelineManager.months.find(
|
||||
({ yearMonth: { year, month } }) => year === scrubberMonth.year && month === scrubberMonth.month,
|
||||
({ yearMonth: { year, month } }) => year === scrubMonth.year && month === scrubMonth.month,
|
||||
);
|
||||
if (!monthGroup) {
|
||||
return;
|
||||
@@ -329,7 +325,7 @@
|
||||
|
||||
// note: don't throttle, debounch, or otherwise make this function async - it causes flicker
|
||||
const handleTimelineScroll = () => {
|
||||
isInLeadOutSection = false;
|
||||
leadout = false;
|
||||
|
||||
if (!element) {
|
||||
return;
|
||||
@@ -338,19 +334,19 @@
|
||||
if (timelineManager.timelineHeight < timelineManager.viewportHeight * 2) {
|
||||
// edge case - scroll limited due to size of content, must adjust - use the overall percent instead
|
||||
const maxScroll = getMaxScroll();
|
||||
timelineScrollPercent = Math.min(1, element.scrollTop / maxScroll);
|
||||
scrubOverallPercent = Math.min(1, element.scrollTop / maxScroll);
|
||||
|
||||
viewportTopMonth = undefined;
|
||||
viewportTopMonthScrollPercent = 0;
|
||||
scrubberMonth = undefined;
|
||||
scrubberMonthPercent = 0;
|
||||
} else {
|
||||
let top = element.scrollTop;
|
||||
if (top < timelineManager.topSectionHeight) {
|
||||
// in the lead-in area
|
||||
viewportTopMonth = undefined;
|
||||
viewportTopMonthScrollPercent = 0;
|
||||
scrubberMonth = undefined;
|
||||
scrubberMonthPercent = 0;
|
||||
const maxScroll = getMaxScroll();
|
||||
|
||||
timelineScrollPercent = Math.min(1, element.scrollTop / maxScroll);
|
||||
scrubOverallPercent = Math.min(1, element.scrollTop / maxScroll);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -375,15 +371,15 @@
|
||||
let next = top - monthGroupHeight * maxScrollPercent;
|
||||
// instead of checking for < 0, add a little wiggle room for subpixel resolution
|
||||
if (next < -1 && monthGroup) {
|
||||
viewportTopMonth = monthGroup;
|
||||
scrubberMonth = monthGroup;
|
||||
|
||||
// allowing next to be at least 1 may cause percent to go negative, so ensure positive percentage
|
||||
viewportTopMonthScrollPercent = Math.max(0, top / (monthGroupHeight * maxScrollPercent));
|
||||
scrubberMonthPercent = Math.max(0, top / (monthGroupHeight * maxScrollPercent));
|
||||
|
||||
// compensate for lost precision/rounding errors advance to the next bucket, if present
|
||||
if (viewportTopMonthScrollPercent > 0.9999 && i + 1 < monthsLength - 1) {
|
||||
viewportTopMonth = timelineManager.months[i + 1].yearMonth;
|
||||
viewportTopMonthScrollPercent = 0;
|
||||
if (scrubberMonthPercent > 0.9999 && i + 1 < monthsLength - 1) {
|
||||
scrubberMonth = timelineManager.months[i + 1].yearMonth;
|
||||
scrubberMonthPercent = 0;
|
||||
}
|
||||
|
||||
found = true;
|
||||
@@ -392,10 +388,10 @@
|
||||
top = next;
|
||||
}
|
||||
if (!found) {
|
||||
isInLeadOutSection = true;
|
||||
viewportTopMonth = undefined;
|
||||
viewportTopMonthScrollPercent = 0;
|
||||
timelineScrollPercent = 1;
|
||||
leadout = true;
|
||||
scrubberMonth = undefined;
|
||||
scrubberMonthPercent = 0;
|
||||
scrubOverallPercent = 1;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -858,10 +854,10 @@
|
||||
height={timelineManager.viewportHeight}
|
||||
timelineTopOffset={timelineManager.topSectionHeight}
|
||||
timelineBottomOffset={bottomSectionHeight}
|
||||
{isInLeadOutSection}
|
||||
{timelineScrollPercent}
|
||||
{viewportTopMonthScrollPercent}
|
||||
{viewportTopMonth}
|
||||
{leadout}
|
||||
{scrubOverallPercent}
|
||||
{scrubberMonthPercent}
|
||||
{scrubberMonth}
|
||||
{onScrub}
|
||||
bind:scrubberWidth
|
||||
onScrubKeyDown={(evt) => {
|
||||
|
||||
@@ -23,11 +23,11 @@ export type TimelineDateTime = TimelineDate & {
|
||||
millisecond: number;
|
||||
};
|
||||
|
||||
export type ScrubberListener = (scrubberData: {
|
||||
scrubberMonth: { year: number; month: number };
|
||||
overallScrollPercent: number;
|
||||
scrubberMonthScrollPercent: number;
|
||||
}) => void | Promise<void>;
|
||||
export type ScrubberListener = (
|
||||
scrubberMonth: { year: number; month: number },
|
||||
overallScrollPercent: number,
|
||||
scrubberMonthScrollPercent: number,
|
||||
) => void | Promise<void>;
|
||||
|
||||
// used for AssetResponseDto.dateTimeOriginal, amongst others
|
||||
export const fromISODateTime = (isoDateTime: string, timeZone: string): DateTime<true> =>
|
||||
|
||||
Reference in New Issue
Block a user