Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3c5c0ea68f | ||
|
|
2b988e1d5d | ||
|
|
8bcb2558b6 | ||
|
|
b8785a5b93 | ||
|
|
b00631d186 | ||
|
|
de5a6b2c35 | ||
|
|
3beb8193ae | ||
|
|
a2549c5bbd | ||
|
|
98a8be82e2 | ||
|
|
5c86e13239 | ||
|
|
10cb612fb1 | ||
|
|
a9a769d902 | ||
|
|
846e35f57e | ||
|
|
a3b9a0be3a | ||
|
|
2a3235f606 | ||
|
|
08b221c270 | ||
|
|
3102c3128f | ||
|
|
9ebed3c1b4 | ||
|
|
24d672a0ff | ||
|
|
e9f99302c1 | ||
|
|
5cdf7671ed |
2
.github/workflows/build-mobile.yml
vendored
@@ -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:
|
||||
|
||||
2
.github/workflows/prepare-release.yml
vendored
@@ -18,7 +18,7 @@ on:
|
||||
type: boolean
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
group: ${{ github.workflow }}-${{ github.ref }}-root
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
|
||||
38
README.md
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -42,8 +41,6 @@ services:
|
||||
- .env
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
depends_on:
|
||||
- database
|
||||
restart: always
|
||||
|
||||
immich-web:
|
||||
|
||||
BIN
docs/docs/administration/img/admin-jobs.png
Normal file
|
After Width: | Height: | Size: 111 KiB |
@@ -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.
|
||||
|
||||

|
||||
|
||||
## Generate Thumbnails
|
||||
|
||||

|
||||
|
||||
|
||||
## Extract Exif
|
||||
|
||||

|
||||
|
||||
## Detect Objects
|
||||
|
||||

|
||||
|
||||
## 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.
|
||||
|
||||

