Compare commits
21 Commits
fix/failed
...
fix/beta-t
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
46f348825d | ||
|
|
17e6ca962a | ||
|
|
750d21aeba | ||
|
|
990d9ba9a8 | ||
|
|
b05e931ed8 | ||
|
|
4d0c9172e5 | ||
|
|
fab63e6b2a | ||
|
|
094e3a2757 | ||
|
|
278668b8c5 | ||
|
|
3ef9e36f38 | ||
|
|
10141504a2 | ||
|
|
67736c8fce | ||
|
|
b56a272f64 | ||
|
|
5901c2e963 | ||
|
|
be85832b20 | ||
|
|
c8f9a72d3e | ||
|
|
3d633a81c4 | ||
|
|
4efbf36d82 | ||
|
|
e2c3c39597 | ||
|
|
007ba1d9ef | ||
|
|
4d5cd1a6b5 |
96
.github/workflows/close-duplicates.yml
vendored
Normal file
96
.github/workflows/close-duplicates.yml
vendored
Normal file
@@ -0,0 +1,96 @@
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
discussion:
|
||||
types: [created]
|
||||
|
||||
name: Close likely duplicates
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
get_body:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
EVENT: ${{ toJSON(github.event) }}
|
||||
outputs:
|
||||
body: ${{ steps.get_body.outputs.body }}
|
||||
steps:
|
||||
- id: get_body
|
||||
run: |
|
||||
BODY=$(echo """$EVENT""" | jq -r '.issue // .discussion | .body' | base64 -w 0)
|
||||
echo "body=$BODY" >> $GITHUB_OUTPUT
|
||||
|
||||
get_checkbox_json:
|
||||
runs-on: ubuntu-latest
|
||||
needs: get_body
|
||||
container:
|
||||
image: yshavit/mdq:0.7.2
|
||||
outputs:
|
||||
json: ${{ steps.get_checkbox.outputs.json }}
|
||||
steps:
|
||||
- id: get_checkbox
|
||||
env:
|
||||
BODY: ${{ needs.get_body.outputs.body }}
|
||||
run: |
|
||||
JSON=$(echo "$BODY" | base64 -d | /mdq --output json '# I have searched | - [?] Yes')
|
||||
echo "json=$JSON" >> $GITHUB_OUTPUT
|
||||
|
||||
close_and_comment:
|
||||
runs-on: ubuntu-latest
|
||||
needs: get_checkbox_json
|
||||
if: ${{ !fromJSON(needs.get_checkbox_json.outputs.json).items[0].list[0].checked }}
|
||||
permissions:
|
||||
issues: write
|
||||
discussions: write
|
||||
steps:
|
||||
- name: Close issue
|
||||
if: ${{ github.event_name == 'issues' }}
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
NODE_ID: ${{ github.event.issue.node_id }}
|
||||
run: |
|
||||
gh api graphql \
|
||||
-f issueId="$NODE_ID" \
|
||||
-f body="This issue has automatically been closed as it is likely a duplicate. We get a lot of duplicate threads each day, which is why we ask you in the template to confirm that you searched for duplicates before opening one." \
|
||||
-f query='
|
||||
mutation CommentAndCloseIssue($issueId: ID!, $body: String!) {
|
||||
addComment(input: {
|
||||
subjectId: $issueId,
|
||||
body: $body
|
||||
}) {
|
||||
__typename
|
||||
}
|
||||
|
||||
closeIssue(input: {
|
||||
issueId: $issueId,
|
||||
stateReason: DUPLICATE
|
||||
}) {
|
||||
__typename
|
||||
}
|
||||
}'
|
||||
|
||||
- name: Close discussion
|
||||
if: ${{ github.event_name == 'discussion' && github.event.discussion.category.name == 'Feature Request' }}
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
NODE_ID: ${{ github.event.discussion.node_id }}
|
||||
run: |
|
||||
gh api graphql \
|
||||
-f discussionId="$NODE_ID" \
|
||||
-f body="This discussion has automatically been closed as it is likely a duplicate. We get a lot of duplicate threads each day, which is why we ask you in the template to confirm that you searched for duplicates before opening one." \
|
||||
-f query='
|
||||
mutation CommentAndCloseDiscussion($discussionId: ID!, $body: String!) {
|
||||
addDiscussionComment(input: {
|
||||
discussionId: $discussionId,
|
||||
body: $body
|
||||
}) {
|
||||
__typename
|
||||
}
|
||||
|
||||
closeDiscussion(input: {
|
||||
discussionId: $discussionId,
|
||||
reason: DUPLICATE
|
||||
}) {
|
||||
__typename
|
||||
}
|
||||
}'
|
||||
6
cli/package-lock.json
generated
6
cli/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@immich/cli",
|
||||
"version": "2.2.76",
|
||||
"version": "2.2.77",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@immich/cli",
|
||||
"version": "2.2.76",
|
||||
"version": "2.2.77",
|
||||
"license": "GNU Affero General Public License version 3",
|
||||
"dependencies": {
|
||||
"chokidar": "^4.0.3",
|
||||
@@ -54,7 +54,7 @@
|
||||
},
|
||||
"../open-api/typescript-sdk": {
|
||||
"name": "@immich/sdk",
|
||||
"version": "1.137.2",
|
||||
"version": "1.137.3",
|
||||
"dev": true,
|
||||
"license": "GNU Affero General Public License version 3",
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@immich/cli",
|
||||
"version": "2.2.76",
|
||||
"version": "2.2.77",
|
||||
"description": "Command Line Interface (CLI) for Immich",
|
||||
"type": "module",
|
||||
"exports": "./dist/index.js",
|
||||
|
||||
4
docs/static/archived-versions.json
vendored
4
docs/static/archived-versions.json
vendored
@@ -1,4 +1,8 @@
|
||||
[
|
||||
{
|
||||
"label": "v1.137.3",
|
||||
"url": "https://v1.137.3.archive.immich.app"
|
||||
},
|
||||
{
|
||||
"label": "v1.137.2",
|
||||
"url": "https://v1.137.2.archive.immich.app"
|
||||
|
||||
8
e2e/package-lock.json
generated
8
e2e/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "immich-e2e",
|
||||
"version": "1.137.2",
|
||||
"version": "1.137.3",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "immich-e2e",
|
||||
"version": "1.137.2",
|
||||
"version": "1.137.3",
|
||||
"license": "GNU Affero General Public License version 3",
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3.1.0",
|
||||
@@ -46,7 +46,7 @@
|
||||
},
|
||||
"../cli": {
|
||||
"name": "@immich/cli",
|
||||
"version": "2.2.76",
|
||||
"version": "2.2.77",
|
||||
"dev": true,
|
||||
"license": "GNU Affero General Public License version 3",
|
||||
"dependencies": {
|
||||
@@ -95,7 +95,7 @@
|
||||
},
|
||||
"../open-api/typescript-sdk": {
|
||||
"name": "@immich/sdk",
|
||||
"version": "1.137.2",
|
||||
"version": "1.137.3",
|
||||
"dev": true,
|
||||
"license": "GNU Affero General Public License version 3",
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "immich-e2e",
|
||||
"version": "1.137.2",
|
||||
"version": "1.137.3",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
|
||||
@@ -1252,7 +1252,7 @@
|
||||
"manage_your_devices": "Manage your logged-in devices",
|
||||
"manage_your_oauth_connection": "Manage your OAuth connection",
|
||||
"map": "Map",
|
||||
"map_assets_in_bounds": "{count, plural, one {# photo} other {# photos}}",
|
||||
"map_assets_in_bounds": "{count, plural, =0 {No photos in this area} one {# photo} other {# photos}}",
|
||||
"map_cannot_get_user_location": "Cannot get user's location",
|
||||
"map_location_dialog_yes": "Yes",
|
||||
"map_location_picker_page_use_location": "Use this location",
|
||||
@@ -1260,7 +1260,6 @@
|
||||
"map_location_service_disabled_title": "Location Service disabled",
|
||||
"map_marker_for_images": "Map marker for images taken in {city}, {country}",
|
||||
"map_marker_with_image": "Map marker with image",
|
||||
"map_no_assets_in_bounds": "No photos in this area",
|
||||
"map_no_location_permission_content": "Location permission is needed to display assets from your current location. Do you want to allow it now?",
|
||||
"map_no_location_permission_title": "Location Permission denied",
|
||||
"map_settings": "Map settings",
|
||||
|
||||
@@ -36,7 +36,7 @@ platform :android do
|
||||
build_type: 'Release',
|
||||
properties: {
|
||||
"android.injected.version.code" => 3002,
|
||||
"android.injected.version.name" => "1.137.2",
|
||||
"android.injected.version.name" => "1.137.3",
|
||||
}
|
||||
)
|
||||
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')
|
||||
|
||||
@@ -22,7 +22,7 @@ platform :ios do
|
||||
path: "./Runner.xcodeproj",
|
||||
)
|
||||
increment_version_number(
|
||||
version_number: "1.137.2"
|
||||
version_number: "1.137.3"
|
||||
)
|
||||
increment_build_number(
|
||||
build_number: latest_testflight_build_number + 1,
|
||||
|
||||
@@ -96,7 +96,7 @@ class HashService {
|
||||
if (hash?.length == 20) {
|
||||
hashed.add(asset.copyWith(checksum: base64.encode(hash!)));
|
||||
} else {
|
||||
_log.warning("Failed to hash file for ${asset.id}");
|
||||
_log.warning("Failed to hash file for ${asset.id}: ${asset.name} created at ${asset.createdAt}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ class BackgroundSyncManager {
|
||||
this.onHashingError,
|
||||
});
|
||||
|
||||
Future<void> cancel() {
|
||||
Future<void> cancel() async {
|
||||
final futures = <Future>[];
|
||||
|
||||
if (_syncTask != null) {
|
||||
@@ -52,7 +52,11 @@ class BackgroundSyncManager {
|
||||
_syncWebsocketTask?.cancel();
|
||||
_syncWebsocketTask = null;
|
||||
|
||||
return Future.wait(futures);
|
||||
try {
|
||||
await Future.wait(futures);
|
||||
} on CanceledError {
|
||||
// Ignore cancellation errors
|
||||
}
|
||||
}
|
||||
|
||||
// No need to cancel the task, as it can also be run when the user logs out
|
||||
|
||||
@@ -83,7 +83,6 @@ Future<void> initApp() async {
|
||||
};
|
||||
|
||||
PlatformDispatcher.instance.onError = (error, stack) {
|
||||
debugPrint("FlutterError - Catch all: $error \n $stack");
|
||||
log.severe('PlatformDispatcher - Catch all', error, stack);
|
||||
return true;
|
||||
};
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -9,6 +11,7 @@ import 'package:immich_mobile/providers/app_settings.provider.dart';
|
||||
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
|
||||
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/memory.provider.dart';
|
||||
import 'package:immich_mobile/providers/search/search_input_focus.provider.dart';
|
||||
import 'package:immich_mobile/providers/tab.provider.dart';
|
||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||
@@ -122,6 +125,11 @@ void _onNavigationSelected(TabsRouter router, int index, WidgetRef ref) {
|
||||
EventStream.shared.emit(const ScrollToTopEvent());
|
||||
}
|
||||
|
||||
// On Photos page navigation, invalidate memories provider to get the most up-to-date data
|
||||
if (router.activeIndex == 0) {
|
||||
ref.invalidate(driftMemoryFutureProvider);
|
||||
}
|
||||
|
||||
// On Search page tapped
|
||||
if (router.activeIndex == 1 && index == 1) {
|
||||
ref.read(searchInputFocusProvider).requestFocus();
|
||||
@@ -137,25 +145,50 @@ void _onNavigationSelected(TabsRouter router, int index, WidgetRef ref) {
|
||||
ref.read(tabProvider.notifier).state = TabEnum.values[index];
|
||||
}
|
||||
|
||||
class _BottomNavigationBar extends ConsumerWidget {
|
||||
class _BottomNavigationBar extends ConsumerStatefulWidget {
|
||||
const _BottomNavigationBar({required this.tabsRouter, required this.destinations});
|
||||
|
||||
final List<Widget> destinations;
|
||||
final TabsRouter tabsRouter;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final isScreenLandscape = context.orientation == Orientation.landscape;
|
||||
final isMultiselectEnabled = ref.watch(multiSelectProvider.select((s) => s.isEnabled));
|
||||
ConsumerState createState() => _BottomNavigationBarState();
|
||||
}
|
||||
|
||||
if (isScreenLandscape || isMultiselectEnabled) {
|
||||
class _BottomNavigationBarState extends ConsumerState<_BottomNavigationBar> {
|
||||
bool hideNavigationBar = false;
|
||||
StreamSubscription? _eventSubscription;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_eventSubscription = EventStream.shared.listen<MultiSelectToggleEvent>(_onEvent);
|
||||
}
|
||||
|
||||
void _onEvent(MultiSelectToggleEvent event) {
|
||||
setState(() {
|
||||
hideNavigationBar = event.isEnabled;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_eventSubscription?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isScreenLandscape = context.orientation == Orientation.landscape;
|
||||
|
||||
if (isScreenLandscape || hideNavigationBar) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return NavigationBar(
|
||||
selectedIndex: tabsRouter.activeIndex,
|
||||
onDestinationSelected: (index) => _onNavigationSelected(tabsRouter, index, ref),
|
||||
destinations: destinations,
|
||||
selectedIndex: widget.tabsRouter.activeIndex,
|
||||
onDestinationSelected: (index) => _onNavigationSelected(widget.tabsRouter, index, ref),
|
||||
destinations: widget.destinations,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -264,11 +264,15 @@ class SharedLinkEditPage extends HookConsumerWidget {
|
||||
expiresAt: expiryAfter.value == 0 ? null : calculateExpiry(),
|
||||
);
|
||||
ref.invalidate(sharedLinksStateProvider);
|
||||
|
||||
await ref.read(serverInfoProvider.notifier).getServerConfig();
|
||||
final externalDomain = ref.read(serverInfoProvider.select((s) => s.serverConfig.externalDomain));
|
||||
|
||||
var serverUrl = externalDomain.isNotEmpty ? externalDomain : getServerUrl();
|
||||
if (serverUrl != null && !serverUrl.endsWith('/')) {
|
||||
serverUrl += '/';
|
||||
}
|
||||
|
||||
if (newLink != null && serverUrl != null) {
|
||||
newShareLink.value = "${serverUrl}share/${newLink.key}";
|
||||
copyLinkToClipboard();
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/memory/memory_lane.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/memory.provider.dart';
|
||||
import 'package:immich_mobile/providers/user.provider.dart';
|
||||
|
||||
@RoutePage()
|
||||
class MainTimelinePage extends ConsumerWidget {
|
||||
@@ -12,21 +13,24 @@ class MainTimelinePage extends ConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final memoryLaneProvider = ref.watch(driftMemoryFutureProvider);
|
||||
final memoriesEnabled = ref.watch(currentUserProvider.select((user) => user?.memoryEnabled ?? true));
|
||||
|
||||
// TODO: the user preferences need to be updated
|
||||
// from the server to get live hiding/showing of memory lane
|
||||
|
||||
return memoryLaneProvider.maybeWhen(
|
||||
data: (memories) {
|
||||
return memories.isEmpty
|
||||
? const Timeline(showStorageIndicator: true)
|
||||
return memories.isEmpty || !memoriesEnabled
|
||||
? const Timeline()
|
||||
: Timeline(
|
||||
topSliverWidget: SliverToBoxAdapter(
|
||||
key: Key('memory-lane-${memories.first.assets.first.id}'),
|
||||
child: DriftMemoryLane(memories: memories),
|
||||
),
|
||||
topSliverWidgetHeight: 200,
|
||||
showStorageIndicator: true,
|
||||
);
|
||||
},
|
||||
orElse: () => const Timeline(showStorageIndicator: true),
|
||||
orElse: () => const Timeline(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,6 @@ class DriftTrashPage extends StatelessWidget {
|
||||
}),
|
||||
],
|
||||
child: Timeline(
|
||||
showStorageIndicator: true,
|
||||
appBar: SliverAppBar(
|
||||
title: Text('trash'.t(context: context)),
|
||||
floating: true,
|
||||
|
||||
@@ -26,7 +26,6 @@ class LocalTimelinePage extends StatelessWidget {
|
||||
child: Timeline(
|
||||
appBar: MesmerizingSliverAppBar(title: album.name),
|
||||
bottomSheet: const LocalAlbumBottomSheet(),
|
||||
showStorageIndicator: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||
import 'package:immich_mobile/models/search/search_filter.model.dart';
|
||||
import 'package:immich_mobile/presentation/pages/search/paginated_search.provider.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/bottom_sheet/general_bottom_sheet.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
||||
import 'package:immich_mobile/providers/search/search_input_focus.provider.dart';
|
||||
@@ -627,7 +628,12 @@ class _SearchResultGrid extends ConsumerWidget {
|
||||
return timelineService;
|
||||
}),
|
||||
],
|
||||
child: Timeline(key: ValueKey(searchResult.totalAssets), appBar: null, groupBy: GroupAssetsBy.none),
|
||||
child: Timeline(
|
||||
key: ValueKey(searchResult.totalAssets),
|
||||
groupBy: GroupAssetsBy.none,
|
||||
appBar: null,
|
||||
bottomSheet: const GeneralBottomSheet(minChildSize: 0.20),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -580,6 +580,7 @@ class AddToAlbumHeader extends ConsumerWidget {
|
||||
}
|
||||
|
||||
ref.read(currentRemoteAlbumProvider.notifier).setAlbum(newAlbum);
|
||||
ref.read(multiSelectProvider.notifier).reset();
|
||||
context.pushRoute(RemoteAlbumRoute(album: newAlbum));
|
||||
}
|
||||
|
||||
|
||||
@@ -22,13 +22,13 @@ class BaseBottomSheet extends ConsumerStatefulWidget {
|
||||
this.slivers,
|
||||
this.controller,
|
||||
this.initialChildSize = 0.35,
|
||||
this.minChildSize = 0.15,
|
||||
double? minChildSize,
|
||||
this.maxChildSize = 0.65,
|
||||
this.expand = true,
|
||||
this.shouldCloseOnMinExtent = true,
|
||||
this.resizeOnScroll = true,
|
||||
this.backgroundColor,
|
||||
});
|
||||
}) : minChildSize = minChildSize ?? 0.15;
|
||||
|
||||
@override
|
||||
ConsumerState<BaseBottomSheet> createState() => _BaseDraggableScrollableSheetState();
|
||||
|
||||
@@ -6,8 +6,8 @@ import 'package:immich_mobile/domain/models/album/album.model.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/edit_date_time_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/edit_location_action_button.widget.dart';
|
||||
@@ -26,7 +26,8 @@ import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||
|
||||
class GeneralBottomSheet extends ConsumerWidget {
|
||||
const GeneralBottomSheet({super.key});
|
||||
final double? minChildSize;
|
||||
const GeneralBottomSheet({super.key, this.minChildSize});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
@@ -60,6 +61,7 @@ class GeneralBottomSheet extends ConsumerWidget {
|
||||
|
||||
return BaseBottomSheet(
|
||||
initialChildSize: 0.45,
|
||||
minChildSize: minChildSize,
|
||||
maxChildSize: 0.85,
|
||||
shouldCloseOnMinExtent: false,
|
||||
actions: [
|
||||
|
||||
@@ -2,10 +2,12 @@ import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/setting.model.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/duration_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/setting.provider.dart';
|
||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||
|
||||
class ThumbnailTile extends ConsumerWidget {
|
||||
@@ -13,7 +15,7 @@ class ThumbnailTile extends ConsumerWidget {
|
||||
this.asset, {
|
||||
this.size = const Size.square(256),
|
||||
this.fit = BoxFit.cover,
|
||||
this.showStorageIndicator = true,
|
||||
this.showStorageIndicator,
|
||||
this.lockSelection = false,
|
||||
this.heroOffset,
|
||||
super.key,
|
||||
@@ -22,7 +24,7 @@ class ThumbnailTile extends ConsumerWidget {
|
||||
final BaseAsset asset;
|
||||
final Size size;
|
||||
final BoxFit fit;
|
||||
final bool showStorageIndicator;
|
||||
final bool? showStorageIndicator;
|
||||
final bool lockSelection;
|
||||
final int? heroOffset;
|
||||
|
||||
@@ -52,6 +54,9 @@ class ThumbnailTile extends ConsumerWidget {
|
||||
|
||||
final hasStack = asset is RemoteAsset && (asset as RemoteAsset).stackId != null;
|
||||
|
||||
final bool storageIndicator =
|
||||
showStorageIndicator ?? ref.watch(settingsProvider.select((s) => s.get(Setting.showStorageIndicator)));
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
AnimatedContainer(
|
||||
@@ -86,7 +91,7 @@ class ThumbnailTile extends ConsumerWidget {
|
||||
child: _VideoIndicator(asset.duration),
|
||||
),
|
||||
),
|
||||
if (showStorageIndicator)
|
||||
if (storageIndicator)
|
||||
switch (asset.storage) {
|
||||
AssetState.local => const Align(
|
||||
alignment: Alignment.bottomRight,
|
||||
|
||||
@@ -14,7 +14,7 @@ class TimelineArgs {
|
||||
final double maxHeight;
|
||||
final double spacing;
|
||||
final int columnCount;
|
||||
final bool showStorageIndicator;
|
||||
final bool? showStorageIndicator;
|
||||
final bool withStack;
|
||||
final GroupAssetsBy? groupBy;
|
||||
|
||||
@@ -23,7 +23,7 @@ class TimelineArgs {
|
||||
required this.maxHeight,
|
||||
this.spacing = kTimelineSpacing,
|
||||
this.columnCount = kTimelineColumnCount,
|
||||
this.showStorageIndicator = false,
|
||||
this.showStorageIndicator,
|
||||
this.withStack = false,
|
||||
this.groupBy,
|
||||
});
|
||||
|
||||
@@ -31,7 +31,7 @@ class Timeline extends StatelessWidget {
|
||||
super.key,
|
||||
this.topSliverWidget,
|
||||
this.topSliverWidgetHeight,
|
||||
this.showStorageIndicator = false,
|
||||
this.showStorageIndicator,
|
||||
this.withStack = false,
|
||||
this.appBar = const ImmichSliverAppBar(floating: true, pinned: false, snap: false),
|
||||
this.bottomSheet = const GeneralBottomSheet(),
|
||||
@@ -40,7 +40,7 @@ class Timeline extends StatelessWidget {
|
||||
|
||||
final Widget? topSliverWidget;
|
||||
final double? topSliverWidgetHeight;
|
||||
final bool showStorageIndicator;
|
||||
final bool? showStorageIndicator;
|
||||
final Widget? appBar;
|
||||
final Widget? bottomSheet;
|
||||
final bool withStack;
|
||||
@@ -115,6 +115,8 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
|
||||
_baseScaleFactor = _scaleFactor;
|
||||
});
|
||||
});
|
||||
|
||||
ref.listenManual(multiSelectProvider.select((s) => s.isEnabled), _onMultiSelectionToggled);
|
||||
}
|
||||
|
||||
void _onEvent(Event event) {
|
||||
@@ -130,6 +132,10 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
|
||||
}
|
||||
}
|
||||
|
||||
void _onMultiSelectionToggled(_, bool isEnabled) {
|
||||
EventStream.shared.emit(MultiSelectToggleEvent(isEnabled));
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_scrollController.dispose();
|
||||
|
||||
@@ -86,11 +86,12 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
|
||||
// Ensure proper cleanup before starting new background tasks
|
||||
try {
|
||||
await Future.wait([
|
||||
backgroundManager.syncLocal().then((_) {
|
||||
Future(() async {
|
||||
await backgroundManager.syncLocal();
|
||||
Logger("AppLifeCycleNotifier").fine("Hashing assets after syncLocal");
|
||||
// Check if app is still active before hashing
|
||||
if (state == AppLifeCycleEnum.resumed) {
|
||||
backgroundManager.hashAssets();
|
||||
if ([AppLifeCycleEnum.resumed, AppLifeCycleEnum.active].contains(state)) {
|
||||
await backgroundManager.hashAssets();
|
||||
}
|
||||
}),
|
||||
backgroundManager.syncRemote(),
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/services/timeline.service.dart';
|
||||
import 'package:immich_mobile/domain/utils/event_stream.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
||||
|
||||
final multiSelectProvider = NotifierProvider<MultiSelectNotifier, MultiSelectState>(
|
||||
@@ -10,6 +10,11 @@ final multiSelectProvider = NotifierProvider<MultiSelectNotifier, MultiSelectSta
|
||||
dependencies: [timelineServiceProvider],
|
||||
);
|
||||
|
||||
class MultiSelectToggleEvent extends Event {
|
||||
final bool isEnabled;
|
||||
const MultiSelectToggleEvent(this.isEnabled);
|
||||
}
|
||||
|
||||
class MultiSelectState {
|
||||
final Set<BaseAsset> selectedAssets;
|
||||
final Set<BaseAsset> lockedSelectionAssets;
|
||||
|
||||
@@ -25,6 +25,8 @@ class AuthApiRepository extends ApiRepository {
|
||||
}
|
||||
|
||||
Future<void> logout() async {
|
||||
if (_apiService.apiClient.basePath.isEmpty) return;
|
||||
|
||||
await _apiService.authenticationApi.logout().timeout(const Duration(seconds: 7));
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
@@ -18,6 +19,7 @@ import 'package:immich_mobile/providers/infrastructure/current_album.provider.da
|
||||
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';
|
||||
import 'package:immich_mobile/widgets/album/remote_album_shared_user_icons.dart';
|
||||
|
||||
class RemoteAlbumSliverAppBar extends ConsumerStatefulWidget {
|
||||
@@ -93,7 +95,7 @@ class _MesmerizingSliverAppBarState extends ConsumerState<RemoteAlbumSliverAppBa
|
||||
),
|
||||
onPressed: () {
|
||||
ref.read(remoteAlbumProvider.notifier).refresh();
|
||||
context.pop();
|
||||
context.navigateTo(const TabShellRoute(children: [DriftAlbumsRoute()]));
|
||||
},
|
||||
),
|
||||
actions: [
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
@@ -26,7 +25,7 @@ class _SelectionSliverAppBarState extends ConsumerState<SelectionSliverAppBar> {
|
||||
|
||||
onDone(Set<BaseAsset> selected) {
|
||||
ref.read(multiSelectProvider.notifier).reset();
|
||||
context.maybePop<Set<BaseAsset>>(selected);
|
||||
context.pop<Set<BaseAsset>>(selected);
|
||||
}
|
||||
|
||||
return SliverAppBar(
|
||||
|
||||
@@ -8,6 +8,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/collection_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||
import 'package:immich_mobile/models/map/map_event.model.dart';
|
||||
import 'package:immich_mobile/providers/db.provider.dart';
|
||||
import 'package:immich_mobile/providers/timeline.provider.dart';
|
||||
@@ -229,9 +230,7 @@ class _MapSheetDragRegion extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final assetsInBoundsText = assetsInBoundCount > 0
|
||||
? "map_assets_in_bounds".tr(namedArgs: {'count': assetsInBoundCount.toString()})
|
||||
: "map_no_assets_in_bounds".tr();
|
||||
final assetsInBoundsText = "map_assets_in_bounds".t(context: context, args: {'count': assetsInBoundCount});
|
||||
|
||||
return SingleChildScrollView(
|
||||
controller: controller,
|
||||
|
||||
@@ -2,11 +2,13 @@ import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/setting.provider.dart';
|
||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||
import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart';
|
||||
import 'package:immich_mobile/widgets/settings/asset_list_settings/asset_list_group_settings.dart';
|
||||
import 'package:immich_mobile/widgets/settings/settings_sub_page_scaffold.dart';
|
||||
import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart';
|
||||
import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart';
|
||||
|
||||
import 'asset_list_layout_settings.dart';
|
||||
|
||||
class AssetListSettings extends HookConsumerWidget {
|
||||
@@ -20,7 +22,10 @@ class AssetListSettings extends HookConsumerWidget {
|
||||
SettingsSwitchListTile(
|
||||
valueNotifier: showStorageIndicator,
|
||||
title: 'theme_setting_asset_list_storage_indicator_title'.tr(),
|
||||
onChanged: (_) => ref.invalidate(appSettingsServiceProvider),
|
||||
onChanged: (_) {
|
||||
ref.invalidate(appSettingsServiceProvider);
|
||||
ref.invalidate(settingsProvider);
|
||||
},
|
||||
),
|
||||
const LayoutSettings(),
|
||||
const GroupSettings(),
|
||||
|
||||
2
mobile/openapi/README.md
generated
2
mobile/openapi/README.md
generated
@@ -3,7 +3,7 @@ Immich API
|
||||
|
||||
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
|
||||
|
||||
- API version: 1.137.2
|
||||
- API version: 1.137.3
|
||||
- Generator version: 7.8.0
|
||||
- Build package: org.openapitools.codegen.languages.DartClientCodegen
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ name: immich_mobile
|
||||
description: Immich - selfhosted backup media file on mobile phone
|
||||
|
||||
publish_to: 'none'
|
||||
version: 1.137.2+3002
|
||||
version: 1.137.3+3002
|
||||
|
||||
environment:
|
||||
sdk: '>=3.8.0 <4.0.0'
|
||||
|
||||
@@ -9469,7 +9469,7 @@
|
||||
"info": {
|
||||
"title": "Immich",
|
||||
"description": "Immich API",
|
||||
"version": "1.137.2",
|
||||
"version": "1.137.3",
|
||||
"contact": {}
|
||||
},
|
||||
"tags": [],
|
||||
|
||||
4
open-api/typescript-sdk/package-lock.json
generated
4
open-api/typescript-sdk/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@immich/sdk",
|
||||
"version": "1.137.2",
|
||||
"version": "1.137.3",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@immich/sdk",
|
||||
"version": "1.137.2",
|
||||
"version": "1.137.3",
|
||||
"license": "GNU Affero General Public License version 3",
|
||||
"dependencies": {
|
||||
"@oazapfts/runtime": "^1.0.2"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@immich/sdk",
|
||||
"version": "1.137.2",
|
||||
"version": "1.137.3",
|
||||
"description": "Auto-generated TypeScript SDK for the Immich API",
|
||||
"type": "module",
|
||||
"main": "./build/index.js",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Immich
|
||||
* 1.137.2
|
||||
* 1.137.3
|
||||
* DO NOT MODIFY - This file has been generated using oazapfts.
|
||||
* See https://www.npmjs.com/package/oazapfts
|
||||
*/
|
||||
|
||||
4
server/package-lock.json
generated
4
server/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "immich",
|
||||
"version": "1.137.2",
|
||||
"version": "1.137.3",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "immich",
|
||||
"version": "1.137.2",
|
||||
"version": "1.137.3",
|
||||
"license": "GNU Affero General Public License version 3",
|
||||
"dependencies": {
|
||||
"@nestjs/bullmq": "^11.0.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "immich",
|
||||
"version": "1.137.2",
|
||||
"version": "1.137.3",
|
||||
"description": "",
|
||||
"author": "",
|
||||
"private": true,
|
||||
|
||||
@@ -16,9 +16,7 @@ export class AssetUploadInterceptor implements NestInterceptor {
|
||||
const res = context.switchToHttp().getResponse<Response<AssetMediaResponseDto>>();
|
||||
|
||||
const checksum = fromMaybeArray(req.headers[ImmichHeader.Checksum]);
|
||||
console.log('AssetUploadInterceptor checksum:', checksum);
|
||||
const response = await this.service.getUploadAssetIdByChecksum(req.user, checksum);
|
||||
console.log('AssetUploadInterceptor response:', response);
|
||||
if (response) {
|
||||
res.status(200);
|
||||
return of({ status: AssetMediaStatus.DUPLICATE, id: response.id });
|
||||
|
||||
@@ -103,23 +103,6 @@ export class FileUploadInterceptor implements NestInterceptor {
|
||||
}
|
||||
|
||||
private filename(request: AuthRequest, file: Express.Multer.File, callback: DiskStorageCallback) {
|
||||
console.log('File upload started:', file.originalname);
|
||||
request.on('data', () => {
|
||||
console.log('Data event triggered for file upload:', file.originalname);
|
||||
});
|
||||
request.on('close', () => {
|
||||
console.log('Request closed');
|
||||
});
|
||||
request.on('aborted', () => {
|
||||
console.log('Request aborted, cleaning up file');
|
||||
this.defaultStorage._removeFile(request, file, (error) => {
|
||||
if (error) {
|
||||
this.logger.warn('Request aborted, failed to cleanup file', error);
|
||||
} else {
|
||||
this.logger.log('Request aborted, file cleaned up successfully');
|
||||
}
|
||||
});
|
||||
});
|
||||
return callbackify(
|
||||
() => this.assetService.getUploadFilename(asRequest(request, file)),
|
||||
callback as Callback<string>,
|
||||
@@ -145,15 +128,8 @@ export class FileUploadInterceptor implements NestInterceptor {
|
||||
|
||||
const hash = createHash('sha1');
|
||||
file.stream.on('data', (chunk) => hash.update(chunk));
|
||||
file.stream.on('error', (error) => {
|
||||
this.logger.warn('Stream error while uploading file, cleaning up', error);
|
||||
this.assetService.onUploadError(request, file).catch(this.logger.error);
|
||||
callback(error);
|
||||
});
|
||||
console.log('File upload started:', file.originalname);
|
||||
this.defaultStorage._handleFile(request, file, (error, info) => {
|
||||
if (error) {
|
||||
console.error('Error handling file upload:', error);
|
||||
hash.destroy();
|
||||
callback(error);
|
||||
} else {
|
||||
|
||||
@@ -131,7 +131,6 @@ export class AssetMediaService extends BaseService {
|
||||
sidecarFile?: UploadFile,
|
||||
): Promise<AssetMediaResponseDto> {
|
||||
try {
|
||||
console.log(`Uploading asset: ${file.originalPath}, size: ${file.size}`);
|
||||
await this.requireAccess({
|
||||
auth,
|
||||
permission: Permission.AssetUpload,
|
||||
@@ -139,25 +138,20 @@ export class AssetMediaService extends BaseService {
|
||||
ids: [auth.user.id],
|
||||
});
|
||||
|
||||
console.log(`User quota: ${auth.user.quotaSizeInBytes}, usage: ${auth.user.quotaUsageInBytes}`);
|
||||
this.requireQuota(auth, file.size);
|
||||
|
||||
console.log(`Asset type: ${file.originalName}, checksum: ${file.checksum}`);
|
||||
if (dto.livePhotoVideoId) {
|
||||
await onBeforeLink(
|
||||
{ asset: this.assetRepository, event: this.eventRepository },
|
||||
{ userId: auth.user.id, livePhotoVideoId: dto.livePhotoVideoId },
|
||||
);
|
||||
}
|
||||
console.log(`Creating asset with deviceAssetId: ${dto.deviceAssetId}, deviceId: ${dto.deviceId}`);
|
||||
const asset = await this.create(auth.user.id, dto, file, sidecarFile);
|
||||
|
||||
console.log(`Asset created with id: ${asset.id}, originalPath: ${asset.originalPath}`);
|
||||
await this.userRepository.updateUsage(auth.user.id, file.size);
|
||||
|
||||
return { id: asset.id, status: AssetMediaStatus.CREATED };
|
||||
} catch (error: any) {
|
||||
console.log(`Error uploading asset: ${error.message}, ${file.originalPath}`, error);
|
||||
return this.handleUploadError(error, auth, file, sidecarFile);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,7 +101,7 @@ const validateRange = (value: number | undefined, min: number, max: number): Non
|
||||
return null;
|
||||
}
|
||||
|
||||
return val;
|
||||
return Math.round(val);
|
||||
};
|
||||
|
||||
const getLensModel = (exifTags: ImmichTags): string | null => {
|
||||
|
||||
@@ -101,6 +101,10 @@ export class StorageService extends BaseService {
|
||||
const savedValue = await this.systemMetadataRepository.get(SystemMetadataKey.MediaLocation);
|
||||
let previous = savedValue?.location || '';
|
||||
|
||||
if (!previous && this.configRepository.getEnv().storage.mediaLocation) {
|
||||
previous = current;
|
||||
}
|
||||
|
||||
if (!previous) {
|
||||
previous = path.startsWith('upload/') ? 'upload' : '/usr/src/app/upload';
|
||||
}
|
||||
|
||||
6
web/package-lock.json
generated
6
web/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "immich-web",
|
||||
"version": "1.137.2",
|
||||
"version": "1.137.3",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "immich-web",
|
||||
"version": "1.137.2",
|
||||
"version": "1.137.3",
|
||||
"license": "GNU Affero General Public License version 3",
|
||||
"dependencies": {
|
||||
"@formatjs/icu-messageformat-parser": "^2.9.8",
|
||||
@@ -94,7 +94,7 @@
|
||||
},
|
||||
"../open-api/typescript-sdk": {
|
||||
"name": "@immich/sdk",
|
||||
"version": "1.137.2",
|
||||
"version": "1.137.3",
|
||||
"license": "GNU Affero General Public License version 3",
|
||||
"dependencies": {
|
||||
"@oazapfts/runtime": "^1.0.2"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "immich-web",
|
||||
"version": "1.137.2",
|
||||
"version": "1.137.3",
|
||||
"license": "GNU Affero General Public License version 3",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -63,8 +63,9 @@
|
||||
touchmoveTwoFingers: false,
|
||||
mousewheelCtrlKey: false,
|
||||
navbar,
|
||||
minFov: 10,
|
||||
maxFov: 120,
|
||||
minFov: 15,
|
||||
maxFov: 90,
|
||||
zoomSpeed: 0.5,
|
||||
fisheye: false,
|
||||
});
|
||||
const resolutionPlugin = viewer.getPlugin(ResolutionPlugin) as ResolutionPlugin;
|
||||
|
||||
@@ -97,7 +97,7 @@
|
||||
{@render children?.()}
|
||||
</div>
|
||||
|
||||
<div class="me-4 flex place-items-center gap-1 justify-self-end">
|
||||
<div class="max-[350px]:me-0 max-[350px]:gap-0 me-4 flex place-items-center gap-1 justify-self-end">
|
||||
{@render trailing?.()}
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
@@ -230,7 +230,8 @@
|
||||
type="text"
|
||||
name="q"
|
||||
id="main-search-bar"
|
||||
class="w-full transition-all border-2 px-14 py-4 max-md:py-2 text-immich-fg/75 dark:text-immich-dark-fg
|
||||
class="w-full transition-all border-2 ps-14 py-4 max-md:py-2 text-immich-fg/75 dark:text-immich-dark-fg
|
||||
{showClearIcon ? 'pe-[90px]' : 'pe-14'}
|
||||
{grayTheme ? 'dark:bg-immich-dark-gray' : 'dark:bg-immich-dark-bg'}
|
||||
{showSuggestions && isSearchSuggestions ? 'rounded-t-3xl' : 'rounded-3xl bg-gray-200'}
|
||||
{searchStore.isSearchEnabled ? 'border-gray-200 dark:border-gray-700 bg-white' : 'border-transparent'}"
|
||||
@@ -285,6 +286,7 @@
|
||||
{#if isFocus}
|
||||
<div
|
||||
class="absolute inset-y-0 flex items-center"
|
||||
class:max-md:hidden={value}
|
||||
class:end-16={isFocus}
|
||||
class:end-28={isFocus && value.length > 0}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user