mirror of
https://github.com/immich-app/immich.git
synced 2026-07-05 20:27:52 -07:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 947095b453 | |||
| fef42a2cc9 | |||
| 24a7419844 | |||
| 1f3998cda8 |
Vendored
+2
-2
@@ -1,7 +1,7 @@
|
||||
[
|
||||
{
|
||||
"label": "v3.0.1",
|
||||
"url": "https://docs.v3.0.1.archive.immich.app"
|
||||
"label": "v3.0.0",
|
||||
"url": "https://docs.v3.0.0.archive.immich.app"
|
||||
},
|
||||
{
|
||||
"label": "v2.7.5",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "immich-e2e",
|
||||
"version": "3.0.1",
|
||||
"version": "3.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "immich-ml"
|
||||
version = "3.0.1"
|
||||
version = "3.0.0"
|
||||
description = ""
|
||||
authors = [{ name = "Hau Tran", email = "alex.tran1502@gmail.com" }]
|
||||
requires-python = ">=3.11,<4.0"
|
||||
|
||||
Generated
+1
-1
@@ -974,7 +974,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "immich-ml"
|
||||
version = "3.0.1"
|
||||
version = "3.0.0"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "aiocache" },
|
||||
|
||||
@@ -22,8 +22,8 @@ platform :android do
|
||||
task: 'bundle',
|
||||
build_type: 'Release',
|
||||
properties: {
|
||||
"android.injected.version.code" => 3054,
|
||||
"android.injected.version.name" => "3.0.1",
|
||||
"android.injected.version.code" => 3053,
|
||||
"android.injected.version.name" => "3.0.0",
|
||||
}
|
||||
)
|
||||
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab', track: 'beta')
|
||||
@@ -35,8 +35,8 @@ platform :android do
|
||||
task: 'bundle',
|
||||
build_type: 'Release',
|
||||
properties: {
|
||||
"android.injected.version.code" => 3054,
|
||||
"android.injected.version.name" => "3.0.1",
|
||||
"android.injected.version.code" => 3053,
|
||||
"android.injected.version.name" => "3.0.0",
|
||||
}
|
||||
)
|
||||
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>3.0.1</string>
|
||||
<string>3.0.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
|
||||
@@ -25,7 +25,6 @@ enum SyncMigrationTask {
|
||||
v20260128_CopyExifWidthHeightToAsset, // Asset table has incorrect width and height for video ratio calculations.
|
||||
v20260128_ResetAssetV1, // Asset v2.5.0 has width and height information that were edited assets.
|
||||
v20260597_ResetAssetV1AssetV2, // Assets didn't include the uploadedAt column.
|
||||
v20260701_ResetAlbumsV1, // Album user migration dropped the owner. Sync fresh albums from the server to re-populate them.
|
||||
}
|
||||
|
||||
class SyncStreamService {
|
||||
@@ -104,12 +103,6 @@ class SyncStreamService {
|
||||
}
|
||||
|
||||
Future<void> _runPreSyncTasks(List<String> migrations, SemVer semVer) async {
|
||||
if (!migrations.contains(SyncMigrationTask.v20260701_ResetAlbumsV1.name)) {
|
||||
_logger.info("Running pre-sync task: v20260701_ResetAlbumsV1");
|
||||
await _syncApiRepository.deleteSyncAck([SyncEntityType.albumV1]);
|
||||
migrations.add(SyncMigrationTask.v20260701_ResetAlbumsV1.name);
|
||||
}
|
||||
|
||||
if (!migrations.contains(SyncMigrationTask.v20260128_ResetExifV1.name)) {
|
||||
_logger.info("Running pre-sync task: v20260128_ResetExifV1");
|
||||
await _syncApiRepository.deleteSyncAck([
|
||||
|
||||
@@ -106,7 +106,7 @@ class _FixedSegmentRow extends ConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final isScrubbing = ref.watch(timelineStateProvider.select((s) => s.isScrubbing));
|
||||
final recommendDeferredLoading = ref.watch(timelineStateProvider.select((s) => s.recommendDeferredLoading));
|
||||
final timelineService = ref.read(timelineServiceProvider);
|
||||
final isDynamicLayout = columnCount <= (context.isMobile ? 2 : 3);
|
||||
|
||||
@@ -119,7 +119,7 @@ class _FixedSegmentRow extends ConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
if (isScrubbing) {
|
||||
if (recommendDeferredLoading) {
|
||||
return _buildPlaceholder(context);
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ import 'package:immich_mobile/presentation/widgets/timeline/constants.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/timeline/segment.model.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/timeline/timeline.state.dart';
|
||||
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
|
||||
import 'package:immich_mobile/utils/debounce.dart';
|
||||
import 'package:intl/intl.dart' hide TextDirection;
|
||||
|
||||
/// A widget that will display a BoxScrollView with a ScrollThumb that can be dragged
|
||||
@@ -89,8 +88,6 @@ class ScrubberState extends ConsumerState<Scrubber> with TickerProviderStateMixi
|
||||
bool _isDragging = false;
|
||||
List<_Segment> _segments = [];
|
||||
int _monthCount = 0;
|
||||
DateTime? _currentScrubberDate;
|
||||
Debouncer? _scrubberDebouncer;
|
||||
|
||||
late AnimationController _thumbAnimationController;
|
||||
Timer? _fadeOutTimer;
|
||||
@@ -145,7 +142,6 @@ class ScrubberState extends ConsumerState<Scrubber> with TickerProviderStateMixi
|
||||
_thumbAnimationController.dispose();
|
||||
_labelAnimationController.dispose();
|
||||
_fadeOutTimer?.cancel();
|
||||
_scrubberDebouncer?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -189,24 +185,6 @@ class ScrubberState extends ConsumerState<Scrubber> with TickerProviderStateMixi
|
||||
return false;
|
||||
}
|
||||
|
||||
void _onScrubberDateChanged(DateTime date) {
|
||||
if (_currentScrubberDate != date) {
|
||||
// Date changed, immediately set scrubbing to true
|
||||
_currentScrubberDate = date;
|
||||
ref.read(timelineStateProvider.notifier).setScrubbing(true);
|
||||
|
||||
// Initialize debouncer if needed
|
||||
_scrubberDebouncer ??= Debouncer(interval: const Duration(milliseconds: 50));
|
||||
|
||||
// Debounce setting scrubbing to false
|
||||
_scrubberDebouncer!.run(() {
|
||||
if (_currentScrubberDate == date) {
|
||||
ref.read(timelineStateProvider.notifier).setScrubbing(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _onDragStart(DragStartDetails _) {
|
||||
setState(() {
|
||||
_isDragging = true;
|
||||
@@ -237,11 +215,6 @@ class ScrubberState extends ConsumerState<Scrubber> with TickerProviderStateMixi
|
||||
if (_lastLabel != label) {
|
||||
ref.read(hapticFeedbackProvider.notifier).selectionClick();
|
||||
_lastLabel = label;
|
||||
|
||||
// Notify timeline state of the new scrubber date position
|
||||
if (_monthCount >= kMinMonthsToEnableScrubberSnap) {
|
||||
_onScrubberDateChanged(nearestMonthSegment.date);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -349,13 +322,6 @@ class ScrubberState extends ConsumerState<Scrubber> with TickerProviderStateMixi
|
||||
_isDragging = false;
|
||||
});
|
||||
|
||||
ref.read(timelineStateProvider.notifier).setScrubbing(false);
|
||||
|
||||
// Reset scrubber tracking when drag ends
|
||||
_currentScrubberDate = null;
|
||||
_scrubberDebouncer?.dispose();
|
||||
_scrubberDebouncer = null;
|
||||
|
||||
_resetThumbTimer();
|
||||
}
|
||||
|
||||
|
||||
@@ -50,37 +50,43 @@ class TimelineArgs {
|
||||
}
|
||||
|
||||
class TimelineState {
|
||||
final bool isScrubbing;
|
||||
final bool isScrolling;
|
||||
|
||||
const TimelineState({this.isScrubbing = false, this.isScrolling = false});
|
||||
/// Indicates whether the timeline is scrolling beyond some configured "high" speed,
|
||||
/// such as when programmatically scrolling to the top or a really fast user fling
|
||||
final bool recommendDeferredLoading;
|
||||
|
||||
bool get isInteracting => isScrubbing || isScrolling;
|
||||
const TimelineState({this.isScrolling = false, this.recommendDeferredLoading = false});
|
||||
|
||||
bool get isInteracting => isScrolling || recommendDeferredLoading;
|
||||
|
||||
@override
|
||||
bool operator ==(covariant TimelineState other) {
|
||||
return isScrubbing == other.isScrubbing && isScrolling == other.isScrolling;
|
||||
return isScrolling == other.isScrolling && recommendDeferredLoading == other.recommendDeferredLoading;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => isScrubbing.hashCode ^ isScrolling.hashCode;
|
||||
int get hashCode => isScrolling.hashCode ^ recommendDeferredLoading.hashCode;
|
||||
|
||||
TimelineState copyWith({bool? isScrubbing, bool? isScrolling}) {
|
||||
return TimelineState(isScrubbing: isScrubbing ?? this.isScrubbing, isScrolling: isScrolling ?? this.isScrolling);
|
||||
TimelineState copyWith({bool? isScrolling, bool? recommendDeferredLoading}) {
|
||||
return TimelineState(
|
||||
isScrolling: isScrolling ?? this.isScrolling,
|
||||
recommendDeferredLoading: recommendDeferredLoading ?? this.recommendDeferredLoading,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TimelineStateNotifier extends Notifier<TimelineState> {
|
||||
void setScrubbing(bool isScrubbing) {
|
||||
state = state.copyWith(isScrubbing: isScrubbing);
|
||||
}
|
||||
|
||||
void setScrolling(bool isScrolling) {
|
||||
state = state.copyWith(isScrolling: isScrolling);
|
||||
}
|
||||
|
||||
void setRecommendDeferredLoading(bool recommendDeferredLoading) {
|
||||
state = state.copyWith(recommendDeferredLoading: recommendDeferredLoading);
|
||||
}
|
||||
|
||||
@override
|
||||
TimelineState build() => const TimelineState(isScrubbing: false, isScrolling: false);
|
||||
TimelineState build() => const TimelineState(isScrolling: false, recommendDeferredLoading: false);
|
||||
}
|
||||
|
||||
// This provider watches the buckets from the timeline service & args and serves the segments.
|
||||
|
||||
@@ -25,6 +25,7 @@ import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.da
|
||||
import 'package:immich_mobile/providers/infrastructure/settings.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||
import 'package:immich_mobile/utils/debounce.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_sliver_app_bar.dart';
|
||||
import 'package:immich_mobile/widgets/common/mesmerizing_sliver_app_bar.dart';
|
||||
import 'package:immich_mobile/widgets/common/selection_sliver_app_bar.dart';
|
||||
@@ -150,6 +151,8 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
|
||||
double _baseScaleFactor = 3.0;
|
||||
int? _restoreAssetIndex;
|
||||
|
||||
final Debouncer _fastScrollDebouncer = Debouncer(interval: const Duration(milliseconds: 100));
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -182,11 +185,7 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
|
||||
switch (event) {
|
||||
case ScrollToTopEvent():
|
||||
{
|
||||
final timelineState = ref.read(timelineStateProvider.notifier);
|
||||
timelineState.setScrubbing(true);
|
||||
_scrollController
|
||||
.animateTo(0, duration: const Duration(milliseconds: 250), curve: Curves.easeInOut)
|
||||
.whenComplete(() => timelineState.setScrubbing(false));
|
||||
_scrollController.animateTo(0, duration: const Duration(milliseconds: 250), curve: Curves.easeInOut);
|
||||
}
|
||||
|
||||
case ScrollToDateEvent scrollToDateEvent:
|
||||
@@ -246,13 +245,31 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_fastScrollDebouncer.dispose();
|
||||
_scrollController.dispose();
|
||||
_eventSubscription?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// Track whether the timeline is moving fast enough to defer per-row asset loading
|
||||
bool _onScrollVelocityNotification(ScrollNotification notification) {
|
||||
// Only consider the primary timeline ScrollView (no nested views) and update events
|
||||
if (notification.depth != 0 || notification is! ScrollUpdateNotification) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Use Flutter's built in fast velocity tracking
|
||||
if (_scrollController.position.recommendDeferredLoading(context)) {
|
||||
ref.read(timelineStateProvider.notifier).setRecommendDeferredLoading(true);
|
||||
|
||||
// We cannot rely on scroll end events, as the timeline scrubber jumps from position
|
||||
// to position, resulting in large spikes in velocity followed by low velocity
|
||||
_fastScrollDebouncer.run(() => ref.read(timelineStateProvider.notifier).setRecommendDeferredLoading(false));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void _scrollToDate(DateTime date) {
|
||||
final timelineState = ref.read(timelineStateProvider.notifier);
|
||||
final asyncSegments = ref.read(timelineSegmentProvider);
|
||||
asyncSegments.whenData((segments) {
|
||||
// Find the segment that contains assets from the target date
|
||||
@@ -279,16 +296,11 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
|
||||
if (fallbackSegment != null) {
|
||||
// Scroll to the segment with a small offset to show the header
|
||||
final targetOffset = fallbackSegment.startOffset - 50;
|
||||
timelineState.setScrubbing(true);
|
||||
_scrollController
|
||||
.animateTo(
|
||||
targetOffset.clamp(0.0, _scrollController.position.maxScrollExtent),
|
||||
duration: const Duration(milliseconds: 500),
|
||||
curve: Curves.easeInOut,
|
||||
)
|
||||
.whenComplete(() => timelineState.setScrubbing(false));
|
||||
} else {
|
||||
timelineState.setScrubbing(false);
|
||||
_scrollController.animateTo(
|
||||
targetOffset.clamp(0.0, _scrollController.position.maxScrollExtent),
|
||||
duration: const Duration(milliseconds: 500),
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -480,7 +492,10 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
timeline,
|
||||
NotificationListener<ScrollNotification>(
|
||||
onNotification: _onScrollVelocityNotification,
|
||||
child: timeline,
|
||||
),
|
||||
if (isBottomWidgetVisible)
|
||||
Positioned(
|
||||
top: MediaQuery.paddingOf(context).top,
|
||||
|
||||
Generated
+1
-1
@@ -3,7 +3,7 @@ Immich API
|
||||
|
||||
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
|
||||
|
||||
- API version: 3.0.1
|
||||
- API version: 3.0.0
|
||||
- Generator version: 7.22.0
|
||||
- Build package: org.openapitools.codegen.languages.DartClientCodegen
|
||||
|
||||
|
||||
Generated
-8
@@ -681,10 +681,8 @@ class AlbumsApi {
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] id (required):
|
||||
/// Album ID
|
||||
///
|
||||
/// * [String] userId (required):
|
||||
/// Album user ID, or \"me\" to reference the current user
|
||||
Future<Response> removeUserFromAlbumWithHttpInfo(String id, String userId, { Future<void>? abortTrigger, }) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/albums/{id}/user/{userId}'
|
||||
@@ -720,10 +718,8 @@ class AlbumsApi {
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] id (required):
|
||||
/// Album ID
|
||||
///
|
||||
/// * [String] userId (required):
|
||||
/// Album user ID, or \"me\" to reference the current user
|
||||
Future<void> removeUserFromAlbum(String id, String userId, { Future<void>? abortTrigger, }) async {
|
||||
final response = await removeUserFromAlbumWithHttpInfo(id, userId, abortTrigger: abortTrigger,);
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
@@ -802,10 +798,8 @@ class AlbumsApi {
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] id (required):
|
||||
/// Album ID
|
||||
///
|
||||
/// * [String] userId (required):
|
||||
/// Album user ID, or \"me\" to reference the current user
|
||||
///
|
||||
/// * [UpdateAlbumUserDto] updateAlbumUserDto (required):
|
||||
Future<Response> updateAlbumUserWithHttpInfo(String id, String userId, UpdateAlbumUserDto updateAlbumUserDto, { Future<void>? abortTrigger, }) async {
|
||||
@@ -843,10 +837,8 @@ class AlbumsApi {
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] id (required):
|
||||
/// Album ID
|
||||
///
|
||||
/// * [String] userId (required):
|
||||
/// Album user ID, or \"me\" to reference the current user
|
||||
///
|
||||
/// * [UpdateAlbumUserDto] updateAlbumUserDto (required):
|
||||
Future<void> updateAlbumUser(String id, String userId, UpdateAlbumUserDto updateAlbumUserDto, { Future<void>? abortTrigger, }) async {
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ name: immich_mobile
|
||||
description: Immich - selfhosted backup media file on mobile phone
|
||||
|
||||
publish_to: 'none'
|
||||
version: 3.0.1+3054
|
||||
version: 3.0.0+3053
|
||||
|
||||
environment:
|
||||
sdk: '>=3.12.0 <4.0.0'
|
||||
|
||||
@@ -2702,7 +2702,6 @@
|
||||
"name": "id",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"description": "Album ID",
|
||||
"schema": {
|
||||
"format": "uuid",
|
||||
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$",
|
||||
@@ -2713,7 +2712,6 @@
|
||||
"name": "userId",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"description": "Album user ID, or \"me\" to reference the current user",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
@@ -2764,7 +2762,6 @@
|
||||
"name": "id",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"description": "Album ID",
|
||||
"schema": {
|
||||
"format": "uuid",
|
||||
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$",
|
||||
@@ -2775,7 +2772,6 @@
|
||||
"name": "userId",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"description": "Album user ID, or \"me\" to reference the current user",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
@@ -16210,7 +16206,7 @@
|
||||
"info": {
|
||||
"title": "Immich",
|
||||
"description": "Immich API",
|
||||
"version": "3.0.1",
|
||||
"version": "3.0.0",
|
||||
"contact": {}
|
||||
},
|
||||
"tags": [
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "immich-monorepo",
|
||||
"version": "3.0.1",
|
||||
"version": "3.0.0",
|
||||
"description": "Monorepo for Immich",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@immich/cli",
|
||||
"version": "3.0.1",
|
||||
"version": "3.0.0",
|
||||
"description": "Command Line Interface (CLI) for Immich",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@immich/sdk",
|
||||
"version": "3.0.1",
|
||||
"version": "3.0.0",
|
||||
"description": "Auto-generated TypeScript SDK for the Immich API",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Immich
|
||||
* 3.0.1
|
||||
* 3.0.0
|
||||
* DO NOT MODIFY - This file has been generated using oazapfts.
|
||||
* See https://www.npmjs.com/package/oazapfts
|
||||
*/
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "immich",
|
||||
"version": "3.0.1",
|
||||
"version": "3.0.0",
|
||||
"description": "",
|
||||
"author": "",
|
||||
"private": true,
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
AlbumsAddAssetsDto,
|
||||
AlbumsAddAssetsResponseDto,
|
||||
AlbumStatisticsResponseDto,
|
||||
AlbumUserParamDto,
|
||||
CreateAlbumDto,
|
||||
GetAlbumsDto,
|
||||
UpdateAlbumDto,
|
||||
@@ -19,7 +18,7 @@ import { MapMarkerResponseDto } from 'src/dtos/map.dto';
|
||||
import { ApiTag, Permission } from 'src/enum';
|
||||
import { Auth, Authenticated } from 'src/middleware/auth.guard';
|
||||
import { AlbumService } from 'src/services/album.service';
|
||||
import { UUIDParamDto } from 'src/validation';
|
||||
import { ParseMeUUIDPipe, UUIDParamDto } from 'src/validation';
|
||||
|
||||
@ApiTags(ApiTag.Albums)
|
||||
@Controller('albums')
|
||||
@@ -176,7 +175,8 @@ export class AlbumController {
|
||||
})
|
||||
updateAlbumUser(
|
||||
@Auth() auth: AuthDto,
|
||||
@Param() { id, userId }: AlbumUserParamDto,
|
||||
@Param() { id }: UUIDParamDto,
|
||||
@Param('userId', new ParseMeUUIDPipe({ version: '4' })) userId: string,
|
||||
@Body() dto: UpdateAlbumUserDto,
|
||||
): Promise<void> {
|
||||
return this.service.updateUser(auth, id, userId, dto);
|
||||
@@ -190,7 +190,11 @@ export class AlbumController {
|
||||
description: 'Remove a user from an album. Use an ID of "me" to leave a shared album.',
|
||||
history: new HistoryBuilder().added('v1').beta('v1').stable('v2'),
|
||||
})
|
||||
removeUserFromAlbum(@Auth() auth: AuthDto, @Param() { id, userId }: AlbumUserParamDto): Promise<void> {
|
||||
removeUserFromAlbum(
|
||||
@Auth() auth: AuthDto,
|
||||
@Param() { id }: UUIDParamDto,
|
||||
@Param('userId', new ParseMeUUIDPipe({ version: '4' })) userId: string,
|
||||
): Promise<void> {
|
||||
return this.service.removeUser(auth, id, userId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,18 +140,6 @@ export const AlbumResponseSchema = z
|
||||
})
|
||||
.meta({ id: 'AlbumResponseDto' });
|
||||
|
||||
const AlbumUserParamSchema = z.object({
|
||||
id: z.uuidv4().describe('Album ID'),
|
||||
// TODO: disallow 'me' as a shortcut in v4 and type userId as uuidv4
|
||||
userId: z
|
||||
.string()
|
||||
.refine((value) => value === 'me' || z.uuidv4().safeParse(value).success, {
|
||||
error: 'Must be a UUID v4 or "me"',
|
||||
})
|
||||
.describe('Album user ID, or "me" to reference the current user'),
|
||||
});
|
||||
|
||||
export class AlbumUserParamDto extends createZodDto(AlbumUserParamSchema) {}
|
||||
export class AddUsersDto extends createZodDto(AddUsersSchema) {}
|
||||
export class AlbumUserCreateDto extends createZodDto(AlbumUserCreateSchema) {}
|
||||
export class CreateAlbumDto extends createZodDto(CreateAlbumSchema) {}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FileValidator, Injectable } from '@nestjs/common';
|
||||
import { ArgumentMetadata, FileValidator, Injectable, ParseUUIDPipe } from '@nestjs/common';
|
||||
import { createZodDto } from 'nestjs-zod';
|
||||
import sanitize from 'sanitize-filename';
|
||||
import { isIP, isIPRange } from 'validator';
|
||||
@@ -74,6 +74,16 @@ export function IsNotSiblingOf<
|
||||
);
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class ParseMeUUIDPipe extends ParseUUIDPipe {
|
||||
async transform(value: string, metadata: ArgumentMetadata) {
|
||||
if (value == 'me') {
|
||||
return value;
|
||||
}
|
||||
return super.transform(value, metadata);
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class FileNotEmptyValidator extends FileValidator {
|
||||
constructor(private requiredFields: string[]) {
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "immich-web",
|
||||
"version": "3.0.1",
|
||||
"version": "3.0.0",
|
||||
"license": "GNU Affero General Public License version 3",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -101,9 +101,7 @@
|
||||
{description}
|
||||
</p>
|
||||
{:else}
|
||||
<div class="pb-2">
|
||||
{@render descriptionSnippet?.()}
|
||||
</div>
|
||||
{@render descriptionSnippet?.()}
|
||||
{/if}
|
||||
|
||||
{#if inputType !== SettingInputFieldType.PASSWORD}
|
||||
|
||||
Reference in New Issue
Block a user