Compare commits

...

15 Commits

Author SHA1 Message Date
Immich Release Bot
3c5c0ea68f Version v1.49.0 2023-02-23 18:42:23 +00:00
martyfuhry
2b988e1d5d feat(mobile): Background app refresh status (#1839)
* adds background app refresh message

* fixes ios background settings provider

* styling

* capitalization

* changed to watch

* uses settings notifier now

* forgot to commit this file

* changed to watch and added more clarification

---------

Co-authored-by: Marty Fuhry <marty@fuhry.farm>
2023-02-23 12:33:53 -06:00
Alex
8bcb2558b6 chore(doc): update jobs screenshot (#1847)
* chore(doc): update jobs screenshot

* alt text
2023-02-23 10:44:00 -06:00
Alex
b8785a5b93 chore(doc): Update (#1846)
* remove non-avx info

* background app refresh

* user popup

* architecture
2023-02-23 10:14:02 -06:00
Skyler Mäntysaari
b00631d186 fix(machine-learning): Add the command to execute at startup (#1843)
* fix(machine-learning): Add the command to execute at startup

Previously it wasn't set in the Docker container but it should be.

* fix(docker): remove machine-learning command arg

* fix(docker): machine-learning CMD argument
2023-02-23 09:54:04 -06:00
Skyler Mäntysaari
de5a6b2c35 fix(ci): Mobile build should not run on fork PRs (#1844)
* fix(ci): Mobile build should not run on PRs

It doesn't have the necessary secrets exposed for it succeed in PR context.

* ci(mobile): Run only on internal PRs
2023-02-23 09:01:47 -06:00
be bright
3beb8193ae [Localizely] Korean Translations update (#1842) 2023-02-23 08:54:44 -06:00
Alex
a2549c5bbd fix(mobile): no album thumbnail lead to no album selection shown and add global logs (#1841)
* fix(mobile): no album thumbnail lead to no album selection shown

* add more log info

* added global error handling

* better place to init logger

* get more log
2023-02-23 06:36:17 +00:00
martyfuhry
98a8be82e2 removes deleted asset from gallery list (#1837) 2023-02-22 20:50:13 -06:00
Michel Heusschen
5c86e13239 refactor(web): combine api and serverApi (#1833) 2023-02-22 20:49:13 -06:00
Michel Heusschen
10cb612fb1 feat(web): theme/locale preferences and improve SSR (#1832) 2023-02-22 11:53:08 -06:00
Michel Heusschen
a9a769d902 fix(web): hide img alt text while loading (#1834) 2023-02-22 11:52:23 -06:00
Alex
846e35f57e chore: readme and pump script (#1835)
* chore: readme and pump script

* readme
2023-02-22 11:52:08 -06:00
Alex
a3b9a0be3a fix(server): Flask not found (#1830)
* fix(server): Flask not found

* trial 1
2023-02-22 10:53:59 -06:00
Alex
2a3235f606 fix(server): album repo unused variable (#1831) 2023-02-21 22:49:58 -06:00
74 changed files with 662 additions and 450 deletions

View File

@@ -18,6 +18,8 @@ concurrency:
jobs:
build-sign-android:
name: Build and sign Android
# Skip when PR from a fork
if: ${{ !github.event.pull_request.head.repo.fork }}
runs-on: macos-12
steps:

View File

@@ -61,25 +61,25 @@ Spec: Free-tier Oracle VM - Amsterdam - 2.4Ghz quad-core ARM64 CPU, 24GB RAM
# Features
| Features | Mobile | Web |
| ------------------------------------------- | ------- | --- |
| Upload and view videos and photos | Yes | Yes |
| Auto backup when the app is opened | Yes | N/A |
| Selective album(s) for backup | Yes | N/A |
| Download photos and videos to local device | Yes | Yes |
| Multi-user support | Yes | Yes |
| Album and Shared albums | Yes | Yes |
| Scrubbable/draggable scrollbar | Yes | Yes |
| Support RAW (HEIC, HEIF, DNG, Apple ProRaw) | Yes | Yes |
| Metadata view (EXIF, map) | Yes | Yes |
| Search by metadata, objects and image tags | Yes | No |
| Administrative functions (user management) | N/A | Yes |
| Background backup | Android | N/A |
| Virtual scroll | Yes | Yes |
| OAuth support | Yes | Yes |
| LivePhoto backup and playback | iOS | Yes |
| User-defined storage structure | Yes | Yes |
| Public Sharing | N/A | Yes |
| Features | Mobile | Web |
| ------------------------------------------- | ------ | --- |
| Upload and view videos and photos | Yes | Yes |
| Auto backup when the app is opened | Yes | N/A |
| Selective album(s) for backup | Yes | N/A |
| Download photos and videos to local device | Yes | Yes |
| Multi-user support | Yes | Yes |
| Album and Shared albums | Yes | Yes |
| Scrubbable/draggable scrollbar | Yes | Yes |
| Support RAW (HEIC, HEIF, DNG, Apple ProRaw) | Yes | Yes |
| Metadata view (EXIF, map) | Yes | Yes |
| Search by metadata, objects and image tags | Yes | No |
| Administrative functions (user management) | N/A | Yes |
| Background backup | Yes | N/A |
| Virtual scroll | Yes | Yes |
| OAuth support | Yes | Yes |
| LivePhoto backup and playback | iOS | Yes |
| User-defined storage structure | Yes | Yes |
| Public Sharing | N/A | Yes |
# Support the project

View File

@@ -34,7 +34,6 @@ services:
immich-machine-learning:
container_name: immich_machine_learning
image: altran1502/immich-machine-learning:release
command: [ "python", "src/main.py" ]
volumes:
- ${UPLOAD_LOCATION}:/usr/src/app/upload
- model-cache:/cache

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

View File

@@ -2,22 +2,8 @@
Several Immich functionalities are implemented as jobs, which run in the background. To view the status of a job navigate to the Administration Screen, and then the `Jobs` page.
![Admin jobs](./img/admin-jobs.png)
## Generate Thumbnails
![Generate Thumbnails](./img/admin-jobs-thumbnails.png)
## Extract Exif
![Extract Exif](./img/admin-jobs-exif.png)
## Detect Objects
![Detect Objects](./img/admin-jobs-objects.png)
## Storage Migration
This job can be run after changing the [Storage Template](/docs/administration/storage-template.mdx), in order to apply the change to the existing library.
![Storage Migration](./img/admin-jobs-template.png)
:::info
Storage Migration job can be run after changing the [Storage Template](/docs/administration/storage-template.mdx), in order to apply the change to the existing library.
:::

View File

@@ -28,6 +28,7 @@ Immich is a full-stack [TypeScript](https://www.typescriptlang.org/) application
- [Nest.js](https://nestjs.com/)
- [TypeORM](https://typeorm.io/) for database management.
- [Jest](https://jestjs.io/) for testing.
- [Python](https://www.python.org/) for Machine Learning.
### Database

Binary file not shown.

Before

Width:  |  Height:  |  Size: 691 KiB

After

Width:  |  Height:  |  Size: 242 KiB

View File

@@ -4,33 +4,31 @@ A guide on how the foreground and background automatic backup works.
<img src={require('./img/background-foreground-backup.png').default} width="50%" title="Foreground&Background Backup" />
On iOS, there is only one option for automatic backup
- [Automatic Backup](#automatic-backup)
- [Foreground backup](#foreground-backup)
On Android, there are two options for automatic backup
- [Automatic Backup](#automatic-backup)
- [Foreground backup](#foreground-backup)
- [Background backup](#background-backup)
## Foreground backup
If foreground backup is enabled: whenever the app is opened or resumed, it will check if any photos or videos in the selected album(s) have yet to be uploaded to the cloud (the remainder count). If there are any, they will be uploaded.
## Background backup
Background backup is only available on Android thanks to the contribution effort of [@zoodyy](https://github.com/zoodyy).
Background backup is available thanks to the contribution effort of [@zoodyy](https://github.com/zoodyy) and [@martyfuhry](https://github.com/martyfuhry).
If background backup is enabled. The app will periodically check if there are any new photos or videos in the selected album(s) to be uploaded to the cloud. If there are, it will upload them to the cloud in the background.
A native Android notification shows up when the background upload is in progress. You can further customize the notification by going to the app's settings.
:::info Note
#### General
- The app must be in the background for the backup worker to start running.
- It is a well-known problem that some Android models are very strict with battery optimization settings, which can cause a problem with the background worker. Please visit [Don't kill my app](https://dontkillmyapp.com/) for a guide on disabling this setting on your phone.
- If you reopen the app and the first page you see is the backup page, the counts will not reflect the background uploaded result. You have to navigate out of the page and come back to see the updated counts.
#### Android
- It is a well-known problem that some Android models are very strict with battery optimization settings, which can cause a problem with the background worker. Please visit [Don't kill my app](https://dontkillmyapp.com/) for a guide on disabling this setting on your phone.
#### iOS
- You must enable **Background App Refresh** for the app to work in the background. You can enable it in the Settings app under General > Background App Refresh.
<div style={{textAlign: 'center'}}>
<img src={require('./img/background-app-refresh.png').default} width="30%" title="background-app-refresh" />
</div>
:::

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -20,28 +20,3 @@ You can also use Podman to run the application. However, additional configuratio
- **OS**: Preferred unix-based operating system (Ubuntu, Debian, MacOS, etc). Windows works too, with [Docker Desktop on Windows](https://docs.docker.com/desktop/install/windows-install/)
- **RAM**: At least 2GB, preferred 4GB.
- **CPU**: At least 2 cores, preferred 4 cores.
:::info Machine Learning on older CPU
The TensorFlow version used by Immich doesn't run on older CPU architectures. It requires a CPU with AVX and AVX2 instruction sets. If you encounter the error `illegal instruction core dump` check your CPU flags with the command below and make sure you see `avx` and `avx2`:
```bash
grep -E 'avx2?' /proc/cpuinfo
```
#### Promox
If you are running virtualization in Proxmox, the CPU type of the VM is probably configured incorrectly.
You need to change the CPU type from `kvm64` to `host` under VMs hardware tab.
`Hardware > Processors > Edit > Advanced > Type (dropdown menu) > host`
#### Other platforms
You can use the machine learning image that is built for Non-AVX CPU. The image is community maintained and can be found in the repository below
https://github.com/bertmelis/immich-machine-learning-no-avx
Otherwise, you can safely remove the `immich-machine-learning` service if you do not intend to use Immich's object detection features. Simply remove or comment out the declaration of the service in your compose file.
:::

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 52 KiB

View File

@@ -1,11 +1,18 @@
FROM python:3.10
ENV TRANSFORMERS_CACHE=/cache
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
WORKDIR /usr/src/app
RUN pip install --user --no-cache-dir torch==1.13.1+cpu -f https://download.pytorch.org/whl/torch_stable.html
RUN pip install --user transformers tqdm numpy scikit-learn scipy nltk sentencepiece flask Pillow
RUN pip install --user --no-deps sentence-transformers
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
RUN pip install --no-cache-dir torch==1.13.1+cpu -f https://download.pytorch.org/whl/torch_stable.html
RUN pip install transformers tqdm numpy scikit-learn scipy nltk sentencepiece flask Pillow
RUN pip install --no-deps sentence-transformers
COPY . .
CMD ["python", "src/main.py"]

View File

@@ -75,4 +75,11 @@ sed -i "s/version_number: \"$CURRENT_SERVER\"$/version_number: \"$NEXT_SERVER\"/
sed -i "s/\"android\.injected\.version\.code\" => $CURRENT_MOBILE,/\"android\.injected\.version\.code\" => $NEXT_MOBILE,/" mobile/android/fastlane/Fastfile
sed -i "s/^version: $CURRENT_SERVER+$CURRENT_MOBILE$/version: $NEXT_SERVER+$NEXT_MOBILE/" mobile/pubspec.yaml
# OpenApi Generated Files
sed -i "s/\"version\": \"$CURRENT_SERVER\",$/\"version\": \"$NEXT_SERVER\",/" mobile/openapi/README.md
sed -i "s/\"document\": \"$CURRENT_SERVER\",$/\"document\": \"$NEXT_SERVER\",/" web/src/api/open-api/api.ts
sed -i "s/\"document\": \"$CURRENT_SERVER\",$/\"document\": \"$NEXT_SERVER\",/" web/src/api/open-api/base.ts
sed -i "s/\"document\": \"$CURRENT_SERVER\",$/\"document\": \"$NEXT_SERVER\",/" web/src/api/open-api/common.ts
sed -i "s/\"document\": \"$CURRENT_SERVER\",$/\"document\": \"$NEXT_SERVER\",/" web/src/api/open-api/configuration.ts
sed -i "s/\"document\": \"$CURRENT_SERVER\",$/\"document\": \"$NEXT_SERVER\",/" web/src/api/open-api/index.ts
echo "IMMICH_VERSION=v$NEXT_SERVER" >>$GITHUB_ENV

View File

@@ -35,8 +35,8 @@ platform :android do
task: 'bundle',
build_type: 'Release',
properties: {
"android.injected.version.code" => 71,
"android.injected.version.name" => "1.48.1",
"android.injected.version.code" => 72,
"android.injected.version.name" => "1.49.0",
}
)
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')

View File

@@ -226,5 +226,8 @@
"version_announcement_overlay_text_1": "Hi friend, there is a new release of",
"version_announcement_overlay_text_2": "please take your time to visit the ",
"version_announcement_overlay_text_3": " and ensure your docker-compose and .env setup is up-to-date to prevent any misconfigurations, especially if you use WatchTower or any mechanism that handles updating your server application automatically.",
"version_announcement_overlay_title": "New Server Version Available \uD83C\uDF89"
}
"version_announcement_overlay_title": "New Server Version Available \uD83C\uDF89",
"backup_controller_page_background_app_refresh_disabled_title": "Background app refresh disabled",
"backup_controller_page_background_app_refresh_disabled_content": "Enable background app refresh in Settings > General > Background App Refresh in order to use background backup.",
"backup_controller_page_background_app_refresh_enable_button_text": "Go to settings"
}

View File

@@ -1,4 +1,6 @@
{
"add_to_album_bottom_sheet_added": "{album}에 추가",
"add_to_album_bottom_sheet_already_exists": "{album}에 이미 포함되어 있습니다",
"album_info_card_backup_album_excluded": "제외됨",
"album_info_card_backup_album_included": "포함됨",
"album_thumbnail_card_item": "1개 항목",
@@ -12,6 +14,10 @@
"album_viewer_appbar_share_leave": "앨범 나가기",
"album_viewer_appbar_share_remove": "앨범에서 제거",
"album_viewer_page_share_add_users": "사용자 추가",
"asset_list_layout_settings_dynamic_layout_title": "다이나믹 레이아웃",
"asset_list_layout_settings_group_by": "다음으로 그룹화",
"asset_list_layout_settings_group_by_month": "월",
"asset_list_layout_settings_group_by_month_day": "월 + 일",
"asset_list_settings_subtitle": "사진 배열 레이아웃 설정",
"asset_list_settings_title": "사진 배열",
"backup_album_selection_page_albums_device": "기기의 앨범({})",
@@ -35,6 +41,7 @@
"backup_controller_page_background_battery_info_title": "배터리 최적화",
"backup_controller_page_background_charging": "충전 중일 때만",
"backup_controller_page_background_configure_error": "백그라운드 서비스를 구성하지 못했습니다",
"backup_controller_page_background_delay": "새 미디어파일 백업 지연: {}",
"backup_controller_page_background_description": "백그라운드 서비스를 켜서 앱을 열지 않고도 새 미디어파일을 자동으로 백업합니다.",
"backup_controller_page_background_is_off": "자동 백그라운드 백업이 꺼져 있습니다",
"backup_controller_page_background_is_on": "자동 백그라운드 백업이 켜져 있습니다",
@@ -82,11 +89,21 @@
"cache_settings_subtitle": "Immich 앱의 캐싱 동작 제어",
"cache_settings_thumbnail_size": "썸네일 캐시 크기 ({} 미디어)",
"cache_settings_title": "캐시 설정",
"change_password_form_confirm_password": "비밀번호 확인",
"change_password_form_description": "{firstName} {lastName} 님, 안녕하세요.\n\n시스템에 처음 로그인했거나 비밀번호 변경 요청이 있었습니다. 아래에 새 비밀번호를 입력하세요.",
"change_password_form_new_password": "새 비밀번호",
"change_password_form_password_mismatch": "비밀번호가 일치하지 않습니다",
"change_password_form_reenter_new_password": "새 비밀번호 재입력",
"common_add_to_album": "앨범에 추가",
"common_change_password": "비밀번호 변경",
"common_create_new_album": "새 앨범 만들기",
"common_shared": "공유됨",
"control_bottom_app_bar_add_to_album": "앨범에 추가",
"control_bottom_app_bar_album_info": "{} 항목",
"control_bottom_app_bar_album_info_shared": "{} 항목 · 공유됨",
"control_bottom_app_bar_create_new_album": "앨범 생성",
"control_bottom_app_bar_delete": "삭제",
"control_bottom_app_bar_favorite": "즐겨찾기",
"control_bottom_app_bar_share": "공유",
"create_album_page_untitled": "제목없음",
"create_shared_album_page_create": "만들기",
@@ -103,26 +120,49 @@
"exif_bottom_sheet_description": "설명 추가...",
"exif_bottom_sheet_details": "상세정보",
"exif_bottom_sheet_location": "위치",
"experimental_settings_new_asset_list_subtitle": "진행중",
"experimental_settings_new_asset_list_title": "실험적 사진 그리드 적용",
"experimental_settings_subtitle": "문제시 책임지지 않습니다!",
"experimental_settings_title": "실험적기능",
"favorites_page_title": "즐겨찾기",
"home_page_add_to_album_conflicts": "{album} 앨범에 {added} 미디어를 추가했습니다. {failed} 이미 앨범에 있는 항목입니다.",
"home_page_add_to_album_err_local": "앨범에 미디어파일을 추가할 수 없어, 건너뜁니다.",
"home_page_add_to_album_success": "{album} 앨범에 {added} 미디어를 추가했습니다. ",
"home_page_building_timeline": "타임라인 생성",
"home_page_favorite_err_local": "미디어파일을 즐겨찾기에 추가할 수 없어, 건너뜁니다.",
"home_page_first_time_notice": "앱을 처음 사용하는 경우 타임라인이 앨범의 사진과 비디오를 채울 수 있도록 백업대상 앨범을 선택해야 합니다.",
"image_viewer_page_state_provider_download_error": "다운로드 에러",
"image_viewer_page_state_provider_download_success": "다운로드 완료",
"library_page_albums": "앨범",
"library_page_favorites": "즐겨찾기",
"library_page_new_album": "새 앨범",
"library_page_sharing": "공유",
"library_page_sort_created": "최근생성일",
"library_page_sort_title": "앨범 제목",
"login_form_button_text": "로그인",
"login_form_email_hint": "youremail@email.com",
"login_form_endpoint_hint": "https://your-server-ip:port/api",
"login_form_endpoint_url": "서버 엔드포인트 URL",
"login_form_err_http": "엔드포인트는 http:// 또는 https://로 시작해야 합니다",
"login_form_err_invalid_email": "잘못된 이메일 형식입니다",
"login_form_err_invalid_url": "잘못된 URL 형식입니다",
"login_form_err_leading_whitespace": "이메일 앞에 공백문자가 포함되어 있습니다",
"login_form_err_trailing_whitespace": "이메일 뒤에 공백문자가 포함되어 있습니다",
"login_form_failed_get_oauth_server_config": "OAuth 로그인 오류, 서버 URL을 확인해주세요",
"login_form_failed_get_oauth_server_disable": "이 서버에서는 OAuth 기능을 사용할 수 없습니다.",
"login_form_failed_login": "로그인 오류, 서버 URL, 이메일 및 비밀번호를 확인하세요",
"login_form_label_email": "이메일",
"login_form_label_password": "비밀번호",
"login_form_password_hint": "비밀번호",
"login_form_save_login": "로그인상태 유지",
"monthly_title_text_date_format": "y년 M월",
"notification_permission_dialog_cancel": "취소",
"notification_permission_dialog_content": "알림을 활성화하려면 설정으로 이동하여 허용을 선택해주세요.",
"notification_permission_dialog_settings": "설정",
"notification_permission_list_tile_content": "알림 활성화 권한허용",
"notification_permission_list_tile_enable_button": "알림 활성화",
"notification_permission_list_tile_title": "알림 권한",
"profile_drawer_app_logs": "로그",
"profile_drawer_client_server_up_to_date": "클라이언트와 서버가 최신 상태입니다",
"profile_drawer_settings": "설정",
"profile_drawer_sign_out": "로그아웃",
@@ -135,11 +175,19 @@
"select_additional_user_for_sharing_page_suggestions": "초대 가능한 사용자 제안",
"select_user_for_sharing_page_err_album": "앨범 생성 실패",
"select_user_for_sharing_page_share_suggestions": "초대 가능한 사용자 제안",
"server_info_box_app_version": "앱 버전",
"server_info_box_server_version": "서버 버전",
"setting_image_viewer_help": "상세뷰어는 먼저 작은 썸네일을 불러온 다음 중간크기 미리보기를 불러오고(활성화된 경우) 마지막으로 원본을 불러옵니다(활성화된 경우).",
"setting_image_viewer_original_subtitle": "원본 해상도 이미지(고화질)를 로드하려면 활성화합니다. 데이터 사용량을 줄이려면 비활성화합니다.",
"setting_image_viewer_original_title": "원본 이미지 불러오기",
"setting_image_viewer_preview_subtitle": "중간 해상도 이미지를 로드하려면 활성화합니다. 원본을 직접 로드하거나 썸네일만 사용하려면 비활성화 하세요.",
"setting_image_viewer_preview_title": "미리보기 이미지 불러오기",
"setting_notifications_notify_failures_grace_period": "백그라운드 백업 실패 알림: {}",
"setting_notifications_notify_hours": "{}시간 뒤",
"setting_notifications_notify_immediately": "즉시",
"setting_notifications_notify_minutes": "{}분 뒤",
"setting_notifications_notify_never": "알리지 않음",
"setting_notifications_notify_seconds": "{} 초",
"setting_notifications_single_progress_subtitle": "미디어별 상세 진행률 표시",
"setting_notifications_single_progress_title": "백그라운드 작업 세부 진행률 표시",
"setting_notifications_subtitle": "알림 기본 설정 조정",
@@ -147,6 +195,7 @@
"setting_notifications_total_progress_subtitle": "전체 업로드 진행률(완료/전체)",
"setting_notifications_total_progress_title": "백그라운드 작업 전체 진행률 표시",
"setting_pages_app_bar_settings": "설정",
"settings_require_restart": "설정을 적용하려면 Immich를 다시 시작하세요.",
"share_add": "추가",
"share_add_photos": "사진 추가",
"share_add_title": "새 앨범제목",

View File

@@ -90,12 +90,18 @@ class BackgroundServicePlugin: NSObject, FlutterPlugin {
let defaults = UserDefaults.standard
let lastRunTime = defaults.value(forKey: "last_background_fetch_run_time")
result(lastRunTime)
break
case "lastBackgroundProcessingTime":
let defaults = UserDefaults.standard
let lastRunTime = defaults.value(forKey: "last_background_processing_run_time")
result(lastRunTime)
break
case "numberOfBackgroundProcesses":
handleNumberOfProcesses(call: call, result: result)
break
case "backgroundAppRefreshEnabled":
handleBackgroundRefreshStatus(call: call, result: result)
break
default:
result(FlutterMethodNotImplemented)
break
@@ -138,11 +144,10 @@ class BackgroundServicePlugin: NSObject, FlutterPlugin {
// This is not used yet and will need to be implemented
defaults.set(notificationTitle, forKey: "notification_title")
// Schedule the background services if instant
if (instant ?? true) {
BackgroundServicePlugin.scheduleBackgroundSync()
BackgroundServicePlugin.scheduleBackgroundFetch()
}
// Schedule the background services
BackgroundServicePlugin.scheduleBackgroundSync()
BackgroundServicePlugin.scheduleBackgroundFetch()
result(true)
}
@@ -209,15 +214,31 @@ class BackgroundServicePlugin: NSObject, FlutterPlugin {
result(true)
}
// Checks the status of the Background App Refresh from the system
// Returns true if the service is enabled for Immich, and false otherwise
func handleBackgroundRefreshStatus(call: FlutterMethodCall, result: FlutterResult) {
switch UIApplication.shared.backgroundRefreshStatus {
case .available:
result(true)
break
case .denied:
result(false)
break
case .restricted:
result(false)
break
default:
result(false)
break
}
}
// Schedules a short-running background sync to sync only a few photos
static func scheduleBackgroundFetch() {
// We will only schedule this task to run if the user has explicitely allowed us to backup while
// not connected to power
let defaults = UserDefaults.standard
if defaults.value(forKey: "require_charging") as? Bool == true {
return
}
// We will schedule this task to run no matter the charging or wifi requirents from the end user
// 1. They can set Background App Refresh to Off / Wi-Fi / Wi-Fi & Cellular Data from Settings
// 2. We will check the battery connectivity when we begin running the background activity
let backgroundFetch = BGAppRefreshTaskRequest(identifier: BackgroundServicePlugin.backgroundFetchTaskID)
// Use 5 minutes from now as earliest begin date
@@ -255,10 +276,26 @@ class BackgroundServicePlugin: NSObject, FlutterPlugin {
// This function runs when the system kicks off the BGAppRefreshTask from the Background Task Scheduler
static func handleBackgroundFetch(task: BGAppRefreshTask) {
// Schedule the next sync task so we can run this again later
scheduleBackgroundFetch()
// Log the time of last background processing to now
let defaults = UserDefaults.standard
defaults.set(Date().timeIntervalSince1970, forKey: "last_background_fetch_run_time")
// If we have required charging, we should check the charging status
let requireCharging = defaults.value(forKey: "require_charging") as? Bool
if (requireCharging ?? false) {
UIDevice.current.isBatteryMonitoringEnabled = true
if (UIDevice.current.batteryState == .unplugged) {
// The device is unplugged and we have required charging
// Therefore, we will simply complete the task without
// running it.
task.setTaskCompleted(success: true)
return
}
}
// Schedule the next sync task so we can run this again later
scheduleBackgroundFetch()
@@ -268,13 +305,13 @@ class BackgroundServicePlugin: NSObject, FlutterPlugin {
// This function runs when the system kicks off the BGProcessingTask from the Background Task Scheduler
static func handleBackgroundProcessing(task: BGProcessingTask) {
// Schedule the next sync task so we run this again later
scheduleBackgroundSync()
// Log the time of last background processing to now
let defaults = UserDefaults.standard
defaults.set(Date().timeIntervalSince1970, forKey: "last_background_processing_run_time")
// Schedule the next sync task so we run this again later
scheduleBackgroundSync()
// We won't specify a max time for the background sync service, so this can run for longer
BackgroundServicePlugin.runBackgroundSync(task, maxSeconds: nil)
}

View File

@@ -19,7 +19,7 @@ platform :ios do
desc "iOS Beta"
lane :beta do
increment_version_number(
version_number: "1.48.1"
version_number: "1.49.0"
)
increment_build_number(
build_number: latest_testflight_build_number + 1,

View File

@@ -12,6 +12,7 @@ import 'package:immich_mobile/modules/backup/background_service/background.servi
import 'package:immich_mobile/modules/backup/models/hive_backup_albums.model.dart';
import 'package:immich_mobile/modules/backup/models/hive_duplicated_assets.model.dart';
import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
import 'package:immich_mobile/modules/backup/providers/ios_background_settings.provider.dart';
import 'package:immich_mobile/modules/login/models/hive_saved_login_info.model.dart';
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
import 'package:immich_mobile/modules/settings/providers/permission.provider.dart';
@@ -31,6 +32,7 @@ import 'package:immich_mobile/shared/views/version_announcement_overlay.dart';
import 'package:immich_mobile/utils/immich_app_theme.dart';
import 'package:immich_mobile/utils/migration.dart';
import 'package:isar/isar.dart';
import 'package:logging/logging.dart';
import 'package:path_provider/path_provider.dart';
import 'constants/hive_box.dart';
@@ -72,6 +74,18 @@ Future<void> initApp() async {
// Initialize Immich Logger Service
ImmichLogger().init();
var log = Logger("ImmichErrorLogger");
FlutterError.onError = (details) {
FlutterError.presentError(details);
log.severe(details.toString(), details, details.stack);
};
PlatformDispatcher.instance.onError = (error, stack) {
log.severe(error.toString(), error, stack);
return true;
};
}
Future<Isar> loadDb() async {
@@ -127,8 +141,11 @@ class ImmichAppState extends ConsumerState<ImmichApp>
ref.watch(releaseInfoProvider.notifier).checkGithubReleaseInfo();
ref.watch(notificationPermissionProvider.notifier)
.getNotificationPermission();
ref
.watch(notificationPermissionProvider.notifier)
.getNotificationPermission();
ref.read(iOSBackgroundSettingsProvider.notifier).refresh();
break;

View File

@@ -31,7 +31,7 @@ import 'package:openapi/api.dart' as api;
// ignore: must_be_immutable
class GalleryViewerPage extends HookConsumerWidget {
late List<Asset> assetList;
final List<Asset> assetList;
final Asset asset;
GalleryViewerPage({
@@ -43,7 +43,7 @@ class GalleryViewerPage extends HookConsumerWidget {
Asset? assetDetail;
late PageController controller;
final PageController controller;
@override
Widget build(BuildContext context, WidgetRef ref) {
@@ -197,6 +197,7 @@ class GalleryViewerPage extends HookConsumerWidget {
curve: Curves.fastLinearToSlowEaseIn,
);
}
assetList.remove(deleteAsset);
ref.watch(assetProvider.notifier).deleteAssets({deleteAsset});
},
);

View File

@@ -574,6 +574,10 @@ class BackgroundService {
Future<int> getIOSBackupNumberOfProcesses() async {
return await _foregroundChannel.invokeMethod('numberOfBackgroundProcesses');
}
Future<bool> getIOSBackgroundAppRefreshEnabled() async {
return await _foregroundChannel.invokeMethod('backgroundAppRefreshEnabled');
}
}
enum IosBackgroundTask { fetch, processing }

View File

@@ -204,10 +204,19 @@ class BackupNotifier extends StateNotifier<BackUpState> {
if (assetList.isNotEmpty) {
var thumbnailAsset = assetList.first;
var thumbnailData = await thumbnailAsset
.thumbnailDataWithSize(const ThumbnailSize(512, 512));
availableAlbum =
availableAlbum.copyWith(thumbnailData: thumbnailData);
try {
var thumbnailData = await thumbnailAsset
.thumbnailDataWithSize(const ThumbnailSize(512, 512));
availableAlbum =
availableAlbum.copyWith(thumbnailData: thumbnailData);
} catch (e, stack) {
log.severe(
"Failed to get thumbnail for album ${album.name}",
e.toString(),
stack,
);
}
}
availableAlbums.add(availableAlbum);

View File

@@ -0,0 +1,57 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/backup/background_service/background.service.dart';
class IOSBackgroundSettings {
final bool appRefreshEnabled;
final int numberOfBackgroundTasksQueued;
final DateTime? timeOfLastFetch;
final DateTime? timeOfLastProcessing;
IOSBackgroundSettings({
required this.appRefreshEnabled,
required this.numberOfBackgroundTasksQueued,
this.timeOfLastFetch,
this.timeOfLastProcessing,
});
}
class IOSBackgroundSettingsNotifier extends StateNotifier<IOSBackgroundSettings?> {
final BackgroundService _service;
IOSBackgroundSettingsNotifier(this._service) : super(null);
IOSBackgroundSettings? get settings => state;
Future<IOSBackgroundSettings> refresh() async {
final lastFetchTime = await _service.getIOSBackupLastRun(IosBackgroundTask.fetch);
final lastProcessingTime = await _service.getIOSBackupLastRun(IosBackgroundTask.processing);
int numberOfProcesses = await _service.getIOSBackupNumberOfProcesses();
final appRefreshEnabled = await _service.getIOSBackgroundAppRefreshEnabled();
// If this is enabled and there are no background processes,
// the user just enabled app refresh in Settings.
// But we don't have any background services running, since it was disabled
// before.
if (await _service.isBackgroundBackupEnabled() &&
numberOfProcesses == 0) {
// We need to restart the background service
await _service.enableService();
numberOfProcesses = await _service.getIOSBackupNumberOfProcesses();
}
final settings = IOSBackgroundSettings(
appRefreshEnabled: appRefreshEnabled,
numberOfBackgroundTasksQueued: numberOfProcesses,
timeOfLastFetch: lastFetchTime,
timeOfLastProcessing: lastProcessingTime,
);
state = settings;
return settings;
}
}
final iOSBackgroundSettingsProvider = StateNotifierProvider<IOSBackgroundSettingsNotifier, IOSBackgroundSettings?>(
(ref) => IOSBackgroundSettingsNotifier(ref.watch(backgroundServiceProvider)),
);

View File

@@ -1,78 +1,61 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/backup/background_service/background.service.dart';
import 'package:immich_mobile/modules/backup/providers/ios_background_settings.provider.dart';
import 'package:intl/intl.dart';
/// This is a simple debug widget which should be removed later on when we are
/// more confident about background sync
class IosDebugInfoTile extends HookConsumerWidget {
const IosDebugInfoTile({super.key});
final IOSBackgroundSettings settings;
const IosDebugInfoTile({
super.key,
required this.settings,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final futures = [
ref
.read(backgroundServiceProvider)
.getIOSBackupLastRun(IosBackgroundTask.fetch),
ref
.read(backgroundServiceProvider)
.getIOSBackupLastRun(IosBackgroundTask.processing),
ref.read(backgroundServiceProvider).getIOSBackupNumberOfProcesses(),
];
return FutureBuilder<List<dynamic>>(
future: Future.wait(futures),
builder: (context, snapshot) {
String? title;
String? subtitle;
if (snapshot.hasData) {
final results = snapshot.data as List<dynamic>;
final fetch = results[0] as DateTime?;
final processing = results[1] as DateTime?;
final processes = results[2] as int;
final fetch = settings.timeOfLastFetch;
final processing = settings.timeOfLastProcessing;
final processes = settings.numberOfBackgroundTasksQueued;
final processOrProcesses = processes == 1 ? 'process' : 'processes';
final numberOrZero = processes == 0 ? 'No' : processes.toString();
title = '$numberOrZero background $processOrProcesses queued';
final processOrProcesses = processes == 1 ? 'process' : 'processes';
final numberOrZero = processes == 0 ? 'No' : processes.toString();
final title = '$numberOrZero background $processOrProcesses queued';
final df = DateFormat.yMd().add_jm();
if (fetch == null && processing == null) {
subtitle = 'No background sync job has run yet';
} else if (fetch != null && processing == null) {
subtitle = 'Fetch ran ${df.format(fetch)}';
} else if (processing != null && fetch == null) {
subtitle = 'Processing ran ${df.format(processing)}';
} else {
final fetchOrProcessing =
fetch!.isAfter(processing!) ? fetch : processing;
subtitle = 'Last sync ${df.format(fetchOrProcessing)}';
}
}
final df = DateFormat.yMd().add_jm();
final String subtitle;
if (fetch == null && processing == null) {
subtitle = 'No background sync job has run yet';
} else if (fetch != null && processing == null) {
subtitle = 'Fetch ran ${df.format(fetch)}';
} else if (processing != null && fetch == null) {
subtitle = 'Processing ran ${df.format(processing)}';
} else {
final fetchOrProcessing =
fetch!.isAfter(processing!) ? fetch : processing;
subtitle = 'Last sync ${df.format(fetchOrProcessing)}';
}
return AnimatedSwitcher(
duration: const Duration(milliseconds: 200),
child: ListTile(
key: ValueKey(title),
title: Text(
title ?? '',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
color: Theme.of(context).primaryColor,
),
),
subtitle: Text(
subtitle ?? '',
style: const TextStyle(
fontSize: 14,
),
),
leading: Icon(
Icons.bug_report,
color: Theme.of(context).primaryColor,
),
),
);
},
return ListTile(
key: ValueKey(title),
title: Text(
title,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
color: Theme.of(context).primaryColor,
),
),
subtitle: Text(
subtitle,
style: const TextStyle(
fontSize: 14,
),
),
leading: Icon(
Icons.bug_report,
color: Theme.of(context).primaryColor,
),
);
}
}

View File

@@ -5,7 +5,9 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/backup/background_service/background.service.dart';
import 'package:immich_mobile/modules/backup/providers/error_backup_list.provider.dart';
import 'package:immich_mobile/modules/backup/providers/ios_background_settings.provider.dart';
import 'package:immich_mobile/modules/backup/ui/current_backup_asset_info_box.dart';
import 'package:immich_mobile/modules/backup/ui/ios_debug_info_tile.dart';
import 'package:immich_mobile/modules/login/models/authentication_state.model.dart';
@@ -15,6 +17,7 @@ import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/shared/providers/websocket.provider.dart';
import 'package:immich_mobile/modules/backup/ui/backup_info_card.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:url_launcher/url_launcher.dart';
class BackupControllerPage extends HookConsumerWidget {
@@ -24,6 +27,10 @@ class BackupControllerPage extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
BackUpState backupState = ref.watch(backupProvider);
AuthenticationState authenticationState = ref.watch(authenticationProvider);
final settings = ref.watch(iOSBackgroundSettingsProvider.notifier).settings;
final appRefreshDisabled = Platform.isIOS &&
settings?.appRefreshEnabled != true;
bool hasExclusiveAccess =
backupState.backupProgress != BackUpProgressEnum.inBackground;
bool shouldBackup = backupState.allUniqueAssets.length -
@@ -40,6 +47,13 @@ class BackupControllerPage extends HookConsumerWidget {
ref.watch(backupProvider.notifier).getBackupInfo();
}
// Update the background settings information just to make sure we
// have the latest, since the platform channel will not update
// automatically
if (Platform.isIOS) {
ref.watch(iOSBackgroundSettingsProvider.notifier).refresh();
}
ref
.watch(websocketProvider.notifier)
.stopListenToEvent('on_upload_success');
@@ -362,14 +376,65 @@ class BackupControllerPage extends HookConsumerWidget {
],
),
),
if (isBackgroundEnabled)
IosDebugInfoTile(
key: ValueKey(isChargingRequired),
if (isBackgroundEnabled && Platform.isIOS)
FutureBuilder(
future: ref
.read(backgroundServiceProvider)
.getIOSBackgroundAppRefreshEnabled(),
builder: (context, snapshot) {
final enabled = snapshot.data as bool?;
// If it's not enabled, show them some kind of alert that says
// background refresh is not enabled
if (enabled != null && !enabled) {
}
// If it's enabled, no need to bother them
return Container();
},
),
if (isBackgroundEnabled && settings != null)
IosDebugInfoTile(
settings: settings,
),
],
);
}
Widget buildBackgroundAppRefreshWarning() {
return ListTile(
isThreeLine: true,
leading: const Icon(Icons.task_outlined,),
title: const Text(
'backup_controller_page_background_app_refresh_disabled_title',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
),
).tr(),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: const Text(
'backup_controller_page_background_app_refresh_disabled_content',
).tr(),
),
ElevatedButton(
onPressed: () => openAppSettings(),
child: const Text(
'backup_controller_page_background_app_refresh_enable_button_text',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 12,
),
).tr(),
),
],
),
);
}
Widget buildSelectedAlbumName() {
var text = "backup_controller_page_backup_selected".tr();
var albums = ref.watch(backupProvider).selectedBackupAlbums;
@@ -613,7 +678,15 @@ class BackupControllerPage extends HookConsumerWidget {
const Divider(),
buildAutoBackupController(),
const Divider(),
buildBackgroundBackupController(),
AnimatedSwitcher(
duration: const Duration(milliseconds: 500),
child: Platform.isIOS
? (
appRefreshDisabled
? buildBackgroundAppRefreshWarning()
: buildBackgroundBackupController()
) : buildBackgroundBackupController(),
),
const Divider(),
buildStorageInformation(),
const Divider(),
@@ -624,4 +697,6 @@ class BackupControllerPage extends HookConsumerWidget {
),
);
}
}

View File

@@ -77,7 +77,8 @@ class AssetService {
.map((e) => Asset.local(e, userId))
.toList(growable: false);
}
} catch (e) {
} catch (e, stackTrace) {
log.severe('Error while getting local assets', e, stackTrace);
debugPrint("Error [_getLocalAssets] ${e.toString()}");
}
return null;

View File

@@ -3,7 +3,7 @@ Immich API
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
- API version: 1.48.0
- API version: 1.48.1
- Build package: org.openapitools.codegen.languages.DartClientCodegen
## Requirements

View File

@@ -165,10 +165,10 @@ packages:
dependency: transitive
description:
name: cancellation_token
sha256: e40ac742c7faac52e1719ce249934e20975e5772d40112e1e01cfc5abf24185a
sha256: "44891ef71d605bc59ef7974c403630d8e8506fcd897a29c3e38466ef69e5c4eb"
url: "https://pub.dev"
source: hosted
version: "1.5.0"
version: "1.6.1"
cancellation_token_http:
dependency: "direct main"
description:
@@ -333,10 +333,10 @@ packages:
dependency: transitive
description:
name: fixnum
sha256: "04be3e934c52e082558cc9ee21f42f5c1cd7a1262f4c63cd0357c08d5bba81ec"
sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1"
url: "https://pub.dev"
source: hosted
version: "1.0.1"
version: "1.1.0"
flutter:
dependency: "direct main"
description: flutter
@@ -420,10 +420,10 @@ packages:
dependency: transitive
description:
name: flutter_plugin_android_lifecycle
sha256: "60fc7b78455b94e6de2333d2f95196d32cf5c22f4b0b0520a628804cb463503b"
sha256: "4bef634684b2c7f3468c77c766c831229af829a0cd2d4ee6c1b99558bd14e5d2"
url: "https://pub.dev"
source: hosted
version: "2.0.7"
version: "2.0.8"
flutter_riverpod:
dependency: transitive
description:
@@ -462,10 +462,10 @@ packages:
dependency: "direct main"
description:
name: fluttertoast
sha256: "774fa28b07f3a82c93596bc137be33189fec578ed3447a93a5a11c93435de394"
sha256: "2f9c4d3f4836421f7067a28f8939814597b27614e021da9d63e5d3fb6e212d25"
url: "https://pub.dev"
source: hosted
version: "8.1.3"
version: "8.2.1"
frontend_server_client:
dependency: transitive
description:
@@ -483,10 +483,10 @@ packages:
dependency: transitive
description:
name: glob
sha256: c51b4fdfee4d281f49b8c957f1add91b815473597f76bcf07377987f66a55729
sha256: "4515b5b6ddb505ebdd242a5f2cc5d22d3d6a80013789debfbda7777f47ea308c"
url: "https://pub.dev"
source: hosted
version: "2.1.0"
version: "2.1.1"
graphs:
dependency: transitive
description:
@@ -571,42 +571,42 @@ packages:
dependency: "direct main"
description:
name: image_picker
sha256: f98d76672d309c8b7030c323b3394669e122d52b307d2bbd8d06bd70f5b2aabe
sha256: "22207768556b82d55ec70166824350fee32298732d5efa4d6e756f848f51f66a"
url: "https://pub.dev"
source: hosted
version: "0.8.6+1"
version: "0.8.6+3"
image_picker_android:
dependency: transitive
description:
name: image_picker_android
sha256: "385f12ee9c7288575572c7873a332016ec45ebd092e1c2f6bd421b4a9ad21f1d"
sha256: "68d067baf7f6e401b1124ee83dd6967e67847314250fd68012aab34a69beb344"
url: "https://pub.dev"
source: hosted
version: "0.8.5+6"
version: "0.8.5+7"
image_picker_for_web:
dependency: transitive
description:
name: image_picker_for_web
sha256: "7d319fb74955ca46d9bf7011497860e3923bb67feebcf068f489311065863899"
sha256: "66fc6e3877bbde82c33d122f3588777c3784ac5bd7d1cdd79213ef7aecb85b34"
url: "https://pub.dev"
source: hosted
version: "2.1.10"
version: "2.1.11"
image_picker_ios:
dependency: transitive
description:
name: image_picker_ios
sha256: "8ffb14b43713d7c43fb21299cc18181cc5b39bd3ea1cc427a085c6400fe5aa52"
sha256: "39aa70b5f1e5e7c94585b9738632d5fdb764a5655e40cd9e7b95fbd2fc50c519"
url: "https://pub.dev"
source: hosted
version: "0.8.6+7"
version: "0.8.6+9"
image_picker_platform_interface:
dependency: transitive
description:
name: image_picker_platform_interface
sha256: "7cef2f28f4f2fef99180f636c3d446b4ccbafd6ba0fad2adc9a80c4040f656b8"
sha256: "1991219d9dbc42a99aff77e663af8ca51ced592cd6685c9485e3458302d3d4f8"
url: "https://pub.dev"
source: hosted
version: "2.6.2"
version: "2.6.3"
integration_test:
dependency: "direct dev"
description: flutter
@@ -831,26 +831,26 @@ packages:
dependency: "direct main"
description:
name: path_provider
sha256: dcea5feb97d8abf90cab9e9030b497fb7c3cbf26b7a1fe9e3ef7dcb0a1ddec95
sha256: "04890b994ee89bfa80bf3080bfec40d5a92c5c7a785ebb02c13084a099d2b6f9"
url: "https://pub.dev"
source: hosted
version: "2.0.12"
version: "2.0.13"
path_provider_android:
dependency: transitive
description:
name: path_provider_android
sha256: a776c088d671b27f6e3aa8881d64b87b3e80201c64e8869b811325de7a76c15e
sha256: "7623b7d4be0f0f7d9a8b5ee6879fc13e4522d4c875ab86801dee4af32b54b83e"
url: "https://pub.dev"
source: hosted
version: "2.0.22"
version: "2.0.23"
path_provider_foundation:
dependency: transitive
description:
name: path_provider_foundation
sha256: "62a68e7e1c6c459f9289859e2fae58290c981ce21d1697faf54910fe1faa4c74"
sha256: eec003594f19fe2456ea965ae36b3fc967bc5005f508890aafe31fa75e41d972
url: "https://pub.dev"
source: hosted
version: "2.1.1"
version: "2.1.2"
path_provider_ios:
dependency: "direct main"
description:
@@ -863,26 +863,26 @@ packages:
dependency: transitive
description:
name: path_provider_linux
sha256: "2e32f1640f07caef0d3cb993680f181c79e54a3827b997d5ee221490d131fbd9"
sha256: "525ad5e07622d19447ad740b1ed5070031f7a5437f44355ae915ff56e986429a"
url: "https://pub.dev"
source: hosted
version: "2.1.8"
version: "2.1.9"
path_provider_platform_interface:
dependency: transitive
description:
name: path_provider_platform_interface
sha256: f0abc8ebd7253741f05488b4813d936b4d07c6bae3e86148a09e342ee4b08e76
sha256: "57585299a729335f1298b43245842678cb9f43a6310351b18fb577d6e33165ec"
url: "https://pub.dev"
source: hosted
version: "2.0.5"
version: "2.0.6"
path_provider_windows:
dependency: transitive
description:
name: path_provider_windows
sha256: bcabbe399d4042b8ee687e17548d5d3f527255253b4a639f5f8d2094a9c2b45c
sha256: "642ddf65fde5404f83267e8459ddb4556316d3ee6d511ed193357e25caa3632d"
url: "https://pub.dev"
source: hosted
version: "2.1.3"
version: "2.1.4"
pedantic:
dependency: transitive
description:
@@ -959,10 +959,10 @@ packages:
dependency: transitive
description:
name: plugin_platform_interface
sha256: dbf0f707c78beedc9200146ad3cb0ab4d5da13c246336987be6940f026500d3a
sha256: "6a2128648c854906c53fa8e33986fc0247a1116122f9534dd20e3ab9e16a32bc"
url: "https://pub.dev"
source: hosted
version: "2.1.3"
version: "2.1.4"
pool:
dependency: transitive
description:
@@ -1063,58 +1063,58 @@ packages:
dependency: transitive
description:
name: shared_preferences
sha256: "5949029e70abe87f75cfe59d17bf5c397619c4b74a099b10116baeb34786fad9"
sha256: ee6257848f822b8481691f20c3e6d2bfee2e9eccb2a3d249907fcfb198c55b41
url: "https://pub.dev"
source: hosted
version: "2.0.17"
version: "2.0.18"
shared_preferences_android:
dependency: transitive
description:
name: shared_preferences_android
sha256: "955e9736a12ba776bdd261cf030232b30eadfcd9c79b32a3250dd4a494e8c8f7"
sha256: a51a4f9375097f94df1c6e0a49c0374440d31ab026b59d58a7e7660675879db4
url: "https://pub.dev"
source: hosted
version: "2.0.15"
version: "2.0.16"
shared_preferences_foundation:
dependency: transitive
description:
name: shared_preferences_foundation
sha256: "2b55c18636a4edc529fa5cd44c03d3f3100c00513f518c5127c951978efcccd0"
sha256: "6b84fdf06b32bb336f972d373cd38b63734f3461ba56ac2ba01b56d052796259"
url: "https://pub.dev"
source: hosted
version: "2.1.3"
version: "2.1.4"
shared_preferences_linux:
dependency: transitive
description:
name: shared_preferences_linux
sha256: f8ea038aa6da37090093974ebdcf4397010605fd2ff65c37a66f9d28394cb874
sha256: d7fb71e6e20cd3dfffcc823a28da3539b392e53ed5fc5c2b90b55fdaa8a7e8fa
url: "https://pub.dev"
source: hosted
version: "2.1.3"
version: "2.1.4"
shared_preferences_platform_interface:
dependency: transitive
description:
name: shared_preferences_platform_interface
sha256: da9431745ede5ece47bc26d5d73a9d3c6936ef6945c101a5aca46f62e52c1cf3
sha256: "824bfd02713e37603b2bdade0842e47d56e7db32b1dcdd1cae533fb88e2913fc"
url: "https://pub.dev"
source: hosted
version: "2.1.0"
version: "2.1.1"
shared_preferences_web:
dependency: transitive
description:
name: shared_preferences_web
sha256: a4b5bc37fe1b368bbc81f953197d55e12f49d0296e7e412dfe2d2d77d6929958
sha256: "6737b757e49ba93de2a233df229d0b6a87728cea1684da828cbc718b65dcf9d7"
url: "https://pub.dev"
source: hosted
version: "2.0.4"
version: "2.0.5"
shared_preferences_windows:
dependency: transitive
description:
name: shared_preferences_windows
sha256: "5eaf05ae77658d3521d0e993ede1af962d4b326cd2153d312df716dc250f00c9"
sha256: bd014168e8484837c39ef21065b78f305810ceabc1d4f90be6e3b392ce81b46d
url: "https://pub.dev"
source: hosted
version: "2.1.3"
version: "2.1.4"
shelf:
dependency: transitive
description:
@@ -1284,10 +1284,10 @@ packages:
dependency: transitive
description:
name: transparent_image
sha256: e566a616922a781489f4d91cc939b1b3203b6e4a093317805f2f82f0bb0f8dec
sha256: e8991d955a2094e197ca24c645efec2faf4285772a4746126ca12875e54ca02f
url: "https://pub.dev"
source: hosted
version: "2.0.0"
version: "2.0.1"
tuple:
dependency: transitive
description:
@@ -1316,74 +1316,74 @@ packages:
dependency: transitive
description:
name: universal_io
sha256: "79f78ddad839ee3aae3ec7c01eb4575faf0d5c860f8e5223bc9f9c17f7f03cef"
sha256: "06866290206d196064fd61df4c7aea1ffe9a4e7c4ccaa8fcded42dd41948005d"
url: "https://pub.dev"
source: hosted
version: "2.0.4"
version: "2.2.0"
url_launcher:
dependency: "direct main"
description:
name: url_launcher
sha256: e8f2efc804810c0f2f5b485f49e7942179f56eabcfe81dce3387fec4bb55876b
sha256: "75f2846facd11168d007529d6cd8fcb2b750186bea046af9711f10b907e1587e"
url: "https://pub.dev"
source: hosted
version: "6.1.9"
version: "6.1.10"
url_launcher_android:
dependency: transitive
description:
name: url_launcher_android
sha256: "3e2f6dfd2c7d9cd123296cab8ef66cfc2c1a13f5845f42c7a0f365690a8a7dd1"
sha256: "1f4d9ebe86f333c15d318f81dcdc08b01d45da44af74552608455ebdc08d9732"
url: "https://pub.dev"
source: hosted
version: "6.0.23"
version: "6.0.24"
url_launcher_ios:
dependency: transitive
description:
name: url_launcher_ios
sha256: "0a5af0aefdd8cf820dd739886efb1637f1f24489900204f50984634c07a54815"
sha256: c9cd648d2f7ab56968e049d4e9116f96a85517f1dd806b96a86ea1018a3a82e5
url: "https://pub.dev"
source: hosted
version: "6.1.0"
version: "6.1.1"
url_launcher_linux:
dependency: transitive
description:
name: url_launcher_linux
sha256: "318c42cba924e18180c029be69caf0a1a710191b9ec49bb42b5998fdcccee3cc"
sha256: e29039160ab3730e42f3d811dc2a6d5f2864b90a70fb765ea60144b03307f682
url: "https://pub.dev"
source: hosted
version: "3.0.2"
version: "3.0.3"
url_launcher_macos:
dependency: transitive
description:
name: url_launcher_macos
sha256: "41988b55570df53b3dd2a7fc90c76756a963de6a8c5f8e113330cb35992e2094"
sha256: "2dddb3291a57b074dade66b5e07e64401dd2487caefd4e9e2f467138d8c7eb06"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
version: "3.0.3"
url_launcher_platform_interface:
dependency: transitive
description:
name: url_launcher_platform_interface
sha256: "4eae912628763eb48fc214522e58e942fd16ce195407dbf45638239523c759a6"
sha256: "6c9ca697a5ae218ce56cece69d46128169a58aa8653c1b01d26fcd4aad8c4370"
url: "https://pub.dev"
source: hosted
version: "2.1.1"
version: "2.1.2"
url_launcher_web:
dependency: transitive
description:
name: url_launcher_web
sha256: "44d79408ce9f07052095ef1f9a693c258d6373dc3944249374e30eff7219ccb0"
sha256: "574cfbe2390666003c3a1d129bdc4574aaa6728f0c00a4829a81c316de69dd9b"
url: "https://pub.dev"
source: hosted
version: "2.0.14"
version: "2.0.15"
url_launcher_windows:
dependency: transitive
description:
name: url_launcher_windows
sha256: b6217370f8eb1fd85c8890c539f5a639a01ab209a36db82c921ebeacefc7a615
sha256: "97c9067950a0d09cbd93e2e3f0383d1403989362b97102fbf446473a48079a4b"
url: "https://pub.dev"
source: hosted
version: "3.0.3"
version: "3.0.4"
uuid:
dependency: transitive
description:
@@ -1404,42 +1404,42 @@ packages:
dependency: "direct main"
description:
name: video_player
sha256: "59f7f31c919c59cbedd37c617317045f5f650dc0eeb568b0b0de9a36472bdb28"
sha256: "6cec15c21974282994577ffcfb5b42e64a699d38583138ec8dcb3d0a6902a41c"
url: "https://pub.dev"
source: hosted
version: "2.5.1"
version: "2.5.2"
video_player_android:
dependency: transitive
description:
name: video_player_android
sha256: "984388511230bac63feb53b2911a70e829fe0976b6b2213f5c579c4e0a882db3"
sha256: "0fc42778d794465f12456ccdade3e729e4339c8a112f9e58d170dc00f17b75f2"
url: "https://pub.dev"
source: hosted
version: "2.3.10"
version: "2.3.11"
video_player_avfoundation:
dependency: transitive
description:
name: video_player_avfoundation
sha256: d9f7a46d6a77680adb03ec05a381025d6e890ebe636637c6c3014cc3926b97e9
sha256: "5df5411ff9d316f1dcbfee284e9838aa686e314f2a722b15c02cb7ce40ef9446"
url: "https://pub.dev"
source: hosted
version: "2.3.8"
version: "2.3.9"
video_player_platform_interface:
dependency: transitive
description:
name: video_player_platform_interface
sha256: "42bb75de5e9b79e1f20f1d95f688fac0f95beac4d89c6eb2cd421724d4432dae"
sha256: "72ba04ad0eff76123c6d782ac46621cb8be476a89c33c89173fce982b6ec049b"
url: "https://pub.dev"
source: hosted
version: "6.0.1"
version: "6.0.2"
video_player_web:
dependency: transitive
description:
name: video_player_web
sha256: b649b07b8f8f553bee4a97a0a53d0fe78a70b115eafaf0105b612b32b05ddb99
sha256: d635bb2834f2b14cfd52c7fc9307a95dffbe768d116dd6047a4ecbba203289c8
url: "https://pub.dev"
source: hosted
version: "2.0.13"
version: "2.0.14"
vm_service:
dependency: transitive
description:
@@ -1540,10 +1540,10 @@ packages:
dependency: transitive
description:
name: xml
sha256: ac0e3f4bf00ba2708c33fbabbbe766300e509f8c82dbd4ab6525039813f7e2fb
sha256: "979ee37d622dec6365e2efa4d906c37470995871fe9ae080d967e192d88286b5"
url: "https://pub.dev"
source: hosted
version: "6.1.0"
version: "6.2.2"
xxh3:
dependency: transitive
description:
@@ -1561,5 +1561,5 @@ packages:
source: hosted
version: "3.1.1"
sdks:
dart: ">=2.18.0 <3.0.0"
dart: ">=2.19.0 <3.0.0"
flutter: ">=3.3.0"

View File

@@ -2,7 +2,7 @@ name: immich_mobile
description: Immich - selfhosted backup media file on mobile phone
publish_to: "none"
version: 1.48.1+71
version: 1.49.0+72
isar_version: &isar_version 3.0.5
environment:

View File

@@ -1,7 +1,7 @@
import { AlbumEntity, AssetEntity, UserEntity } from '@app/infra';
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, Not, IsNull, FindManyOptions, In } from 'typeorm';
import { Repository, Not, IsNull, FindManyOptions } from 'typeorm';
import { AddAssetsDto } from './dto/add-assets.dto';
import { AddUsersDto } from './dto/add-users.dto';
import { CreateAlbumDto } from './dto/create-album.dto';

View File

@@ -2728,7 +2728,7 @@
"info": {
"title": "Immich",
"description": "Immich API",
"version": "1.48.1",
"version": "1.49.0",
"contact": {}
},
"tags": [],

View File

@@ -1,6 +1,6 @@
{
"name": "immich",
"version": "1.48.1",
"version": "1.49.0",
"lockfileVersion": 2,
"requires": true,
"packages": {

View File

@@ -1,6 +1,6 @@
{
"name": "immich",
"version": "1.48.1",
"version": "1.49.0",
"description": "",
"author": "",
"private": true,

View File

@@ -0,0 +1,3 @@
module.exports = {
browser: false
};

18
web/package-lock.json generated
View File

@@ -16,6 +16,7 @@
"luxon": "^3.1.1",
"rxjs": "^7.8.0",
"socket.io-client": "^4.5.1",
"svelte-local-storage-store": "^0.4.0",
"svelte-material-icons": "^2.0.2"
},
"devDependencies": {
@@ -10584,6 +10585,17 @@
"svelte": ">= 3"
}
},
"node_modules/svelte-local-storage-store": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/svelte-local-storage-store/-/svelte-local-storage-store-0.4.0.tgz",
"integrity": "sha512-ctPykTt4S3BE5bF0mfV0jKiUR1qlmqLvnAkQvYHLeb9wRyO1MdIFDVI23X+TZEFleATHkTaOpYZswIvf3b2tWA==",
"engines": {
"node": ">=0.14"
},
"peerDependencies": {
"svelte": "^3.48.0"
}
},
"node_modules/svelte-material-icons": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/svelte-material-icons/-/svelte-material-icons-2.0.4.tgz",
@@ -19014,6 +19026,12 @@
"dev": true,
"requires": {}
},
"svelte-local-storage-store": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/svelte-local-storage-store/-/svelte-local-storage-store-0.4.0.tgz",
"integrity": "sha512-ctPykTt4S3BE5bF0mfV0jKiUR1qlmqLvnAkQvYHLeb9wRyO1MdIFDVI23X+TZEFleATHkTaOpYZswIvf3b2tWA==",
"requires": {}
},
"svelte-material-icons": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/svelte-material-icons/-/svelte-material-icons-2.0.4.tgz",

View File

@@ -68,6 +68,7 @@
"luxon": "^3.1.1",
"rxjs": "^7.8.0",
"socket.io-client": "^4.5.1",
"svelte-local-storage-store": "^0.4.0",
"svelte-material-icons": "^2.0.2"
}
}

View File

@@ -1,3 +1,4 @@
import { browser } from '$app/environment';
import { env } from '$env/dynamic/public';
import {
AlbumApi,
@@ -56,10 +57,11 @@ class ImmichApi {
}
}
// Browser side (public) API client
export const api = new ImmichApi();
const api = new ImmichApi();
// Server side API client
export const serverApi = new ImmichApi();
const immich_server_url = env.PUBLIC_IMMICH_SERVER_URL || 'http://immich-server:3001';
serverApi.setBaseUrl(immich_server_url);
if (!browser) {
const serverUrl = env.PUBLIC_IMMICH_SERVER_URL || 'http://immich-server:3001';
api.setBaseUrl(serverUrl);
}
export { api };

View File

@@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.48.0
* The version of the OpenAPI document: 1.48.1
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View File

@@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.48.0
* The version of the OpenAPI document: 1.48.1
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View File

@@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.48.0
* The version of the OpenAPI document: 1.48.1
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View File

@@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.48.0
* The version of the OpenAPI document: 1.48.1
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View File

@@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.48.0
* The version of the OpenAPI document: 1.48.1
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View File

@@ -4,6 +4,15 @@
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
<script>
/**
* Prevent FOUC on page load.
*/
const theme = localStorage.getItem('color-theme') || 'dark';
if (theme === 'light') {
document.documentElement.classList.remove('dark');
}
</script>
</head>
<body class="bg-immich-bg dark:bg-immich-dark-bg">

View File

@@ -3,7 +3,7 @@
import SelectionSearch from 'svelte-material-icons/SelectionSearch.svelte';
import Play from 'svelte-material-icons/Play.svelte';
import AllInclusive from 'svelte-material-icons/AllInclusive.svelte';
import { locale } from '$lib/stores/preferences.store';
import { createEventDispatcher } from 'svelte';
import { JobCounts } from '@api';
@@ -22,8 +22,6 @@
const run = (includeAllAssets: boolean) => {
dispatch('click', { includeAllAssets });
};
const locale = navigator.language;
</script>
<div class="flex justify-between rounded-3xl bg-gray-100 dark:bg-immich-dark-gray">
@@ -45,7 +43,7 @@
<p>Active</p>
<p class="text-2xl">
{#if jobCounts.active !== undefined}
{jobCounts.active.toLocaleString(locale)}
{jobCounts.active.toLocaleString($locale)}
{:else}
<LoadingSpinner />
{/if}
@@ -57,7 +55,7 @@
>
<p class="text-2xl">
{#if jobCounts.waiting !== undefined}
{jobCounts.waiting.toLocaleString(locale)}
{jobCounts.waiting.toLocaleString($locale)}
{:else}
<LoadingSpinner />
{/if}

View File

@@ -7,6 +7,7 @@
import { getBytesWithUnit, asByteUnitString } from '../../../utils/byte-units';
import { onMount, onDestroy } from 'svelte';
import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
import { locale } from '$lib/stores/preferences.store';
export let allUsers: Array<UserResponseDto>;
@@ -37,8 +38,6 @@
// Stats are unavailable if data is not loaded yet
$: [spaceUsage, spaceUnit] = getBytesWithUnit(stats ? stats.usageRaw : 0);
const locale = navigator.language;
</script>
<div class="flex flex-col gap-5">
@@ -83,8 +82,10 @@
}`}
>
<td class="text-sm px-2 w-1/4 text-ellipsis">{getFullName(user.userId)}</td>
<td class="text-sm px-2 w-1/4 text-ellipsis">{user.photos.toLocaleString(locale)}</td>
<td class="text-sm px-2 w-1/4 text-ellipsis">{user.videos.toLocaleString(locale)}</td>
<td class="text-sm px-2 w-1/4 text-ellipsis">{user.photos.toLocaleString($locale)}</td
>
<td class="text-sm px-2 w-1/4 text-ellipsis">{user.videos.toLocaleString($locale)}</td
>
<td class="text-sm px-2 w-1/4 text-ellipsis">{asByteUnitString(user.usageRaw)}</td>
</tr>
{/each}

View File

@@ -17,6 +17,7 @@
import DotsVertical from 'svelte-material-icons/DotsVertical.svelte';
import CircleIconButton from '../shared-components/circle-icon-button.svelte';
import noThumbnailUrl from '$lib/assets/no-thumbnail.png';
import { locale } from '$lib/stores/preferences.store';
export let album: AlbumResponseDto;
@@ -52,8 +53,6 @@
onMount(async () => {
imageData = (await loadHighQualityThumbnail(album.albumThumbnailAssetId)) || noThumbnailUrl;
});
const locale = navigator.language;
</script>
<div
@@ -91,7 +90,10 @@
</p>
<span class="text-xs flex gap-2 dark:text-immich-dark-fg" data-testid="album-details">
<p>{album.assetCount.toLocaleString(locale)} {album.assetCount == 1 ? `item` : `items`}</p>
<p>
{album.assetCount.toLocaleString($locale)}
{album.assetCount == 1 ? `item` : `items`}
</p>
{#if album.shared}
<p>·</p>

View File

@@ -39,6 +39,7 @@
import ThemeButton from '../shared-components/theme-button.svelte';
import { openFileUploadDialog } from '$lib/utils/file-uploader';
import { bulkDownload } from '$lib/utils/asset-utils';
import { locale } from '$lib/stores/preferences.store';
import GalleryViewer from '../shared-components/gallery-viewer/gallery-viewer.svelte';
import ImmichLogo from '../shared-components/immich-logo.svelte';
@@ -88,7 +89,6 @@
}
});
const locale = navigator.language;
const albumDateFormat: Intl.DateTimeFormatOptions = {
month: 'short',
day: 'numeric',
@@ -99,8 +99,8 @@
const startDate = new Date(album.assets[0].fileCreatedAt);
const endDate = new Date(album.assets[album.assetCount - 1].fileCreatedAt);
const startDateString = startDate.toLocaleDateString(locale, albumDateFormat);
const endDateString = endDate.toLocaleDateString(locale, albumDateFormat);
const startDateString = startDate.toLocaleDateString($locale, albumDateFormat);
const endDateString = endDate.toLocaleDateString($locale, albumDateFormat);
// If the start and end date are the same, only show one date
return startDateString === endDateString
@@ -380,7 +380,7 @@
>
<svelte:fragment slot="leading">
<p class="font-medium text-immich-primary dark:text-immich-dark-primary">
Selected {multiSelectAsset.size.toLocaleString(locale)}
Selected {multiSelectAsset.size.toLocaleString($locale)}
</p>
</svelte:fragment>
<svelte:fragment slot="trailing">

View File

@@ -11,12 +11,12 @@
assetsInAlbumStoreState,
selectedAssets
} from '$lib/stores/asset-interaction.store';
import { locale } from '$lib/stores/preferences.store';
const dispatch = createEventDispatcher();
export let albumId: string;
export let assetsInAlbum: AssetResponseDto[];
const locale = navigator.language;
onMount(() => {
$assetsInAlbumStoreState = assetsInAlbum;
@@ -51,7 +51,7 @@
<p class="text-lg dark:text-immich-dark-fg">Add to album</p>
{:else}
<p class="text-lg dark:text-immich-dark-fg">
{$selectedAssets.size.toLocaleString(locale)} selected
{$selectedAssets.size.toLocaleString($locale)} selected
</p>
{/if}
</svelte:fragment>

View File

@@ -24,6 +24,7 @@
import { assetStore } from '$lib/stores/assets.store';
import { addAssetsToAlbum } from '$lib/utils/asset-utils';
import { browser } from '$app/environment';
export let asset: AssetResponseDto;
export let publicSharedKey = '';
@@ -54,7 +55,9 @@
});
onDestroy(() => {
document.removeEventListener('keydown', onKeyboardPress);
if (browser) {
document.removeEventListener('keydown', onKeyboardPress);
}
});
$: asset.id && getAllAlbums(); // Update the album information when the asset ID changes

View File

@@ -8,6 +8,7 @@
import { browser } from '$app/environment';
import { AssetResponseDto, AlbumResponseDto } from '@api';
import { asByteUnitString } from '../../utils/byte-units';
import { locale } from '$lib/stores/preferences.store';
type Leaflet = typeof import('leaflet');
type LeafletMap = import('leaflet').Map;
@@ -69,8 +70,6 @@
return undefined;
};
const locale = navigator.language;
</script>
<section class="p-2 dark:bg-immich-dark-bg dark:text-immich-dark-fg">
@@ -101,7 +100,7 @@
<div>
<p>
{assetDateTimeOriginal.toLocaleDateString(locale, {
{assetDateTimeOriginal.toLocaleDateString($locale, {
month: 'short',
day: 'numeric',
year: 'numeric'
@@ -109,7 +108,7 @@
</p>
<div class="flex gap-2 text-sm">
<p>
{assetDateTimeOriginal.toLocaleString(locale, {
{assetDateTimeOriginal.toLocaleString($locale, {
weekday: 'short',
hour: 'numeric',
minute: '2-digit',
@@ -149,14 +148,14 @@
<div>
<p>{asset.exifInfo.make || ''} {asset.exifInfo.model || ''}</p>
<div class="flex text-sm gap-2">
<p>{`ƒ/${asset.exifInfo.fNumber.toLocaleString(locale)}` || ''}</p>
<p>{`ƒ/${asset.exifInfo.fNumber.toLocaleString($locale)}` || ''}</p>
{#if asset.exifInfo.exposureTime}
<p>{`${asset.exifInfo.exposureTime}`}</p>
{/if}
{#if asset.exifInfo.focalLength}
<p>{`${asset.exifInfo.focalLength.toLocaleString(locale)} mm`}</p>
<p>{`${asset.exifInfo.focalLength.toLocaleString($locale)} mm`}</p>
{/if}
{#if asset.exifInfo.iso}

View File

@@ -13,12 +13,13 @@
selectedAssets,
selectedGroup
} from '$lib/stores/asset-interaction.store';
import { locale } from '$lib/stores/preferences.store';
export let assets: AssetResponseDto[];
export let bucketDate: string;
export let bucketHeight: number;
export let isAlbumSelectionMode = false;
const locale = navigator.language;
const groupDateFormat: Intl.DateTimeFormatOptions = {
weekday: 'short',
month: 'short',
@@ -31,7 +32,7 @@
let hoveredDateGroup = '';
$: assetsGroupByDate = lodash
.chain(assets)
.groupBy((a) => new Date(a.fileCreatedAt).toLocaleDateString(locale, groupDateFormat))
.groupBy((a) => new Date(a.fileCreatedAt).toLocaleDateString($locale, groupDateFormat))
.sortBy((group) => assets.indexOf(group[0]))
.value();
@@ -115,7 +116,7 @@
>
{#each assetsGroupByDate as assetsInDateGroup, groupIndex (assetsInDateGroup[0].id)}
{@const dateGroupTitle = new Date(assetsInDateGroup[0].fileCreatedAt).toLocaleDateString(
locale,
$locale,
groupDateFormat
)}
<!-- Asset Group By Date -->

View File

@@ -18,6 +18,7 @@
notificationController,
NotificationType
} from '../shared-components/notification/notification';
import { locale } from '$lib/stores/preferences.store';
export let sharedLink: SharedLinkResponseDto;
export let isOwned: boolean;
@@ -86,8 +87,6 @@
clearMultiSelectAssetAssetHandler();
}
};
const locale = navigator.language;
</script>
<section class="bg-immich-bg dark:bg-immich-dark-bg">
@@ -99,7 +98,7 @@
>
<svelte:fragment slot="leading">
<p class="font-medium text-immich-primary dark:text-immich-dark-primary">
Selected {selectedAssets.size.toLocaleString(locale)}
Selected {selectedAssets.size.toLocaleString($locale)}
</p>
</svelte:fragment>
<svelte:fragment slot="trailing">

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import IntersectionObserver from '$lib/components/asset-viewer/intersection-observer.svelte';
import { AssetResponseDto, AssetTypeEnum, getFileUrl, ThumbnailFormat } from '@api';
import { createEventDispatcher, onDestroy } from 'svelte';
import { createEventDispatcher } from 'svelte';
import CheckCircle from 'svelte-material-icons/CheckCircle.svelte';
import MotionPauseOutline from 'svelte-material-icons/MotionPauseOutline.svelte';
import MotionPlayOutline from 'svelte-material-icons/MotionPlayOutline.svelte';
@@ -22,14 +22,13 @@
export let publicSharedKey = '';
export let isRoundedCorner = false;
let imageData: string;
let mouseOver = false;
let playMotionVideo = false;
$: dispatch('mouse-event', { isMouseOver: mouseOver, selectedGroupIndex: groupIndex });
let mouseOverIcon = false;
let videoPlayerNode: HTMLVideoElement;
let isImageLoading = true;
let isThumbnailVideoPlaying = false;
let calculateVideoDurationIntervalHandler: NodeJS.Timer;
let videoProgress = '00:00';
@@ -69,10 +68,6 @@
}
};
onDestroy(() => {
URL.revokeObjectURL(imageData);
});
const getSize = () => {
if (thumbnailSize) {
return `w-[${thumbnailSize}px] h-[${thumbnailSize}px]`;
@@ -255,12 +250,13 @@
id={asset.id}
style:width={`${thumbnailSize}px`}
style:height={`${thumbnailSize}px`}
in:fade={{ duration: 150 }}
src={`/api/asset/thumbnail/${asset.id}?format=${format}&key=${publicSharedKey}`}
alt={asset.id}
class={`object-cover ${getSize()} transition-all z-0 ${getThumbnailBorderStyle()}`}
class:opacity-0={isImageLoading}
loading="lazy"
draggable="false"
on:load|once={() => (isImageLoading = false)}
/>
{/if}
@@ -305,3 +301,9 @@
{/if}
</div>
</IntersectionObserver>
<style>
img {
transition: 0.2s ease all;
}
</style>

View File

@@ -9,6 +9,7 @@
import LoadingSpinner from '../loading-spinner.svelte';
import StatusBox from '../status-box.svelte';
import SideBarButton from './side-bar-button.svelte';
import { locale } from '$lib/stores/preferences.store';
const getAssetCount = async () => {
const { data: assetCount } = await api.assetApi.getAssetCountByUserId();
@@ -35,8 +36,6 @@
owned: albumCount.owned
};
};
const locale = navigator.language;
</script>
<section id="sidebar" class="flex flex-col gap-1 pt-8 pr-6 bg-immich-bg dark:bg-immich-dark-bg">
@@ -56,8 +55,8 @@
<LoadingSpinner />
{:then data}
<div>
<p>{data.videos.toLocaleString(locale)} Videos</p>
<p>{data.photos.toLocaleString(locale)} Photos</p>
<p>{data.videos.toLocaleString($locale)} Videos</p>
<p>{data.photos.toLocaleString($locale)} Photos</p>
</div>
{/await}
</svelte:fragment>
@@ -74,7 +73,7 @@
<LoadingSpinner />
{:then data}
<div>
<p>{(data.shared + data.sharing).toLocaleString(locale)} Albums</p>
<p>{(data.shared + data.sharing).toLocaleString($locale)} Albums</p>
</div>
{/await}
</svelte:fragment>
@@ -108,7 +107,7 @@
<LoadingSpinner />
{:then data}
<div>
<p>{data.owned.toLocaleString(locale)} Albums</p>
<p>{data.owned.toLocaleString($locale)} Albums</p>
</div>
{/await}
</svelte:fragment>

View File

@@ -1,74 +1,38 @@
<script lang="ts">
import { onMount } from 'svelte';
onMount(() => {
var themeToggleDarkIcon = document.getElementById('theme-toggle-dark-icon');
var themeToggleLightIcon = document.getElementById('theme-toggle-light-icon');
// Change the icons inside the button based on previous settings
if (
localStorage.getItem('color-theme') === 'dark' ||
(!('color-theme' in localStorage) &&
window.matchMedia('(prefers-color-scheme: dark)').matches)
) {
themeToggleLightIcon?.classList.remove('hidden');
} else {
themeToggleDarkIcon?.classList.remove('hidden');
}
});
import { browser } from '$app/environment';
import { colorTheme } from '$lib/stores/preferences.store';
const toggleTheme = () => {
var themeToggleDarkIcon = document.getElementById('theme-toggle-dark-icon');
var themeToggleLightIcon = document.getElementById('theme-toggle-light-icon');
// toggle icons inside button
themeToggleDarkIcon?.classList.toggle('hidden');
themeToggleLightIcon?.classList.toggle('hidden');
$colorTheme = $colorTheme === 'dark' ? 'light' : 'dark';
};
// if set via local storage previously
if (localStorage.getItem('color-theme')) {
if (localStorage.getItem('color-theme') === 'light') {
document.documentElement.classList.add('dark');
localStorage.setItem('color-theme', 'dark');
} else {
$: {
if (browser) {
if ($colorTheme === 'light') {
document.documentElement.classList.remove('dark');
localStorage.setItem('color-theme', 'light');
}
} else {
if (document.documentElement.classList.contains('dark')) {
document.documentElement.classList.remove('dark');
localStorage.setItem('color-theme', 'light');
} else {
document.documentElement.classList.add('dark');
localStorage.setItem('color-theme', 'dark');
}
}
};
}
</script>
<button
on:click={toggleTheme}
id="theme-toggle"
type="button"
class="text-gray-500 dark:text-immich-dark-primary hover:bg-gray-100 dark:hover:bg-gray-700 focus:outline-none rounded-full text-sm p-2.5"
class="text-gray-500 dark:text-immich-dark-primary hover:bg-gray-100 dark:hover:bg-gray-700 focus:outline-none rounded-full p-2.5"
>
<svg
id="theme-toggle-dark-icon"
class="hidden w-6 h-6"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
><path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z" /></svg
>
<svg
id="theme-toggle-light-icon"
class="hidden w-6 h-6"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
><path
d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z"
fill-rule="evenodd"
clip-rule="evenodd"
/></svg
>
{#if $colorTheme === 'light'}
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"
><path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z" /></svg
>
{:else}
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"
><path
d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z"
fill-rule="evenodd"
clip-rule="evenodd"
/></svg
>
{/if}
</button>

View File

@@ -12,6 +12,7 @@
notificationController,
NotificationType
} from '../shared-components/notification/notification';
import { locale } from '$lib/stores/preferences.store';
let keys: APIKeyResponseDto[] = [];
@@ -20,7 +21,6 @@
let deleteKey: APIKeyResponseDto | null = null;
let secret = '';
const locale = navigator.language;
const format: Intl.DateTimeFormatOptions = {
month: 'short',
day: 'numeric',
@@ -154,7 +154,7 @@
>
<td class="text-sm px-4 w-1/3 text-ellipsis">{key.name}</td>
<td class="text-sm px-4 w-1/3 text-ellipsis"
>{new Date(key.createdAt).toLocaleDateString(locale, format)}
>{new Date(key.createdAt).toLocaleDateString($locale, format)}
</td>
<td class="text-sm px-4 w-1/3 text-ellipsis">
<button

View File

@@ -0,0 +1,21 @@
import { browser } from '$app/environment';
import { persisted } from 'svelte-local-storage-store';
const initialTheme =
browser && !window.matchMedia('(prefers-color-scheme: dark)').matches ? 'light' : 'dark';
// The 'color-theme' key is also used by app.html to prevent FOUC on page load.
export const colorTheme = persisted<'dark' | 'light'>('color-theme', initialTheme, {
serializer: {
parse: (text) => (text === 'light' ? text : 'dark'),
stringify: (obj) => obj
}
});
// Locale to use for formatting dates, numbers, etc.
export const locale = persisted<string | undefined>('locale', undefined, {
serializer: {
parse: (text) => text,
stringify: (obj) => obj ?? ''
}
});

View File

@@ -1,4 +1,4 @@
import { serverApi } from '@api';
import { api } from '@api';
import type { LayoutServerLoad } from './$types';
export const load = (async ({ cookies }) => {
@@ -8,8 +8,8 @@ export const load = (async ({ cookies }) => {
return { user: undefined };
}
serverApi.setAccessToken(accessToken);
const { data: user } = await serverApi.userApi.getMyUserInfo();
api.setAccessToken(accessToken);
const { data: user } = await api.userApi.getMyUserInfo();
return { user };
} catch (e) {

View File

@@ -19,11 +19,9 @@
let localVersion: string;
let remoteVersion: string;
let showNavigationLoadingBar = false;
let canShow = false;
let showUploadCover = false;
onMount(async () => {
checkUserTheme();
const res = await checkAppVersion();
shouldShowAnnouncement = res.shouldShowAnnouncement;
@@ -31,21 +29,6 @@
remoteVersion = res.remoteVersion ?? 'unknown';
});
const checkUserTheme = () => {
// On page load or when changing themes, best to add inline in `head` to avoid FOUC
if (
localStorage.getItem('color-theme') === 'dark' ||
(!('color-theme' in localStorage) &&
window.matchMedia('(prefers-color-scheme: dark)').matches)
) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
canShow = true;
};
beforeNavigate(() => {
showNavigationLoadingBar = true;
});
@@ -99,32 +82,30 @@
</svelte:head>
<main on:dragenter={() => (showUploadCover = true)}>
{#if canShow}
<div in:fade={{ duration: 100 }}>
{#if showNavigationLoadingBar}
<NavigationLoadingBar />
{/if}
<div in:fade={{ duration: 100 }}>
{#if showNavigationLoadingBar}
<NavigationLoadingBar />
{/if}
<slot />
<slot />
{#if showUploadCover}
<UploadCover
{dropHandler}
{dragOverHandler}
dragLeaveHandler={() => (showUploadCover = false)}
/>
{/if}
{#if showUploadCover}
<UploadCover
{dropHandler}
{dragOverHandler}
dragLeaveHandler={() => (showUploadCover = false)}
/>
{/if}
<DownloadPanel />
<UploadPanel />
<NotificationList />
{#if shouldShowAnnouncement}
<AnnouncementBox
{localVersion}
{remoteVersion}
on:close={() => (shouldShowAnnouncement = false)}
/>
{/if}
</div>
{/if}
<DownloadPanel />
<UploadPanel />
<NotificationList />
{#if shouldShowAnnouncement}
<AnnouncementBox
{localVersion}
{remoteVersion}
on:close={() => (shouldShowAnnouncement = false)}
/>
{/if}
</div>
</main>

View File

@@ -1,5 +1,5 @@
export const prerender = false;
import { serverApi } from '@api';
import { api } from '@api';
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
@@ -9,7 +9,7 @@ export const load: PageServerLoad = async ({ parent }) => {
throw redirect(302, '/photos');
}
const { data } = await serverApi.userApi.getUserCount(true);
const { data } = await api.userApi.getUserCount(true);
if (data.userCount > 0) {
// Redirect to login page if an admin is already registered.

View File

@@ -1,5 +1,5 @@
import { redirect } from '@sveltejs/kit';
import { serverApi } from '@api';
import { api } from '@api';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ parent }) => {
@@ -11,7 +11,7 @@ export const load: PageServerLoad = async ({ parent }) => {
throw redirect(302, '/photos');
}
const { data: allUsers } = await serverApi.userApi.getAllUsers(false);
const { data: allUsers } = await api.userApi.getAllUsers(false);
return {
allUsers,

View File

@@ -1,5 +1,5 @@
import { redirect } from '@sveltejs/kit';
import { serverApi } from '@api';
import { api } from '@api';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ parent }) => {
@@ -11,7 +11,7 @@ export const load: PageServerLoad = async ({ parent }) => {
throw redirect(302, '/photos');
}
const { data: allUsers } = await serverApi.userApi.getAllUsers(false);
const { data: allUsers } = await api.userApi.getAllUsers(false);
return {
user,

View File

@@ -11,6 +11,7 @@
import DeleteConfirmDialog from '$lib/components/admin-page/delete-confirm-dialoge.svelte';
import RestoreDialogue from '$lib/components/admin-page/restore-dialoge.svelte';
import { page } from '$app/stores';
import { locale } from '$lib/stores/preferences.store';
let allUsers: UserResponseDto[] = [];
let shouldShowEditUserForm = false;
@@ -28,7 +29,6 @@
return user.deletedAt != null;
};
const locale = navigator.language;
const deleteDateFormat: Intl.DateTimeFormatOptions = {
month: 'long',
day: 'numeric',
@@ -38,7 +38,7 @@
const getDeleteDate = (user: UserResponseDto): string => {
let deletedAt = new Date(user.deletedAt ? user.deletedAt : Date.now());
deletedAt.setDate(deletedAt.getDate() + 7);
return deletedAt.toLocaleString(locale, deleteDateFormat);
return deletedAt.toLocaleString($locale, deleteDateFormat);
};
const onUserCreated = async () => {

View File

@@ -1,6 +1,6 @@
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
import { serverApi } from '@api';
import { api } from '@api';
export const load: PageServerLoad = async ({ parent }) => {
try {
@@ -10,7 +10,7 @@ export const load: PageServerLoad = async ({ parent }) => {
throw Error('User is not logged in');
}
const { data: albums } = await serverApi.albumApi.getAllAlbums();
const { data: albums } = await api.albumApi.getAllAlbums();
return {
user: user,

View File

@@ -1,7 +1,7 @@
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
import { serverApi } from '@api';
import { api } from '@api';
export const load: PageServerLoad = async ({ parent, params }) => {
const { user } = await parent();
@@ -13,7 +13,7 @@ export const load: PageServerLoad = async ({ parent, params }) => {
const albumId = params['albumId'];
try {
const { data: album } = await serverApi.albumApi.getAlbumInfo(albumId);
const { data: album } = await api.albumApi.getAlbumInfo(albumId);
return {
album,
meta: {

View File

@@ -1,9 +1,9 @@
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
import { serverApi } from '@api';
import { api } from '@api';
export const load: PageServerLoad = async () => {
const { data } = await serverApi.userApi.getUserCount(true);
const { data } = await api.userApi.getUserCount(true);
if (data.userCount === 0) {
// Admin not registered
throw redirect(302, '/auth/register');

View File

@@ -1,10 +1,9 @@
import { json } from '@sveltejs/kit';
import { api, serverApi } from '@api';
import { api } from '@api';
import type { RequestHandler } from '@sveltejs/kit';
export const POST = (async ({ cookies }) => {
api.removeAccessToken();
serverApi.removeAccessToken();
cookies.delete('immich_auth_type', { path: '/' });
cookies.delete('immich_access_token', { path: '/' });

View File

@@ -1,9 +1,9 @@
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
import { serverApi } from '@api';
import { api } from '@api';
export const load: PageServerLoad = async () => {
const { data } = await serverApi.userApi.getUserCount(true);
const { data } = await api.userApi.getUserCount(true);
if (data.userCount != 0) {
// Admin has been registered, redirect to login
throw redirect(302, '/auth/login');

View File

@@ -28,6 +28,7 @@
import Plus from 'svelte-material-icons/Plus.svelte';
import ShareVariantOutline from 'svelte-material-icons/ShareVariantOutline.svelte';
import type { PageData } from './$types';
import { locale } from '$lib/stores/preferences.store';
export let data: PageData;
let isShowCreateSharedLinkModal = false;
@@ -141,8 +142,6 @@
assetInteractionStore.clearMultiselect();
isShowCreateSharedLinkModal = false;
};
const locale = navigator.language;
</script>
<section>
@@ -154,7 +153,7 @@
>
<svelte:fragment slot="leading">
<p class="font-medium text-immich-primary dark:text-immich-dark-primary">
Selected {$selectedAssets.size.toLocaleString(locale)}
Selected {$selectedAssets.size.toLocaleString($locale)}
</p>
</svelte:fragment>
<svelte:fragment slot="trailing">

View File

@@ -2,7 +2,7 @@ export const prerender = false;
import { error } from '@sveltejs/kit';
import { getThumbnailUrl } from '$lib/utils/asset-utils';
import { serverApi, ThumbnailFormat } from '@api';
import { api, ThumbnailFormat } from '@api';
import type { PageServerLoad } from './$types';
import featurePanelUrl from '$lib/assets/feature-panel.png';
@@ -12,7 +12,7 @@ export const load: PageServerLoad = async ({ params, parent }) => {
const { key } = params;
try {
const { data: sharedLink } = await serverApi.shareApi.getMySharedLink({ params: { key } });
const { data: sharedLink } = await api.shareApi.getMySharedLink({ params: { key } });
const assetCount = sharedLink.assets.length;
const assetId = sharedLink.album?.albumThumbnailAssetId || sharedLink.assets[0]?.id;

View File

@@ -1,13 +1,13 @@
export const prerender = false;
import { error } from '@sveltejs/kit';
import { serverApi } from '@api';
import { api } from '@api';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ params }) => {
try {
const { key, assetId } = params;
const { data: asset } = await serverApi.assetApi.getAssetById(assetId, {
const { data: asset } = await api.assetApi.getAssetById(assetId, {
params: { key }
});

View File

@@ -1,7 +1,7 @@
import { redirect } from '@sveltejs/kit';
export const prerender = false;
import { serverApi } from '@api';
import { api } from '@api';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ parent }) => {
@@ -11,7 +11,7 @@ export const load: PageServerLoad = async ({ parent }) => {
throw redirect(302, '/auth/login');
}
const { data: sharedAlbums } = await serverApi.albumApi.getAllAlbums(true);
const { data: sharedAlbums } = await api.albumApi.getAllAlbums(true);
return {
user: user,