mirror of
https://github.com/immich-app/immich.git
synced 2026-06-04 13:15:22 -04:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c314d721ce |
@@ -0,0 +1 @@
|
||||
custom: ['https://buy.immich.app', 'https://immich.store']
|
||||
@@ -0,0 +1,134 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation
|
||||
in our community a harassment-free experience for everyone, regardless
|
||||
of age, body size, visible or invisible disability, ethnicity, sex
|
||||
characteristics, gender identity and expression, level of experience,
|
||||
education, socio-economic status, nationality, personal appearance,
|
||||
race, religion, or sexual identity and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open,
|
||||
welcoming, diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for
|
||||
our community include:
|
||||
|
||||
- Demonstrating empathy and kindness toward other people
|
||||
- Being respectful of differing opinions, viewpoints, and experiences
|
||||
- Giving and gracefully accepting constructive feedback
|
||||
- Accepting responsibility and apologizing to those affected by our
|
||||
mistakes, and learning from the experience
|
||||
- Focusing on what is best not just for us as individuals, but for the
|
||||
overall community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
- The use of sexualized language or imagery, and sexual attention or
|
||||
advances of any kind
|
||||
- Trolling, insulting or derogatory comments, and personal or
|
||||
political attacks
|
||||
- Public or private harassment
|
||||
- Publishing others' private information, such as a physical or email
|
||||
address, without their explicit permission
|
||||
- Other conduct which could reasonably be considered inappropriate in
|
||||
a professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our
|
||||
standards of acceptable behavior and will take appropriate and fair
|
||||
corrective action in response to any behavior that they deem
|
||||
inappropriate, threatening, offensive, or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit,
|
||||
or reject comments, commits, code, wiki edits, issues, and other
|
||||
contributions that are not aligned to this Code of Conduct, and will
|
||||
communicate reasons for moderation decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also
|
||||
applies when an individual is officially representing the community in
|
||||
public spaces. Examples of representing our community include using an
|
||||
official e-mail address, posting via an official social media account,
|
||||
or acting as an appointed representative at an online or offline
|
||||
event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior
|
||||
may be reported to the community leaders responsible for enforcement
|
||||
at our Discord channel. All complaints
|
||||
will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and
|
||||
security of the reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in
|
||||
determining the consequences for any action they deem in violation of
|
||||
this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior
|
||||
deemed unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders,
|
||||
providing clarity around the nature of the violation and an
|
||||
explanation of why the behavior was inappropriate. A public apology
|
||||
may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series
|
||||
of actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued
|
||||
behavior. No interaction with the people involved, including
|
||||
unsolicited interaction with those enforcing the Code of Conduct, for
|
||||
a specified period of time. This includes avoiding interactions in
|
||||
community spaces as well as external channels like social
|
||||
media. Violating these terms may lead to a temporary or permanent ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards,
|
||||
including sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or
|
||||
public communication with the community for a specified period of
|
||||
time. No public or private interaction with the people involved,
|
||||
including unsolicited interaction with those enforcing the Code of
|
||||
Conduct, is allowed during this period. Violating these terms may lead
|
||||
to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of
|
||||
community standards, including sustained inappropriate behavior,
|
||||
harassment of an individual, or aggression toward or disparagement of
|
||||
classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction
|
||||
within the community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor
|
||||
Covenant][homepage], version 2.0, available at
|
||||
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||
|
||||
Community Impact Guidelines were inspired by [Mozilla's code of
|
||||
conduct enforcement ladder](https://github.com/mozilla/diversity).
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see the
|
||||
FAQ at https://www.contributor-covenant.org/faq. Translations are
|
||||
available at https://www.contributor-covenant.org/translations.
|
||||
@@ -0,0 +1,5 @@
|
||||
# Security Policy
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Please report security issues to `security@immich.app`
|
||||
@@ -1,8 +1,8 @@
|
||||
ARG DEVICE=cpu
|
||||
|
||||
FROM python:3.11-bookworm@sha256:121d86b6d08752968a7dddbc708849e5f3a839bbff47f32212b46d2a1d842bab AS builder-cpu
|
||||
FROM python:3.11-bookworm@sha256:970c99f886b839fc8829289040c1845dadaf2cae46b37acc7710333158ec29b4 AS builder-cpu
|
||||
|
||||
FROM python:3.13-slim-trixie@sha256:b04b5d7233d2ad9c379e22ea8927cd1378cd15c60d4ef876c065b25ea8fb3bf3 AS builder-openvino
|
||||
FROM python:3.13-slim-trixie@sha256:d168b8d9eb761f4d3fe305ebd04aeb7e7f2de0297cec5fb2f8f6403244621664 AS builder-openvino
|
||||
|
||||
FROM builder-cpu AS builder-cuda
|
||||
|
||||
@@ -39,12 +39,12 @@ RUN --mount=type=cache,target=/root/.cache/uv \
|
||||
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
|
||||
uv sync --frozen --extra ${DEVICE} --no-dev --no-editable --no-install-project --compile-bytecode --no-progress --active --link-mode copy
|
||||
|
||||
FROM python:3.11-slim-bookworm@sha256:8dca233de9f3d9bb410665f00a4da6dd06f331083137e0e98ccf227236fcc438 AS prod-cpu
|
||||
FROM python:3.11-slim-bookworm@sha256:9c6f90801e6b68e772b7c0ca74260cbf7af9f320acec894e26fccdaccfbe3b47 AS prod-cpu
|
||||
|
||||
ENV LD_PRELOAD=/usr/lib/libmimalloc.so.2 \
|
||||
MACHINE_LEARNING_MODEL_ARENA=false
|
||||
|
||||
FROM python:3.13-slim-trixie@sha256:b04b5d7233d2ad9c379e22ea8927cd1378cd15c60d4ef876c065b25ea8fb3bf3 AS prod-openvino
|
||||
FROM python:3.13-slim-trixie@sha256:d168b8d9eb761f4d3fe305ebd04aeb7e7f2de0297cec5fb2f8f6403244621664 AS prod-openvino
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install --no-install-recommends -yqq ocl-icd-libopencl1 wget && \
|
||||
|
||||
@@ -49,7 +49,6 @@ try:
|
||||
str(settings.http_keepalive_timeout_s),
|
||||
"--graceful-timeout",
|
||||
"10",
|
||||
"--no-control-socket",
|
||||
],
|
||||
) as cmd:
|
||||
cmd.wait()
|
||||
|
||||
@@ -12,7 +12,7 @@ from zipfile import BadZipFile
|
||||
|
||||
import orjson
|
||||
from fastapi import Depends, FastAPI, File, Form, HTTPException
|
||||
from fastapi.responses import PlainTextResponse
|
||||
from fastapi.responses import ORJSONResponse, PlainTextResponse
|
||||
from onnxruntime.capi.onnxruntime_pybind11_state import InvalidProtobuf, NoSuchFile
|
||||
from PIL.Image import Image
|
||||
from pydantic import ValidationError
|
||||
@@ -32,7 +32,6 @@ from .schemas import (
|
||||
ModelIdentity,
|
||||
ModelTask,
|
||||
ModelType,
|
||||
ORJSONResponse,
|
||||
PipelineRequest,
|
||||
T,
|
||||
)
|
||||
|
||||
@@ -89,9 +89,7 @@ class OpenClipTextualEncoder(BaseCLIPTextualEncoder):
|
||||
|
||||
tokenizer: Tokenizer = Tokenizer.from_file(self.tokenizer_file_path.as_posix())
|
||||
|
||||
pad_id = tokenizer.token_to_id(pad_token)
|
||||
if pad_id is None:
|
||||
raise ValueError(f"Pad token '{pad_token}' not found in tokenizer vocab")
|
||||
pad_id: int = tokenizer.token_to_id(pad_token)
|
||||
tokenizer.enable_padding(length=context_length, pad_token=pad_token, pad_id=pad_id)
|
||||
tokenizer.enable_truncation(max_length=context_length)
|
||||
|
||||
|
||||
@@ -3,16 +3,9 @@ from typing import Any, Literal, Protocol, TypeGuard, TypeVar
|
||||
|
||||
import numpy as np
|
||||
import numpy.typing as npt
|
||||
import orjson
|
||||
from fastapi.responses import JSONResponse
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
|
||||
class ORJSONResponse(JSONResponse):
|
||||
def render(self, content: Any) -> bytes:
|
||||
return orjson.dumps(content, option=orjson.OPT_SERIALIZE_NUMPY)
|
||||
|
||||
|
||||
class StrEnum(str, Enum):
|
||||
value: str
|
||||
|
||||
|
||||
Generated
+450
-498
File diff suppressed because it is too large
Load Diff
@@ -1,30 +1,30 @@
|
||||
# @generated - this file is auto-generated by `mise lock` https://mise.en.dev/dev-tools/mise-lock.html
|
||||
|
||||
[[tools."aqua:flutter/flutter"]]
|
||||
version = "3.44.1"
|
||||
version = "3.44.0"
|
||||
backend = "aqua:flutter/flutter"
|
||||
|
||||
[tools."aqua:flutter/flutter"."platforms.linux-arm64"]
|
||||
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.44.1-stable.tar.xz"
|
||||
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.44.0-stable.tar.xz"
|
||||
|
||||
[tools."aqua:flutter/flutter"."platforms.linux-arm64-musl"]
|
||||
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.44.1-stable.tar.xz"
|
||||
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.44.0-stable.tar.xz"
|
||||
|
||||
[tools."aqua:flutter/flutter"."platforms.linux-x64"]
|
||||
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.44.1-stable.tar.xz"
|
||||
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.44.0-stable.tar.xz"
|
||||
|
||||
[tools."aqua:flutter/flutter"."platforms.linux-x64-musl"]
|
||||
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.44.1-stable.tar.xz"
|
||||
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.44.0-stable.tar.xz"
|
||||
|
||||
[tools."aqua:flutter/flutter"."platforms.macos-arm64"]
|
||||
checksum = "blake3:15069c982a30ca0189a83edb5627b69d91485ad94fb74d2de8585b43364e9e8e"
|
||||
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/macos/flutter_macos_arm64_3.44.1-stable.zip"
|
||||
checksum = "blake3:fb03aa5d9790205c948922ec3f0751c16e4575b09d6ae9dd4fbeb664a69f0e00"
|
||||
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/macos/flutter_macos_arm64_3.44.0-stable.zip"
|
||||
|
||||
[tools."aqua:flutter/flutter"."platforms.macos-x64"]
|
||||
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/macos/flutter_macos_3.44.1-stable.zip"
|
||||
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/macos/flutter_macos_3.44.0-stable.zip"
|
||||
|
||||
[tools."aqua:flutter/flutter"."platforms.windows-x64"]
|
||||
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/windows/flutter_windows_3.44.1-stable.zip"
|
||||
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/windows/flutter_windows_3.44.0-stable.zip"
|
||||
|
||||
[[tools.flutter]]
|
||||
version = "3.41.9-stable"
|
||||
|
||||
@@ -16,7 +16,7 @@ config_roots = [
|
||||
|
||||
[tools]
|
||||
node = "24.15.0"
|
||||
"aqua:flutter/flutter" = "3.44.1"
|
||||
"aqua:flutter/flutter" = "3.44.0"
|
||||
pnpm = "10.33.4"
|
||||
terragrunt = "1.0.3"
|
||||
opentofu = "1.11.6"
|
||||
|
||||
@@ -13,10 +13,6 @@ class DriftMemoryService {
|
||||
return _repository.getAll(ownerId);
|
||||
}
|
||||
|
||||
Future<List<DriftMemory>> getAllMemories(String ownerId) {
|
||||
return _repository.getAll(ownerId, onlyToday: false);
|
||||
}
|
||||
|
||||
Future<DriftMemory?> get(String memoryId) {
|
||||
return _repository.get(memoryId);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,10 @@ class DriftMemoryRepository extends DriftDatabaseRepository {
|
||||
final Drift _db;
|
||||
const DriftMemoryRepository(this._db) : super(_db);
|
||||
|
||||
Future<List<DriftMemory>> getAll(String ownerId, {bool onlyToday = true}) async {
|
||||
Future<List<DriftMemory>> getAll(String ownerId) async {
|
||||
final now = DateTime.now();
|
||||
final localUtc = DateTime.utc(now.year, now.month, now.day, 0, 0, 0);
|
||||
|
||||
final query =
|
||||
_db.select(_db.memoryEntity).join([
|
||||
innerJoin(_db.memoryAssetEntity, _db.memoryAssetEntity.memoryId.equalsExp(_db.memoryEntity.id)),
|
||||
@@ -21,17 +24,10 @@ class DriftMemoryRepository extends DriftDatabaseRepository {
|
||||
),
|
||||
])
|
||||
..where(_db.memoryEntity.ownerId.equals(ownerId))
|
||||
..where(_db.memoryEntity.deletedAt.isNull());
|
||||
|
||||
if (onlyToday) {
|
||||
final now = DateTime.now();
|
||||
final localUtc = DateTime.utc(now.year, now.month, now.day, 0, 0, 0);
|
||||
|
||||
query.where(_db.memoryEntity.showAt.isSmallerOrEqualValue(localUtc));
|
||||
query.where(_db.memoryEntity.hideAt.isBiggerOrEqualValue(localUtc));
|
||||
}
|
||||
|
||||
query.orderBy([OrderingTerm.desc(_db.memoryEntity.memoryAt), OrderingTerm.asc(_db.remoteAssetEntity.createdAt)]);
|
||||
..where(_db.memoryEntity.deletedAt.isNull())
|
||||
..where(_db.memoryEntity.showAt.isSmallerOrEqualValue(localUtc))
|
||||
..where(_db.memoryEntity.hideAt.isBiggerOrEqualValue(localUtc))
|
||||
..orderBy([OrderingTerm.desc(_db.memoryEntity.memoryAt), OrderingTerm.asc(_db.remoteAssetEntity.createdAt)]);
|
||||
|
||||
final rows = await query.get();
|
||||
if (rows.isEmpty) {
|
||||
|
||||
@@ -112,7 +112,7 @@ void _onNavigationSelected(TabsRouter router, int index, WidgetRef ref) {
|
||||
}
|
||||
|
||||
if (index == kPhotoTabIndex) {
|
||||
ref.invalidate(driftMemoryLaneProvider);
|
||||
ref.invalidate(driftMemoryFutureProvider);
|
||||
}
|
||||
|
||||
if (router.activeIndex != kSearchTabIndex && index == kSearchTabIndex) {
|
||||
|
||||
@@ -11,7 +11,7 @@ class MainTimelinePage extends ConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final hasMemories = ref.watch(driftMemoryLaneProvider.select((state) => state.value?.isNotEmpty ?? false));
|
||||
final hasMemories = ref.watch(driftMemoryFutureProvider.select((state) => state.value?.isNotEmpty ?? false));
|
||||
return Timeline(
|
||||
topSliverWidget: const SliverToBoxAdapter(child: DriftMemoryLane()),
|
||||
topSliverWidgetHeight: hasMemories ? 200 : 0,
|
||||
|
||||
@@ -7,10 +7,8 @@ import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/images/local_album_thumbnail.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/people/partner_user_avatar.widget.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/memory.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/partner.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/people.provider.dart';
|
||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||
@@ -135,12 +133,7 @@ class _CollectionCards extends StatelessWidget {
|
||||
child: Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
_PeopleCollectionCard(),
|
||||
_PlacesCollectionCard(),
|
||||
_LocalAlbumsCollectionCard(),
|
||||
_MemoriesCollectionCard(),
|
||||
],
|
||||
children: [_PeopleCollectionCard(), _PlacesCollectionCard(), _LocalAlbumsCollectionCard()],
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -334,76 +327,6 @@ class _LocalAlbumsCollectionCard extends ConsumerWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _MemoriesCollectionCard extends ConsumerWidget {
|
||||
const _MemoriesCollectionCard();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final memories = ref.watch(driftAllMemoriesProvider);
|
||||
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final isTablet = constraints.maxWidth > 600;
|
||||
final widthFactor = isTablet ? 0.25 : 0.5;
|
||||
final size = context.width * widthFactor - 20.0;
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () => context.pushRoute(const DriftMemoryListRoute()),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
height: size,
|
||||
width: size,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(20)),
|
||||
gradient: LinearGradient(
|
||||
colors: [context.colorScheme.primary.withAlpha(30), context.colorScheme.primary.withAlpha(25)],
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
),
|
||||
),
|
||||
child: memories.widgetWhen(
|
||||
onLoading: () => const Center(child: CircularProgressIndicator()),
|
||||
onData: (memories) {
|
||||
return GridView.count(
|
||||
crossAxisCount: 2,
|
||||
padding: const EdgeInsets.all(12),
|
||||
crossAxisSpacing: 8,
|
||||
mainAxisSpacing: 8,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
children: memories.take(4).map((memory) {
|
||||
return ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(10)),
|
||||
child: Thumbnail.remote(
|
||||
remoteId: memory.assets[0].id,
|
||||
thumbhash: memory.assets[0].thumbHash ?? "",
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(
|
||||
'memories'.t(context: context),
|
||||
style: context.textTheme.titleSmall?.copyWith(
|
||||
color: context.colorScheme.onSurface,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _QuickAccessButtonList extends ConsumerWidget {
|
||||
const _QuickAccessButtonList();
|
||||
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
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';
|
||||
import 'package:immich_mobile/presentation/pages/drift_memory.page.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/memory.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
|
||||
@RoutePage()
|
||||
class DriftMemoryListPage extends ConsumerStatefulWidget {
|
||||
const DriftMemoryListPage({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<DriftMemoryListPage> createState() => _DriftMemoryListPageState();
|
||||
}
|
||||
|
||||
class _DriftMemoryListPageState extends ConsumerState<DriftMemoryListPage> {
|
||||
bool _onlyFavorites = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final memories = ref.watch(driftAllMemoriesProvider);
|
||||
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('memories'.tr()),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: Icon(_onlyFavorites ? Icons.favorite : Icons.favorite_outline),
|
||||
onPressed: () {
|
||||
setState(() => _onlyFavorites = !_onlyFavorites);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
body: SafeArea(
|
||||
child: memories.when(
|
||||
data: (memories) {
|
||||
if (_onlyFavorites) {
|
||||
memories = memories.where((memory) => memory.isSaved).toList();
|
||||
}
|
||||
|
||||
return GridView.builder(
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: constraints.maxWidth > 600 ? 4 : 2,
|
||||
childAspectRatio: 0.5625,
|
||||
mainAxisSpacing: 16,
|
||||
crossAxisSpacing: 16,
|
||||
),
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: memories.length,
|
||||
itemBuilder: (context, index) => GestureDetector(
|
||||
onTap: () {
|
||||
if (memories[index].assets.isNotEmpty) {
|
||||
DriftMemoryPage.setMemory(ref, memories[index]);
|
||||
}
|
||||
context.pushRoute(DriftMemoryRoute(memories: memories, memoryIndex: index));
|
||||
},
|
||||
child: Stack(
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(10)),
|
||||
child: ColorFiltered(
|
||||
colorFilter: ColorFilter.mode(Colors.black.withValues(alpha: 0.2), BlendMode.darken),
|
||||
child: AbsorbPointer(
|
||||
child: Thumbnail.remote(
|
||||
remoteId: memories[index].assets[0].id,
|
||||
thumbhash: memories[index].assets[0].thumbHash ?? "",
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
bottom: 16,
|
||||
left: 16,
|
||||
child: Text(
|
||||
DateFormat.yMMMMd().format(memories[index].memoryAt),
|
||||
style: const TextStyle(fontWeight: FontWeight.w600, color: Colors.white, fontSize: 15),
|
||||
),
|
||||
),
|
||||
if (memories[index].isSaved)
|
||||
const Positioned(
|
||||
top: 16,
|
||||
right: 16,
|
||||
child: Icon(Icons.favorite, color: Colors.white, size: 24),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
error: (error, stack) => const Text("Error loading memories"),
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ class DriftMemoryLane extends ConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final memoryLaneProvider = ref.watch(driftMemoryLaneProvider);
|
||||
final memoryLaneProvider = ref.watch(driftMemoryFutureProvider);
|
||||
final memories = memoryLaneProvider.value ?? const [];
|
||||
if (memories.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
|
||||
@@ -13,7 +13,7 @@ final driftMemoryServiceProvider = Provider<DriftMemoryService>(
|
||||
(ref) => DriftMemoryService(ref.watch(driftMemoryRepositoryProvider)),
|
||||
);
|
||||
|
||||
final driftMemoryLaneProvider = FutureProvider.autoDispose<List<DriftMemory>>((ref) {
|
||||
final driftMemoryFutureProvider = FutureProvider.autoDispose<List<DriftMemory>>((ref) {
|
||||
final (userId, enabled) = ref.watch(currentUserProvider.select((user) => (user?.id, user?.memoryEnabled ?? true)));
|
||||
if (userId == null || !enabled) {
|
||||
return const [];
|
||||
@@ -22,13 +22,3 @@ final driftMemoryLaneProvider = FutureProvider.autoDispose<List<DriftMemory>>((r
|
||||
final service = ref.watch(driftMemoryServiceProvider);
|
||||
return service.getMemoryLane(userId);
|
||||
});
|
||||
|
||||
final driftAllMemoriesProvider = FutureProvider.autoDispose<List<DriftMemory>>((ref) {
|
||||
final (userId, enabled) = ref.watch(currentUserProvider.select((user) => (user?.id, user?.memoryEnabled ?? true)));
|
||||
if (userId == null || !enabled) {
|
||||
return const [];
|
||||
}
|
||||
|
||||
final service = ref.watch(driftMemoryServiceProvider);
|
||||
return service.getAllMemories(userId);
|
||||
});
|
||||
|
||||
@@ -52,7 +52,6 @@ import 'package:immich_mobile/presentation/pages/drift_local_album.page.dart';
|
||||
import 'package:immich_mobile/presentation/pages/drift_locked_folder.page.dart';
|
||||
import 'package:immich_mobile/presentation/pages/drift_map.page.dart';
|
||||
import 'package:immich_mobile/presentation/pages/drift_memory.page.dart';
|
||||
import 'package:immich_mobile/presentation/pages/drift_memory_list.page.dart';
|
||||
import 'package:immich_mobile/presentation/pages/drift_partner_detail.page.dart';
|
||||
import 'package:immich_mobile/presentation/pages/drift_people_collection.page.dart';
|
||||
import 'package:immich_mobile/presentation/pages/drift_person.page.dart';
|
||||
@@ -192,7 +191,6 @@ class AppRouter extends RootStackRouter {
|
||||
AutoRoute(page: DownloadInfoRoute.page, guards: [_authGuard, _duplicateGuard]),
|
||||
AutoRoute(page: CleanupPreviewRoute.page, guards: [_authGuard, _duplicateGuard]),
|
||||
AutoRoute(page: DriftSlideshowRoute.page, guards: [_authGuard, _duplicateGuard]),
|
||||
AutoRoute(page: DriftMemoryListRoute.page, guards: [_authGuard, _duplicateGuard]),
|
||||
// required to handle all deeplinks in deep_link.service.dart
|
||||
// auto_route_library#1722
|
||||
RedirectRoute(path: '*', redirectTo: '/'),
|
||||
|
||||
@@ -754,22 +754,6 @@ class DriftMapRouteArgs {
|
||||
int get hashCode => key.hashCode ^ initialLocation.hashCode;
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [DriftMemoryListPage]
|
||||
class DriftMemoryListRoute extends PageRouteInfo<void> {
|
||||
const DriftMemoryListRoute({List<PageRouteInfo>? children})
|
||||
: super(DriftMemoryListRoute.name, initialChildren: children);
|
||||
|
||||
static const String name = 'DriftMemoryListRoute';
|
||||
|
||||
static PageInfo page = PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
return const DriftMemoryListPage();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [DriftMemoryPage]
|
||||
class DriftMemoryRoute extends PageRouteInfo<DriftMemoryRouteArgs> {
|
||||
|
||||
@@ -19,7 +19,6 @@ dynamic upgradeDto(dynamic value, String targetType) {
|
||||
if (value is Map) {
|
||||
addDefault(value, 'mapLightStyleUrl', 'https://tiles.immich.cloud/v1/style/light.json');
|
||||
addDefault(value, 'mapDarkStyleUrl', 'https://tiles.immich.cloud/v1/style/dark.json');
|
||||
addDefault(value, 'minFaces', 3);
|
||||
}
|
||||
case 'UserResponseDto':
|
||||
if (value is Map) {
|
||||
@@ -55,7 +54,6 @@ dynamic upgradeDto(dynamic value, String targetType) {
|
||||
case 'ServerFeaturesDto':
|
||||
if (value is Map) {
|
||||
addDefault(value, 'ocr', false);
|
||||
addDefault(value, 'realtimeTranscoding', false);
|
||||
}
|
||||
break;
|
||||
case 'MemoriesResponse':
|
||||
|
||||
Generated
-1
@@ -454,7 +454,6 @@ Class | Method | HTTP request | Description
|
||||
- [MemoryCreateDto](doc//MemoryCreateDto.md)
|
||||
- [MemoryResponseDto](doc//MemoryResponseDto.md)
|
||||
- [MemorySearchOrder](doc//MemorySearchOrder.md)
|
||||
- [MemorySearchResponseDto](doc//MemorySearchResponseDto.md)
|
||||
- [MemoryStatisticsResponseDto](doc//MemoryStatisticsResponseDto.md)
|
||||
- [MemoryType](doc//MemoryType.md)
|
||||
- [MemoryUpdateDto](doc//MemoryUpdateDto.md)
|
||||
|
||||
Generated
-1
@@ -195,7 +195,6 @@ part 'model/memories_update.dart';
|
||||
part 'model/memory_create_dto.dart';
|
||||
part 'model/memory_response_dto.dart';
|
||||
part 'model/memory_search_order.dart';
|
||||
part 'model/memory_search_response_dto.dart';
|
||||
part 'model/memory_statistics_response_dto.dart';
|
||||
part 'model/memory_type.dart';
|
||||
part 'model/memory_update_dto.dart';
|
||||
|
||||
Generated
+5
-20
@@ -265,9 +265,6 @@ class MemoriesApi {
|
||||
///
|
||||
/// * [MemorySearchOrder] order:
|
||||
///
|
||||
/// * [int] page:
|
||||
/// Page number
|
||||
///
|
||||
/// * [int] size:
|
||||
/// Number of memories to return
|
||||
///
|
||||
@@ -295,9 +292,6 @@ class MemoriesApi {
|
||||
if (order != null) {
|
||||
queryParams.addAll(_queryParams('', 'order', order));
|
||||
}
|
||||
if (page != null) {
|
||||
queryParams.addAll(_queryParams('', 'page', page));
|
||||
}
|
||||
if (size != null) {
|
||||
queryParams.addAll(_queryParams('', 'size', size));
|
||||
}
|
||||
@@ -337,9 +331,6 @@ class MemoriesApi {
|
||||
///
|
||||
/// * [MemorySearchOrder] order:
|
||||
///
|
||||
/// * [int] page:
|
||||
/// Page number
|
||||
///
|
||||
/// * [int] size:
|
||||
/// Number of memories to return
|
||||
///
|
||||
@@ -443,9 +434,6 @@ class MemoriesApi {
|
||||
///
|
||||
/// * [MemorySearchOrder] order:
|
||||
///
|
||||
/// * [int] page:
|
||||
/// Page number
|
||||
///
|
||||
/// * [int] size:
|
||||
/// Number of memories to return
|
||||
///
|
||||
@@ -473,9 +461,6 @@ class MemoriesApi {
|
||||
if (order != null) {
|
||||
queryParams.addAll(_queryParams('', 'order', order));
|
||||
}
|
||||
if (page != null) {
|
||||
queryParams.addAll(_queryParams('', 'page', page));
|
||||
}
|
||||
if (size != null) {
|
||||
queryParams.addAll(_queryParams('', 'size', size));
|
||||
}
|
||||
@@ -515,9 +500,6 @@ class MemoriesApi {
|
||||
///
|
||||
/// * [MemorySearchOrder] order:
|
||||
///
|
||||
/// * [int] page:
|
||||
/// Page number
|
||||
///
|
||||
/// * [int] size:
|
||||
/// Number of memories to return
|
||||
///
|
||||
@@ -531,8 +513,11 @@ class MemoriesApi {
|
||||
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
|
||||
// FormatException when trying to decode an empty string.
|
||||
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
|
||||
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'MemorySearchResponseDto',) as MemorySearchResponseDto;
|
||||
|
||||
final responseBody = await _decodeBodyBytes(response);
|
||||
return (await apiClient.deserializeAsync(responseBody, 'List<MemoryResponseDto>') as List)
|
||||
.cast<MemoryResponseDto>()
|
||||
.toList(growable: false);
|
||||
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Generated
-2
@@ -437,8 +437,6 @@ class ApiClient {
|
||||
return MemoryResponseDto.fromJson(value);
|
||||
case 'MemorySearchOrder':
|
||||
return MemorySearchOrderTypeTransformer().decode(value);
|
||||
case 'MemorySearchResponseDto':
|
||||
return MemorySearchResponseDto.fromJson(value);
|
||||
case 'MemoryStatisticsResponseDto':
|
||||
return MemoryStatisticsResponseDto.fromJson(value);
|
||||
case 'MemoryType':
|
||||
|
||||
@@ -1,120 +0,0 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.18
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
class MemorySearchResponseDto {
|
||||
/// Returns a new [MemorySearchResponseDto] instance.
|
||||
MemorySearchResponseDto({
|
||||
required this.hasNextPage,
|
||||
this.items = const [],
|
||||
required this.total,
|
||||
});
|
||||
|
||||
/// Whether there are more pages
|
||||
bool hasNextPage;
|
||||
|
||||
List<MemoryResponseDto> items;
|
||||
|
||||
/// Total number of matching memories
|
||||
///
|
||||
/// Minimum value: 0
|
||||
/// Maximum value: 9007199254740991
|
||||
int total;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is MemorySearchResponseDto &&
|
||||
other.hasNextPage == hasNextPage &&
|
||||
_deepEquality.equals(other.items, items) &&
|
||||
other.total == total;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(hasNextPage.hashCode) +
|
||||
(items.hashCode) +
|
||||
(total.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'MemorySearchResponseDto[hasNextPage=$hasNextPage, items=$items, total=$total]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'hasNextPage'] = this.hasNextPage;
|
||||
json[r'items'] = this.items;
|
||||
json[r'total'] = this.total;
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [MemorySearchResponseDto] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static MemorySearchResponseDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "MemorySearchResponseDto");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return MemorySearchResponseDto(
|
||||
hasNextPage: mapValueOfType<bool>(json, r'hasNextPage')!,
|
||||
items: MemoryResponseDto.listFromJson(json[r'items']),
|
||||
total: mapValueOfType<int>(json, r'total')!,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<MemorySearchResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <MemorySearchResponseDto>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = MemorySearchResponseDto.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, MemorySearchResponseDto> mapFromJson(dynamic json) {
|
||||
final map = <String, MemorySearchResponseDto>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = MemorySearchResponseDto.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of MemorySearchResponseDto-objects as value to a dart map
|
||||
static Map<String, List<MemorySearchResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<MemorySearchResponseDto>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = MemorySearchResponseDto.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'hasNextPage',
|
||||
'items',
|
||||
'total',
|
||||
};
|
||||
}
|
||||
|
||||
+1
-1
@@ -1997,4 +1997,4 @@ packages:
|
||||
version: "3.1.3"
|
||||
sdks:
|
||||
dart: ">=3.12.0 <4.0.0"
|
||||
flutter: "3.44.1"
|
||||
flutter: "3.44.0"
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ version: 3.0.0+3047
|
||||
|
||||
environment:
|
||||
sdk: '>=3.12.0 <4.0.0'
|
||||
flutter: 3.44.1
|
||||
flutter: 3.44.0
|
||||
|
||||
dependencies:
|
||||
async: ^2.13.1
|
||||
|
||||
@@ -6728,17 +6728,6 @@
|
||||
"$ref": "#/components/schemas/MemorySearchOrder"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "page",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "Page number",
|
||||
"schema": {
|
||||
"minimum": 1,
|
||||
"maximum": 9007199254740991,
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "size",
|
||||
"required": false,
|
||||
@@ -6764,7 +6753,10 @@
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/MemorySearchResponseDto"
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/MemoryResponseDto"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -6905,17 +6897,6 @@
|
||||
"$ref": "#/components/schemas/MemorySearchOrder"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "page",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "Page number",
|
||||
"schema": {
|
||||
"minimum": 1,
|
||||
"maximum": 9007199254740991,
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "size",
|
||||
"required": false,
|
||||
@@ -19192,32 +19173,6 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"MemorySearchResponseDto": {
|
||||
"properties": {
|
||||
"hasNextPage": {
|
||||
"description": "Whether there are more pages",
|
||||
"type": "boolean"
|
||||
},
|
||||
"items": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/MemoryResponseDto"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"total": {
|
||||
"description": "Total number of matching memories",
|
||||
"maximum": 9007199254740991,
|
||||
"minimum": 0,
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"hasNextPage",
|
||||
"items",
|
||||
"total"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"MemoryStatisticsResponseDto": {
|
||||
"properties": {
|
||||
"total": {
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
"@types/node": "^24.12.4",
|
||||
"esbuild": "^0.28.0",
|
||||
"tsc-alias": "^1.8.16",
|
||||
"typescript": "^5.9.3"
|
||||
"typescript": "^6.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@extism/js-pdk": "^1.1.1"
|
||||
|
||||
@@ -1320,13 +1320,6 @@ export type MemoryResponseDto = {
|
||||
/** Last update date */
|
||||
updatedAt: string;
|
||||
};
|
||||
export type MemorySearchResponseDto = {
|
||||
/** Whether there are more pages */
|
||||
hasNextPage: boolean;
|
||||
items: MemoryResponseDto[];
|
||||
/** Total number of matching memories */
|
||||
total: number;
|
||||
};
|
||||
export type MemoryCreateDto = {
|
||||
/** Asset IDs to associate with memory */
|
||||
assetIds?: string[];
|
||||
@@ -4793,24 +4786,22 @@ export function reverseGeocode({ lat, lon }: {
|
||||
/**
|
||||
* Retrieve memories
|
||||
*/
|
||||
export function searchMemories({ $for, isSaved, isTrashed, order, page, size, $type }: {
|
||||
export function searchMemories({ $for, isSaved, isTrashed, order, size, $type }: {
|
||||
$for?: string;
|
||||
isSaved?: boolean;
|
||||
isTrashed?: boolean;
|
||||
order?: MemorySearchOrder;
|
||||
page?: number;
|
||||
size?: number;
|
||||
$type?: MemoryType;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchJson<{
|
||||
status: 200;
|
||||
data: MemorySearchResponseDto;
|
||||
data: MemoryResponseDto[];
|
||||
}>(`/memories${QS.query(QS.explode({
|
||||
"for": $for,
|
||||
isSaved,
|
||||
isTrashed,
|
||||
order,
|
||||
page,
|
||||
size,
|
||||
"type": $type
|
||||
}))}`, {
|
||||
@@ -4835,12 +4826,11 @@ export function createMemory({ memoryCreateDto }: {
|
||||
/**
|
||||
* Retrieve memories statistics
|
||||
*/
|
||||
export function memoriesStatistics({ $for, isSaved, isTrashed, order, page, size, $type }: {
|
||||
export function memoriesStatistics({ $for, isSaved, isTrashed, order, size, $type }: {
|
||||
$for?: string;
|
||||
isSaved?: boolean;
|
||||
isTrashed?: boolean;
|
||||
order?: MemorySearchOrder;
|
||||
page?: number;
|
||||
size?: number;
|
||||
$type?: MemoryType;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
@@ -4852,7 +4842,6 @@ export function memoriesStatistics({ $for, isSaved, isTrashed, order, page, size
|
||||
isSaved,
|
||||
isTrashed,
|
||||
order,
|
||||
page,
|
||||
size,
|
||||
"type": $type
|
||||
}))}`, {
|
||||
|
||||
Generated
+2
-2
@@ -348,8 +348,8 @@ importers:
|
||||
specifier: ^1.8.16
|
||||
version: 1.8.17
|
||||
typescript:
|
||||
specifier: ^5.9.3
|
||||
version: 5.9.3
|
||||
specifier: ^6.0.0
|
||||
version: 6.0.3
|
||||
|
||||
packages/sdk:
|
||||
dependencies:
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
FROM ghcr.io/immich-app/base-server-dev:202606021219@sha256:63fa91aa011f6f2921dd32fe6d1be8d637e9bd7f3e3dd0c8e446afb31b282af4 AS builder
|
||||
FROM ghcr.io/immich-app/base-server-dev:202605121138@sha256:127cc323590e1d64d765016492e1de1e9355da8f658f078aef7be6bede0fdd0f AS builder
|
||||
ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \
|
||||
CI=1 \
|
||||
COREPACK_HOME=/tmp \
|
||||
@@ -80,7 +80,7 @@ RUN --mount=type=cache,id=pnpm-packages,target=/buildcache/pnpm-store \
|
||||
--mount=type=cache,id=mise-tools-${TARGETPLATFORM},target=/buildcache/mise \
|
||||
mise //:plugins
|
||||
|
||||
FROM ghcr.io/immich-app/base-server-prod:202606021219@sha256:6ef9ef5859492149af770a6c884b5e2ddbaeef99f8885ea5f2d9f73625a3d9ec
|
||||
FROM ghcr.io/immich-app/base-server-prod:202605121138@sha256:b346d42d86f42799fede6b6c9f6feed0018c5ee46e4ac31d1165f50737cee842
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
ENV NODE_ENV=production \
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# dev build
|
||||
FROM ghcr.io/immich-app/base-server-dev:202606021219@sha256:63fa91aa011f6f2921dd32fe6d1be8d637e9bd7f3e3dd0c8e446afb31b282af4 AS dev
|
||||
FROM ghcr.io/immich-app/base-server-dev:202605121138@sha256:127cc323590e1d64d765016492e1de1e9355da8f658f078aef7be6bede0fdd0f AS dev
|
||||
|
||||
|
||||
COPY --from=ghcr.io/jdx/mise:2026.5.18@sha256:5bb3311994fa78cef307ca3077cdb18f9551da0886371fc26ea91ab56220ffc5 /usr/local/bin/mise /usr/local/bin/mise
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
MemoryCreateDto,
|
||||
MemoryResponseDto,
|
||||
MemorySearchDto,
|
||||
MemorySearchResponseDto,
|
||||
MemoryStatisticsResponseDto,
|
||||
MemoryUpdateDto,
|
||||
} from 'src/dtos/memory.dto';
|
||||
@@ -29,7 +28,7 @@ export class MemoryController {
|
||||
'Retrieve a list of memories. Memories are sorted descending by creation date by default, although they can also be sorted in ascending order, or randomly.',
|
||||
history: new HistoryBuilder().added('v1').beta('v1').stable('v2'),
|
||||
})
|
||||
searchMemories(@Auth() auth: AuthDto, @Query() dto: MemorySearchDto): Promise<MemorySearchResponseDto> {
|
||||
searchMemories(@Auth() auth: AuthDto, @Query() dto: MemorySearchDto): Promise<MemoryResponseDto[]> {
|
||||
return this.service.search(auth, dto);
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ const MemorySearchSchema = z
|
||||
isTrashed: stringToBool.optional().describe('Include trashed memories'),
|
||||
isSaved: stringToBool.optional().describe('Filter by saved status'),
|
||||
size: z.coerce.number().int().min(1).optional().describe('Number of memories to return'),
|
||||
page: z.coerce.number().int().min(1).optional().describe('Page number'),
|
||||
order: AssetOrderWithRandomSchema.optional(),
|
||||
})
|
||||
.meta({ id: 'MemorySearchDto' });
|
||||
@@ -76,20 +75,11 @@ const MemoryResponseSchema = z
|
||||
})
|
||||
.meta({ id: 'MemoryResponseDto' });
|
||||
|
||||
const MemorySearchResponseSchema = z
|
||||
.object({
|
||||
total: z.int().min(0).describe('Total number of matching memories'),
|
||||
items: z.array(MemoryResponseSchema),
|
||||
hasNextPage: z.boolean().describe('Whether there are more pages'),
|
||||
})
|
||||
.meta({ id: 'MemorySearchResponseDto' });
|
||||
|
||||
export class MemorySearchDto extends createZodDto(MemorySearchSchema) {}
|
||||
export class MemoryUpdateDto extends createZodDto(MemoryUpdateSchema) {}
|
||||
export class MemoryCreateDto extends createZodDto(MemoryCreateSchema) {}
|
||||
export class MemoryStatisticsResponseDto extends createZodDto(MemoryStatisticsResponseSchema) {}
|
||||
export class MemoryResponseDto extends createZodDto(MemoryResponseSchema) {}
|
||||
export class MemorySearchResponseDto extends createZodDto(MemorySearchResponseSchema) {}
|
||||
|
||||
export const mapMemory = (entity: Memory, auth: AuthDto): MemoryResponseDto => {
|
||||
return {
|
||||
|
||||
@@ -9,7 +9,6 @@ import { AssetOrderWithRandom, AssetVisibility } from 'src/enum';
|
||||
import { DB } from 'src/schema';
|
||||
import { MemoryTable } from 'src/schema/tables/memory.table';
|
||||
import { IBulkAsset } from 'src/types';
|
||||
import { paginationHelper } from 'src/utils/pagination';
|
||||
|
||||
@Injectable()
|
||||
export class MemoryRepository implements IBulkAsset {
|
||||
@@ -58,8 +57,8 @@ export class MemoryRepository implements IBulkAsset {
|
||||
{ params: [DummyValue.UUID, {}] },
|
||||
{ name: 'date filter', params: [DummyValue.UUID, { for: DummyValue.DATE }] },
|
||||
)
|
||||
async search(ownerId: string, dto: MemorySearchDto) {
|
||||
const items = await this.searchBuilder(ownerId, dto)
|
||||
search(ownerId: string, dto: MemorySearchDto) {
|
||||
return this.searchBuilder(ownerId, dto)
|
||||
.select((eb) =>
|
||||
jsonArrayFrom(
|
||||
eb
|
||||
@@ -90,11 +89,8 @@ export class MemoryRepository implements IBulkAsset {
|
||||
? qb.orderBy(sql`RANDOM()`)
|
||||
: qb.orderBy('memoryAt', (dto.order?.toLowerCase() || 'desc') as OrderByDirection),
|
||||
)
|
||||
.$if(dto.size !== undefined, (qb) => qb.limit(dto.size! + 1))
|
||||
.$if(dto.page !== undefined && dto.size !== undefined, (qb) => qb.offset((dto.page! - 1) * dto.size!))
|
||||
.$if(dto.size !== undefined, (qb) => qb.limit(dto.size!))
|
||||
.execute();
|
||||
|
||||
return paginationHelper(items, dto.size ?? items.length);
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import { Kysely, sql } from 'kysely';
|
||||
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
// Delete unauthorized cross-owner asset faces
|
||||
await sql`
|
||||
DELETE FROM asset_face
|
||||
USING person, asset
|
||||
WHERE asset_face."personId" = person.id
|
||||
AND asset_face."assetId" = asset.id
|
||||
AND person."ownerId" != asset."ownerId"
|
||||
`.execute(db);
|
||||
}
|
||||
|
||||
export async function down(): Promise<void> {
|
||||
// Not implemented: the deleted rows were unauthorized cross-owner entries
|
||||
}
|
||||
@@ -34,28 +34,21 @@ describe(MemoryService.name, () => {
|
||||
const asset = AssetFactory.create();
|
||||
const memory1 = MemoryFactory.from({ ownerId: userId }).asset(asset).build();
|
||||
const memory2 = MemoryFactory.create({ ownerId: userId });
|
||||
mocks.memory.search.mockResolvedValue({
|
||||
items: [getForMemory(memory1), getForMemory(memory2)],
|
||||
hasNextPage: false,
|
||||
});
|
||||
mocks.memory.statistics.mockResolvedValue({ total: 2 });
|
||||
mocks.memory.search.mockResolvedValue([getForMemory(memory1), getForMemory(memory2)]);
|
||||
|
||||
await expect(sut.search(factory.auth({ user: { id: userId } }), {})).resolves.toMatchObject({
|
||||
items: expect.arrayContaining([
|
||||
await expect(sut.search(factory.auth({ user: { id: userId } }), {})).resolves.toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: memory1.id,
|
||||
assets: expect.arrayContaining([expect.objectContaining({ id: asset.id })]),
|
||||
}),
|
||||
]),
|
||||
hasNextPage: false,
|
||||
total: 2,
|
||||
});
|
||||
);
|
||||
});
|
||||
|
||||
it('should map empty result', async () => {
|
||||
mocks.memory.search.mockResolvedValue({ items: [], hasNextPage: false });
|
||||
mocks.memory.statistics.mockResolvedValue({ total: 0 });
|
||||
await expect(sut.search(factory.auth(), {})).resolves.toMatchObject({ items: [], hasNextPage: false, total: 0 });
|
||||
mocks.memory.search.mockResolvedValue([]);
|
||||
await expect(sut.search(factory.auth(), {})).resolves.toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -71,14 +71,10 @@ export class MemoryService extends BaseService {
|
||||
}
|
||||
|
||||
async search(auth: AuthDto, dto: MemorySearchDto) {
|
||||
const { items, hasNextPage } = await this.memoryRepository.search(auth.user.id, dto);
|
||||
const { total } = await this.memoryRepository.statistics(auth.user.id, dto);
|
||||
|
||||
return {
|
||||
total,
|
||||
items: items.map((memory: Memory) => mapMemory(memory, auth)),
|
||||
hasNextPage,
|
||||
};
|
||||
const memories = await this.memoryRepository.search(auth.user.id, dto);
|
||||
return memories
|
||||
.filter((memory: Memory) => memory.assets && memory.assets.length > 0)
|
||||
.map((memory: Memory) => mapMemory(memory, auth));
|
||||
}
|
||||
|
||||
statistics(auth: AuthDto, dto: MemorySearchDto) {
|
||||
|
||||
@@ -452,30 +452,6 @@ describe(PersonService.name, () => {
|
||||
expect(mocks.person.update).not.toHaveBeenCalled();
|
||||
expect(mocks.job.queueAll).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should reject creating a face on an asset the user does not own', async () => {
|
||||
const auth = AuthFactory.create();
|
||||
const asset = AssetFactory.create();
|
||||
const person = PersonFactory.create({ faceAssetId: null });
|
||||
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set());
|
||||
mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set([person.id]));
|
||||
|
||||
await expect(
|
||||
sut.createFace(auth, {
|
||||
assetId: asset.id,
|
||||
personId: person.id,
|
||||
imageHeight: 500,
|
||||
imageWidth: 400,
|
||||
x: 10,
|
||||
y: 20,
|
||||
width: 100,
|
||||
height: 110,
|
||||
}),
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
|
||||
expect(mocks.person.createAssetFace).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('createNewFeaturePhoto', () => {
|
||||
|
||||
@@ -625,7 +625,7 @@ export class PersonService extends BaseService {
|
||||
// TODO return a asset face response
|
||||
async createFace(auth: AuthDto, dto: AssetFaceCreateDto): Promise<void> {
|
||||
await Promise.all([
|
||||
this.requireAccess({ auth, permission: Permission.AssetUpdate, ids: [dto.assetId] }),
|
||||
this.requireAccess({ auth, permission: Permission.AssetRead, ids: [dto.assetId] }),
|
||||
this.requireAccess({ auth, permission: Permission.PersonRead, ids: [dto.personId] }),
|
||||
]);
|
||||
|
||||
|
||||
@@ -133,8 +133,8 @@ describe(MemoryService.name, () => {
|
||||
await sut.onMemoriesCreate();
|
||||
|
||||
const memories = await memoryRepo.search(user.id, {});
|
||||
expect(memories.items.length).toBe(1);
|
||||
expect(memories.items[0]).toEqual(
|
||||
expect(memories.length).toBe(1);
|
||||
expect(memories[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
createdAt: expect.any(Date),
|
||||
@@ -173,8 +173,8 @@ describe(MemoryService.name, () => {
|
||||
await sut.onMemoriesCreate();
|
||||
|
||||
const memories = await memoryRepo.search(user.id, {});
|
||||
expect(memories.items.length).toBe(1);
|
||||
expect(memories.items[0]).toEqual(
|
||||
expect(memories.length).toBe(1);
|
||||
expect(memories[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
createdAt: expect.any(Date),
|
||||
@@ -228,12 +228,12 @@ describe(MemoryService.name, () => {
|
||||
await sut.onMemoriesCreate();
|
||||
|
||||
const memories = await memoryRepo.search(user.id, {});
|
||||
expect(memories.items.length).toBe(1);
|
||||
expect(memories.length).toBe(1);
|
||||
|
||||
await sut.onMemoriesCreate();
|
||||
|
||||
const memoriesAfter = await memoryRepo.search(user.id, {});
|
||||
expect(memoriesAfter.items.length).toBe(1);
|
||||
expect(memoriesAfter.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,16 +1,9 @@
|
||||
import {
|
||||
deleteMemory,
|
||||
type MemoryResponseDto,
|
||||
removeMemoryAssets,
|
||||
searchMemories,
|
||||
updateMemory,
|
||||
MemorySearchOrder,
|
||||
MemoryType,
|
||||
} from '@immich/sdk';
|
||||
import { deleteMemory, type MemoryResponseDto, removeMemoryAssets, searchMemories, updateMemory } from '@immich/sdk';
|
||||
import { DateTime } from 'luxon';
|
||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||
import { eventManager } from '$lib/managers/event-manager.svelte';
|
||||
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
||||
import { asLocalTimeISO } from '$lib/utils/date-time';
|
||||
import { toTimelineAsset } from '$lib/utils/timeline-util';
|
||||
|
||||
type MemoryIndex = {
|
||||
@@ -27,31 +20,10 @@ export type MemoryAsset = MemoryIndex & {
|
||||
nextMemory?: MemoryResponseDto;
|
||||
};
|
||||
|
||||
const PAGE_SIZE = 250;
|
||||
|
||||
class MemoryManager {
|
||||
#loading: Promise<void> | undefined;
|
||||
#filters:
|
||||
| {
|
||||
$for?: string;
|
||||
isSaved?: boolean;
|
||||
isTrashed?: boolean;
|
||||
order?: MemorySearchOrder;
|
||||
page?: number;
|
||||
size?: number;
|
||||
$type?: MemoryType;
|
||||
}
|
||||
| undefined;
|
||||
#hasNextPage: boolean;
|
||||
#page: number;
|
||||
#total: number;
|
||||
|
||||
constructor() {
|
||||
this.#filters = undefined;
|
||||
this.#hasNextPage = true;
|
||||
this.#page = 1;
|
||||
this.#total = 0;
|
||||
|
||||
eventManager.on({
|
||||
AuthLogout: () => this.clearCache(),
|
||||
AuthUserLoaded: () => this.initialize(),
|
||||
@@ -65,16 +37,6 @@ class MemoryManager {
|
||||
this.scheduleHourlyRefresh();
|
||||
}
|
||||
|
||||
get filters() {
|
||||
return this.#filters;
|
||||
}
|
||||
|
||||
set filters(filters) {
|
||||
this.#filters = filters;
|
||||
this.clearCache();
|
||||
void this.loadNextPage();
|
||||
}
|
||||
|
||||
ready() {
|
||||
return this.initialize();
|
||||
}
|
||||
@@ -155,46 +117,22 @@ class MemoryManager {
|
||||
}
|
||||
}
|
||||
|
||||
loadNextPage() {
|
||||
if (this.#hasNextPage) {
|
||||
if (this.#loading === undefined) {
|
||||
this.#loading = this.load(this.#page++);
|
||||
} else {
|
||||
void this.#loading.then(() => (this.#loading = this.load(this.#page++)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get hasNextPage() {
|
||||
return this.#hasNextPage;
|
||||
}
|
||||
|
||||
get total() {
|
||||
return this.#total;
|
||||
}
|
||||
|
||||
private clearCache() {
|
||||
this.#loading = undefined;
|
||||
this.#hasNextPage = true;
|
||||
this.#page = 1;
|
||||
this.memories = [];
|
||||
}
|
||||
|
||||
private initialize() {
|
||||
if (!this.#loading) {
|
||||
this.#loading = this.load(this.#page++);
|
||||
this.#loading = this.load();
|
||||
}
|
||||
|
||||
return this.#loading;
|
||||
}
|
||||
|
||||
private async load(page: number) {
|
||||
if (this.#filters !== undefined) {
|
||||
const { items, hasNextPage, total } = await searchMemories({ size: PAGE_SIZE, ...this.#filters, page });
|
||||
this.memories.push(...items);
|
||||
this.#hasNextPage = hasNextPage;
|
||||
this.#total = total;
|
||||
}
|
||||
private async load() {
|
||||
const memories = await searchMemories({ $for: asLocalTimeISO(DateTime.now()) });
|
||||
this.memories = memories.filter((memory) => memory.assets.length > 0);
|
||||
}
|
||||
|
||||
private scheduleHourlyRefresh() {
|
||||
@@ -208,18 +146,12 @@ class MemoryManager {
|
||||
const initialDelay = nextEvent.diff(now).as('milliseconds');
|
||||
|
||||
setTimeout(() => {
|
||||
if (this.#page <= 2) {
|
||||
this.clearCache();
|
||||
this.loadNextPage();
|
||||
}
|
||||
this.#loading = this.load();
|
||||
|
||||
// Schedule subsequent events hourly
|
||||
setInterval(
|
||||
() => {
|
||||
if (this.#page <= 2) {
|
||||
this.clearCache();
|
||||
this.loadNextPage();
|
||||
}
|
||||
this.#loading = this.load();
|
||||
},
|
||||
60 * 60 * 1000,
|
||||
);
|
||||
|
||||
@@ -20,13 +20,12 @@ import {
|
||||
type UserResponseDto,
|
||||
} from '@immich/sdk';
|
||||
import { toastManager, type ActionItem, type IfLike } from '@immich/ui';
|
||||
import { DateTime } from 'luxon';
|
||||
import { init, register, t } from 'svelte-i18n';
|
||||
import { derived, get } from 'svelte/store';
|
||||
import { defaultLang, locales } from '$lib/constants';
|
||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||
import { downloadManager } from '$lib/managers/download-manager.svelte';
|
||||
import { alwaysLoadOriginalFile, lang, locale } from '$lib/stores/preferences.store';
|
||||
import { alwaysLoadOriginalFile, lang } from '$lib/stores/preferences.store';
|
||||
import { isWebCompatibleImage } from '$lib/utils/asset-utils';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { langs } from '$lib/utils/i18n';
|
||||
@@ -367,13 +366,9 @@ export const handlePromiseError = <T>(promise: Promise<T>): void => {
|
||||
|
||||
export const memoryLaneTitle = derived(t, ($t) => {
|
||||
return (memory: MemoryResponseDto) => {
|
||||
const now = new Date();
|
||||
if (memory.type === MemoryType.OnThisDay) {
|
||||
const now = new Date();
|
||||
const memoryDate = new Date(memory.memoryAt);
|
||||
|
||||
return memoryDate.getUTCDate() === now.getDate() && memoryDate.getUTCMonth() === now.getMonth()
|
||||
? $t('years_ago', { values: { years: now.getFullYear() - memory.data.year } })
|
||||
: DateTime.fromJSDate(memoryDate).toLocaleString(DateTime.DATE_MED, { locale: get(locale) });
|
||||
return $t('years_ago', { values: { years: now.getFullYear() - memory.data.year } });
|
||||
}
|
||||
|
||||
return $t('unknown');
|
||||
|
||||
@@ -6,10 +6,10 @@
|
||||
import SingleGridRow from '$lib/components/shared-components/SingleGridRow.svelte';
|
||||
import { assetViewerManager } from '$lib/managers/asset-viewer-manager.svelte';
|
||||
import { Route } from '$lib/route';
|
||||
import { getAssetMediaUrl, getPeopleThumbnailUrl, memoryLaneTitle } from '$lib/utils';
|
||||
import { getAssetMediaUrl, getPeopleThumbnailUrl } from '$lib/utils';
|
||||
import { getAssetInfo, AssetMediaSize, type SearchExploreResponseDto } from '@immich/sdk';
|
||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||
import { Icon, ImageCarousel } from '@immich/ui';
|
||||
import { Icon } from '@immich/ui';
|
||||
import { mdiHeart } from '@mdi/js';
|
||||
import { t } from 'svelte-i18n';
|
||||
import type { PageData } from './$types';
|
||||
@@ -28,22 +28,13 @@
|
||||
return targetField?.items || [];
|
||||
};
|
||||
|
||||
let places = $derived(getFieldItems(data.explore, 'exifInfo.city'));
|
||||
let places = $derived(getFieldItems(data.items, 'exifInfo.city'));
|
||||
let recents = $derived(
|
||||
getFieldItems(data.explore, 'createdAt').sort((a, b) => new Date(b.value).getTime() - new Date(a.value).getTime()),
|
||||
);
|
||||
let people = $state(data.people.people);
|
||||
let memories = $derived(
|
||||
data.memories.map((memory) => ({
|
||||
id: memory.id,
|
||||
title: $memoryLaneTitle(memory),
|
||||
href: Route.memories({ id: memory.assets[0].id }),
|
||||
alt: $t('memory_lane_title', { values: { title: $getAltText(toTimelineAsset(memory.assets[0])) } }),
|
||||
src: getAssetMediaUrl({ id: memory.assets[0].id }),
|
||||
})),
|
||||
getFieldItems(data.items, 'createdAt').sort((a, b) => new Date(b.value).getTime() - new Date(a.value).getTime()),
|
||||
);
|
||||
let people = $state(data.response.people);
|
||||
|
||||
let hasPeople = $derived(data.people.total > 0);
|
||||
let hasPeople = $derived(data.response.total > 0);
|
||||
|
||||
const onPersonThumbnailReady = ({ id }: { id: string }) => {
|
||||
for (const person of people) {
|
||||
@@ -133,20 +124,6 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if memories.length > 0}
|
||||
<div class="mt-2 mb-6">
|
||||
<div class="flex justify-between">
|
||||
<p class="mb-4 font-medium dark:text-immich-dark-fg">{$t('memories')}</p>
|
||||
<a
|
||||
href={Route.memories()}
|
||||
class="pe-4 text-sm font-medium hover:text-immich-primary dark:text-immich-dark-fg dark:hover:text-immich-dark-primary"
|
||||
draggable="false">{$t('view_all')}</a
|
||||
>
|
||||
</div>
|
||||
<ImageCarousel items={memories} />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if recents.length > 0}
|
||||
<div class="mt-2 mb-6">
|
||||
<div class="flex justify-between">
|
||||
|
||||
@@ -1,24 +1,16 @@
|
||||
import { getAllPeople, getExploreData, MemorySearchOrder } from '@immich/sdk';
|
||||
import { memoryManager } from '$lib/managers/memory-manager.svelte';
|
||||
import { getAllPeople, getExploreData } from '@immich/sdk';
|
||||
import { authenticate } from '$lib/utils/auth';
|
||||
import { getFormatter } from '$lib/utils/i18n';
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load = (async ({ url }) => {
|
||||
await authenticate(url);
|
||||
memoryManager.filters = { size: 12, order: MemorySearchOrder.Desc };
|
||||
|
||||
const [explore, people] = await Promise.all([
|
||||
getExploreData(),
|
||||
getAllPeople({ withHidden: false }),
|
||||
memoryManager.ready(),
|
||||
]);
|
||||
const [items, response] = await Promise.all([getExploreData(), getAllPeople({ withHidden: false })]);
|
||||
const $t = await getFormatter();
|
||||
|
||||
return {
|
||||
explore,
|
||||
people,
|
||||
memories: memoryManager.memories,
|
||||
items,
|
||||
response,
|
||||
meta: {
|
||||
title: $t('explore'),
|
||||
},
|
||||
|
||||
@@ -1,109 +1,5 @@
|
||||
<script lang="ts">
|
||||
import UserPageLayout from '$lib/components/layouts/UserPageLayout.svelte';
|
||||
import type { PageData } from './$types';
|
||||
import { Route } from '$lib/route';
|
||||
import { getAssetMediaUrl, memoryLaneTitle } from '$lib/utils';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { mdiHeartOutline, mdiHeart } from '@mdi/js';
|
||||
import { Button, Icon, LoadingSpinner } from '@immich/ui';
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
import { getAltText } from '$lib/utils/thumbnail-util';
|
||||
import { toTimelineAsset } from '$lib/utils/timeline-util';
|
||||
import { page } from '$app/state';
|
||||
<script>
|
||||
import MemoryViewer from './MemoryViewer.svelte';
|
||||
import { QueryParameter } from '$lib/constants';
|
||||
import { memoryManager } from '$lib/managers/memory-manager.svelte';
|
||||
import { clearQueryParam, setQueryValue } from '$lib/utils/navigation';
|
||||
|
||||
interface Props {
|
||||
data: PageData;
|
||||
}
|
||||
|
||||
let { data }: Props = $props();
|
||||
let onlyFavorites = $state(page.url.searchParams.get('favorites') === 'true');
|
||||
let lastElement: HTMLElement | undefined = $state();
|
||||
|
||||
const toggleFavorites = async () => {
|
||||
onlyFavorites = !onlyFavorites;
|
||||
memoryManager.filters = onlyFavorites ? { isSaved: true } : {};
|
||||
await memoryManager.ready();
|
||||
|
||||
if (onlyFavorites) {
|
||||
void setQueryValue('favorites', 'true');
|
||||
} else {
|
||||
void clearQueryParam('favorites', page.url);
|
||||
}
|
||||
};
|
||||
|
||||
const intersectionObserver = new IntersectionObserver((entries) => {
|
||||
const entry = entries.find((entry) => entry.target === lastElement);
|
||||
if (entry?.isIntersecting && memoryManager.hasNextPage) {
|
||||
void memoryManager.loadNextPage();
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (lastElement) {
|
||||
intersectionObserver.disconnect();
|
||||
intersectionObserver.observe(lastElement);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if page.url.searchParams.has(QueryParameter.ID)}
|
||||
<MemoryViewer />
|
||||
{:else}
|
||||
<UserPageLayout title={data.meta.title} description={`(${memoryManager.total.toLocaleString($locale)})`}>
|
||||
{#snippet buttons()}
|
||||
<div class="flex place-items-center gap-2">
|
||||
<Button
|
||||
leadingIcon={mdiHeartOutline}
|
||||
size="small"
|
||||
variant={onlyFavorites ? 'filled' : 'ghost'}
|
||||
color="secondary"
|
||||
onclick={() => toggleFavorites()}>{$t('only_favorites')}</Button
|
||||
>
|
||||
</div>
|
||||
{/snippet}
|
||||
{#if memoryManager.memories.length > 0}
|
||||
<div class="grid w-full grid-cols-3 gap-7 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-7">
|
||||
{#each memoryManager.memories as memory, index (memory.id)}
|
||||
<a
|
||||
href={Route.memories({ id: memory.assets[0].id })}
|
||||
class="relative rounded-md bg-gray-50 p-2 pb-0 shadow-md sm:p-5 sm:pb-0"
|
||||
style:transform={`rotate(${Math.random() * 5 - 2.5}deg)`}
|
||||
bind:this={
|
||||
() => (index === memoryManager.memories.length - 1 ? lastElement : null),
|
||||
(e) => {
|
||||
if (index === memoryManager.memories.length - 1) {
|
||||
lastElement = e;
|
||||
}
|
||||
}
|
||||
}
|
||||
>
|
||||
{#if memory.isSaved}
|
||||
<div class="absolute inset-s-2 top-2 z-2">
|
||||
<Icon data-icon-favorite icon={mdiHeart} size="32" class="text-red-400" />
|
||||
</div>
|
||||
{/if}
|
||||
<img
|
||||
src={getAssetMediaUrl({ id: memory.assets[0].id })}
|
||||
alt={$getAltText(toTimelineAsset(memory.assets[0]))}
|
||||
class="aspect-square object-cover brightness-75"
|
||||
loading="lazy"
|
||||
/>
|
||||
<p
|
||||
class="my-2 text-center text-sm font-medium text-ellipsis text-black capitalize hover:cursor-pointer sm:my-5"
|
||||
>
|
||||
{$memoryLaneTitle(memory)}
|
||||
</p>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
{:else if memoryManager.total > 0}
|
||||
<div class="flex items-center justify-center py-16">
|
||||
<LoadingSpinner size="giant" />
|
||||
</div>
|
||||
{/if}
|
||||
</UserPageLayout>
|
||||
{/if}
|
||||
<MemoryViewer />
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { QueryParameter } from '$lib/constants';
|
||||
import { memoryManager } from '$lib/managers/memory-manager.svelte';
|
||||
import { authenticate } from '$lib/utils/auth';
|
||||
import { getFormatter } from '$lib/utils/i18n';
|
||||
import type { PageLoad } from './$types';
|
||||
@@ -9,19 +6,10 @@ export const load = (async ({ url }) => {
|
||||
const user = await authenticate(url);
|
||||
const $t = await getFormatter();
|
||||
|
||||
const filters = url.searchParams.get('favorites') === 'true' ? { isSaved: true } : {};
|
||||
if (
|
||||
!(url.searchParams.has(QueryParameter.ID) && memoryManager.memories.length > 0) &&
|
||||
!isEqual(memoryManager.filters, filters)
|
||||
) {
|
||||
memoryManager.filters = filters;
|
||||
await memoryManager.ready();
|
||||
}
|
||||
|
||||
return {
|
||||
user,
|
||||
meta: {
|
||||
title: $t('memories'),
|
||||
title: $t('memory'),
|
||||
},
|
||||
};
|
||||
}) satisfies PageLoad;
|
||||
|
||||
@@ -82,7 +82,6 @@
|
||||
let progressBarController: Tween<number> | undefined = $state(undefined);
|
||||
let videoPlayer: HTMLVideoElement | undefined = $state();
|
||||
const asHref = (asset: { id: string }) => `?${QueryParameter.ID}=${asset.id}`;
|
||||
let previousPage = $state(Route.memories());
|
||||
|
||||
const handleNavigate = async (asset?: { id: string }) => {
|
||||
if (assetViewerManager.isViewing) {
|
||||
@@ -107,7 +106,7 @@
|
||||
const handlePreviousAsset = () => handleNavigate(current?.previous?.asset);
|
||||
const handleNextMemory = () => handleNavigate(current?.nextMemory?.assets[0]);
|
||||
const handlePreviousMemory = () => handleNavigate(current?.previousMemory?.assets[0]);
|
||||
const handleEscape = async () => goto(previousPage);
|
||||
const handleEscape = async () => goto(Route.photos());
|
||||
const handleSelectAll = () =>
|
||||
assetMultiSelectManager.selectAssets(current?.memory.assets.map((a) => toTimelineAsset(a)) || []);
|
||||
|
||||
@@ -250,7 +249,7 @@
|
||||
|
||||
const init = (target: Page | NavigationTarget | null) => {
|
||||
if (memoryManager.memories.length === 0) {
|
||||
return handlePromiseError(goto(previousPage));
|
||||
return handlePromiseError(goto(Route.photos()));
|
||||
}
|
||||
|
||||
current = loadFromParams(target);
|
||||
@@ -282,10 +281,6 @@
|
||||
};
|
||||
|
||||
afterNavigate(({ from, to }) => {
|
||||
if (from?.url !== null && !from?.url.searchParams.has(QueryParameter.ID)) {
|
||||
previousPage = from!.url.toString();
|
||||
}
|
||||
|
||||
memoryManager.ready().then(
|
||||
() => {
|
||||
let target;
|
||||
@@ -386,7 +381,7 @@
|
||||
icon={mdiClose}
|
||||
aria-label={$t('close')}
|
||||
size="large"
|
||||
onclick={() => goto(previousPage)}
|
||||
onclick={() => goto(Route.photos())}
|
||||
/>
|
||||
<p class="text-lg">
|
||||
{$memoryLaneTitle(current.memory)}
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import { DateTime } from 'luxon';
|
||||
import { memoryManager } from '$lib/managers/memory-manager.svelte';
|
||||
import { authenticate } from '$lib/utils/auth';
|
||||
import { asLocalTimeISO } from '$lib/utils/date-time';
|
||||
import { getFormatter } from '$lib/utils/i18n';
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
@@ -9,10 +6,6 @@ export const load = (async ({ url }) => {
|
||||
await authenticate(url);
|
||||
const $t = await getFormatter();
|
||||
|
||||
if (memoryManager.filters === undefined || memoryManager.filters.$for !== asLocalTimeISO(DateTime.now())) {
|
||||
memoryManager.filters = { $for: asLocalTimeISO(DateTime.now()) };
|
||||
}
|
||||
|
||||
return {
|
||||
meta: {
|
||||
title: $t('photos'),
|
||||
|
||||
Reference in New Issue
Block a user