0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2025-01-21 00:52:43 -05:00

Merge branch 'main' into main

This commit is contained in:
Chuckame 2024-12-27 21:48:36 +01:00 committed by GitHub
commit 246453806d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 81 additions and 44 deletions

View file

@ -127,18 +127,29 @@ class GalleryViewerPage extends HookConsumerWidget {
context: context,
useSafeArea: true,
builder: (context) {
return FractionallySizedBox(
heightFactor: 0.75,
child: Padding(
padding: EdgeInsets.only(
bottom: context.viewInsets.bottom,
),
child: ref
.watch(appSettingsServiceProvider)
.getSetting<bool>(AppSettingsEnum.advancedTroubleshooting)
? AdvancedBottomSheet(assetDetail: asset)
: DetailPanel(asset: asset),
),
return DraggableScrollableSheet(
minChildSize: 0.5,
maxChildSize: 1,
initialChildSize: 0.75,
expand: false,
builder: (context, scrollController) {
return Padding(
padding: EdgeInsets.only(
bottom: context.viewInsets.bottom,
),
child: ref.watch(appSettingsServiceProvider).getSetting<bool>(
AppSettingsEnum.advancedTroubleshooting,
)
? AdvancedBottomSheet(
assetDetail: asset,
scrollController: scrollController,
)
: DetailPanel(
asset: asset,
scrollController: scrollController,
),
);
},
);
},
);

View file

@ -372,7 +372,6 @@ class BackgroundService {
HttpOverrides.global = HttpSSLCertOverride();
ApiService apiService = ApiService();
apiService.setAccessToken(Store.get(StoreKey.accessToken));
AppSettingsService settingService = AppSettingsService();
AppSettingsService settingsService = AppSettingsService();
AlbumRepository albumRepository = AlbumRepository(db);
AssetRepository assetRepository = AssetRepository(db);
@ -422,7 +421,7 @@ class BackgroundService {
);
BackupService backupService = BackupService(
apiService,
settingService,
settingsService,
albumService,
albumMediaRepository,
fileMediaRepository,

View file

@ -6,12 +6,18 @@ import 'package:immich_mobile/entities/asset.entity.dart';
class AdvancedBottomSheet extends HookConsumerWidget {
final Asset assetDetail;
final ScrollController? scrollController;
const AdvancedBottomSheet({super.key, required this.assetDetail});
const AdvancedBottomSheet({
super.key,
required this.assetDetail,
this.scrollController,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
return SingleChildScrollView(
controller: scrollController,
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 8.0),
child: LayoutBuilder(

View file

@ -9,12 +9,14 @@ import 'package:immich_mobile/entities/asset.entity.dart';
class DetailPanel extends HookConsumerWidget {
final Asset asset;
final ScrollController? scrollController;
const DetailPanel({super.key, required this.asset});
const DetailPanel({super.key, required this.asset, this.scrollController});
@override
Widget build(BuildContext context, WidgetRef ref) {
return ListView(
controller: scrollController,
shrinkWrap: true,
children: [
Padding(

View file

@ -18,7 +18,7 @@ class FileInfo extends StatelessWidget {
final height = asset.orientatedHeight ?? asset.height;
final width = asset.orientatedWidth ?? asset.width;
String resolution =
height != null && width != null ? "$height x $width " : "";
height != null && width != null ? "$width x $height " : "";
String fileSize = asset.exifInfo?.fileSize != null
? formatBytes(asset.exifInfo!.fileSize!)
: "";

View file

@ -1,5 +1,5 @@
# dev build
FROM ghcr.io/immich-app/base-server-dev:20241217@sha256:7e69fa317cf90a0345927bbea13438dc39efc584bac13ff77ea5735c57cd008a AS dev
FROM ghcr.io/immich-app/base-server-dev:20241224@sha256:6832c632c2a8cba5e20053ab694c9a8080e621841c784ed5d4675ef9dd203588 AS dev
RUN apt-get install --no-install-recommends -yqq tini
WORKDIR /usr/src/app
@ -42,7 +42,7 @@ RUN npm run build
# prod build
FROM ghcr.io/immich-app/base-server-prod:20241217@sha256:040c83a6d3e45755419837747fa70fa68cf92433d483c116a971b3400bb8415d
FROM ghcr.io/immich-app/base-server-prod:20241224@sha256:69da007c241a961d6927d3d03f1c83ef0ec5c70bf656bff3ced32546a777e6f6
WORKDIR /usr/src/app
ENV NODE_ENV=production \

View file

@ -204,7 +204,7 @@ describe('getEnv', () => {
it('should return default network options', () => {
const { network } = getEnv();
expect(network).toEqual({
trustedProxies: [],
trustedProxies: ['linklocal', 'uniquelocal'],
});
});

View file

@ -177,7 +177,7 @@ const getEnv = (): EnvData => {
licensePublicKey: isProd ? productionKeys : stagingKeys,
network: {
trustedProxies: dto.IMMICH_TRUSTED_PROXIES ?? [],
trustedProxies: dto.IMMICH_TRUSTED_PROXIES ?? ['linklocal', 'uniquelocal'],
},
otel: {

View file

@ -32,7 +32,7 @@ async function bootstrap() {
logger.setContext('Bootstrap');
app.useLogger(logger);
app.set('trust proxy', ['loopback', 'linklocal', 'uniquelocal', ...network.trustedProxies]);
app.set('trust proxy', ['loopback', ...network.trustedProxies]);
app.set('etag', 'strong');
app.use(cookieParser());
app.use(json({ limit: '10mb' }));

View file

@ -5,10 +5,8 @@
import Combobox, { type ComboBoxOption } from '../shared-components/combobox.svelte';
import FullScreenModal from '../shared-components/full-screen-modal.svelte';
import { onMount } from 'svelte';
import { getAllTags, type TagResponseDto } from '@immich/sdk';
import { getAllTags, upsertTags, type TagResponseDto } from '@immich/sdk';
import Icon from '$lib/components/elements/icon.svelte';
import { AppRoute } from '$lib/constants';
import FormatMessage from '$lib/components/i18n/format-message.svelte';
import { SvelteSet } from 'svelte/reactivity';
interface Props {
@ -22,6 +20,7 @@
let tagMap = $derived(Object.fromEntries(allTags.map((tag) => [tag.id, tag])));
let selectedIds = $state(new SvelteSet<string>());
let disabled = $derived(selectedIds.size === 0);
let allowCreate: boolean = $state(true);
onMount(async () => {
allTags = await getAllTags();
@ -29,12 +28,18 @@
const handleSubmit = () => onTag([...selectedIds]);
const handleSelect = (option?: ComboBoxOption) => {
const handleSelect = async (option?: ComboBoxOption) => {
if (!option) {
return;
}
selectedIds.add(option.value);
if (option.id) {
selectedIds.add(option.value);
} else {
const [newTag] = await upsertTags({ tagUpsertDto: { tags: [option.label] } });
allTags.push(newTag);
selectedIds.add(newTag.id);
}
};
const handleRemove = (tag: string) => {
@ -48,22 +53,13 @@
</script>
<FullScreenModal title={$t('tag_assets')} icon={mdiTag} onClose={onCancel}>
<div class="text-sm">
<p>
<FormatMessage key="tag_not_found_question">
{#snippet children({ message })}
<a href={AppRoute.TAGS} class="text-immich-primary dark:text-immich-dark-primary underline">
{message}
</a>
{/snippet}
</FormatMessage>
</p>
</div>
<form {onsubmit} autocomplete="off" id="create-tag-form">
<div class="my-4 flex flex-col gap-2">
<Combobox
onSelect={handleSelect}
label={$t('tag')}
{allowCreate}
defaultFirstOption
options={allTags.map((tag) => ({ id: tag.id, label: tag.value, value: tag.id }))}
placeholder={$t('search_tags')}
/>

View file

@ -36,6 +36,14 @@
options?: ComboBoxOption[];
selectedOption?: ComboBoxOption | undefined;
placeholder?: string;
/**
* whether creating new items is allowed.
*/
allowCreate?: boolean;
/**
* select first matching option on enter key.
*/
defaultFirstOption?: boolean;
onSelect?: (option: ComboBoxOption | undefined) => void;
}
@ -45,6 +53,8 @@
options = [],
selectedOption = $bindable(),
placeholder = '',
allowCreate = false,
defaultFirstOption = false,
onSelect = () => {},
}: Props = $props();
@ -141,7 +151,7 @@
const onInput: FormEventHandler<HTMLInputElement> = (event) => {
openDropdown();
searchQuery = event.currentTarget.value;
selectedIndex = undefined;
selectedIndex = defaultFirstOption ? 0 : undefined;
optionRefs[0]?.scrollIntoView({ block: 'nearest' });
};
@ -221,9 +231,15 @@
searchQuery = selectedOption ? selectedOption.label : '';
});
let filteredOptions = $derived(
options.filter((option) => option.label.toLowerCase().includes(searchQuery.toLowerCase())),
);
let filteredOptions = $derived.by(() => {
const _options = options.filter((option) => option.label.toLowerCase().includes(searchQuery.toLowerCase()));
if (allowCreate && searchQuery !== '' && _options.filter((option) => option.label === searchQuery).length === 0) {
_options.unshift({ label: searchQuery, value: searchQuery });
}
return _options;
});
let position = $derived(calculatePosition(bounds));
let dropdownDirection: 'bottom' | 'top' = $derived(getComboboxDirection(bounds, visualViewport));
</script>
@ -352,7 +368,7 @@
id={`${listboxId}-${0}`}
onclick={() => closeDropdown()}
>
{$t('no_results')}
{allowCreate ? searchQuery : $t('no_results')}
</li>
{/if}
{#each filteredOptions as option, index (option.id || option.label)}

View file

@ -398,7 +398,9 @@ export class AssetStore {
}
async updateOptions(options: AssetStoreOptions) {
if (!this.initialized) {
// Make sure to re-initialize if the personId changes
const needsReinitializing = this.options.personId !== options.personId;
if (!this.initialized && !needsReinitializing) {
this.setOptions(options);
return;
}

View file

@ -74,8 +74,13 @@
const assetStore = new AssetStore(assetStoreOptions);
$effect(() => {
// Check to trigger rebuild the timeline when navigating between people from the info panel
const change = assetStoreOptions.personId !== data.person.id;
assetStoreOptions.personId = data.person.id;
handlePromiseError(assetStore.updateOptions(assetStoreOptions));
if (change) {
assetStore.triggerUpdate();
}
});
const assetInteraction = new AssetInteraction();