Merge branch 'main' into feature/hide-album-from-timeline
commit
f17d8c4836
|
|
@ -52,7 +52,7 @@ Password login has been enabled.
|
||||||
Disable Maintenance Mode
|
Disable Maintenance Mode
|
||||||
|
|
||||||
```
|
```
|
||||||
immich-admin disable-maintenace-mode
|
immich-admin disable-maintenance-mode
|
||||||
Maintenance mode has been disabled.
|
Maintenance mode has been disabled.
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -109,7 +109,10 @@ class _AddActionButtonState extends ConsumerState<AddActionButton> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final List<Widget> slivers = [AlbumSelector(onAlbumSelected: (album) => _addCurrentAssetToAlbum(album))];
|
final List<Widget> slivers = [
|
||||||
|
const CreateAlbumButton(),
|
||||||
|
AlbumSelector(onAlbumSelected: (album) => _addCurrentAssetToAlbum(album)),
|
||||||
|
];
|
||||||
|
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
|
|
@ -153,6 +156,9 @@ class _AddActionButtonState extends ConsumerState<AddActionButton> {
|
||||||
context: context,
|
context: context,
|
||||||
msg: 'add_to_album_bottom_sheet_added'.tr(namedArgs: {'album': album.name}),
|
msg: 'add_to_album_bottom_sheet_added'.tr(namedArgs: {'album': album.name}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Invalidate using the asset's remote ID to refresh the "Appears in" list
|
||||||
|
ref.invalidate(albumsContainingAssetProvider(latest.remoteId!));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!context.mounted) {
|
if (!context.mounted) {
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,10 @@ import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
import 'package:immich_mobile/models/albums/album_search.model.dart';
|
import 'package:immich_mobile/models/albums/album_search.model.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/album/album_tile.dart';
|
import 'package:immich_mobile/presentation/widgets/album/album_tile.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/album/new_album_name_modal.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
|
||||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||||
import 'package:immich_mobile/providers/user.provider.dart';
|
import 'package:immich_mobile/providers/user.provider.dart';
|
||||||
import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart';
|
import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart';
|
||||||
|
|
@ -766,3 +768,68 @@ class AddToAlbumHeader extends ConsumerWidget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class CreateAlbumButton extends ConsumerWidget {
|
||||||
|
const CreateAlbumButton({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
Future<void> onCreateAlbum() async {
|
||||||
|
var albumName = await showDialog<String?>(context: context, builder: (context) => const NewAlbumNameModal());
|
||||||
|
if (albumName == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final asset = ref.read(currentAssetNotifier);
|
||||||
|
|
||||||
|
if (asset == null) {
|
||||||
|
ImmichToast.show(context: context, msg: "Cannot load asset information.", toastType: ToastType.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final album = await ref
|
||||||
|
.read(remoteAlbumProvider.notifier)
|
||||||
|
.createAlbum(title: albumName, assetIds: [asset.remoteId!]);
|
||||||
|
|
||||||
|
if (album == null) {
|
||||||
|
ImmichToast.show(context: context, toastType: ToastType.error, msg: 'errors.failed_to_create_album'.tr());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImmichToast.show(
|
||||||
|
context: context,
|
||||||
|
msg: 'add_to_album_bottom_sheet_added'.tr(namedArgs: {'album': album.name}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Invalidate using the asset's remote ID to refresh the "Appears in" list
|
||||||
|
ref.invalidate(albumsContainingAssetProvider(asset.remoteId!));
|
||||||
|
|
||||||
|
context.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
return SliverPadding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
sliver: SliverToBoxAdapter(
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text("add_to_album", style: context.textTheme.titleSmall).tr(),
|
||||||
|
TextButton.icon(
|
||||||
|
style: TextButton.styleFrom(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
|
minimumSize: const Size(0, 0),
|
||||||
|
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||||
|
),
|
||||||
|
onPressed: onCreateAlbum,
|
||||||
|
icon: Icon(Icons.add, color: context.primaryColor),
|
||||||
|
label: Text(
|
||||||
|
"common_create_new_album",
|
||||||
|
style: TextStyle(color: context.primaryColor, fontWeight: FontWeight.bold, fontSize: 14),
|
||||||
|
).tr(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
|
||||||
|
class NewAlbumNameModal extends StatefulWidget {
|
||||||
|
const NewAlbumNameModal({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<NewAlbumNameModal> createState() => _NewAlbumNameModalState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _NewAlbumNameModalState extends State<NewAlbumNameModal> {
|
||||||
|
TextEditingController nameController = TextEditingController();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
nameController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: const Text("album_name", style: TextStyle(fontWeight: FontWeight.bold)).tr(),
|
||||||
|
content: SingleChildScrollView(
|
||||||
|
child: TextFormField(
|
||||||
|
controller: nameController,
|
||||||
|
textCapitalization: TextCapitalization.words,
|
||||||
|
autofocus: true,
|
||||||
|
decoration: InputDecoration(hintText: 'name'.tr(), border: const OutlineInputBorder()),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => context.pop(null),
|
||||||
|
child: Text(
|
||||||
|
"cancel",
|
||||||
|
style: TextStyle(color: Colors.red[300], fontWeight: FontWeight.bold),
|
||||||
|
).tr(),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
context.pop(nameController.text.trim());
|
||||||
|
},
|
||||||
|
child: Text(
|
||||||
|
"create_album",
|
||||||
|
style: TextStyle(color: context.primaryColor, fontWeight: FontWeight.bold),
|
||||||
|
).tr(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -9,6 +9,7 @@ import 'package:immich_mobile/domain/models/setting.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||||
import 'package:immich_mobile/domain/services/setting.service.dart';
|
import 'package:immich_mobile/domain/services/setting.service.dart';
|
||||||
import 'package:immich_mobile/entities/store.entity.dart';
|
import 'package:immich_mobile/entities/store.entity.dart';
|
||||||
|
import 'package:immich_mobile/extensions/platform_extensions.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart';
|
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/asset_viewer/video_viewer_controls.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/asset_viewer/video_viewer_controls.widget.dart';
|
||||||
|
|
@ -104,7 +105,12 @@ class NativeVideoViewer extends HookConsumerWidget {
|
||||||
throw Exception('No file found for the video');
|
throw Exception('No file found for the video');
|
||||||
}
|
}
|
||||||
|
|
||||||
final source = await VideoSource.init(path: file.path, type: VideoSourceType.file);
|
// Pass a file:// URI so Android's Uri.parse doesn't
|
||||||
|
// interpret characters like '#' as fragment identifiers.
|
||||||
|
final source = await VideoSource.init(
|
||||||
|
path: CurrentPlatform.isAndroid ? file.uri.toString() : file.path,
|
||||||
|
type: VideoSourceType.file,
|
||||||
|
);
|
||||||
return source;
|
return source;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,10 @@ class PersonApiRepository extends ApiRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<PersonDto> update(String id, {String? name, DateTime? birthday}) async {
|
Future<PersonDto> update(String id, {String? name, DateTime? birthday}) async {
|
||||||
final dto = await checkNull(_api.updatePerson(id, PersonUpdateDto(name: name, birthDate: birthday)));
|
final birthdayUtc = birthday == null ? null : DateTime.utc(birthday.year, birthday.month, birthday.day);
|
||||||
return _toPerson(dto);
|
final dto = PersonUpdateDto(name: name, birthDate: birthdayUtc);
|
||||||
|
final response = await checkNull(_api.updatePerson(id, dto));
|
||||||
|
return _toPerson(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
static PersonDto _toPerson(PersonResponseDto dto) => PersonDto(
|
static PersonDto _toPerson(PersonResponseDto dto) => PersonDto(
|
||||||
|
|
|
||||||
|
|
@ -89,6 +89,7 @@ export class MetadataRepository {
|
||||||
// Enable exiftool LFS to parse metadata for files larger than 2GB.
|
// Enable exiftool LFS to parse metadata for files larger than 2GB.
|
||||||
readArgs: ['-api', 'largefilesupport=1'],
|
readArgs: ['-api', 'largefilesupport=1'],
|
||||||
writeArgs: ['-api', 'largefilesupport=1', '-overwrite_original'],
|
writeArgs: ['-api', 'largefilesupport=1', '-overwrite_original'],
|
||||||
|
taskTimeoutMillis: 2 * 60 * 1000,
|
||||||
});
|
});
|
||||||
|
|
||||||
constructor(private logger: LoggingRepository) {
|
constructor(private logger: LoggingRepository) {
|
||||||
|
|
|
||||||
|
|
@ -114,7 +114,11 @@
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await modalManager.show(AssetChangeDateModal, { asset: toTimelineAsset(asset), initialDate: dateTime });
|
await modalManager.show(AssetChangeDateModal, {
|
||||||
|
asset: toTimelineAsset(asset),
|
||||||
|
initialDate: dateTime,
|
||||||
|
initialTimeZone: timeZone,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,9 +15,24 @@
|
||||||
import { joinPaths, TreeNode } from '$lib/utils/tree-utils';
|
import { joinPaths, TreeNode } from '$lib/utils/tree-utils';
|
||||||
import { deleteTag, getAllTags, type TagResponseDto } from '@immich/sdk';
|
import { deleteTag, getAllTags, type TagResponseDto } from '@immich/sdk';
|
||||||
import { Button, HStack, modalManager, Text } from '@immich/ui';
|
import { Button, HStack, modalManager, Text } from '@immich/ui';
|
||||||
import { mdiPencil, mdiPlus, mdiTag, mdiTagMultiple, mdiTrashCanOutline } from '@mdi/js';
|
import { mdiDotsVertical, mdiPencil, mdiPlus, mdiTag, mdiTagMultiple, mdiTrashCanOutline } from '@mdi/js';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
import type { PageData } from './$types';
|
import type { PageData } from './$types';
|
||||||
|
import AssetSelectControlBar from '$lib/components/timeline/AssetSelectControlBar.svelte';
|
||||||
|
import AddToAlbum from '$lib/components/timeline/actions/AddToAlbumAction.svelte';
|
||||||
|
import ArchiveAction from '$lib/components/timeline/actions/ArchiveAction.svelte';
|
||||||
|
import ChangeDate from '$lib/components/timeline/actions/ChangeDateAction.svelte';
|
||||||
|
import ChangeDescription from '$lib/components/timeline/actions/ChangeDescriptionAction.svelte';
|
||||||
|
import ChangeLocation from '$lib/components/timeline/actions/ChangeLocationAction.svelte';
|
||||||
|
import CreateSharedLink from '$lib/components/timeline/actions/CreateSharedLinkAction.svelte';
|
||||||
|
import DeleteAssets from '$lib/components/timeline/actions/DeleteAssetsAction.svelte';
|
||||||
|
import DownloadAction from '$lib/components/timeline/actions/DownloadAction.svelte';
|
||||||
|
import FavoriteAction from '$lib/components/timeline/actions/FavoriteAction.svelte';
|
||||||
|
import SelectAllAssets from '$lib/components/timeline/actions/SelectAllAction.svelte';
|
||||||
|
import SetVisibilityAction from '$lib/components/timeline/actions/SetVisibilityAction.svelte';
|
||||||
|
import TagAction from '$lib/components/timeline/actions/TagAction.svelte';
|
||||||
|
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
|
||||||
|
import { preferences, user } from '$lib/stores/user.store';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
data: PageData;
|
data: PageData;
|
||||||
|
|
@ -79,6 +94,11 @@
|
||||||
// navigate to parent
|
// navigate to parent
|
||||||
await navigateToView(tag.parent ? tag.parent.path : '');
|
await navigateToView(tag.parent ? tag.parent.path : '');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleSetVisibility = (assetIds: string[]) => {
|
||||||
|
timelineManager.removeAssets(assetIds);
|
||||||
|
assetInteraction.clearMultiselect();
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<UserPageLayout title={data.meta.title}>
|
<UserPageLayout title={data.meta.title}>
|
||||||
|
|
@ -131,3 +151,45 @@
|
||||||
{/if}
|
{/if}
|
||||||
</section>
|
</section>
|
||||||
</UserPageLayout>
|
</UserPageLayout>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
{#if assetInteraction.selectionActive}
|
||||||
|
<div class="fixed top-0 start-0 w-full">
|
||||||
|
<AssetSelectControlBar
|
||||||
|
ownerId={$user.id}
|
||||||
|
assets={assetInteraction.selectedAssets}
|
||||||
|
clearSelect={() => assetInteraction.clearMultiselect()}
|
||||||
|
>
|
||||||
|
<CreateSharedLink />
|
||||||
|
<SelectAllAssets {timelineManager} {assetInteraction} />
|
||||||
|
<ButtonContextMenu icon={mdiPlus} title={$t('add_to')}>
|
||||||
|
<AddToAlbum />
|
||||||
|
<AddToAlbum shared />
|
||||||
|
</ButtonContextMenu>
|
||||||
|
<FavoriteAction
|
||||||
|
removeFavorite={assetInteraction.isAllFavorite}
|
||||||
|
onFavorite={(ids, isFavorite) => timelineManager.update(ids, (asset) => (asset.isFavorite = isFavorite))}
|
||||||
|
></FavoriteAction>
|
||||||
|
<ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')}>
|
||||||
|
<DownloadAction menuItem />
|
||||||
|
<ChangeDate menuItem />
|
||||||
|
<ChangeDescription menuItem />
|
||||||
|
<ChangeLocation menuItem />
|
||||||
|
<ArchiveAction
|
||||||
|
menuItem
|
||||||
|
onArchive={(ids, visibility) => timelineManager.update(ids, (asset) => (asset.visibility = visibility))}
|
||||||
|
/>
|
||||||
|
{#if $preferences.tags.enabled}
|
||||||
|
<TagAction menuItem />
|
||||||
|
{/if}
|
||||||
|
<DeleteAssets
|
||||||
|
menuItem
|
||||||
|
onAssetDelete={(assetIds) => timelineManager.removeAssets(assetIds)}
|
||||||
|
onUndoDelete={(assets) => timelineManager.upsertAssets(assets)}
|
||||||
|
/>
|
||||||
|
<SetVisibilityAction menuItem onVisibilitySet={handleSetVisibility} />
|
||||||
|
</ButtonContextMenu>
|
||||||
|
</AssetSelectControlBar>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</section>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue