mirror of
https://github.com/immich-app/immich.git
synced 2025-12-26 09:14:58 +03:00
Compare commits
8 Commits
deps/immic
...
feature/mo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c90c7a56ce | ||
|
|
501617ba93 | ||
|
|
5050f8fbb4 | ||
|
|
ea37b12b39 | ||
|
|
cc3307d335 | ||
|
|
85086cdb02 | ||
|
|
9262f69519 | ||
|
|
f13a5ba418 |
@@ -20,7 +20,7 @@
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/micromatch": "^4.0.9",
|
||||
"@types/mock-fs": "^4.13.1",
|
||||
"@types/node": "^24.10.3",
|
||||
"@types/node": "^24.10.1",
|
||||
"@vitest/coverage-v8": "^3.0.0",
|
||||
"byte-size": "^9.0.0",
|
||||
"cli-progress": "^3.12.0",
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
"@playwright/test": "^1.44.1",
|
||||
"@socket.io/component-emitter": "^3.1.2",
|
||||
"@types/luxon": "^3.4.2",
|
||||
"@types/node": "^24.10.3",
|
||||
"@types/node": "^24.10.1",
|
||||
"@types/oidc-provider": "^9.0.0",
|
||||
"@types/pg": "^8.15.1",
|
||||
"@types/pngjs": "^6.0.4",
|
||||
|
||||
@@ -2062,6 +2062,7 @@
|
||||
"theme": "Theme",
|
||||
"theme_selection": "Theme selection",
|
||||
"theme_selection_description": "Automatically set the theme to light or dark based on your browser's system preference",
|
||||
"theme_setting_asset_list_show_owner_name_title": "Show asset owner name",
|
||||
"theme_setting_asset_list_storage_indicator_title": "Show storage indicator on asset tiles",
|
||||
"theme_setting_asset_list_tiles_per_row_title": "Number of assets per row ({count})",
|
||||
"theme_setting_colorful_interface_subtitle": "Apply primary color to background surfaces.",
|
||||
|
||||
@@ -9,7 +9,8 @@ enum Setting<T> {
|
||||
autoPlayVideo<bool>(StoreKey.autoPlayVideo, true),
|
||||
preferRemoteImage<bool>(StoreKey.preferRemoteImage, false),
|
||||
advancedTroubleshooting<bool>(StoreKey.advancedTroubleshooting, false),
|
||||
enableBackup<bool>(StoreKey.enableBackup, false);
|
||||
enableBackup<bool>(StoreKey.enableBackup, false),
|
||||
showOwnerName<bool>(StoreKey.showOwnerName, false);
|
||||
|
||||
const Setting(this.storeKey, this.defaultValue);
|
||||
|
||||
|
||||
@@ -72,6 +72,7 @@ enum StoreKey<T> {
|
||||
|
||||
autoPlayVideo<bool>._(139),
|
||||
albumGridView<bool>._(140),
|
||||
showOwnerName<bool>._(141),
|
||||
|
||||
// Experimental stuff
|
||||
photoManagerCustomFilter<bool>._(1000),
|
||||
|
||||
@@ -47,7 +47,7 @@ class LikeActivityActionButton extends ConsumerWidget {
|
||||
|
||||
return BaseActionButton(
|
||||
maxWidth: 60,
|
||||
iconData: liked != null ? Icons.thumb_up : Icons.thumb_up_off_alt,
|
||||
iconData: liked != null ? Icons.favorite : Icons.favorite_border,
|
||||
label: "like".t(context: context),
|
||||
onPressed: () => onTap(liked),
|
||||
iconOnly: iconOnly,
|
||||
@@ -57,7 +57,7 @@ class LikeActivityActionButton extends ConsumerWidget {
|
||||
|
||||
// default to empty heart during loading
|
||||
loading: () => BaseActionButton(
|
||||
iconData: Icons.thumb_up_off_alt,
|
||||
iconData: Icons.favorite_border,
|
||||
label: "like".t(context: context),
|
||||
iconOnly: iconOnly,
|
||||
menuItem: menuItem,
|
||||
|
||||
@@ -79,7 +79,7 @@ class ActivitiesBottomSheet extends HookConsumerWidget {
|
||||
expand: false,
|
||||
shouldCloseOnMinExtent: false,
|
||||
resizeOnScroll: false,
|
||||
backgroundColor: context.isDarkTheme ? context.colorScheme.surface : Colors.white,
|
||||
backgroundColor: context.isDarkTheme ? Colors.black : Colors.white,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,7 +97,7 @@ class AssetViewer extends ConsumerStatefulWidget {
|
||||
}
|
||||
|
||||
const double _kBottomSheetMinimumExtent = 0.4;
|
||||
const double _kBottomSheetSnapExtent = 0.67;
|
||||
const double _kBottomSheetSnapExtent = 0.7;
|
||||
|
||||
class _AssetViewerState extends ConsumerState<AssetViewer> {
|
||||
static final _dummyListener = ImageStreamListener((image, _) => image.dispose());
|
||||
@@ -399,14 +399,10 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
||||
final isDraggingDown = currentExtent < previousExtent;
|
||||
previousExtent = currentExtent;
|
||||
// Closes the bottom sheet if the user is dragging down
|
||||
if (isDraggingDown && delta.extent < 0.67) {
|
||||
if (isDraggingDown && delta.extent < 0.55) {
|
||||
if (dragInProgress) {
|
||||
blockGestures = true;
|
||||
}
|
||||
// Jump to a lower position before starting close animation to prevent glitch
|
||||
if (bottomSheetController.isAttached) {
|
||||
bottomSheetController.jumpTo(0.67);
|
||||
}
|
||||
sheetCloseController?.close();
|
||||
}
|
||||
|
||||
@@ -484,7 +480,7 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
||||
previousExtent = _kBottomSheetMinimumExtent;
|
||||
sheetCloseController = showBottomSheet(
|
||||
context: ctx,
|
||||
sheetAnimationStyle: const AnimationStyle(duration: Durations.medium2, reverseDuration: Durations.medium2),
|
||||
sheetAnimationStyle: const AnimationStyle(duration: Durations.short4, reverseDuration: Durations.short2),
|
||||
constraints: const BoxConstraints(maxWidth: double.infinity),
|
||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(20.0))),
|
||||
backgroundColor: ctx.colorScheme.surfaceContainerLowest,
|
||||
@@ -692,20 +688,16 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
||||
backgroundDecoration: BoxDecoration(color: backgroundColor),
|
||||
enablePanAlways: true,
|
||||
),
|
||||
if (!showingBottomSheet)
|
||||
const Positioned(
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [AssetStackRow(), ViewerBottomBar()],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
bottomNavigationBar: showingBottomSheet
|
||||
? const SizedBox.shrink()
|
||||
: const Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [AssetStackRow(), ViewerBottomBar()],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ class AssetDetailBottomSheet extends ConsumerWidget {
|
||||
expand: false,
|
||||
shouldCloseOnMinExtent: false,
|
||||
resizeOnScroll: false,
|
||||
backgroundColor: context.isDarkTheme ? context.colorScheme.surface : Colors.white,
|
||||
backgroundColor: context.isDarkTheme ? Colors.black : Colors.white,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -299,7 +299,7 @@ class _AssetDetailBottomSheet extends ConsumerWidget {
|
||||
// Appears in (Albums)
|
||||
Padding(padding: const EdgeInsets.only(top: 16.0), child: _buildAppearsInList(ref, context)),
|
||||
// padding at the bottom to avoid cut-off
|
||||
const SizedBox(height: 30),
|
||||
const SizedBox(height: 100),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/stack_action_b
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/unarchive_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/unstack_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart';
|
||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||
@@ -46,7 +47,10 @@ class ArchiveBottomSheet extends ConsumerWidget {
|
||||
if (multiselect.selectedAssets.length > 1) const StackActionButton(source: ActionSource.timeline),
|
||||
if (multiselect.hasStacked) const UnStackActionButton(source: ActionSource.timeline),
|
||||
],
|
||||
if (multiselect.hasMerged) const DeleteLocalActionButton(source: ActionSource.timeline),
|
||||
if (multiselect.hasLocal) ...[
|
||||
const DeleteLocalActionButton(source: ActionSource.timeline),
|
||||
const UploadActionButton(source: ActionSource.timeline),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ class _BaseDraggableScrollableSheetState extends ConsumerState<BaseBottomSheet>
|
||||
child: CustomScrollView(
|
||||
controller: scrollController,
|
||||
slivers: [
|
||||
const SliverToBoxAdapter(child: _DragHandle()),
|
||||
const SliverPersistentHeader(delegate: _DragHandleDelegate(), pinned: true),
|
||||
if (widget.actions.isNotEmpty)
|
||||
SliverToBoxAdapter(
|
||||
child: Column(
|
||||
@@ -108,13 +108,31 @@ class _BaseDraggableScrollableSheetState extends ConsumerState<BaseBottomSheet>
|
||||
}
|
||||
}
|
||||
|
||||
class _DragHandleDelegate extends SliverPersistentHeaderDelegate {
|
||||
const _DragHandleDelegate();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
|
||||
return const _DragHandle();
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRebuild(_DragHandleDelegate oldDelegate) => false;
|
||||
|
||||
@override
|
||||
double get minExtent => 50.0;
|
||||
|
||||
@override
|
||||
double get maxExtent => 50.0;
|
||||
}
|
||||
|
||||
class _DragHandle extends StatelessWidget {
|
||||
const _DragHandle();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: 38,
|
||||
height: 50,
|
||||
child: Center(
|
||||
child: SizedBox(
|
||||
width: 32,
|
||||
|
||||
@@ -17,6 +17,7 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/stack_action_b
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/unfavorite_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/unstack_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
||||
@@ -85,7 +86,10 @@ class FavoriteBottomSheet extends ConsumerWidget {
|
||||
if (multiselect.selectedAssets.length > 1) const StackActionButton(source: ActionSource.timeline),
|
||||
if (multiselect.hasStacked) const UnStackActionButton(source: ActionSource.timeline),
|
||||
],
|
||||
if (multiselect.hasMerged) const DeleteLocalActionButton(source: ActionSource.timeline),
|
||||
if (multiselect.hasLocal) ...[
|
||||
const DeleteLocalActionButton(source: ActionSource.timeline),
|
||||
const UploadActionButton(source: ActionSource.timeline),
|
||||
],
|
||||
],
|
||||
slivers: multiselect.hasRemote
|
||||
? [const AddToAlbumHeader(), AlbumSelector(onAlbumSelected: addAssetsToAlbum)]
|
||||
|
||||
@@ -18,6 +18,7 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_act
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/stack_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/unstack_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
||||
@@ -111,7 +112,10 @@ class _RemoteAlbumBottomSheetState extends ConsumerState<RemoteAlbumBottomSheet>
|
||||
if (multiselect.hasStacked) const UnStackActionButton(source: ActionSource.timeline),
|
||||
],
|
||||
],
|
||||
if (multiselect.hasMerged) const DeleteLocalActionButton(source: ActionSource.timeline),
|
||||
if (multiselect.hasLocal) ...[
|
||||
const DeleteLocalActionButton(source: ActionSource.timeline),
|
||||
const UploadActionButton(source: ActionSource.timeline),
|
||||
],
|
||||
if (ownsAlbum) RemoveFromAlbumActionButton(source: ActionSource.timeline, albumId: widget.album.id),
|
||||
],
|
||||
slivers: ownsAlbum
|
||||
|
||||
@@ -19,6 +19,7 @@ class ThumbnailTile extends ConsumerWidget {
|
||||
this.showStorageIndicator = false,
|
||||
this.lockSelection = false,
|
||||
this.heroOffset,
|
||||
this.ownerName,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@@ -28,6 +29,7 @@ class ThumbnailTile extends ConsumerWidget {
|
||||
final bool showStorageIndicator;
|
||||
final bool lockSelection;
|
||||
final int? heroOffset;
|
||||
final String? ownerName;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
@@ -45,68 +47,90 @@ class ThumbnailTile extends ConsumerWidget {
|
||||
final bool storageIndicator =
|
||||
ref.watch(settingsProvider.select((s) => s.get(Setting.showStorageIndicator))) && showStorageIndicator;
|
||||
|
||||
final bool showOwnerNameSetting = ref.watch(settingsProvider.select((s) => s.get(Setting.showOwnerName)));
|
||||
final shouldShowOwnerName = showOwnerNameSetting && ownerName != null;
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
Container(color: lockSelection ? context.colorScheme.surfaceContainerHighest : assetContainerColor),
|
||||
AnimatedContainer(
|
||||
duration: Durations.short4,
|
||||
curve: Curves.decelerate,
|
||||
padding: EdgeInsets.all(isSelected || lockSelection ? 6 : 0),
|
||||
child: TweenAnimationBuilder<double>(
|
||||
tween: Tween<double>(begin: 0.0, end: (isSelected || lockSelection) ? 15.0 : 0.0),
|
||||
duration: Durations.short4,
|
||||
curve: Curves.decelerate,
|
||||
builder: (context, value, child) {
|
||||
return ClipRRect(borderRadius: BorderRadius.all(Radius.circular(value)), child: child);
|
||||
},
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: Hero(
|
||||
tag: '${asset?.heroTag ?? ''}_$heroIndex',
|
||||
child: Thumbnail.fromAsset(asset: asset, size: size),
|
||||
),
|
||||
LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final metrics = _OverlayMetrics.fromConstraints(constraints);
|
||||
|
||||
return AnimatedContainer(
|
||||
duration: Durations.short4,
|
||||
curve: Curves.decelerate,
|
||||
padding: EdgeInsets.all(isSelected || lockSelection ? 6 : 0),
|
||||
child: TweenAnimationBuilder<double>(
|
||||
tween: Tween<double>(begin: 0.0, end: (isSelected || lockSelection) ? 15.0 : 0.0),
|
||||
duration: Durations.short4,
|
||||
curve: Curves.decelerate,
|
||||
builder: (context, value, child) {
|
||||
return ClipRRect(borderRadius: BorderRadius.all(Radius.circular(value)), child: child);
|
||||
},
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: Hero(
|
||||
tag: '${asset?.heroTag ?? ''}_$heroIndex',
|
||||
child: Thumbnail.fromAsset(asset: asset, size: size),
|
||||
),
|
||||
),
|
||||
if (asset != null)
|
||||
Align(
|
||||
alignment: Alignment.topRight,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: metrics.padding, vertical: metrics.padding),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [_AssetTypeIcons(asset: asset, metrics: metrics)],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (shouldShowOwnerName ||
|
||||
(storageIndicator && asset != null) ||
|
||||
(asset != null && asset.isFavorite))
|
||||
Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: metrics.padding, vertical: metrics.padding),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
if (asset != null && asset.isFavorite)
|
||||
Padding(
|
||||
padding: EdgeInsets.only(right: metrics.iconSpacing),
|
||||
child: _TileOverlayIcon(Icons.favorite_rounded, metrics: metrics),
|
||||
)
|
||||
else
|
||||
const SizedBox.shrink(),
|
||||
if (shouldShowOwnerName)
|
||||
Flexible(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: metrics.iconSpacing),
|
||||
child: _OwnerNameLabel(ownerName: ownerName!, metrics: metrics),
|
||||
),
|
||||
),
|
||||
if (storageIndicator && asset != null)
|
||||
Padding(
|
||||
padding: EdgeInsets.only(right: metrics.iconSpacing),
|
||||
child: _TileOverlayIcon(switch (asset.storage) {
|
||||
AssetState.local => Icons.cloud_off_outlined,
|
||||
AssetState.remote => Icons.cloud_outlined,
|
||||
AssetState.merged => Icons.cloud_done_outlined,
|
||||
}, metrics: metrics),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (asset != null)
|
||||
Align(
|
||||
alignment: Alignment.topRight,
|
||||
child: _AssetTypeIcons(asset: asset),
|
||||
),
|
||||
if (storageIndicator && asset != null)
|
||||
switch (asset.storage) {
|
||||
AssetState.local => const Align(
|
||||
alignment: Alignment.bottomRight,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(right: 10.0, bottom: 6.0),
|
||||
child: _TileOverlayIcon(Icons.cloud_off_outlined),
|
||||
),
|
||||
),
|
||||
AssetState.remote => const Align(
|
||||
alignment: Alignment.bottomRight,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(right: 10.0, bottom: 6.0),
|
||||
child: _TileOverlayIcon(Icons.cloud_outlined),
|
||||
),
|
||||
),
|
||||
AssetState.merged => const Align(
|
||||
alignment: Alignment.bottomRight,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(right: 10.0, bottom: 6.0),
|
||||
child: _TileOverlayIcon(Icons.cloud_done_outlined),
|
||||
),
|
||||
),
|
||||
},
|
||||
if (asset != null && asset.isFavorite)
|
||||
const Align(
|
||||
alignment: Alignment.bottomLeft,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(left: 10.0, bottom: 6.0),
|
||||
child: _TileOverlayIcon(Icons.favorite_rounded),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
TweenAnimationBuilder<double>(
|
||||
tween: Tween<double>(begin: 0.0, end: (isSelected || lockSelection) ? 1.0 : 0.0),
|
||||
@@ -157,12 +181,12 @@ class _SelectionIndicator extends StatelessWidget {
|
||||
|
||||
class _VideoIndicator extends StatelessWidget {
|
||||
final Duration duration;
|
||||
const _VideoIndicator(this.duration);
|
||||
final _OverlayMetrics metrics;
|
||||
const _VideoIndicator(this.duration, {required this.metrics});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
spacing: 3,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
// CrossAxisAlignment.start looks more centered vertically than CrossAxisAlignment.center
|
||||
@@ -170,14 +194,15 @@ class _VideoIndicator extends StatelessWidget {
|
||||
children: [
|
||||
Text(
|
||||
duration.format(),
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12,
|
||||
fontSize: metrics.fontSize,
|
||||
fontWeight: FontWeight.bold,
|
||||
shadows: [Shadow(blurRadius: 5.0, color: Color.fromRGBO(0, 0, 0, 0.6))],
|
||||
shadows: [Shadow(blurRadius: metrics.blurRadius, color: const Color.fromRGBO(0, 0, 0, 0.6))],
|
||||
),
|
||||
),
|
||||
const _TileOverlayIcon(Icons.play_circle_outline_rounded),
|
||||
SizedBox(width: metrics.iconSpacing),
|
||||
_TileOverlayIcon(Icons.play_circle_outline_rounded, metrics: metrics),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -185,47 +210,115 @@ class _VideoIndicator extends StatelessWidget {
|
||||
|
||||
class _TileOverlayIcon extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final _OverlayMetrics metrics;
|
||||
|
||||
const _TileOverlayIcon(this.icon);
|
||||
const _TileOverlayIcon(this.icon, {required this.metrics});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Icon(
|
||||
icon,
|
||||
color: Colors.white,
|
||||
size: 16,
|
||||
shadows: [const Shadow(blurRadius: 5.0, color: Color.fromRGBO(0, 0, 0, 0.6), offset: Offset(0.0, 0.0))],
|
||||
size: metrics.iconSize,
|
||||
shadows: [
|
||||
Shadow(
|
||||
blurRadius: metrics.blurRadius,
|
||||
color: const Color.fromRGBO(0, 0, 0, 0.6),
|
||||
offset: const Offset(0.0, 0.0),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AssetTypeIcons extends StatelessWidget {
|
||||
final BaseAsset asset;
|
||||
final _OverlayMetrics metrics;
|
||||
|
||||
const _AssetTypeIcons({required this.asset});
|
||||
const _AssetTypeIcons({required this.asset, required this.metrics});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final hasStack = asset is RemoteAsset && (asset as RemoteAsset).stackId != null;
|
||||
final isLivePhoto = asset is RemoteAsset && asset.livePhotoVideoId != null;
|
||||
|
||||
return Column(
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
if (asset.isVideo)
|
||||
Padding(padding: const EdgeInsets.only(right: 10.0, top: 6.0), child: _VideoIndicator(asset.duration)),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(left: metrics.iconSpacing),
|
||||
child: _VideoIndicator(asset.duration, metrics: metrics),
|
||||
),
|
||||
if (hasStack)
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(right: 10.0, top: 6.0),
|
||||
child: _TileOverlayIcon(Icons.burst_mode_rounded),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(left: metrics.iconSpacing),
|
||||
child: _TileOverlayIcon(Icons.burst_mode_rounded, metrics: metrics),
|
||||
),
|
||||
if (isLivePhoto)
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(right: 10.0, top: 6.0),
|
||||
child: _TileOverlayIcon(Icons.motion_photos_on_rounded),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(left: metrics.iconSpacing),
|
||||
child: _TileOverlayIcon(Icons.motion_photos_on_rounded, metrics: metrics),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _OwnerNameLabel extends StatelessWidget {
|
||||
final String ownerName;
|
||||
final _OverlayMetrics metrics;
|
||||
|
||||
const _OwnerNameLabel({required this.ownerName, required this.metrics});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Text(
|
||||
ownerName,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: metrics.fontSize,
|
||||
fontWeight: FontWeight.w500,
|
||||
shadows: [
|
||||
Shadow(
|
||||
blurRadius: metrics.blurRadius,
|
||||
color: const Color.fromRGBO(0, 0, 0, 0.6),
|
||||
offset: const Offset(0.0, 0.0),
|
||||
),
|
||||
],
|
||||
),
|
||||
overflow: TextOverflow.fade,
|
||||
softWrap: false,
|
||||
maxLines: 1,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _OverlayMetrics {
|
||||
final double padding;
|
||||
final double iconSize;
|
||||
final double fontSize;
|
||||
final double iconSpacing;
|
||||
final double blurRadius;
|
||||
|
||||
const _OverlayMetrics({
|
||||
required this.padding,
|
||||
required this.iconSize,
|
||||
required this.fontSize,
|
||||
required this.iconSpacing,
|
||||
required this.blurRadius,
|
||||
});
|
||||
|
||||
factory _OverlayMetrics.fromConstraints(BoxConstraints constraints) {
|
||||
const baseSize = 120.0;
|
||||
final scale = (constraints.maxWidth / baseSize).clamp(0.5, 2.0);
|
||||
|
||||
return _OverlayMetrics(
|
||||
padding: (2.0 * scale).clamp(1.0, 4.0),
|
||||
iconSize: (16.0 * scale).clamp(14.0, 20.0),
|
||||
fontSize: (12.0 * scale).clamp(11.0, 14.0),
|
||||
iconSpacing: (2.0 * scale).clamp(1.0, 4.0),
|
||||
blurRadius: (5.0 * scale).clamp(3.0, 7.0),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'dart:async';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
@@ -18,6 +19,7 @@ import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.pro
|
||||
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/remote_album.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
@@ -191,6 +193,29 @@ class _AssetTileWidget extends ConsumerWidget {
|
||||
return lockSelectionAssets.contains(asset);
|
||||
}
|
||||
|
||||
String? _getOwnerName(WidgetRef ref) {
|
||||
final album = ref.watch(currentRemoteAlbumProvider);
|
||||
if (album == null || !album.isShared) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (asset case RemoteAsset remoteAsset) {
|
||||
final ownerId = remoteAsset.ownerId;
|
||||
|
||||
if (album.ownerId == ownerId) {
|
||||
return album.ownerName;
|
||||
}
|
||||
|
||||
final sharedUsersAsync = ref.watch(remoteAlbumSharedUsersProvider(album.id));
|
||||
return sharedUsersAsync.maybeWhen(
|
||||
data: (sharedUsers) => sharedUsers.firstWhereOrNull((user) => user.id == ownerId)?.name,
|
||||
orElse: () => null,
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final heroOffset = TabsRouterScope.of(context)?.controller.activeIndex ?? 0;
|
||||
@@ -198,6 +223,7 @@ class _AssetTileWidget extends ConsumerWidget {
|
||||
final lockSelection = _getLockSelectionStatus(ref);
|
||||
final showStorageIndicator = ref.watch(timelineArgsProvider.select((args) => args.showStorageIndicator));
|
||||
final isReadonlyModeEnabled = ref.watch(readonlyModeProvider);
|
||||
final ownerName = _getOwnerName(ref);
|
||||
|
||||
return RepaintBoundary(
|
||||
child: GestureDetector(
|
||||
@@ -208,6 +234,7 @@ class _AssetTileWidget extends ConsumerWidget {
|
||||
lockSelection: lockSelection,
|
||||
showStorageIndicator: showStorageIndicator,
|
||||
heroOffset: heroOffset,
|
||||
ownerName: ownerName,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -54,7 +54,8 @@ enum AppSettingsEnum<T> {
|
||||
readonlyModeEnabled<bool>(StoreKey.readonlyModeEnabled, "readonlyModeEnabled", false),
|
||||
albumGridView<bool>(StoreKey.albumGridView, "albumGridView", false),
|
||||
backupRequireCharging<bool>(StoreKey.backupRequireCharging, null, false),
|
||||
backupTriggerDelay<int>(StoreKey.backupTriggerDelay, null, 30);
|
||||
backupTriggerDelay<int>(StoreKey.backupTriggerDelay, null, 30),
|
||||
showOwnerName<bool>(StoreKey.showOwnerName, "showOwnerName", false);
|
||||
|
||||
const AppSettingsEnum(this.storeKey, this.hiveKey, this.defaultValue);
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/providers/activity.provider.dart';
|
||||
import 'package:immich_mobile/providers/album/current_album.provider.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart';
|
||||
@@ -69,11 +68,11 @@ class ActivityTextField extends HookConsumerWidget {
|
||||
suffixIcon: Padding(
|
||||
padding: const EdgeInsets.only(right: 10),
|
||||
child: IconButton(
|
||||
icon: Icon(liked ? Icons.thumb_up : Icons.thumb_up_off_alt),
|
||||
icon: Icon(liked ? Icons.favorite_rounded : Icons.favorite_border_rounded),
|
||||
onPressed: liked ? removeLike : addLike,
|
||||
),
|
||||
),
|
||||
suffixIconColor: liked ? context.primaryColor : null,
|
||||
suffixIconColor: liked ? Colors.red[700] : null,
|
||||
hintText: !isEnabled ? 'shared_album_activities_input_disable'.tr() : 'say_something'.tr(),
|
||||
hintStyle: TextStyle(fontWeight: FontWeight.normal, fontSize: 14, color: Colors.grey[600]),
|
||||
),
|
||||
|
||||
@@ -37,7 +37,7 @@ class ActivityTile extends HookConsumerWidget {
|
||||
? Container(
|
||||
width: isBottomSheet ? 30 : 44,
|
||||
alignment: Alignment.center,
|
||||
child: Icon(Icons.thumb_up, color: context.primaryColor),
|
||||
child: Icon(Icons.favorite_rounded, color: Colors.red[700]),
|
||||
)
|
||||
: isBottomSheet
|
||||
? UserCircleAvatar(user: activity.user, size: 30, radius: 15)
|
||||
|
||||
@@ -67,8 +67,8 @@ class CommentBubble extends ConsumerWidget {
|
||||
bottom: 6,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(color: context.colorScheme.surfaceContainer, shape: BoxShape.circle),
|
||||
child: Icon(Icons.thumb_up, color: context.primaryColor, size: 18),
|
||||
decoration: BoxDecoration(color: Colors.white.withValues(alpha: 0.7), shape: BoxShape.circle),
|
||||
child: Icon(Icons.favorite, color: Colors.red[600], size: 18),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -81,8 +81,8 @@ class CommentBubble extends ConsumerWidget {
|
||||
if (isLike && !showThumbnail) {
|
||||
likes = Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(color: context.colorScheme.surfaceContainer, shape: BoxShape.circle),
|
||||
child: Icon(Icons.thumb_up, color: context.primaryColor, size: 18),
|
||||
decoration: BoxDecoration(color: Colors.white.withValues(alpha: 0.7), shape: BoxShape.circle),
|
||||
child: Icon(Icons.favorite, color: Colors.red[600], size: 18),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ class AssetListSettings extends HookConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final showStorageIndicator = useAppSettingsState(AppSettingsEnum.storageIndicator);
|
||||
final showOwnerName = useAppSettingsState(AppSettingsEnum.showOwnerName);
|
||||
|
||||
final assetListSetting = [
|
||||
SettingsSwitchListTile(
|
||||
@@ -27,6 +28,14 @@ class AssetListSettings extends HookConsumerWidget {
|
||||
ref.invalidate(settingsProvider);
|
||||
},
|
||||
),
|
||||
SettingsSwitchListTile(
|
||||
valueNotifier: showOwnerName,
|
||||
title: 'theme_setting_asset_list_show_owner_name_title'.tr(),
|
||||
onChanged: (_) {
|
||||
ref.invalidate(appSettingsServiceProvider);
|
||||
ref.invalidate(settingsProvider);
|
||||
},
|
||||
),
|
||||
const LayoutSettings(),
|
||||
const GroupSettings(),
|
||||
];
|
||||
|
||||
@@ -77,15 +77,15 @@ void main() {
|
||||
overrides: overrides,
|
||||
);
|
||||
|
||||
expect(find.widgetWithIcon(IconButton, Icons.thumb_up), findsOneWidget);
|
||||
expect(find.widgetWithIcon(IconButton, Icons.thumb_up_off_alt), findsNothing);
|
||||
expect(find.widgetWithIcon(IconButton, Icons.favorite_rounded), findsOneWidget);
|
||||
expect(find.widgetWithIcon(IconButton, Icons.favorite_border_rounded), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('Bordered icon if likedId == null', (tester) async {
|
||||
await tester.pumpConsumerWidget(ActivityTextField(onSubmit: (_) {}), overrides: overrides);
|
||||
|
||||
expect(find.widgetWithIcon(IconButton, Icons.thumb_up_off_alt), findsOneWidget);
|
||||
expect(find.widgetWithIcon(IconButton, Icons.thumb_up), findsNothing);
|
||||
expect(find.widgetWithIcon(IconButton, Icons.favorite_border_rounded), findsOneWidget);
|
||||
expect(find.widgetWithIcon(IconButton, Icons.favorite_rounded), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('Adds new like', (tester) async {
|
||||
|
||||
@@ -91,17 +91,17 @@ void main() {
|
||||
group('Like Activity', () {
|
||||
final activity = Activity(id: '1', createdAt: DateTime(100), type: ActivityType.like, user: UserStub.admin);
|
||||
|
||||
testWidgets('Like contains filled thumbs-up as leading', (tester) async {
|
||||
testWidgets('Like contains filled heart as leading', (tester) async {
|
||||
await tester.pumpConsumerWidget(ActivityTile(activity), overrides: overrides);
|
||||
|
||||
// Leading widget should not be null
|
||||
final listTile = tester.widget<ListTile>(find.byType(ListTile));
|
||||
expect(listTile.leading, isNotNull);
|
||||
|
||||
// And should have a thumb_up icon
|
||||
final thumbUpIconFinder = find.widgetWithIcon(listTile.leading!.runtimeType, Icons.thumb_up);
|
||||
// And should have a favorite icon
|
||||
final favoIconFinder = find.widgetWithIcon(listTile.leading!.runtimeType, Icons.favorite_rounded);
|
||||
|
||||
expect(thumbUpIconFinder, findsOneWidget);
|
||||
expect(favoIconFinder, findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('Like title is center aligned', (tester) async {
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
"@oazapfts/runtime": "^1.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.10.3",
|
||||
"@types/node": "^24.10.1",
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"repository": {
|
||||
|
||||
30
pnpm-lock.yaml
generated
30
pnpm-lock.yaml
generated
@@ -63,7 +63,7 @@ importers:
|
||||
specifier: ^4.13.1
|
||||
version: 4.13.4
|
||||
'@types/node':
|
||||
specifier: ^24.10.3
|
||||
specifier: ^24.10.1
|
||||
version: 24.10.4
|
||||
'@vitest/coverage-v8':
|
||||
specifier: ^3.0.0
|
||||
@@ -214,7 +214,7 @@ importers:
|
||||
specifier: ^3.4.2
|
||||
version: 3.7.1
|
||||
'@types/node':
|
||||
specifier: ^24.10.3
|
||||
specifier: ^24.10.1
|
||||
version: 24.10.4
|
||||
'@types/oidc-provider':
|
||||
specifier: ^9.0.0
|
||||
@@ -299,7 +299,7 @@ importers:
|
||||
version: 1.1.0
|
||||
devDependencies:
|
||||
'@types/node':
|
||||
specifier: ^24.10.3
|
||||
specifier: ^24.10.1
|
||||
version: 24.10.4
|
||||
typescript:
|
||||
specifier: ^5.3.3
|
||||
@@ -615,7 +615,7 @@ importers:
|
||||
specifier: ^2.0.0
|
||||
version: 2.0.0
|
||||
'@types/node':
|
||||
specifier: ^24.10.3
|
||||
specifier: ^24.10.1
|
||||
version: 24.10.4
|
||||
'@types/nodemailer':
|
||||
specifier: ^7.0.0
|
||||
@@ -717,8 +717,8 @@ importers:
|
||||
specifier: file:../open-api/typescript-sdk
|
||||
version: link:../open-api/typescript-sdk
|
||||
'@immich/ui':
|
||||
specifier: ^0.51.0
|
||||
version: 0.51.0(@sveltejs/kit@2.49.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3)
|
||||
specifier: ^0.50.1
|
||||
version: 0.50.1(@sveltejs/kit@2.49.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3)
|
||||
'@mapbox/mapbox-gl-rtl-text':
|
||||
specifier: 0.2.3
|
||||
version: 0.2.3(mapbox-gl@1.13.3)
|
||||
@@ -3012,8 +3012,8 @@ packages:
|
||||
peerDependencies:
|
||||
svelte: ^5.0.0
|
||||
|
||||
'@immich/ui@0.51.0':
|
||||
resolution: {integrity: sha512-suCArLt/h0fPwT1BnpDpycuyx0cZYkuZaW8NOKvzTTAulIT6q67SWCz2Gb+Ae3blKh/b7r5Qoz65v5FzWqZdPQ==}
|
||||
'@immich/ui@0.50.1':
|
||||
resolution: {integrity: sha512-fNlQGh75ZFa/UZAgJaYk9/ItHOXHNNzN4CunjCmE7WocVVkUZbUxopN9Ku3F5GULSqD/zJ5gNO6PQAZ1ZoSaaQ==}
|
||||
peerDependencies:
|
||||
svelte: ^5.0.0
|
||||
|
||||
@@ -10424,10 +10424,6 @@ packages:
|
||||
resolution: {integrity: sha512-i/w5Ie4tENfGYbdCo2iJ+oies0vOFd8QXWHopKOUzudfLCvnmeheF2PpHp89Z2azpc+c2su3lMiWO/SpP+429A==}
|
||||
engines: {node: '>=0.12.18'}
|
||||
|
||||
simple-icons@16.2.0:
|
||||
resolution: {integrity: sha512-SCV4cAX8O2M2WQ3QJm+3k4QdcUIJiBiwKNViQiOwPjWbFWgypzelviW+Bp0A2ij6r/DIEZpTTDcLgD/BE4aexQ==}
|
||||
engines: {node: '>=0.12.18'}
|
||||
|
||||
sirv@2.0.4:
|
||||
resolution: {integrity: sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==}
|
||||
engines: {node: '>= 10'}
|
||||
@@ -14657,14 +14653,14 @@ snapshots:
|
||||
dependencies:
|
||||
svelte: 5.43.3
|
||||
|
||||
'@immich/ui@0.51.0(@sveltejs/kit@2.49.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3)':
|
||||
'@immich/ui@0.50.1(@sveltejs/kit@2.49.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3)':
|
||||
dependencies:
|
||||
'@immich/svelte-markdown-preprocess': 0.1.0(svelte@5.43.3)
|
||||
'@internationalized/date': 3.10.0
|
||||
'@mdi/js': 7.4.47
|
||||
bits-ui: 2.14.4(@internationalized/date@3.10.0)(@sveltejs/kit@2.49.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3)
|
||||
luxon: 3.7.2
|
||||
simple-icons: 16.2.0
|
||||
simple-icons: 15.22.0
|
||||
svelte: 5.43.3
|
||||
svelte-highlight: 7.9.0
|
||||
tailwind-merge: 3.4.0
|
||||
@@ -16571,7 +16567,7 @@ snapshots:
|
||||
|
||||
'@types/connect-history-api-fallback@1.5.4':
|
||||
dependencies:
|
||||
'@types/express-serve-static-core': 5.1.0
|
||||
'@types/express-serve-static-core': 4.19.7
|
||||
'@types/node': 24.10.4
|
||||
|
||||
'@types/connect@3.4.38':
|
||||
@@ -16817,7 +16813,7 @@ snapshots:
|
||||
|
||||
'@types/pg-pool@2.0.6':
|
||||
dependencies:
|
||||
'@types/pg': 8.16.0
|
||||
'@types/pg': 8.15.6
|
||||
|
||||
'@types/pg@8.15.6':
|
||||
dependencies:
|
||||
@@ -23456,8 +23452,6 @@ snapshots:
|
||||
|
||||
simple-icons@15.22.0: {}
|
||||
|
||||
simple-icons@16.2.0: {}
|
||||
|
||||
sirv@2.0.4:
|
||||
dependencies:
|
||||
'@polka/url': 1.0.0-next.29
|
||||
|
||||
@@ -134,7 +134,7 @@
|
||||
"@types/luxon": "^3.6.2",
|
||||
"@types/mock-fs": "^4.13.1",
|
||||
"@types/multer": "^2.0.0",
|
||||
"@types/node": "^24.10.3",
|
||||
"@types/node": "^24.10.1",
|
||||
"@types/nodemailer": "^7.0.0",
|
||||
"@types/picomatch": "^4.0.0",
|
||||
"@types/pngjs": "^6.0.5",
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
"@formatjs/icu-messageformat-parser": "^2.9.8",
|
||||
"@immich/justified-layout-wasm": "^0.4.3",
|
||||
"@immich/sdk": "file:../open-api/typescript-sdk",
|
||||
"@immich/ui": "^0.51.0",
|
||||
"@immich/ui": "^0.50.1",
|
||||
"@mapbox/mapbox-gl-rtl-text": "0.2.3",
|
||||
"@mdi/js": "^7.4.47",
|
||||
"@photo-sphere-viewer/core": "^5.14.0",
|
||||
|
||||
@@ -12,8 +12,6 @@
|
||||
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
||||
import { dragAndDropFilesStore } from '$lib/stores/drag-and-drop-files.store';
|
||||
import { mobileDevice } from '$lib/stores/mobile-device.svelte';
|
||||
import { SlideshowNavigation, SlideshowState, slideshowStore } from '$lib/stores/slideshow.store';
|
||||
import { handlePromiseError } from '$lib/utils';
|
||||
import { cancelMultiselect } from '$lib/utils/asset-utils';
|
||||
import { fileUploadHandler, openFileUploadDialog } from '$lib/utils/file-uploader';
|
||||
@@ -24,6 +22,7 @@
|
||||
import ControlAppBar from '../shared-components/control-app-bar.svelte';
|
||||
import ThemeButton from '../shared-components/theme-button.svelte';
|
||||
import AlbumSummary from './album-summary.svelte';
|
||||
import { SlideshowNavigation, SlideshowState, slideshowStore } from '$lib/stores/slideshow.store';
|
||||
|
||||
interface Props {
|
||||
sharedLink: SharedLinkResponseDto;
|
||||
@@ -111,7 +110,7 @@
|
||||
<ControlAppBar showBackButton={false}>
|
||||
{#snippet leading()}
|
||||
<a data-sveltekit-preload-data="hover" class="ms-4" href="/">
|
||||
<Logo variant={mobileDevice.maxMd ? 'icon' : 'inline'} class="min-w-10" />
|
||||
<Logo variant="inline" class="min-w-min" />
|
||||
</a>
|
||||
{/snippet}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
import type { ActivityResponseDto } from '@immich/sdk';
|
||||
import { Icon } from '@immich/ui';
|
||||
import { mdiCommentOutline, mdiThumbUp, mdiThumbUpOutline } from '@mdi/js';
|
||||
import { mdiCommentOutline, mdiHeart, mdiHeartOutline } from '@mdi/js';
|
||||
|
||||
interface Props {
|
||||
isLiked: ActivityResponseDto | null;
|
||||
@@ -19,7 +19,7 @@
|
||||
<div class="w-full flex p-4 items-center justify-center rounded-full gap-5 bg-subtle border bg-opacity-60">
|
||||
<button type="button" class={disabled ? 'cursor-not-allowed' : ''} onclick={onFavorite} {disabled}>
|
||||
<div class="flex gap-2 items-center justify-center">
|
||||
<Icon icon={isLiked ? mdiThumbUp : mdiThumbUpOutline} size="24" class={isLiked ? 'text-primary' : 'text-fg'} />
|
||||
<Icon icon={isLiked ? mdiHeart : mdiHeartOutline} size="24" class={isLiked ? 'text-red-400' : 'text-fg'} />
|
||||
{#if numberOfLikes}
|
||||
<div class="text-l">{numberOfLikes.toLocaleString($locale)}</div>
|
||||
{/if}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
import { isTenMinutesApart } from '$lib/utils/timesince';
|
||||
import { ReactionType, type ActivityResponseDto, type AssetTypeEnum, type UserResponseDto } from '@immich/sdk';
|
||||
import { Icon, IconButton, LoadingSpinner, toastManager } from '@immich/ui';
|
||||
import { mdiClose, mdiDeleteOutline, mdiDotsVertical, mdiSend, mdiThumbUp } from '@mdi/js';
|
||||
import { mdiClose, mdiDeleteOutline, mdiDotsVertical, mdiHeart, mdiSend } from '@mdi/js';
|
||||
import * as luxon from 'luxon';
|
||||
import { t } from 'svelte-i18n';
|
||||
import UserAvatar from '../shared-components/user-avatar.svelte';
|
||||
@@ -181,7 +181,7 @@
|
||||
{:else if reaction.type === ReactionType.Like}
|
||||
<div class="relative">
|
||||
<div class="flex py-3 ps-3 mt-3 gap-4 items-center text-sm">
|
||||
<div class="text-primary"><Icon icon={mdiThumbUp} size="20" /></div>
|
||||
<div class="text-red-600"><Icon icon={mdiHeart} size="20" /></div>
|
||||
|
||||
<div class="w-full" title={`${reaction.user.name} (${reaction.user.email})`}>
|
||||
{$t('user_liked', {
|
||||
@@ -254,7 +254,7 @@
|
||||
shortcut: { key: 'Enter' },
|
||||
onShortcut: () => handleSendComment(),
|
||||
}}
|
||||
class="h-4.5 {disabled
|
||||
class="h-[18px] {disabled
|
||||
? 'cursor-not-allowed'
|
||||
: ''} w-full max-h-56 pe-2 items-center overflow-y-auto leading-4 outline-none resize-none bg-gray-200"
|
||||
></textarea>
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
import type { Viewport } from '$lib/managers/timeline-manager/types';
|
||||
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||
import { dragAndDropFilesStore } from '$lib/stores/drag-and-drop-files.store';
|
||||
import { mobileDevice } from '$lib/stores/mobile-device.svelte';
|
||||
import { handlePromiseError } from '$lib/utils';
|
||||
import { cancelMultiselect, downloadArchive } from '$lib/utils/asset-utils';
|
||||
import { fileUploadHandler, openFileUploadDialog } from '$lib/utils/file-uploader';
|
||||
@@ -109,7 +108,7 @@
|
||||
<ControlAppBar onClose={() => goto(AppRoute.PHOTOS)} backIcon={mdiArrowLeft} showBackButton={false}>
|
||||
{#snippet leading()}
|
||||
<a data-sveltekit-preload-data="hover" class="ms-4" href="/">
|
||||
<Logo variant={mobileDevice.maxMd ? 'icon' : 'inline'} class="min-w-10" />
|
||||
<Logo variant="inline" />
|
||||
</a>
|
||||
{/snippet}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import { getSharedLinkActions } from '$lib/services/shared-link.service';
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
import { SharedLinkType, type SharedLinkResponseDto } from '@immich/sdk';
|
||||
import { ContextMenuButton, MenuItemType, Text } from '@immich/ui';
|
||||
import { Badge, ContextMenuButton, MenuItemType, Text } from '@immich/ui';
|
||||
import { DateTime, type ToRelativeUnit } from 'luxon';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
@@ -32,28 +32,6 @@
|
||||
};
|
||||
|
||||
const { Edit, Copy, Delete } = $derived(getSharedLinkActions($t, sharedLink));
|
||||
|
||||
const capabilities = $derived.by(() => {
|
||||
const items = [];
|
||||
|
||||
if (sharedLink.allowUpload) {
|
||||
items.push($t('upload'));
|
||||
}
|
||||
|
||||
if (sharedLink.allowDownload) {
|
||||
items.push($t('download'));
|
||||
}
|
||||
|
||||
if (sharedLink.showMetadata) {
|
||||
items.push($t('exif'));
|
||||
}
|
||||
|
||||
if (sharedLink.password) {
|
||||
items.push($t('password'));
|
||||
}
|
||||
|
||||
return items;
|
||||
});
|
||||
</script>
|
||||
|
||||
<div
|
||||
@@ -66,41 +44,50 @@
|
||||
>
|
||||
<ShareCover class="transition-all duration-300 hover:shadow-lg" {sharedLink} />
|
||||
|
||||
<div class="flex flex-col gap-4 justify-between">
|
||||
<div class="flex flex-col">
|
||||
<Text size="tiny" color={isExpired ? 'danger' : 'muted'} class="font-medium">
|
||||
{#if isExpired}
|
||||
{$t('expired')}
|
||||
{:else if expiresAt}
|
||||
<div class="flex flex-col gap-2">
|
||||
<Text size="large" color="primary" class="flex place-items-center gap-2 break-all">
|
||||
{#if sharedLink.type === SharedLinkType.Album}
|
||||
{sharedLink.album?.albumName}
|
||||
{:else if sharedLink.type === SharedLinkType.Individual}
|
||||
{$t('individual_share')}
|
||||
{/if}
|
||||
</Text>
|
||||
|
||||
<div class="flex flex-wrap gap-1">
|
||||
{#if isExpired}
|
||||
<Badge size="small" color="danger">{$t('expired')}</Badge>
|
||||
{:else if expiresAt}
|
||||
<Badge size="small" color="secondary">
|
||||
{$t('expires_date', { values: { date: getCountDownExpirationDate(expiresAt, now) } })}
|
||||
{:else}
|
||||
{$t('expires_date', { values: { date: '∞' } })}
|
||||
{/if}
|
||||
</Text>
|
||||
</Badge>
|
||||
{:else}
|
||||
<Badge size="small" color="secondary">{$t('expires_date', { values: { date: '∞' } })}</Badge>
|
||||
{/if}
|
||||
|
||||
<Text size="large" color="primary" class="flex place-items-center gap-2 break-all font-medium">
|
||||
{#if sharedLink.type === SharedLinkType.Album}
|
||||
{sharedLink.album?.albumName}
|
||||
{:else if sharedLink.type === SharedLinkType.Individual}
|
||||
{$t('individual_share')}
|
||||
{/if}
|
||||
</Text>
|
||||
{#if sharedLink.slug}
|
||||
<Badge size="small" color="secondary">{$t('custom_url')}</Badge>
|
||||
{/if}
|
||||
|
||||
{#if sharedLink.description}
|
||||
<Text size="small" class="line-clamp-1">{sharedLink.description}</Text>
|
||||
{#if sharedLink.allowUpload}
|
||||
<Badge size="small" color="secondary">{$t('upload')}</Badge>
|
||||
{/if}
|
||||
|
||||
{#if sharedLink.showMetadata && sharedLink.allowDownload}
|
||||
<Badge size="small" color="secondary">{$t('download')}</Badge>
|
||||
{/if}
|
||||
|
||||
{#if sharedLink.showMetadata}
|
||||
<Badge size="small" color="secondary">{$t('exif')}</Badge>
|
||||
{/if}
|
||||
|
||||
{#if sharedLink.password}
|
||||
<Badge size="small" color="secondary">{$t('password')}</Badge>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
{#each capabilities as capability, index (index)}
|
||||
<Text size="small" color="primary" class="font-medium">
|
||||
{capability}
|
||||
</Text>
|
||||
{#if index < capabilities.length - 1}
|
||||
<Text size="small" color="muted">•</Text>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
{#if sharedLink.description}
|
||||
<Text size="small" class="line-clamp-1">{sharedLink.description}</Text>
|
||||
{/if}
|
||||
</div>
|
||||
</svelte:element>
|
||||
<div class="flex flex-auto flex-col place-content-center place-items-end text-end ms-4">
|
||||
@@ -3,7 +3,7 @@
|
||||
import { page } from '$app/state';
|
||||
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
|
||||
import OnEvents from '$lib/components/OnEvents.svelte';
|
||||
import SharedLinkCard from '$lib/components/sharedlinks-page/SharedLinkCard.svelte';
|
||||
import SharedLinkCard from '$lib/components/sharedlinks-page/shared-link-card.svelte';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import GroupTab from '$lib/elements/GroupTab.svelte';
|
||||
import SharedLinkUpdateModal from '$lib/modals/SharedLinkUpdateModal.svelte';
|
||||
|
||||
@@ -21,14 +21,7 @@
|
||||
import { copyToClipboard, getReleaseType, semverToName } from '$lib/utils';
|
||||
import { maintenanceShouldRedirect } from '$lib/utils/maintenance';
|
||||
import { isAssetViewerRoute } from '$lib/utils/navigation';
|
||||
import {
|
||||
CommandPaletteContext,
|
||||
TooltipProvider,
|
||||
modalManager,
|
||||
setTranslations,
|
||||
toastManager,
|
||||
type ActionItem,
|
||||
} from '@immich/ui';
|
||||
import { CommandPaletteContext, modalManager, setTranslations, toastManager, type ActionItem } from '@immich/ui';
|
||||
import { mdiAccountMultipleOutline, mdiBookshelf, mdiCog, mdiServer, mdiSync, mdiThemeLightDark } from '@mdi/js';
|
||||
import { onMount, type Snippet } from 'svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
@@ -235,17 +228,15 @@
|
||||
}}
|
||||
/>
|
||||
|
||||
<TooltipProvider>
|
||||
{#if page.data.error}
|
||||
<ErrorLayout error={page.data.error}></ErrorLayout>
|
||||
{:else}
|
||||
{@render children?.()}
|
||||
{/if}
|
||||
{#if page.data.error}
|
||||
<ErrorLayout error={page.data.error}></ErrorLayout>
|
||||
{:else}
|
||||
{@render children?.()}
|
||||
{/if}
|
||||
|
||||
{#if showNavigationLoadingBar}
|
||||
<NavigationLoadingBar />
|
||||
{/if}
|
||||
{#if showNavigationLoadingBar}
|
||||
<NavigationLoadingBar />
|
||||
{/if}
|
||||
|
||||
<DownloadPanel />
|
||||
<UploadPanel />
|
||||
</TooltipProvider>
|
||||
<DownloadPanel />
|
||||
<UploadPanel />
|
||||
|
||||
Reference in New Issue
Block a user