|
||||
:::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.
|
||||
:::
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 691 KiB After Width: | Height: | Size: 242 KiB |
@@ -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>
|
||||
|
||||
:::
|
||||
|
||||
BIN
docs/docs/features/img/background-app-refresh.png
Normal file
|
After Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 16 KiB |
@@ -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.
|
||||
:::
|
||||
|
||||
|
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 52 KiB |
@@ -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"]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -35,8 +35,8 @@ platform :android do
|
||||
task: 'bundle',
|
||||
build_type: 'Release',
|
||||
properties: {
|
||||
"android.injected.version.code" => 71,
|
||||
"android.injected.version.name" => "1.48.0",
|
||||
"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')
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
* Scroll to top when tapping photos while already on photo page.
|
||||
* Delete goes to next page instead of popping back to the main timeline.
|
||||
* Improve date formatting.
|
||||
* Styling and linter.
|
||||
* User get logged out upon clicking on any thing after logging in.
|
||||
* Improve reliability of asset loading and indexing.
|
||||
@@ -5,17 +5,17 @@
|
||||
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000209">
|
||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000284">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="1: bundleRelease" time="79.840593">
|
||||
<testcase classname="fastlane.lanes" name="1: bundleRelease" time="282.422814">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="21.361905">
|
||||
<testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="43.555992">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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": "새 앨범제목",
|
||||
|
||||
@@ -378,7 +378,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 86;
|
||||
CURRENT_PROJECT_VERSION = 87;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
@@ -514,7 +514,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 86;
|
||||
CURRENT_PROJECT_VERSION = 87;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
@@ -542,7 +542,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 86;
|
||||
CURRENT_PROJECT_VERSION = 87;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -44,11 +44,11 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.47.0</string>
|
||||
<string>1.48.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>86</string>
|
||||
<string>87</string>
|
||||
<key>FLTEnableImpeller</key>
|
||||
<true/>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
|
||||
@@ -19,7 +19,7 @@ platform :ios do
|
||||
desc "iOS Beta"
|
||||
lane :beta do
|
||||
increment_version_number(
|
||||
version_number: "1.48.0"
|
||||
version_number: "1.49.0"
|
||||
)
|
||||
increment_build_number(
|
||||
build_number: latest_testflight_build_number + 1,
|
||||
|
||||
@@ -5,32 +5,32 @@
|
||||
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000269">
|
||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000542">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="1: increment_version_number" time="3.705535">
|
||||
<testcase classname="fastlane.lanes" name="1: increment_version_number" time="11.028001">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="2: latest_testflight_build_number" time="5.23144">
|
||||
<testcase classname="fastlane.lanes" name="2: latest_testflight_build_number" time="5.912096">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="3: increment_build_number" time="2.423549">
|
||||
<testcase classname="fastlane.lanes" name="3: increment_build_number" time="1.65488">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="4: build_app" time="98.940158">
|
||||
<testcase classname="fastlane.lanes" name="4: build_app" time="110.93313">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="5: upload_to_testflight" time="64.950609">
|
||||
<testcase classname="fastlane.lanes" name="5: upload_to_testflight" time="80.292248">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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});
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)),
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
2
mobile/openapi/README.md
generated
@@ -3,7 +3,7 @@ Immich API
|
||||
|
||||
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
|
||||
|
||||
- API version: 1.47.3
|
||||
- API version: 1.48.1
|
||||
- Build package: org.openapitools.codegen.languages.DartClientCodegen
|
||||
|
||||
## Requirements
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -2,7 +2,7 @@ name: immich_mobile
|
||||
description: Immich - selfhosted backup media file on mobile phone
|
||||
|
||||
publish_to: "none"
|
||||
version: 1.48.0+71
|
||||
version: 1.49.0+72
|
||||
isar_version: &isar_version 3.0.5
|
||||
|
||||
environment:
|
||||
|
||||
@@ -121,12 +121,12 @@ export class AlbumRepository implements IAlbumRepository {
|
||||
|
||||
async getListByAssetId(userId: string, assetId: string): Promise<AlbumEntity[]> {
|
||||
const albums = await this.albumRepository.find({
|
||||
where: { ownerId: userId, assets: { id: assetId } },
|
||||
where: { ownerId: userId },
|
||||
relations: { owner: true, assets: true, sharedUsers: true },
|
||||
order: { assets: { fileCreatedAt: 'ASC' } },
|
||||
});
|
||||
|
||||
return albums;
|
||||
return albums.filter((album) => album.assets.some((asset) => asset.id === assetId));
|
||||
}
|
||||
|
||||
async get(albumId: string): Promise<AlbumEntity | null> {
|
||||
|
||||
@@ -80,7 +80,7 @@ export class AssetRepository implements IAssetRepository {
|
||||
.createQueryBuilder('asset')
|
||||
.leftJoinAndSelect('asset.smartInfo', 'si')
|
||||
.where('asset.resizePath IS NOT NULL')
|
||||
.andWhere('si.id IS NULL')
|
||||
.andWhere('si.assetId IS NULL')
|
||||
.andWhere('asset.isVisible = true')
|
||||
.getMany();
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ export class ServerInfoService {
|
||||
assetType: string;
|
||||
assetCount: string;
|
||||
totalSizeInBytes: string;
|
||||
userId: string;
|
||||
ownerId: string;
|
||||
};
|
||||
|
||||
const userStatsQueryResponse: UserStatsQueryResponse[] = await this.assetRepository
|
||||
@@ -56,9 +56,8 @@ export class ServerInfoService {
|
||||
|
||||
const tmpMap = new Map<string, UsageByUserDto>();
|
||||
const getUsageByUser = (id: string) => tmpMap.get(id) || new UsageByUserDto(id);
|
||||
|
||||
userStatsQueryResponse.forEach((r) => {
|
||||
const usageByUser = getUsageByUser(r.userId);
|
||||
const usageByUser = getUsageByUser(r.ownerId);
|
||||
usageByUser.photos += r.assetType === 'IMAGE' ? parseInt(r.assetCount) : 0;
|
||||
usageByUser.videos += r.assetType === 'VIDEO' ? parseInt(r.assetCount) : 0;
|
||||
usageByUser.usageRaw += parseInt(r.totalSizeInBytes);
|
||||
@@ -68,7 +67,7 @@ export class ServerInfoService {
|
||||
serverStats.videos += r.assetType === 'VIDEO' ? parseInt(r.assetCount) : 0;
|
||||
serverStats.usageRaw += parseInt(r.totalSizeInBytes);
|
||||
serverStats.usage = asHumanReadable(serverStats.usageRaw);
|
||||
tmpMap.set(r.userId, usageByUser);
|
||||
tmpMap.set(r.ownerId, usageByUser);
|
||||
});
|
||||
|
||||
serverStats.usageByUser = Array.from(tmpMap.values());
|
||||
|
||||
@@ -2728,7 +2728,7 @@
|
||||
"info": {
|
||||
"title": "Immich",
|
||||
"description": "Immich API",
|
||||
"version": "1.48.0",
|
||||
"version": "1.49.0",
|
||||
"contact": {}
|
||||
},
|
||||
"tags": [],
|
||||
|
||||
2
server/package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "immich",
|
||||
"version": "1.48.0",
|
||||
"version": "1.49.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "immich",
|
||||
"version": "1.48.0",
|
||||
"version": "1.49.0",
|
||||
"description": "",
|
||||
"author": "",
|
||||
"private": true,
|
||||
|
||||
3
web/__mocks__/$app/environment.js
Normal file
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
browser: false
|
||||
};
|
||||
18
web/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
|
||||
2
web/src/api/open-api/api.ts
generated
@@ -4,7 +4,7 @@
|
||||
* Immich
|
||||
* Immich API
|
||||
*
|
||||
* The version of the OpenAPI document: 1.47.3
|
||||
* The version of the OpenAPI document: 1.48.1
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
|
||||
2
web/src/api/open-api/base.ts
generated
@@ -4,7 +4,7 @@
|
||||
* Immich
|
||||
* Immich API
|
||||
*
|
||||
* The version of the OpenAPI document: 1.47.3
|
||||
* The version of the OpenAPI document: 1.48.1
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
|
||||
2
web/src/api/open-api/common.ts
generated
@@ -4,7 +4,7 @@
|
||||
* Immich
|
||||
* Immich API
|
||||
*
|
||||
* The version of the OpenAPI document: 1.47.3
|
||||
* The version of the OpenAPI document: 1.48.1
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
|
||||
2
web/src/api/open-api/configuration.ts
generated
@@ -4,7 +4,7 @@
|
||||
* Immich
|
||||
* Immich API
|
||||
*
|
||||
* The version of the OpenAPI document: 1.47.3
|
||||
* The version of the OpenAPI document: 1.48.1
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
|
||||
2
web/src/api/open-api/index.ts
generated
@@ -4,7 +4,7 @@
|
||||
* Immich
|
||||
* Immich API
|
||||
*
|
||||
* The version of the OpenAPI document: 1.47.3
|
||||
* The version of the OpenAPI document: 1.48.1
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 -->
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
21
web/src/lib/stores/preferences.store.ts
Normal 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 ?? ''
|
||||
}
|
||||
});
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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: '/' });
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 }
|
||||
});
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||