Compare commits

...

30 Commits

Author SHA1 Message Date
Thomas
412633d0aa Merge branch 'main' into feat-no-thumbhash-cache 2025-09-15 15:30:09 +01:00
shenlong
dcee34095b fix: reset sqlite on beta migration (#20735)
reset sync stream on migration

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2025-09-14 16:30:25 -05:00
Alex
15f182902f fix: check if preferencesStore is defined (#21958) 2025-09-14 20:30:15 +00:00
shenlong
b26b452530 fix: do not listen for store updates in isolates (#21947)
* dispose store on isolate cleanup

* do not listen for store updates in isolates

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
Co-authored-by: Alex <alex.tran1502@gmail.com>
2025-09-14 14:50:17 -05:00
shenlong
2dcb32f7d0 chore: update background downloader (#21909)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2025-09-14 14:44:48 -05:00
Brandon Wees
27d2f3efe2 feat: disable snapping when a timeline has less than 12 months (#21649)
* feat: disable snapping when a timeline has less than 12 months

* fix: disable placeholders when not snapping

also moved month constant to constants.dart

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2025-09-14 19:24:52 +00:00
shenlong
d38468439b fix: complete does not destroy engine on close (#21943)
* fix: complete does not destroy engine on close

* reset flutterApi on cleanup

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2025-09-14 14:17:12 -05:00
Alex
0166e99d90 chore: remove main timeline query watch throttle (#21942) 2025-09-14 02:09:07 -05:00
Alex
71e33e35dc chore: check before sync linked albums from websocket events (#21941) 2025-09-14 02:08:41 -05:00
Mert
a122d4b969 fix(mobile): double hero animation (#21927)
fix double hero animation
2025-09-13 16:47:07 -05:00
shenlong
dad81af6e3 fix: show view in timeline from search page (#21873)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2025-09-12 22:44:31 -05:00
shenlong
ac6b42e1e8 fix: do not show stack action if there is only one selection (#21868)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2025-09-12 22:43:51 -05:00
Stewart Rand
4059638151 fix: context menu jank (#21844)
* Fix issue with context menu jank by only applying overflow styling when transition is complete

* Remove comment

Co-authored-by: Alex <alex.tran1502@gmail.com>

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2025-09-12 22:43:22 -05:00
Stewart Rand
1823a28e59 chore: improve date text slide-in transition (#21879)
* Make date text slide-in transition smooth

* fix: lint

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2025-09-13 03:42:42 +00:00
Stewart Rand
b6bf1852cd fix: keep adequate space around page title (#21881)
Keep space around page title
2025-09-12 22:42:25 -05:00
Stewart Rand
cdc26f2c7b fix: z-index of top bar on show/hide people view (#21847)
Fix z-index of top bar on show/hide people view
2025-09-12 22:32:50 -05:00
Alex
913b3789cc chore: simplify timeline switcher toggle (#21864)
chore: timeline switcher option simplify
2025-09-12 22:32:15 -05:00
Stewart Rand
994a770921 chore: improve context button accessibility (#21876)
Make context menu button filled on album list and faces page
2025-09-12 22:31:52 -05:00
Mert
17bbcdf584 chore(mobile): add debugPrint lint rule (#21872)
* add lint rule

* update usages

* stragglers

* use dcm

* formatting

* test ci

* Revert "test ci"

This reverts commit 8f864c4e4d.

* revert whitespace change
2025-09-12 18:56:00 -04:00
bo0tzz
23aa661324 fix: use mdq image with jq (#21860) 2025-09-12 21:46:39 +02:00
Min Idzelis
a10a946d1a fix: let dev docker compose service runs as root (#21579) 2025-09-12 16:20:41 +01:00
Stewart Rand
04c9531624 fix: format point count numbers on map view (#21848)
Format numbers on map view
2025-09-12 07:20:05 +00:00
Alex
d84cc450f1 chore: post release tasks (#21834) 2025-09-11 15:15:10 -05:00
github-actions
4153848c68 chore: version v1.142.0 2025-09-11 19:39:05 +00:00
Jason Rasmussen
f29230c8a6 fix(web): handle buckets before year 1000 (#21832) 2025-09-11 14:36:16 -05:00
Weblate (bot)
03af60e8eb chore(web): update translations (#21814)
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ja/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ru/
Translation: Immich/immich

Co-authored-by: DevServs <bonov@mail.ru>
Co-authored-by: Taiki M <vexingly-many-mace@duck.com>
2025-09-11 19:34:40 +00:00
bo0tzz
ae827e1406 fix: define call secrets in merge-translations (#21831) 2025-09-11 19:29:58 +00:00
shenlong
7893ac25fb fix: always use en locale for parsing timeline datetime (#21796)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2025-09-11 14:20:39 -05:00
Alex
42a03f2556 fix: concurrency issue (#21830) 2025-09-11 19:02:03 +00:00
Thomas Way
2bba33f834 feat(web): don't animate cached thumbnails
Thumbnails for assets always are displayed with a thumbhash which fades out
over 100ms, even if the thumbnail is cached and ready immediately. This can be
a bit distracting and make Immich feel 'slow', or inefficient as it feels like
the thumbnails are always being reloaded. Skipping the thumbhash and animation
for cached thumbnails makes it feel much more responsive.
2025-08-10 20:59:02 +01:00
97 changed files with 629 additions and 637 deletions

View File

@@ -26,7 +26,7 @@ services:
env_file: !reset []
init:
env_file: !reset []
command: sh -c 'find /data -maxdepth 1 ! -path "/data/postgres" -type d -exec chown ${UID:-1000}:${GID:-1000} {} + 2>/dev/null || true; for path in /usr/src/app/.pnpm-store /usr/src/app/server/node_modules /usr/src/app/server/dist /usr/src/app/.github/node_modules /usr/src/app/cli/node_modules /usr/src/app/docs/node_modules /usr/src/app/e2e/node_modules /usr/src/app/open-api/typescript-sdk/node_modules /usr/src/app/web/.svelte-kit /usr/src/app/web/coverage /usr/src/app/node_modules /usr/src/app/web/node_modules; do [ -e "$$path" ] && chown -R ${UID:-1000}:${GID:-1000} "$$path" || true; done'
command: sh -c 'find /data -maxdepth 1 ! -path "/data/postgres" -type d -exec chown ${UID:-0}:${GID:-0} {} + 2>/dev/null || true; for path in /usr/src/app/.pnpm-store /usr/src/app/server/node_modules /usr/src/app/server/dist /usr/src/app/.github/node_modules /usr/src/app/cli/node_modules /usr/src/app/docs/node_modules /usr/src/app/e2e/node_modules /usr/src/app/open-api/typescript-sdk/node_modules /usr/src/app/web/.svelte-kit /usr/src/app/web/coverage /usr/src/app/node_modules /usr/src/app/web/node_modules; do [ -e "$$path" ] && chown -R ${UID:-0}:${GID:-0} "$$path" || true; done'
immich-machine-learning:
env_file: !reset []
database:

View File

@@ -35,7 +35,7 @@ jobs:
needs: [get_body, should_run]
if: ${{ needs.should_run.outputs.should_run == 'true' }}
container:
image: yshavit/mdq:0.9.0@sha256:4399483ca857fb1a7ed28a596f754c7373e358647de31ce14b79a27c91e1e35e
image: ghcr.io/immich-app/mdq:main@sha256:1669c75a5542333ff6b03c13d5fd259ea8d798188b84d5d99093d62e4542eb05
outputs:
checked: ${{ steps.get_checkbox.outputs.checked }}
steps:

View File

@@ -1,8 +1,15 @@
name: Merge translations
on:
workflow_call:
workflow_dispatch:
workflow_call:
secrets:
PUSH_O_MATIC_APP_ID:
required: true
PUSH_O_MATIC_APP_KEY:
required: true
WEBLATE_TOKEN:
required: true
permissions: {}

View File

@@ -70,11 +70,9 @@ VOLUME_DIRS = \
# Helper function to chown, on error suggest remediation and exit
define safe_chown
if chown $(2) $(or $(UID),1000):$(or $(GID),1000) "$(1)" 2>/dev/null; then \
true; \
else \
STATUS=$$?; echo "Exit code: $$STATUS $(1)"; \
echo "$$STATUS $(1)"; \
CURRENT_OWNER=$$(stat -c '%u:%g' "$(1)" 2>/dev/null || echo "none"); \
DESIRED_OWNER="$(or $(UID),0):$(or $(GID),0)"; \
if [ "$$CURRENT_OWNER" != "$$DESIRED_OWNER" ] && ! chown -v $(2) $$DESIRED_OWNER "$(1)" 2>/dev/null; then \
echo "Permission denied when changing owner of volumes and upload location. Try running 'sudo make prepare-volumes' first."; \
exit 1; \
fi;

View File

@@ -1,6 +1,6 @@
{
"name": "@immich/cli",
"version": "2.2.88",
"version": "2.2.89",
"description": "Command Line Interface (CLI) for Immich",
"type": "module",
"exports": "./dist/index.js",

View File

@@ -21,7 +21,7 @@ services:
# extends:
# file: hwaccel.transcoding.yml
# service: cpu # set to one of [nvenc, quicksync, rkmpp, vaapi, vaapi-wsl] for accelerated transcoding
user: '${UID:-1000}:${GID:-1000}'
user: '${UID:-0}:${GID:-0}'
build:
context: ../
dockerfile: server/Dockerfile
@@ -82,7 +82,7 @@ services:
image: immich-web-dev:latest
# Needed for rootless docker setup, see https://github.com/moby/moby/issues/45919
# user: 0:0
user: '${UID:-1000}:${GID:-1000}'
user: '${UID:-0}:${GID:-0}'
build:
context: ../
dockerfile: server/Dockerfile
@@ -189,7 +189,7 @@ services:
env_file:
- .env
user: 0:0
command: sh -c 'find /data -maxdepth 1 -type d -exec chown ${UID:-1000}:${GID:-1000} {} + 2>/dev/null || true; for path in /usr/src/app/.pnpm-store /usr/src/app/server/node_modules /usr/src/app/server/dist /usr/src/app/.github/node_modules /usr/src/app/cli/node_modules /usr/src/app/docs/node_modules /usr/src/app/e2e/node_modules /usr/src/app/open-api/typescript-sdk/node_modules /usr/src/app/web/.svelte-kit /usr/src/app/web/coverage /usr/src/app/node_modules /usr/src/app/web/node_modules; do [ -e "$$path" ] && chown -R ${UID:-1000}:${GID:-1000} "$$path" || true; done'
command: sh -c 'find /data -maxdepth 1 -type d -exec chown ${UID:-0}:${GID:-0} {} + 2>/dev/null || true; for path in /usr/src/app/.pnpm-store /usr/src/app/server/node_modules /usr/src/app/server/dist /usr/src/app/.github/node_modules /usr/src/app/cli/node_modules /usr/src/app/docs/node_modules /usr/src/app/e2e/node_modules /usr/src/app/open-api/typescript-sdk/node_modules /usr/src/app/web/.svelte-kit /usr/src/app/web/coverage /usr/src/app/node_modules /usr/src/app/web/node_modules; do [ -e "$$path" ] && chown -R ${UID:-0}:${GID:-0} "$$path" || true; done'
volumes:
- pnpm-store:/usr/src/app/.pnpm-store
- server-node_modules:/usr/src/app/server/node_modules

View File

@@ -1,4 +1,8 @@
[
{
"label": "v1.142.0",
"url": "https://v1.142.0.archive.immich.app"
},
{
"label": "v1.141.1",
"url": "https://v1.141.1.archive.immich.app"

View File

@@ -1,6 +1,6 @@
{
"name": "immich-e2e",
"version": "1.141.1",
"version": "1.142.0",
"description": "",
"main": "index.js",
"type": "module",

View File

@@ -1473,9 +1473,9 @@
"permission_onboarding_permission_limited": "写真へのアクセスが制限されています。Immichが写真のバックアップと管理を行うには、システム設定から写真と動画のアクセス権限を変更してください。",
"permission_onboarding_request": "Immichは写真へのアクセス許可が必要です",
"person": "人物",
"person_age_months": "{months, plural, one {# ヶ月} other {# ヶ月}}",
"person_age_year_months": "1 , {months, plural, one {# ヶ月} other {# ヶ月}}",
"person_age_years": "{years, plural, other {# }}",
"person_age_months": "生後 {months, plural, one {# ヶ月} other {# ヶ月}}",
"person_age_year_months": "1 歳と, {months, plural, one {# ヶ月} other {# ヶ月}}",
"person_age_years": "{years, plural, other {# }}",
"person_birthdate": "{date}生まれ",
"person_hidden": "{name}{hidden, select, true { (非表示)} other {}}",
"photo_shared_all_users": "写真をすべてのユーザーと共有したか、共有するユーザーがいないようです。",
@@ -2024,7 +2024,7 @@
"upload_success": "アップロード成功、新しくアップロードされたアセットを見るにはページを更新してください。",
"upload_to_immich": "Immichにアップロード ({count})",
"uploading": "アップロード中",
"uploading_media": "メディアをアップロード",
"uploading_media": "メディアをアップロード",
"url": "URL",
"usage": "使用容量",
"use_biometric": "生体認証をご利用ください",

View File

@@ -35,7 +35,7 @@
"add_url": "Добавить URL",
"added_to_archive": "Добавлено в архив",
"added_to_favorites": "Добавлено в избранное",
"added_to_favorites_count": "Добавлено{count, number} в избранное",
"added_to_favorites_count": "{count, plural, one {# объект добавлен} many {# объектов добавлено} other {# объекта добавлено}} в избранное",
"admin": {
"add_exclusion_pattern_description": "Добавьте шаблоны исключений. Поддерживаются символы подстановки *, ** и ?. Чтобы игнорировать все файлы в любом каталоге с именем \"Raw\", укажите \"**/Raw/**\". Чтобы игнорировать все файлы, заканчивающиеся на \".tif\", используйте \"**/*.tif\". Чтобы игнорировать путь целиком, укажите \"/path/to/ignore/**\".",
"admin_user": "Администратор",
@@ -448,7 +448,7 @@
"all_albums": "Все альбомы",
"all_people": "Все люди",
"all_videos": "Все видео",
"allow_dark_mode": "Разрешить темный режим",
"allow_dark_mode": "Разрешить тёмный режим",
"allow_edits": "Разрешить редактирование",
"allow_public_user_to_download": "Разрешить скачивание",
"allow_public_user_to_upload": "Разрешить добавление файлов",
@@ -1112,14 +1112,14 @@
"home_page_add_to_album_conflicts": "Добавлено {added} медиа в альбом {album}. {failed} медиа уже в альбоме.",
"home_page_add_to_album_err_local": "Пока нельзя добавлять локальные объекты в альбомы, пропуск",
"home_page_add_to_album_success": "Добавлено {added} медиа в альбом {album}.",
"home_page_album_err_partner": "Пока нельзя добавить медиа партнера в альбом, пропуск",
"home_page_album_err_partner": "Невозможно добавить объекты партнёра в альбом, пропуск",
"home_page_archive_err_local": "Пока нельзя добавить локальные файлы в архив, пропуск",
"home_page_archive_err_partner": "Невозможно архивировать медиа партнера, пропуск",
"home_page_archive_err_partner": "Невозможно добавить объекты партнёра в архив, пропуск",
"home_page_building_timeline": "Построение хронологии",
"home_page_delete_err_partner": "Невозможно удалить медиа партнера, пропуск",
"home_page_delete_err_partner": "Невозможно удалить объекты партнёра, пропуск",
"home_page_delete_remote_err_local": "Невозможно удалить локальные файлы с сервера, пропуск",
"home_page_favorite_err_local": "Пока нельзя добавить в избранное локальные файлы, пропуск",
"home_page_favorite_err_partner": "Пока нельзя добавить в избранное медиа партнера, пропуск",
"home_page_favorite_err_partner": "Невозможно добавить объекты партнёра в избранное, пропуск",
"home_page_first_time_notice": "Перед началом использования приложения выберите альбом с объектами для резервного копирования, чтобы они отобразились на временной шкале",
"home_page_locked_error_local": "Невозможно переместить локальные объекты в личную папку, пропуск",
"home_page_locked_error_partner": "Невозможно переместить объекты партнёра в личную папку, пропуск",
@@ -1154,8 +1154,8 @@
"in_albums": "В {count, plural, one {# альбоме} other {# альбомах}}",
"in_archive": "В архиве",
"include_archived": "Отображать архив",
"include_shared_albums": "Включать общие альбомы",
"include_shared_partner_assets": "Включать общие ресурсы партнера",
"include_shared_albums": "Включать объекты общих альбомов",
"include_shared_partner_assets": "Включать объекты партнёров",
"individual_share": "Индивидуальная подборка",
"individual_shares": "Подборки",
"info": "Информация",
@@ -1367,7 +1367,7 @@
"no_duplicates_found": "Дубликатов не обнаружено.",
"no_exif_info_available": "Нет доступной информации exif",
"no_explore_results_message": "Загружайте больше фотографий, чтобы наслаждаться вашей коллекцией.",
"no_favorites_message": "Добавляйте в избранное, чтобы быстро найти свои лучшие фотографии и видео",
"no_favorites_message": "Добавляйте объекты в избранное, чтобы быстрее находить свои лучшие фото и видео",
"no_libraries_message": "Создайте внешнюю библиотеку для просмотра в Immich сторонних фотографий и видео",
"no_locked_photos_message": "Фото и видео, перемещенные в личную папку, скрыты и не отображаются при просмотре библиотеки.",
"no_name": "Нет имени",
@@ -1422,18 +1422,18 @@
"other_variables": "Другие переменные",
"owned": "Мои",
"owner": "Владелец",
"partner": "Партнер",
"partner_can_access": "{partner} имеет доступ",
"partner_can_access_assets": "Все ваши фотографии и видеозаписи, кроме тех, которые находятся в Архиве и Корзине",
"partner_can_access_location": "Местоположение, где были сделаны ваши фотографии",
"partner_list_user_photos": "Фотографии пользователя {user}",
"partner": "Партнёр",
"partner_can_access": "Пользователю {partner} доступны",
"partner_can_access_assets": "Все ваши фото и видео, кроме тех, что находятся в архиве и корзине",
"partner_can_access_location": "Места, где были сделаны ваши фото и видео",
"partner_list_user_photos": "Фото и видео пользователя {user}",
"partner_list_view_all": "Посмотреть все",
"partner_page_empty_message": "У вашего партнёра еще нет доступа к вашим фото.",
"partner_page_empty_message": "Вы пока никому из партнёров не предоставили доступ к своим фото и видео.",
"partner_page_no_more_users": "Выбраны все доступные пользователи",
"partner_page_partner_add_failed": "Не удалось добавить партнёра",
"partner_page_select_partner": "Выбрать партнёра",
"partner_page_shared_to_title": "Поделиться с...",
"partner_page_stop_sharing_content": "Пользователь {partner} больше не сможет получить доступ к вашим фото.",
"partner_page_stop_sharing_content": "Пользователь {partner} больше не будет иметь доступ к вашим фото и видео.",
"partner_sharing": "Совместное использование",
"partners": "Партнёры",
"password": "Пароль",
@@ -1796,7 +1796,7 @@
"shared_by": "Поделился",
"shared_by_user": "Владелец: {user}",
"shared_by_you": "Вы поделились",
"shared_from_partner": "Фото и видео пользователя {partner}",
"shared_from_partner": "Пользователь {partner} предоставил вам доступ",
"shared_intent_upload_button_progress_text": "{current} / {total} Загружено",
"shared_link_app_bar_title": "Публичные ссылки",
"shared_link_clipboard_copied_massage": "Скопировано в буфер обмена",
@@ -1833,7 +1833,7 @@
"shared_links_description": "Делитесь фотографиями и видео по ссылке",
"shared_photos_and_videos_count": "{assetCount, plural, other {# фото и видео.}}",
"shared_with_me": "Доступные мне",
"shared_with_partner": "Совместно с {partner}",
"shared_with_partner": "Вы предоставили доступ пользователю {partner}",
"sharing": "Общие",
"sharing_enter_password": "Пожалуйста, введите пароль для просмотра этой страницы.",
"sharing_page_album": "Общие альбомы",
@@ -1851,7 +1851,7 @@
"show_gallery": "Показать галерею",
"show_hidden_people": "Показать скрытых людей",
"show_in_timeline": "Показать на временной шкале",
"show_in_timeline_setting_description": "Отображать фото и видео этого пользователя в своей ленте",
"show_in_timeline_setting_description": "Отображать фото и видео этого пользователя на своей временной шкале",
"show_keyboard_shortcuts": "Показать сочетания клавиш",
"show_metadata": "Показывать метаданные",
"show_or_hide_info": "Показать или скрыть информацию",
@@ -1897,9 +1897,9 @@
"status": "Состояние",
"stop_casting": "Остановить трансляцию",
"stop_motion_photo": "Покадровая анимация",
"stop_photo_sharing": "Закрыть доступ партнёра к вашим фото?",
"stop_photo_sharing": "Закрыть доступ партнёру?",
"stop_photo_sharing_description": "Пользователь {partner} больше не имеет доступа к вашим фотографиям.",
"stop_sharing_photos_with_user": "Прекратить делиться своими фотографиями с этим пользователем",
"stop_sharing_photos_with_user": "Прекратить делиться своими фото и видео с этим пользователем",
"storage": "Хранилище",
"storage_label": "Метка хранилища",
"storage_quota": "Квота хранилища",

View File

@@ -134,6 +134,13 @@ custom_lint:
dart_code_metrics:
rules:
- banned-usage:
entries:
- name: debugPrint
description: Use dPrint instead of debugPrint for proper tree-shaking in release builds.
exclude-paths:
- 'lib/utils/debug_print.dart'
severity: perf
# All rules from "recommended" preset
# Show potential errors
# - avoid-cascade-after-if-null

View File

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

View File

@@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 77;
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
@@ -133,6 +133,8 @@
/* Begin PBXFileSystemSynchronizedRootGroup section */
B2CF7F8C2DDE4EBB00744BF6 /* Sync */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
);
path = Sync;
sourceTree = "<group>";
};
@@ -519,14 +521,10 @@
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
);
inputPaths = (
);
name = "[CP] Copy Pods Resources";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
@@ -555,14 +553,10 @@
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
inputPaths = (
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
@@ -711,7 +705,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 219;
CURRENT_PROJECT_VERSION = 223;
CUSTOM_GROUP_ID = group.app.immich.share;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
@@ -855,7 +849,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 219;
CURRENT_PROJECT_VERSION = 223;
CUSTOM_GROUP_ID = group.app.immich.share;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
@@ -885,7 +879,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 219;
CURRENT_PROJECT_VERSION = 223;
CUSTOM_GROUP_ID = group.app.immich.share;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
@@ -919,7 +913,7 @@
CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 219;
CURRENT_PROJECT_VERSION = 223;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
@@ -962,7 +956,7 @@
CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 219;
CURRENT_PROJECT_VERSION = 223;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
@@ -1002,7 +996,7 @@
CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 219;
CURRENT_PROJECT_VERSION = 223;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
@@ -1041,7 +1035,7 @@
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 219;
CURRENT_PROJECT_VERSION = 223;
CUSTOM_GROUP_ID = group.app.immich.share;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
@@ -1085,7 +1079,7 @@
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 219;
CURRENT_PROJECT_VERSION = 223;
CUSTOM_GROUP_ID = group.app.immich.share;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
@@ -1126,7 +1120,7 @@
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 219;
CURRENT_PROJECT_VERSION = 223;
CUSTOM_GROUP_ID = group.app.immich.share;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_USER_SCRIPT_SANDBOXING = YES;

View File

@@ -133,7 +133,6 @@ class BackgroundWorker: BackgroundWorkerBgHostApi {
return
}
isComplete = true
flutterApi?.cancel { result in
self.complete(success: false)
}
@@ -174,6 +173,7 @@ class BackgroundWorker: BackgroundWorkerBgHostApi {
isComplete = true
engine.destroyContext()
flutterApi = nil
completionHandler(success)
}
}

View File

@@ -80,7 +80,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.140.0</string>
<string>1.142.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
@@ -107,7 +107,7 @@
</dict>
</array>
<key>CFBundleVersion</key>
<string>219</string>
<string>223</string>
<key>FLTEnableImpeller</key>
<true/>
<key>ITSAppUsesNonExemptEncryption</key>

View File

@@ -22,7 +22,7 @@ platform :ios do
path: "./Runner.xcodeproj",
)
increment_version_number(
version_number: "1.141.1"
version_number: "1.142.0"
)
increment_build_number(
build_number: latest_testflight_build_number + 1,

View File

@@ -45,3 +45,5 @@ const List<(String, String)> kWidgetNames = [
const double kUploadStatusFailed = -1.0;
const double kUploadStatusCanceled = -2.0;
const int kMinMonthsToEnableScrubberSnap = 12;

View File

@@ -77,7 +77,9 @@ enum StoreKey<T> {
enableBackup<bool>._(1003),
useWifiForUploadVideos<bool>._(1004),
useWifiForUploadPhotos<bool>._(1005),
needBetaMigration<bool>._(1006);
needBetaMigration<bool>._(1006),
// TODO: Remove this after patching open-api
shouldResetSync<bool>._(1007);
const StoreKey._(this.id);
final int id;

View File

@@ -7,6 +7,8 @@ import 'package:cancellation_token_http/http.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/constants.dart';
import 'package:immich_mobile/domain/services/log.service.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/extensions/network_capability_extensions.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/generated/intl_keys.g.dart';
@@ -27,6 +29,7 @@ import 'package:immich_mobile/services/localization.service.dart';
import 'package:immich_mobile/services/server_info.service.dart';
import 'package:immich_mobile/services/upload.service.dart';
import 'package:immich_mobile/utils/bootstrap.dart';
import 'package:immich_mobile/utils/debug_print.dart';
import 'package:immich_mobile/utils/http_ssl_options.dart';
import 'package:isar/isar.dart';
import 'package:logging/logging.dart';
@@ -159,7 +162,7 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
try {
await _cleanup();
} catch (error, stack) {
debugPrint('Failed to cleanup background worker: $error with stack: $stack');
dPrint(() => 'Failed to cleanup background worker: $error with stack: $stack');
}
}
@@ -169,7 +172,10 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
}
try {
final backgroundSyncManager = _ref.read(backgroundSyncProvider);
_isCleanedUp = true;
_ref.dispose();
_cancellationToken.cancel();
_logger.info("Cleaning up background worker");
final cleanupFutures = [
@@ -177,53 +183,61 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
// Discard any errors on the dispose call
return;
}),
LogService.I.dispose(),
Store.dispose(),
_drift.close(),
_driftLogger.close(),
_ref.read(backgroundSyncProvider).cancel(),
_ref.read(backgroundSyncProvider).cancelLocal(),
backgroundSyncManager.cancel(),
backgroundSyncManager.cancelLocal(),
];
if (_isar.isOpen) {
cleanupFutures.add(_isar.close());
}
_ref.dispose();
await Future.wait(cleanupFutures);
_logger.info("Background worker resources cleaned up");
} catch (error, stack) {
debugPrint('Failed to cleanup background worker: $error with stack: $stack');
dPrint(() => 'Failed to cleanup background worker: $error with stack: $stack');
}
}
Future<void> _handleBackup() async {
if (!_isBackupEnabled || _isCleanedUp) {
_logger.info("[_handleBackup 1] Backup is disabled. Skipping backup routine");
return;
}
await runZonedGuarded(
() async {
if (!_isBackupEnabled || _isCleanedUp) {
_logger.info("[_handleBackup 1] Backup is disabled. Skipping backup routine");
return;
}
_logger.info("[_handleBackup 2] Enqueuing assets for backup from the background service");
_logger.info("[_handleBackup 2] Enqueuing assets for backup from the background service");
final currentUser = _ref.read(currentUserProvider);
if (currentUser == null) {
_logger.warning("[_handleBackup 3] No current user found. Skipping backup from background");
return;
}
final currentUser = _ref.read(currentUserProvider);
if (currentUser == null) {
_logger.warning("[_handleBackup 3] No current user found. Skipping backup from background");
return;
}
_logger.info("[_handleBackup 4] Resume backup from background");
if (Platform.isIOS) {
return _ref.read(driftBackupProvider.notifier).handleBackupResume(currentUser.id);
}
_logger.info("[_handleBackup 4] Resume backup from background");
if (Platform.isIOS) {
return _ref.read(driftBackupProvider.notifier).handleBackupResume(currentUser.id);
}
final canPing = await _ref.read(serverInfoServiceProvider).ping();
if (!canPing) {
_logger.warning("[_handleBackup 5] Server is not reachable. Skipping backup from background");
return;
}
final canPing = await _ref.read(serverInfoServiceProvider).ping();
if (!canPing) {
_logger.warning("[_handleBackup 5] Server is not reachable. Skipping backup from background");
return;
}
final networkCapabilities = await _ref.read(connectivityApiProvider).getCapabilities();
final networkCapabilities = await _ref.read(connectivityApiProvider).getCapabilities();
return _ref
.read(uploadServiceProvider)
.startBackupWithHttpClient(currentUser.id, networkCapabilities.hasWifi, _cancellationToken);
return _ref
.read(uploadServiceProvider)
.startBackupWithHttpClient(currentUser.id, networkCapabilities.hasWifi, _cancellationToken);
},
(error, stack) {
dPrint(() => "Error in backup zone $error, $stack");
},
);
}
Future<void> _syncAssets({Duration? hashTimeout}) async {
@@ -259,6 +273,6 @@ Future<void> backgroundSyncNativeEntrypoint() async {
DartPluginRegistrant.ensureInitialized();
final (isar, drift, logDB) = await Bootstrap.initDB();
await Bootstrap.initDomain(isar, drift, logDB, shouldBufferLogs: false);
await Bootstrap.initDomain(isar, drift, logDB, shouldBufferLogs: false, listenStoreUpdates: false);
await BackgroundWorkerBgService(isar: isar, drift: drift, driftLogger: logDB).init();
}

View File

@@ -1,11 +1,11 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:immich_mobile/constants/constants.dart';
import 'package:immich_mobile/domain/models/log.model.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/infrastructure/repositories/log.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
import 'package:immich_mobile/utils/debug_print.dart';
import 'package:logging/logging.dart';
/// Service responsible for handling application logging.
@@ -66,13 +66,12 @@ class LogService {
}
void _handleLogRecord(LogRecord r) {
if (kDebugMode) {
debugPrint(
'[${r.level.name}] [${r.time}] [${r.loggerName}] ${r.message}'
'${r.error == null ? '' : '\nError: ${r.error}'}'
'${r.stackTrace == null ? '' : '\nStack: ${r.stackTrace}'}',
);
}
dPrint(
() =>
'[${r.level.name}] [${r.time}] [${r.loggerName}] ${r.message}'
'${r.error == null ? '' : '\nError: ${r.error}'}'
'${r.stackTrace == null ? '' : '\nStack: ${r.stackTrace}'}',
);
final record = LogMessage(
message: r.message,

View File

@@ -1,7 +1,7 @@
import 'package:flutter/foundation.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/infrastructure/repositories/partner.repository.dart';
import 'package:immich_mobile/repositories/partner_api.repository.dart';
import 'package:immich_mobile/utils/debug_print.dart';
class DriftPartnerService {
final DriftPartnerRepository _driftPartnerRepository;
@@ -30,7 +30,7 @@ class DriftPartnerService {
Future<void> toggleShowInTimeline(String partnerId, String userId) async {
final partner = await _driftPartnerRepository.getPartner(partnerId, userId);
if (partner == null) {
debugPrint("Partner not found: $partnerId for user: $userId");
dPrint(() => "Partner not found: $partnerId for user: $userId");
return;
}

View File

@@ -10,7 +10,7 @@ class StoreService {
/// In-memory cache. Keys are [StoreKey.id]
final Map<int, Object?> _cache = {};
late final StreamSubscription<List<StoreDto>> _storeUpdateSubscription;
StreamSubscription<List<StoreDto>>? _storeUpdateSubscription;
StoreService._({required IStoreRepository isarStoreRepository}) : _storeRepository = isarStoreRepository;
@@ -24,15 +24,17 @@ class StoreService {
}
// TODO: Replace the implementation with the one from create after removing the typedef
static Future<StoreService> init({required IStoreRepository storeRepository}) async {
_instance ??= await create(storeRepository: storeRepository);
static Future<StoreService> init({required IStoreRepository storeRepository, bool listenUpdates = true}) async {
_instance ??= await create(storeRepository: storeRepository, listenUpdates: listenUpdates);
return _instance!;
}
static Future<StoreService> create({required IStoreRepository storeRepository}) async {
static Future<StoreService> create({required IStoreRepository storeRepository, bool listenUpdates = true}) async {
final instance = StoreService._(isarStoreRepository: storeRepository);
await instance.populateCache();
instance._storeUpdateSubscription = instance._listenForChange();
if (listenUpdates) {
instance._storeUpdateSubscription = instance._listenForChange();
}
return instance;
}
@@ -50,8 +52,8 @@ class StoreService {
});
/// Disposes the store and cancels the subscription. To reuse the store call init() again
void dispose() async {
await _storeUpdateSubscription.cancel();
Future<void> dispose() async {
await _storeUpdateSubscription?.cancel();
_cache.clear();
}

View File

@@ -1,4 +1,3 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart';
@@ -6,6 +5,7 @@ import 'package:immich_mobile/infrastructure/repositories/remote_album.repositor
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
import 'package:immich_mobile/repositories/drift_album_api_repository.dart';
import 'package:logging/logging.dart';
import 'package:immich_mobile/utils/debug_print.dart';
final syncLinkedAlbumServiceProvider = Provider(
(ref) => SyncLinkedAlbumService(
@@ -100,7 +100,7 @@ class SyncLinkedAlbumService {
/// Creates a new remote album and links it to the local album
Future<void> _createAndLinkNewRemoteAlbum(LocalAlbum localAlbum) async {
debugPrint("Creating new remote album for local album: ${localAlbum.name}");
dPrint(() => "Creating new remote album for local album: ${localAlbum.name}");
final newRemoteAlbum = await _albumApiRepository.createDriftAlbum(localAlbum.name, assetIds: []);
await _remoteAlbumRepository.create(newRemoteAlbum, []);
return _localAlbumRepository.linkRemoteAlbum(localAlbum.id, newRemoteAlbum.id);

View File

@@ -29,6 +29,7 @@ class SyncStreamService {
bool shouldReset = false;
await _syncApiRepository.streamChanges(_handleEvents, onReset: () => shouldReset = true);
if (shouldReset) {
_logger.info("Resetting sync state as requested by server");
await _syncApiRepository.streamChanges(_handleEvents);
}
}

View File

@@ -100,8 +100,14 @@ class BackgroundSyncManager {
// We use a ternary operator to avoid [_deviceAlbumSyncTask] from being
// captured by the closure passed to [runInIsolateGentle].
_deviceAlbumSyncTask = full
? runInIsolateGentle(computation: (ref) => ref.read(localSyncServiceProvider).sync(full: true))
: runInIsolateGentle(computation: (ref) => ref.read(localSyncServiceProvider).sync(full: false));
? runInIsolateGentle(
computation: (ref) => ref.read(localSyncServiceProvider).sync(full: true),
debugLabel: 'local-sync-full-true',
)
: runInIsolateGentle(
computation: (ref) => ref.read(localSyncServiceProvider).sync(full: false),
debugLabel: 'local-sync-full-false',
);
return _deviceAlbumSyncTask!
.whenComplete(() {
@@ -122,7 +128,10 @@ class BackgroundSyncManager {
onHashingStart?.call();
_hashTask = runInIsolateGentle(computation: (ref) => ref.read(hashServiceProvider).hashAssets());
_hashTask = runInIsolateGentle(
computation: (ref) => ref.read(hashServiceProvider).hashAssets(),
debugLabel: 'hash-assets',
);
return _hashTask!
.whenComplete(() {
@@ -142,7 +151,10 @@ class BackgroundSyncManager {
onRemoteSyncStart?.call();
_syncTask = runInIsolateGentle(computation: (ref) => ref.read(syncStreamServiceProvider).sync());
_syncTask = runInIsolateGentle(
computation: (ref) => ref.read(syncStreamServiceProvider).sync(),
debugLabel: 'remote-sync',
);
return _syncTask!
.whenComplete(() {
onRemoteSyncComplete?.call();
@@ -169,7 +181,7 @@ class BackgroundSyncManager {
return _linkedAlbumSyncTask!.future;
}
_linkedAlbumSyncTask = runInIsolateGentle(computation: syncLinkedAlbumsIsolated);
_linkedAlbumSyncTask = runInIsolateGentle(computation: syncLinkedAlbumsIsolated, debugLabel: 'linked-album-sync');
return _linkedAlbumSyncTask!.whenComplete(() {
_linkedAlbumSyncTask = null;
});
@@ -178,4 +190,5 @@ class BackgroundSyncManager {
Cancelable<void> _handleWsAssetUploadReadyV1Batch(List<dynamic> batchData) => runInIsolateGentle(
computation: (ref) => ref.read(syncStreamServiceProvider).handleWsAssetUploadReadyV1Batch(batchData),
debugLabel: 'websocket-batch',
);

View File

@@ -1,9 +1,10 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/services/sync_linked_album.service.dart';
import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/entities/store.entity.dart';
Future<void> syncLinkedAlbumsIsolated(ProviderContainer ref) {
final user = ref.read(currentUserProvider);
final user = Store.tryGet(StoreKey.currentUser);
if (user == null) {
return Future.value();
}

View File

@@ -1,6 +1,7 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:intl/message_format.dart';
import 'package:flutter/material.dart';
import 'package:immich_mobile/utils/debug_print.dart';
extension StringTranslateExtension on String {
String t({BuildContext? context, Map<String, Object>? args}) {
@@ -39,7 +40,7 @@ String _translateHelper(BuildContext? context, String key, [Map<String, Object>?
? MessageFormat(translatedMessage, locale: Intl.defaultLocale ?? 'en').format(args)
: translatedMessage;
} catch (e) {
debugPrint('Translation failed for key "$key". Error: $e');
dPrint(() => 'Translation failed for key "$key". Error: $e');
return key;
}
}

View File

@@ -69,6 +69,29 @@ class Drift extends $Drift implements IDatabaseRepository {
Drift([QueryExecutor? executor])
: super(executor ?? driftDatabase(name: 'immich', native: const DriftNativeOptions(shareAcrossIsolates: true)));
Future<void> reset() async {
// https://github.com/simolus3/drift/commit/bd80a46264b6dd833ef4fd87fffc03f5a832ab41#diff-3f879e03b4a35779344ef16170b9353608dd9c42385f5402ec6035aac4dd8a04R76-R94
await exclusively(() async {
// https://stackoverflow.com/a/65743498/25690041
await customStatement('PRAGMA writable_schema = 1;');
await customStatement('DELETE FROM sqlite_master;');
await customStatement('VACUUM;');
await customStatement('PRAGMA writable_schema = 0;');
await customStatement('PRAGMA integrity_check');
await customStatement('PRAGMA user_version = 0');
await beforeOpen(
// ignore: invalid_use_of_internal_member
resolvedEngine.executor,
OpeningDetails(null, schemaVersion),
);
await customStatement('PRAGMA user_version = $schemaVersion');
// Refresh all stream queries
notifyUpdates({for (final table in allTables) TableUpdate.onTable(table)});
});
}
@override
int get schemaVersion => 10;

View File

@@ -3,7 +3,9 @@ import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:immich_mobile/constants/constants.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/models/sync_event.model.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/services/api.service.dart';
import 'package:logging/logging.dart';
import 'package:openapi/api.dart';
@@ -33,6 +35,7 @@ class SyncApiRepository {
await _api.applyToParams([], headerParams);
headers.addAll(headerParams);
final shouldReset = Store.get(StoreKey.shouldResetSync, false);
final request = http.Request('POST', Uri.parse(endpoint));
request.headers.addAll(headers);
request.body = jsonEncode(
@@ -58,6 +61,7 @@ class SyncApiRepository {
SyncRequestType.peopleV1,
SyncRequestType.assetFacesV1,
],
reset: shouldReset,
).toJson(),
);
@@ -81,6 +85,9 @@ class SyncApiRepository {
throw ApiException(response.statusCode, 'Failed to get sync stream: $errorBody');
}
// Reset after successful stream start
await Store.put(StoreKey.shouldResetSync, false);
await for (final chunk in response.stream.transform(utf8.decoder)) {
if (shouldAbort) {
break;

View File

@@ -42,14 +42,10 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
throw UnsupportedError("GroupAssetsBy.none is not supported for watchMainBucket");
}
return _db.mergedAssetDrift
.mergedBucket(userIds: userIds, groupBy: groupBy.index)
.map((row) {
final date = row.bucketDate.dateFmt(groupBy);
return TimeBucket(date: date, assetCount: row.assetCount);
})
.watch()
.throttle(const Duration(seconds: 3), trailing: true);
return _db.mergedAssetDrift.mergedBucket(userIds: userIds, groupBy: groupBy.index).map((row) {
final date = row.bucketDate.dateFmt(groupBy);
return TimeBucket(date: date, assetCount: row.assetCount);
}).watch();
}
Future<List<BaseAsset>> _getMainBucketAssets(List<String> userIds, {required int offset, required int count}) {
@@ -595,7 +591,7 @@ extension on String {
GroupAssetsBy.none => throw ArgumentError("GroupAssetsBy.none is not supported for date formatting"),
};
try {
return DateFormat(format).parse(this);
return DateFormat(format, 'en').parse(this);
} catch (e) {
throw FormatException("Invalid date format: $this", e);
}

View File

@@ -39,6 +39,7 @@ import 'package:intl/date_symbol_data_local.dart';
import 'package:logging/logging.dart';
import 'package:timezone/data/latest.dart';
import 'package:worker_manager/worker_manager.dart';
import 'package:immich_mobile/utils/debug_print.dart';
void main() async {
ImmichWidgetsBinding();
@@ -46,7 +47,7 @@ void main() async {
await Bootstrap.initDomain(isar, drift, logDb);
await initApp();
// Warm-up isolate pool for worker manager
await workerManager.init(dynamicSpawning: false);
await workerManager.init(dynamicSpawning: true);
await migrateDatabaseIfNeeded(isar, drift);
HttpSSLOptions.apply();
@@ -69,9 +70,9 @@ Future<void> initApp() async {
if (kReleaseMode && Platform.isAndroid) {
try {
await FlutterDisplayMode.setHighRefreshRate();
debugPrint("Enabled high refresh mode");
dPrint(() => "Enabled high refresh mode");
} catch (e) {
debugPrint("Error setting high refresh rate: $e");
dPrint(() => "Error setting high refresh rate: $e");
}
}
@@ -126,23 +127,23 @@ class ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserve
void didChangeAppLifecycleState(AppLifecycleState state) {
switch (state) {
case AppLifecycleState.resumed:
debugPrint("[APP STATE] resumed");
dPrint(() => "[APP STATE] resumed");
ref.read(appStateProvider.notifier).handleAppResume();
break;
case AppLifecycleState.inactive:
debugPrint("[APP STATE] inactive");
dPrint(() => "[APP STATE] inactive");
ref.read(appStateProvider.notifier).handleAppInactivity();
break;
case AppLifecycleState.paused:
debugPrint("[APP STATE] paused");
dPrint(() => "[APP STATE] paused");
ref.read(appStateProvider.notifier).handleAppPause();
break;
case AppLifecycleState.detached:
debugPrint("[APP STATE] detached");
dPrint(() => "[APP STATE] detached");
ref.read(appStateProvider.notifier).handleAppDetached();
break;
case AppLifecycleState.hidden:
debugPrint("[APP STATE] hidden");
dPrint(() => "[APP STATE] hidden");
ref.read(appStateProvider.notifier).handleAppHidden();
break;
}
@@ -200,7 +201,7 @@ class ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserve
@override
initState() {
super.initState();
initApp().then((_) => debugPrint("App Init Completed"));
initApp().then((_) => dPrint(() => "App Init Completed"));
WidgetsBinding.instance.addPostFrameCallback((_) {
// needs to be delayed so that EasyLocalization is working
if (Store.isBetaTimelineEnabled) {
@@ -239,7 +240,7 @@ class ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserve
theme: getThemeData(colorScheme: immichTheme.light, locale: context.locale),
routerConfig: router.config(
deepLinkBuilder: _deepLinkBuilder,
navigatorObservers: () => [AppNavigationObserver(ref: ref), HeroController()],
navigatorObservers: () => [AppNavigationObserver(ref: ref)],
),
),
);

View File

@@ -91,6 +91,8 @@ class _ChangeExperiencePageState extends ConsumerState<ChangeExperiencePage> {
ref.read(websocketProvider.notifier).stopListenToOldEvents();
ref.read(websocketProvider.notifier).startListeningToBetaEvents();
await ref.read(driftProvider).reset();
await Store.put(StoreKey.shouldResetSync, true);
final permission = await ref.read(galleryPermissionNotifier.notifier).requestGalleryPermission();
if (permission.isGranted) {

View File

@@ -12,7 +12,6 @@ import 'package:immich_mobile/widgets/settings/asset_viewer_settings/asset_viewe
import 'package:immich_mobile/widgets/settings/backup_settings/backup_settings.dart';
import 'package:immich_mobile/widgets/settings/backup_settings/drift_backup_settings.dart';
import 'package:immich_mobile/widgets/settings/beta_sync_settings/sync_status_and_actions.dart';
import 'package:immich_mobile/widgets/settings/beta_timeline_list_tile.dart';
import 'package:immich_mobile/widgets/settings/language_settings.dart';
import 'package:immich_mobile/widgets/settings/networking_settings/networking_settings.dart';
import 'package:immich_mobile/widgets/settings/notification_setting.dart';
@@ -20,7 +19,6 @@ import 'package:immich_mobile/widgets/settings/preference_settings/preference_se
import 'package:immich_mobile/widgets/settings/settings_card.dart';
enum SettingSection {
beta('sync_status', Icons.sync_outlined, "sync_status_subtitle"),
advanced('advanced', Icons.build_outlined, "advanced_settings_tile_subtitle"),
assetViewer('asset_viewer_settings_title', Icons.image_outlined, "asset_viewer_settings_subtitle"),
backup('backup', Icons.cloud_upload_outlined, "backup_settings_subtitle"),
@@ -28,14 +26,14 @@ enum SettingSection {
networking('networking_settings', Icons.wifi, "networking_subtitle"),
notifications('notifications', Icons.notifications_none_rounded, "setting_notifications_subtitle"),
preferences('preferences_settings_title', Icons.interests_outlined, "preferences_settings_subtitle"),
timeline('asset_list_settings_title', Icons.auto_awesome_mosaic_outlined, "asset_list_settings_subtitle");
timeline('asset_list_settings_title', Icons.auto_awesome_mosaic_outlined, "asset_list_settings_subtitle"),
beta('sync_status', Icons.sync_outlined, "sync_status_subtitle");
final String title;
final String subtitle;
final IconData icon;
Widget get widget => switch (this) {
SettingSection.beta => const _BetaLandscapeToggle(),
SettingSection.advanced => const AdvancedSettings(),
SettingSection.assetViewer => const AssetViewerSettings(),
SettingSection.backup =>
@@ -45,6 +43,7 @@ enum SettingSection {
SettingSection.notifications => const NotificationSetting(),
SettingSection.preferences => const PreferenceSetting(),
SettingSection.timeline => const AssetListSettings(),
SettingSection.beta => const SyncStatusAndActions(),
};
const SettingSection(this.title, this.icon, this.subtitle);
@@ -59,7 +58,7 @@ class SettingsPage extends StatelessWidget {
context.locale;
return Scaffold(
appBar: AppBar(centerTitle: false, title: const Text('settings').tr()),
body: context.isMobile ? const _MobileLayout() : const _TabletLayout(),
body: context.isMobile ? const SafeArea(child: _MobileLayout()) : const SafeArea(child: _TabletLayout()),
);
}
}
@@ -72,7 +71,6 @@ class _MobileLayout extends StatelessWidget {
.expand(
(setting) => setting == SettingSection.beta
? [
const BetaTimelineListTile(),
if (Store.isBetaTimelineEnabled)
SettingsCard(
icon: Icons.sync_outlined,
@@ -93,7 +91,7 @@ class _MobileLayout extends StatelessWidget {
.toList();
return ListView(
physics: const ClampingScrollPhysics(),
padding: const EdgeInsets.only(top: 10.0, bottom: 56),
padding: const EdgeInsets.only(top: 10.0, bottom: 16),
children: [...settings],
);
}
@@ -134,21 +132,6 @@ class _TabletLayout extends HookWidget {
}
}
class _BetaLandscapeToggle extends HookWidget {
const _BetaLandscapeToggle();
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
const SizedBox(height: 100, child: BetaTimelineListTile()),
if (Store.isBetaTimelineEnabled) const Expanded(child: SyncStatusAndActions()),
],
);
}
}
@RoutePage()
class SettingsSubPage extends StatelessWidget {
const SettingsSubPage(this.section, {super.key});
@@ -158,9 +141,14 @@ class SettingsSubPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
context.locale;
return Scaffold(
appBar: AppBar(centerTitle: false, title: Text(section.title).tr()),
body: section.widget,
return SafeArea(
bottom: true,
top: false,
right: true,
child: Scaffold(
appBar: AppBar(centerTitle: false, title: Text(section.title).tr()),
body: section.widget,
),
);
}
}

View File

@@ -10,6 +10,7 @@ import 'package:immich_mobile/providers/infrastructure/user.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/widgets/common/immich_toast.dart';
import 'package:immich_mobile/widgets/common/mesmerizing_sliver_app_bar.dart';
import 'package:immich_mobile/utils/debug_print.dart';
@RoutePage()
class DriftPartnerDetailPage extends StatelessWidget {
@@ -68,7 +69,7 @@ class _InfoBoxState extends ConsumerState<_InfoBox> {
_inTimeline = !_inTimeline;
});
} catch (error, stack) {
debugPrint("Failed to toggle in timeline: $error $stack");
dPrint(() => "Failed to toggle in timeline: $error $stack");
ImmichToast.show(
context: context,
toastType: ToastType.error,

View File

@@ -14,9 +14,10 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/unfavorite_act
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart';
import 'package:immich_mobile/providers/cast.provider.dart';
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart';
import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart';
import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart';
import 'package:immich_mobile/providers/routes.provider.dart';
import 'package:immich_mobile/providers/tab.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/routing/router.dart';
@@ -38,8 +39,9 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
final isReadonlyModeEnabled = ref.watch(readonlyModeProvider);
final previousRouteName = ref.watch(previousRouteNameProvider);
final tabRoute = ref.watch(tabProvider);
final showViewInTimelineButton =
previousRouteName != TabShellRoute.name &&
(previousRouteName != TabShellRoute.name || tabRoute == TabEnum.search) &&
previousRouteName != AssetViewerRoute.name &&
previousRouteName != null;

View File

@@ -43,7 +43,7 @@ class ArchiveBottomSheet extends ConsumerWidget {
const EditDateTimeActionButton(source: ActionSource.timeline),
const EditLocationActionButton(source: ActionSource.timeline),
const MoveToLockFolderActionButton(source: ActionSource.timeline),
const StackActionButton(source: ActionSource.timeline),
if (multiselect.selectedAssets.length > 1) const StackActionButton(source: ActionSource.timeline),
],
if (multiselect.hasLocal) ...[
const DeleteLocalActionButton(source: ActionSource.timeline),

View File

@@ -2,8 +2,8 @@ import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/edit_date_time_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/edit_location_action_button.widget.dart';
@@ -43,7 +43,7 @@ class FavoriteBottomSheet extends ConsumerWidget {
const EditDateTimeActionButton(source: ActionSource.timeline),
const EditLocationActionButton(source: ActionSource.timeline),
const MoveToLockFolderActionButton(source: ActionSource.timeline),
const StackActionButton(source: ActionSource.timeline),
if (multiselect.selectedAssets.length > 1) const StackActionButton(source: ActionSource.timeline),
],
if (multiselect.hasLocal) ...[
const DeleteLocalActionButton(source: ActionSource.timeline),

View File

@@ -104,7 +104,7 @@ class _GeneralBottomSheetState extends ConsumerState<GeneralBottomSheet> {
const EditDateTimeActionButton(source: ActionSource.timeline),
const EditLocationActionButton(source: ActionSource.timeline),
const MoveToLockFolderActionButton(source: ActionSource.timeline),
const StackActionButton(source: ActionSource.timeline),
if (multiselect.selectedAssets.length > 1) const StackActionButton(source: ActionSource.timeline),
isTrashEnable
? const TrashActionButton(source: ActionSource.timeline)
: const DeletePermanentActionButton(source: ActionSource.timeline),

View File

@@ -1,12 +1,12 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/domain/models/album/album.model.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/edit_date_time_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/edit_location_action_button.widget.dart';
@@ -100,7 +100,7 @@ class _RemoteAlbumBottomSheetState extends ConsumerState<RemoteAlbumBottomSheet>
const EditDateTimeActionButton(source: ActionSource.timeline),
const EditLocationActionButton(source: ActionSource.timeline),
const MoveToLockFolderActionButton(source: ActionSource.timeline),
const StackActionButton(source: ActionSource.timeline),
if (multiselect.selectedAssets.length > 1) const StackActionButton(source: ActionSource.timeline),
],
if (multiselect.hasLocal) ...[
const DeleteLocalActionButton(source: ActionSource.timeline),

View File

@@ -8,6 +8,7 @@ import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/providers/infrastructure/people.provider.dart';
import 'package:immich_mobile/widgets/common/immich_toast.dart';
import 'package:scroll_date_picker/scroll_date_picker.dart';
import 'package:immich_mobile/utils/debug_print.dart';
class DriftPersonBirthdayEditForm extends ConsumerStatefulWidget {
final DriftPerson person;
@@ -36,7 +37,7 @@ class _DriftPersonNameEditFormState extends ConsumerState<DriftPersonBirthdayEdi
context.pop<DateTime>(_selectedDate);
}
} catch (error) {
debugPrint('Error updating birthday: $error');
dPrint(() => 'Error updating birthday: $error');
if (!context.mounted) {
return;

View File

@@ -7,6 +7,7 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/providers/infrastructure/people.provider.dart';
import 'package:immich_mobile/widgets/common/immich_toast.dart';
import 'package:immich_mobile/utils/debug_print.dart';
class DriftPersonNameEditForm extends ConsumerStatefulWidget {
final DriftPerson person;
@@ -34,7 +35,7 @@ class _DriftPersonNameEditFormState extends ConsumerState<DriftPersonNameEditFor
context.pop<String>(newName);
}
} catch (error) {
debugPrint('Error updating name: $error');
dPrint(() => 'Error updating name: $error');
if (!context.mounted) {
return;

View File

@@ -3,14 +3,15 @@ import 'dart:async';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/constants.dart';
import 'package:immich_mobile/domain/models/timeline.model.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/theme_extensions.dart';
import 'package:immich_mobile/presentation/widgets/timeline/constants.dart';
import 'package:immich_mobile/presentation/widgets/timeline/segment.model.dart';
import 'package:immich_mobile/presentation/widgets/timeline/timeline.state.dart';
import 'package:intl/intl.dart' hide TextDirection;
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
import 'package:intl/intl.dart' hide TextDirection;
/// A widget that will display a BoxScrollView with a ScrollThumb that can be dragged
/// for quick navigation of the BoxScrollView.
@@ -79,6 +80,7 @@ class ScrubberState extends ConsumerState<Scrubber> with TickerProviderStateMixi
double _thumbTopOffset = 0.0;
bool _isDragging = false;
List<_Segment> _segments = [];
int _monthCount = 0;
late AnimationController _thumbAnimationController;
Timer? _fadeOutTimer;
@@ -105,6 +107,7 @@ class ScrubberState extends ConsumerState<Scrubber> with TickerProviderStateMixi
_thumbAnimationController = AnimationController(vsync: this, duration: kTimelineScrubberFadeInDuration);
_thumbAnimation = CurvedAnimation(parent: _thumbAnimationController, curve: Curves.fastEaseInToSlowEaseOut);
_labelAnimationController = AnimationController(vsync: this, duration: kTimelineScrubberFadeInDuration);
_monthCount = getMonthCount();
_labelAnimation = CurvedAnimation(parent: _labelAnimationController, curve: Curves.fastOutSlowIn);
}
@@ -121,6 +124,7 @@ class ScrubberState extends ConsumerState<Scrubber> with TickerProviderStateMixi
if (oldWidget.layoutSegments.lastOrNull?.endOffset != widget.layoutSegments.lastOrNull?.endOffset) {
_segments = _buildSegments(layoutSegments: widget.layoutSegments, timelineHeight: _scrubberHeight);
_monthCount = getMonthCount();
}
}
@@ -140,6 +144,10 @@ class ScrubberState extends ConsumerState<Scrubber> with TickerProviderStateMixi
});
}
int getMonthCount() {
return _segments.map((e) => "${e.date.month}_${e.date.year}").toSet().length;
}
bool _onScrollNotification(ScrollNotification notification) {
if (_isDragging) {
// If the user is dragging the thumb, we don't want to update the position
@@ -169,7 +177,10 @@ class ScrubberState extends ConsumerState<Scrubber> with TickerProviderStateMixi
}
void _onDragStart(DragStartDetails _) {
ref.read(timelineStateProvider.notifier).setScrubbing(true);
if (_monthCount >= kMinMonthsToEnableScrubberSnap) {
ref.read(timelineStateProvider.notifier).setScrubbing(true);
}
setState(() {
_isDragging = true;
_labelAnimationController.forward();
@@ -191,13 +202,22 @@ class ScrubberState extends ConsumerState<Scrubber> with TickerProviderStateMixi
final nearestMonthSegment = _findNearestMonthSegment(dragPosition);
if (nearestMonthSegment != null) {
_snapToSegment(nearestMonthSegment);
final label = nearestMonthSegment.scrollLabel;
if (_lastLabel != label) {
ref.read(hapticFeedbackProvider.notifier).selectionClick();
_lastLabel = label;
}
}
if (_monthCount < kMinMonthsToEnableScrubberSnap) {
// If there are less than kMinMonthsToEnableScrubberSnap months, we don't need to snap to segments
setState(() {
_thumbTopOffset = dragPosition;
_scrollController.jumpTo((dragPosition / _scrubberHeight) * _scrollController.position.maxScrollExtent);
});
} else if (nearestMonthSegment != null) {
_snapToSegment(nearestMonthSegment);
}
}
/// Calculate the drag position relative to the scrubber area

View File

@@ -1,4 +1,3 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
@@ -13,6 +12,7 @@ import 'package:immich_mobile/services/etag.service.dart';
import 'package:immich_mobile/services/exif.service.dart';
import 'package:immich_mobile/services/sync.service.dart';
import 'package:logging/logging.dart';
import 'package:immich_mobile/utils/debug_print.dart';
final assetProvider = StateNotifierProvider<AssetNotifier, bool>((ref) {
return AssetNotifier(
@@ -68,7 +68,7 @@ class AssetNotifier extends StateNotifier<bool> {
}
final bool newRemote = await _assetService.refreshRemoteAssets();
final bool newLocal = await _albumService.refreshDeviceAlbums();
debugPrint("changedUsers: $changedUsers, newRemote: $newRemote, newLocal: $newLocal");
dPrint(() => "changedUsers: $changedUsers, newRemote: $newRemote, newLocal: $newLocal");
if (newRemote) {
_ref.invalidate(memoryFutureProvider);
}

View File

@@ -1,4 +1,3 @@
import 'package:flutter/foundation.dart';
import 'package:flutter_udid/flutter_udid.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/constants.dart';
@@ -18,6 +17,7 @@ import 'package:immich_mobile/services/widget.service.dart';
import 'package:immich_mobile/utils/hash.dart';
import 'package:logging/logging.dart';
import 'package:openapi/api.dart';
import 'package:immich_mobile/utils/debug_print.dart';
final authProvider = StateNotifierProvider<AuthNotifier, AuthState>((ref) {
return AuthNotifier(
@@ -150,10 +150,7 @@ class AuthNotifier extends StateNotifier<AuthState> {
_log.severe("Error getting user information from the server [API EXCEPTION]", stackTrace);
} catch (error, stackTrace) {
_log.severe("Error getting user information from the server [CATCH ALL]", error, stackTrace);
if (kDebugMode) {
debugPrint("Error getting user information from the server [CATCH ALL] $error $stackTrace");
}
dPrint(() => "Error getting user information from the server [CATCH ALL] $error $stackTrace");
}
// If the user is null, the login was not successful

View File

@@ -2,8 +2,6 @@ import 'dart:io';
import 'package:cancellation_token_http/http.dart';
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/album.entity.dart';
@@ -33,6 +31,7 @@ import 'package:immich_mobile/utils/diff.dart';
import 'package:logging/logging.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:photo_manager/photo_manager.dart' show PMProgressHandler;
import 'package:immich_mobile/utils/debug_print.dart';
final backupProvider = StateNotifierProvider<BackupNotifier, BackUpState>((ref) {
return BackupNotifier(
@@ -286,7 +285,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
state = state.copyWith(selectedBackupAlbums: selectedAlbums, excludedBackupAlbums: excludedAlbums);
log.info("_getBackupAlbumsInfo: Found ${availableAlbums.length} available albums");
debugPrint("_getBackupAlbumsInfo takes ${stopwatch.elapsedMilliseconds}ms");
dPrint(() => "_getBackupAlbumsInfo takes ${stopwatch.elapsedMilliseconds}ms");
}
///
@@ -428,7 +427,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
/// Invoke backup process
Future<void> startBackupProcess() async {
debugPrint("Start backup process");
dPrint(() => "Start backup process");
assert(state.backupProgress == BackUpProgressEnum.idle);
state = state.copyWith(backupProgress: BackUpProgressEnum.inProgress);

View File

@@ -4,7 +4,6 @@ import 'dart:convert';
import 'package:background_downloader/background_downloader.dart';
import 'package:collection/collection.dart';
import 'package:flutter/widgets.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/constants.dart';
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
@@ -14,6 +13,7 @@ import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/services/upload.service.dart';
import 'package:logging/logging.dart';
import 'package:immich_mobile/utils/debug_print.dart';
class EnqueueStatus {
final int enqueueCount;
@@ -329,16 +329,16 @@ class DriftBackupNotifier extends StateNotifier<DriftBackupState> {
}
Future<void> cancel() async {
debugPrint("Canceling backup tasks...");
dPrint(() => "Canceling backup tasks...");
state = state.copyWith(enqueueCount: 0, enqueueTotalCount: 0, isCanceling: true);
final activeTaskCount = await _uploadService.cancelBackup();
if (activeTaskCount > 0) {
debugPrint("$activeTaskCount tasks left, continuing to cancel...");
dPrint(() => "$activeTaskCount tasks left, continuing to cancel...");
await cancel();
} else {
debugPrint("All tasks canceled successfully.");
dPrint(() => "All tasks canceled successfully.");
// Clear all upload items when cancellation is complete
state = state.copyWith(isCanceling: false, uploadItems: {});
}

View File

@@ -30,6 +30,7 @@ import 'package:immich_mobile/widgets/common/immich_toast.dart';
import 'package:logging/logging.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:photo_manager/photo_manager.dart' show PMProgressHandler;
import 'package:immich_mobile/utils/debug_print.dart';
final manualUploadProvider = StateNotifierProvider<ManualUploadNotifier, ManualUploadState>((ref) {
return ManualUploadNotifier(
@@ -216,7 +217,7 @@ class ManualUploadNotifier extends StateNotifier<ManualUploadState> {
);
if (uploadAssets.isEmpty) {
debugPrint("[_startUpload] No Assets to upload - Abort Process");
dPrint(() => "[_startUpload] No Assets to upload - Abort Process");
_backupProvider.updateBackupProgress(BackUpProgressEnum.idle);
return false;
}
@@ -294,10 +295,10 @@ class ManualUploadNotifier extends StateNotifier<ManualUploadState> {
}
} else {
openAppSettings();
debugPrint("[_startUpload] Do not have permission to the gallery");
dPrint(() => "[_startUpload] Do not have permission to the gallery");
}
} catch (e) {
debugPrint("ERROR _startUpload: ${e.toString()}");
dPrint(() => "ERROR _startUpload: ${e.toString()}");
hasErrors = true;
} finally {
_backupProvider.updateBackupProgress(BackUpProgressEnum.idle);
@@ -340,7 +341,7 @@ class ManualUploadNotifier extends StateNotifier<ManualUploadState> {
// waits until it has stopped to start the backup.
final bool hasLock = await ref.read(backgroundServiceProvider).acquireLock();
if (!hasLock) {
debugPrint("[uploadAssets] could not acquire lock, exiting");
dPrint(() => "[uploadAssets] could not acquire lock, exiting");
ImmichToast.show(
context: context,
msg: "failed".tr(),
@@ -355,18 +356,18 @@ class ManualUploadNotifier extends StateNotifier<ManualUploadState> {
// check if backup is already in process - then return
if (_backupProvider.backupProgress == BackUpProgressEnum.manualInProgress) {
debugPrint("[uploadAssets] Manual upload is already running - abort");
dPrint(() => "[uploadAssets] Manual upload is already running - abort");
showInProgress = true;
}
if (_backupProvider.backupProgress == BackUpProgressEnum.inProgress) {
debugPrint("[uploadAssets] Auto Backup is already in progress - abort");
dPrint(() => "[uploadAssets] Auto Backup is already in progress - abort");
showInProgress = true;
return false;
}
if (_backupProvider.backupProgress == BackUpProgressEnum.inBackground) {
debugPrint("[uploadAssets] Background backup is running - abort");
dPrint(() => "[uploadAssets] Background backup is running - abort");
showInProgress = true;
}

View File

@@ -7,11 +7,12 @@ import 'package:immich_mobile/theme/theme_data.dart';
import 'package:immich_mobile/theme/dynamic_theme.dart';
import 'package:immich_mobile/providers/app_settings.provider.dart';
import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:immich_mobile/utils/debug_print.dart';
final immichThemeModeProvider = StateProvider<ThemeMode>((ref) {
final themeMode = ref.watch(appSettingsServiceProvider).getSetting(AppSettingsEnum.themeMode);
debugPrint("Current themeMode $themeMode");
dPrint(() => "Current themeMode $themeMode");
if (themeMode == ThemeMode.light.name) {
return ThemeMode.light;
@@ -26,12 +27,12 @@ final immichThemePresetProvider = StateProvider<ImmichColorPreset>((ref) {
final appSettingsProvider = ref.watch(appSettingsServiceProvider);
final primaryColorPreset = appSettingsProvider.getSetting(AppSettingsEnum.primaryColor);
debugPrint("Current theme preset $primaryColorPreset");
dPrint(() => "Current theme preset $primaryColorPreset");
try {
return ImmichColorPreset.values.firstWhere((e) => e.name == primaryColorPreset);
} catch (e) {
debugPrint("Theme preset $primaryColorPreset not found. Applying default preset.");
dPrint(() => "Theme preset $primaryColorPreset not found. Applying default preset.");
appSettingsProvider.setSetting(AppSettingsEnum.primaryColor, defaultColorPresetName);
return defaultColorPreset;
}

View File

@@ -1,10 +1,10 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:image_picker/image_picker.dart';
import 'package:immich_mobile/domain/services/user.service.dart';
import 'package:immich_mobile/providers/infrastructure/user.provider.dart';
import 'package:immich_mobile/utils/debug_print.dart';
enum UploadProfileStatus { idle, loading, success, failure }
@@ -67,7 +67,7 @@ class UploadProfileImageNotifier extends StateNotifier<UploadProfileImageState>
var profileImagePath = await _userService.createProfileImage(file.name, await file.readAsBytes());
if (profileImagePath != null) {
debugPrint("Successfully upload profile image");
dPrint(() => "Successfully upload profile image");
state = state.copyWith(status: UploadProfileStatus.success, profileImagePath: profileImagePath);
return true;
}

View File

@@ -2,8 +2,6 @@ import 'dart:async';
import 'dart:convert';
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
@@ -20,6 +18,7 @@ import 'package:immich_mobile/utils/debounce.dart';
import 'package:logging/logging.dart';
import 'package:openapi/api.dart';
import 'package:socket_io_client/socket_io_client.dart';
import 'package:immich_mobile/utils/debug_print.dart';
enum PendingAction { assetDelete, assetUploaded, assetHidden, assetTrash }
@@ -105,7 +104,7 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
headers["Authorization"] = "Basic ${base64.encode(utf8.encode(endpoint.userInfo))}";
}
debugPrint("Attempting to connect to websocket");
dPrint(() => "Attempting to connect to websocket");
// Configure socket transports must be specified
Socket socket = io(
endpoint.origin,
@@ -121,12 +120,12 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
);
socket.onConnect((_) {
debugPrint("Established Websocket Connection");
dPrint(() => "Established Websocket Connection");
state = WebsocketState(isConnected: true, socket: socket, pendingChanges: state.pendingChanges);
});
socket.onDisconnect((_) {
debugPrint("Disconnect to Websocket Connection");
dPrint(() => "Disconnect to Websocket Connection");
state = WebsocketState(isConnected: false, socket: null, pendingChanges: state.pendingChanges);
});
@@ -150,13 +149,13 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
socket.on('on_config_update', _handleOnConfigUpdate);
socket.on('on_new_release', _handleReleaseUpdates);
} catch (e) {
debugPrint("[WEBSOCKET] Catch Websocket Error - ${e.toString()}");
dPrint(() => "[WEBSOCKET] Catch Websocket Error - ${e.toString()}");
}
}
}
void disconnect() {
debugPrint("Attempting to disconnect from websocket");
dPrint(() => "Attempting to disconnect from websocket");
_batchedAssetUploadReady.clear();
@@ -200,7 +199,7 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
}
void listenUploadEvent() {
debugPrint("Start listening to event on_upload_success");
dPrint(() => "Start listening to event on_upload_success");
state.socket?.on('on_upload_success', _handleOnUploadSuccess);
}
@@ -321,10 +320,13 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
return;
}
final isSyncAlbumEnabled = Store.get(StoreKey.syncAlbums, false);
try {
unawaited(
_ref.read(backgroundSyncProvider).syncWebsocketBatch(_batchedAssetUploadReady.toList()).then((_) {
return _ref.read(backgroundSyncProvider).syncLinkedAlbum();
if (isSyncAlbumEnabled) {
_ref.read(backgroundSyncProvider).syncLinkedAlbum();
}
}),
);
} catch (error) {

View File

@@ -3,12 +3,12 @@ import 'dart:io';
import 'package:background_downloader/background_downloader.dart';
import 'package:cancellation_token_http/http.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/constants.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:logging/logging.dart';
import 'package:immich_mobile/utils/debug_print.dart';
class UploadTaskWithFile {
final File file;
@@ -79,14 +79,17 @@ class UploadRepository {
FileDownloader().database.allRecordsWithStatus(TaskStatus.paused, group: kBackupGroup),
]);
debugPrint("""
dPrint(
() =>
"""
Upload Info:
Enqueued: ${enqueuedTasks.length}
Running: ${runningTasks.length}
Canceled: ${canceledTasks.length}
Waiting: ${waitingTasks.length}
Paused: ${pausedTasks.length}
""");
""",
);
}
Future<void> backupWithDartClient(Iterable<UploadTaskWithFile> tasks, CancellationToken cancelToken) async {

View File

@@ -1,5 +1,5 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/foundation.dart';
import 'package:immich_mobile/utils/debug_print.dart';
/// Guards against duplicate navigation to this route
class DuplicateGuard extends AutoRouteGuard {
@@ -8,7 +8,7 @@ class DuplicateGuard extends AutoRouteGuard {
void onNavigation(NavigationResolver resolver, StackRouter router) async {
// Duplicate navigation
if (resolver.route.name == router.current.name) {
debugPrint('DuplicateGuard: Preventing duplicate route navigation for ${resolver.route.name}');
dPrint(() => 'DuplicateGuard: Preventing duplicate route navigation for ${resolver.route.name}');
resolver.next(false);
} else {
resolver.next(true);

View File

@@ -3,7 +3,6 @@ import 'dart:collection';
import 'dart:io';
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
@@ -24,6 +23,7 @@ import 'package:immich_mobile/services/entity.service.dart';
import 'package:immich_mobile/services/sync.service.dart';
import 'package:immich_mobile/utils/hash.dart';
import 'package:logging/logging.dart';
import 'package:immich_mobile/utils/debug_print.dart';
final albumServiceProvider = Provider(
(ref) => AlbumService(
@@ -124,7 +124,7 @@ class AlbumService {
} finally {
_localCompleter.complete(changes);
}
debugPrint("refreshDeviceAlbums took ${sw.elapsedMilliseconds}ms");
dPrint(() => "refreshDeviceAlbums took ${sw.elapsedMilliseconds}ms");
return changes;
}
@@ -172,7 +172,7 @@ class AlbumService {
} finally {
_remoteCompleter.complete(changes);
}
debugPrint("refreshRemoteAlbums took ${sw.elapsedMilliseconds}ms");
dPrint(() => "refreshRemoteAlbums took ${sw.elapsedMilliseconds}ms");
return changes;
}
@@ -220,7 +220,7 @@ class AlbumService {
return AlbumAddAssetsResponse(alreadyInAlbum: result.duplicates, successfullyAdded: addedAssets.length);
} catch (e) {
debugPrint("Error addAssets ${e.toString()}");
dPrint(() => "Error addAssets ${e.toString()}");
}
return null;
}
@@ -242,7 +242,7 @@ class AlbumService {
await _albumRepository.update(album);
return true;
} catch (e) {
debugPrint("Error setActivityEnabled ${e.toString()}");
dPrint(() => "Error setActivityEnabled ${e.toString()}");
}
return false;
}
@@ -271,7 +271,7 @@ class AlbumService {
}
return true;
} catch (e) {
debugPrint("Error deleteAlbum ${e.toString()}");
dPrint(() => "Error deleteAlbum ${e.toString()}");
}
return false;
}
@@ -281,7 +281,7 @@ class AlbumService {
await _albumApiRepository.removeUser(album.remoteId!, userId: "me");
return true;
} catch (e) {
debugPrint("Error leaveAlbum ${e.toString()}");
dPrint(() => "Error leaveAlbum ${e.toString()}");
return false;
}
}
@@ -293,7 +293,7 @@ class AlbumService {
await _updateAssets(album.id, remove: toRemove.toList());
return true;
} catch (e) {
debugPrint("Error removeAssetFromAlbum ${e.toString()}");
dPrint(() => "Error removeAssetFromAlbum ${e.toString()}");
}
return false;
}
@@ -310,7 +310,7 @@ class AlbumService {
return true;
} catch (error) {
debugPrint("Error removeUser ${error.toString()}");
dPrint(() => "Error removeUser ${error.toString()}");
return false;
}
}
@@ -327,7 +327,7 @@ class AlbumService {
return true;
} catch (error) {
debugPrint("Error addUsers ${error.toString()}");
dPrint(() => "Error addUsers ${error.toString()}");
}
return false;
}
@@ -340,7 +340,7 @@ class AlbumService {
await _albumRepository.update(album);
return true;
} catch (e) {
debugPrint("Error changeTitleAlbum ${e.toString()}");
dPrint(() => "Error changeTitleAlbum ${e.toString()}");
return false;
}
}
@@ -353,7 +353,7 @@ class AlbumService {
await _albumRepository.update(album);
return true;
} catch (e) {
debugPrint("Error changeDescriptionAlbum ${e.toString()}");
dPrint(() => "Error changeDescriptionAlbum ${e.toString()}");
return false;
}
}

View File

@@ -3,7 +3,6 @@ import 'dart:convert';
import 'dart:io';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/material.dart';
import 'package:http/http.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/store.entity.dart';
@@ -11,6 +10,7 @@ import 'package:immich_mobile/utils/url_helper.dart';
import 'package:logging/logging.dart';
import 'package:openapi/api.dart';
import 'package:immich_mobile/utils/user_agent.dart';
import 'package:immich_mobile/utils/debug_print.dart';
class ApiService implements Authentication {
late ApiClient _apiClient;
@@ -155,7 +155,7 @@ class ApiService implements Authentication {
return endpoint;
}
} catch (e) {
debugPrint("Could not locate /.well-known/immich at $baseUrl");
dPrint(() => "Could not locate /.well-known/immich at $baseUrl");
}
return "";

View File

@@ -1,7 +1,6 @@
import 'dart:async';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
@@ -26,6 +25,7 @@ import 'package:immich_mobile/services/sync.service.dart';
import 'package:logging/logging.dart';
import 'package:maplibre_gl/maplibre_gl.dart';
import 'package:openapi/api.dart';
import 'package:immich_mobile/utils/debug_print.dart';
final assetServiceProvider = Provider(
(ref) => AssetService(
@@ -87,7 +87,7 @@ class AssetService {
getChangedAssets: _getRemoteAssetChanges,
loadAssets: _getRemoteAssets,
);
debugPrint("refreshRemoteAssets full took ${sw.elapsedMilliseconds}ms");
dPrint(() => "refreshRemoteAssets full took ${sw.elapsedMilliseconds}ms");
return changes;
}
@@ -156,7 +156,7 @@ class AssetService {
if (a.isInDb) {
await _assetRepository.transaction(() => _assetRepository.update(a));
} else {
debugPrint("[loadExif] parameter Asset is not from DB!");
dPrint(() => "[loadExif] parameter Asset is not from DB!");
}
}
}

View File

@@ -7,7 +7,6 @@ import 'dart:ui' show DartPluginRegistrant, IsolateNameServer, PluginUtilities;
import 'package:cancellation_token_http/http.dart';
import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
@@ -29,6 +28,7 @@ import 'package:immich_mobile/services/backup.service.dart';
import 'package:immich_mobile/services/localization.service.dart';
import 'package:immich_mobile/utils/backup_progress.dart';
import 'package:immich_mobile/utils/bootstrap.dart';
import 'package:immich_mobile/utils/debug_print.dart';
import 'package:immich_mobile/utils/diff.dart';
import 'package:immich_mobile/utils/http_ssl_options.dart';
import 'package:path_provider_foundation/path_provider_foundation.dart';
@@ -165,7 +165,7 @@ class BackgroundService {
]);
}
} catch (error) {
debugPrint("[_updateNotification] failed to communicate with plugin");
dPrint(() => "[_updateNotification] failed to communicate with plugin");
}
return false;
}
@@ -177,7 +177,7 @@ class BackgroundService {
return await _backgroundChannel.invokeMethod('showError', [title, content, individualTag]);
}
} catch (error) {
debugPrint("[_showErrorNotification] failed to communicate with plugin");
dPrint(() => "[_showErrorNotification] failed to communicate with plugin");
}
return false;
}
@@ -188,7 +188,7 @@ class BackgroundService {
return await _backgroundChannel.invokeMethod('clearErrorNotifications');
}
} catch (error) {
debugPrint("[_clearErrorNotifications] failed to communicate with plugin");
dPrint(() => "[_clearErrorNotifications] failed to communicate with plugin");
}
return false;
}
@@ -196,7 +196,7 @@ class BackgroundService {
/// await to ensure this thread (foreground or background) has exclusive access
Future<bool> acquireLock() async {
if (_hasLock) {
debugPrint("WARNING: [acquireLock] called more than once");
dPrint(() => "WARNING: [acquireLock] called more than once");
return true;
}
final int lockTime = Timeline.now;
@@ -302,19 +302,19 @@ class BackgroundService {
final bool hasAccess = await waitForLock;
if (!hasAccess) {
debugPrint("[_callHandler] could not acquire lock, exiting");
dPrint(() => "[_callHandler] could not acquire lock, exiting");
return false;
}
final translationsOk = await loadTranslations();
if (!translationsOk) {
debugPrint("[_callHandler] could not load translations");
dPrint(() => "[_callHandler] could not load translations");
}
final bool ok = await _onAssetsChanged();
return ok;
} catch (error) {
debugPrint(error.toString());
dPrint(() => error.toString());
return false;
} finally {
releaseLock();
@@ -324,14 +324,14 @@ class BackgroundService {
_cancellationToken?.cancel();
return true;
default:
debugPrint("Unknown method ${call.method}");
dPrint(() => "Unknown method ${call.method}");
return false;
}
}
Future<bool> _onAssetsChanged() async {
final (isar, drift, logDb) = await Bootstrap.initDB();
await Bootstrap.initDomain(isar, drift, logDb);
await Bootstrap.initDomain(isar, drift, logDb, shouldBufferLogs: false, listenStoreUpdates: false);
final ref = ProviderContainer(
overrides: [
@@ -344,9 +344,7 @@ class BackgroundService {
HttpSSLOptions.apply();
ref.read(apiServiceProvider).setAccessToken(Store.get(StoreKey.accessToken));
await ref.read(authServiceProvider).setOpenApiServiceEndpoint();
if (kDebugMode) {
debugPrint("[BG UPLOAD] Using endpoint: ${ref.read(apiServiceProvider).apiClient.basePath}");
}
dPrint(() => "[BG UPLOAD] Using endpoint: ${ref.read(apiServiceProvider).apiClient.basePath}");
final selectedAlbums = await ref.read(backupAlbumRepositoryProvider).getAllBySelection(BackupSelection.select);
final excludedAlbums = await ref.read(backupAlbumRepositoryProvider).getAllBySelection(BackupSelection.exclude);

View File

@@ -4,7 +4,6 @@ import 'dart:io';
import 'package:cancellation_token_http/http.dart' as http;
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/album.entity.dart';
@@ -29,6 +28,7 @@ import 'package:openapi/api.dart';
import 'package:path/path.dart' as p;
import 'package:permission_handler/permission_handler.dart' as pm;
import 'package:photo_manager/photo_manager.dart' show PMProgressHandler;
import 'package:immich_mobile/utils/debug_print.dart';
final backupServiceProvider = Provider(
(ref) => BackupService(
@@ -69,7 +69,7 @@ class BackupService {
try {
return await _apiService.assetsApi.getAllUserAssetsByDeviceId(deviceId);
} catch (e) {
debugPrint('Error [getDeviceBackupAsset] ${e.toString()}');
dPrint(() => 'Error [getDeviceBackupAsset] ${e.toString()}');
return null;
}
}
@@ -356,8 +356,9 @@ class BackupService {
final error = responseBody;
final errorMessage = error['message'] ?? error['error'];
debugPrint(
"Error(${error['statusCode']}) uploading ${asset.localId} | $originalFileName | Created on ${asset.fileCreatedAt} | ${error['error']}",
dPrint(
() =>
"Error(${error['statusCode']}) uploading ${asset.localId} | $originalFileName | Created on ${asset.fileCreatedAt} | ${error['error']}",
);
onError(
@@ -398,11 +399,11 @@ class BackupService {
}
}
} on http.CancelledException {
debugPrint("Backup was cancelled by the user");
dPrint(() => "Backup was cancelled by the user");
anyErrors = true;
break;
} catch (error, stackTrace) {
debugPrint("Error backup asset: ${error.toString()}: $stackTrace");
dPrint(() => "Error backup asset: ${error.toString()}: $stackTrace");
anyErrors = true;
continue;
} finally {
@@ -411,7 +412,7 @@ class BackupService {
await file?.delete();
await livePhotoFile?.delete();
} catch (e) {
debugPrint("ERROR deleting file: ${e.toString()}");
dPrint(() => "ERROR deleting file: ${e.toString()}");
}
}
}
@@ -454,7 +455,9 @@ class BackupService {
if (![200, 201].contains(response.statusCode)) {
var error = responseBody;
debugPrint("Error(${error['statusCode']}) uploading livePhoto for assetId | $livePhotoTitle | ${error['error']}");
dPrint(
() => "Error(${error['statusCode']}) uploading livePhoto for assetId | $livePhotoTitle | ${error['error']}",
);
}
return responseBody.containsKey('id') ? responseBody['id'] : null;

View File

@@ -1,9 +1,9 @@
import 'package:flutter/foundation.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/providers/backup/manual_upload.provider.dart';
import 'package:immich_mobile/providers/notification_permission.provider.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:immich_mobile/utils/debug_print.dart';
final localNotificationService = Provider(
(ref) => LocalNotificationService(ref.watch(notificationPermissionProvider), ref),
@@ -110,7 +110,7 @@ class LocalNotificationService {
switch (notificationResponse.actionId) {
case cancelUploadActionID:
{
debugPrint("User cancelled manual upload operation");
dPrint(() => "User cancelled manual upload operation");
ref.read(manualUploadProvider.notifier).cancelBackup();
}
}

View File

@@ -2,9 +2,9 @@
import 'package:easy_localization/src/easy_localization_controller.dart';
import 'package:easy_localization/src/localization.dart';
import 'package:flutter/foundation.dart';
import 'package:immich_mobile/constants/locales.dart';
import 'package:immich_mobile/generated/codegen_loader.g.dart';
import 'package:immich_mobile/utils/debug_print.dart';
/// Workaround to manually load translations in another Isolate
Future<bool> loadTranslations() async {
@@ -17,7 +17,7 @@ Future<bool> loadTranslations() async {
assetLoader: const CodegenLoader(),
path: translationsPath,
useOnlyLangCode: false,
onLoadError: (e) => debugPrint(e.toString()),
onLoadError: (e) => dPrint(() => e.toString()),
fallbackLocale: locales.values.first,
);

View File

@@ -1,4 +1,3 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/string_extensions.dart';
import 'package:immich_mobile/infrastructure/repositories/search_api.repository.dart';
@@ -10,6 +9,7 @@ import 'package:immich_mobile/repositories/asset.repository.dart';
import 'package:immich_mobile/services/api.service.dart';
import 'package:logging/logging.dart';
import 'package:openapi/api.dart';
import 'package:immich_mobile/utils/debug_print.dart';
final searchServiceProvider = Provider(
(ref) => SearchService(
@@ -43,7 +43,7 @@ class SearchService {
model: model,
);
} catch (e) {
debugPrint("[ERROR] [getSearchSuggestions] ${e.toString()}");
dPrint(() => "[ERROR] [getSearchSuggestions] ${e.toString()}");
return [];
}
}

View File

@@ -1,4 +1,3 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/models/server_info/server_config.model.dart';
import 'package:immich_mobile/models/server_info/server_disk_info.model.dart';
@@ -6,6 +5,7 @@ import 'package:immich_mobile/models/server_info/server_features.model.dart';
import 'package:immich_mobile/models/server_info/server_version.model.dart';
import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/services/api.service.dart';
import 'package:immich_mobile/utils/debug_print.dart';
final serverInfoServiceProvider = Provider((ref) => ServerInfoService(ref.watch(apiServiceProvider)));
@@ -30,7 +30,7 @@ class ServerInfoService {
return ServerDiskInfo.fromDto(dto);
}
} catch (e) {
debugPrint("Error [getDiskInfo] ${e.toString()}");
dPrint(() => "Error [getDiskInfo] ${e.toString()}");
}
return null;
}
@@ -42,7 +42,7 @@ class ServerInfoService {
return ServerVersion.fromDto(dto);
}
} catch (e) {
debugPrint("Error [getServerVersion] ${e.toString()}");
dPrint(() => "Error [getServerVersion] ${e.toString()}");
}
return null;
}
@@ -54,7 +54,7 @@ class ServerInfoService {
return ServerFeatures.fromDto(dto);
}
} catch (e) {
debugPrint("Error [getServerFeatures] ${e.toString()}");
dPrint(() => "Error [getServerFeatures] ${e.toString()}");
}
return null;
}
@@ -66,7 +66,7 @@ class ServerInfoService {
return ServerConfig.fromDto(dto);
}
} catch (e) {
debugPrint("Error [getServerConfig] ${e.toString()}");
dPrint(() => "Error [getServerConfig] ${e.toString()}");
}
return null;
}

View File

@@ -1,10 +1,10 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/repositories/asset.repository.dart';
import 'package:immich_mobile/services/api.service.dart';
import 'package:openapi/api.dart';
import 'package:immich_mobile/utils/debug_print.dart';
class StackService {
const StackService(this._api, this._assetRepository);
@@ -16,7 +16,7 @@ class StackService {
try {
return _api.stacksApi.getStack(stackId);
} catch (error) {
debugPrint("Error while fetching stack: $error");
dPrint(() => "Error while fetching stack: $error");
}
return null;
}
@@ -25,7 +25,7 @@ class StackService {
try {
return _api.stacksApi.createStack(StackCreateDto(assetIds: assetIds));
} catch (error) {
debugPrint("Error while creating stack: $error");
dPrint(() => "Error while creating stack: $error");
}
return null;
}
@@ -34,7 +34,7 @@ class StackService {
try {
return await _api.stacksApi.updateStack(stackId, StackUpdateDto(primaryAssetId: primaryAssetId));
} catch (error) {
debugPrint("Error while updating stack children: $error");
dPrint(() => "Error while updating stack children: $error");
}
return null;
}
@@ -54,7 +54,7 @@ class StackService {
}
await _assetRepository.transaction(() => _assetRepository.updateAll(removeAssets));
} catch (error) {
debugPrint("Error while deleting stack: $error");
dPrint(() => "Error while deleting stack: $error");
}
}
}

View File

@@ -4,7 +4,6 @@ import 'dart:io';
import 'package:background_downloader/background_downloader.dart';
import 'package:cancellation_token_http/http.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/constants.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
@@ -22,6 +21,7 @@ import 'package:immich_mobile/services/api.service.dart';
import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:logging/logging.dart';
import 'package:path/path.dart' as p;
import 'package:immich_mobile/utils/debug_print.dart';
final uploadServiceProvider = Provider((ref) {
final service = UploadService(
@@ -253,7 +253,7 @@ class UploadService {
enqueueTasks([uploadTask]);
} catch (error, stackTrace) {
debugPrint("Error handling live photo upload task: $error $stackTrace");
dPrint(() => "Error handling live photo upload task: $error $stackTrace");
}
}

View File

@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:dynamic_color/dynamic_color.dart';
import 'package:immich_mobile/theme/theme_data.dart';
import 'package:immich_mobile/utils/debug_print.dart';
abstract final class DynamicTheme {
const DynamicTheme._();
@@ -13,7 +14,7 @@ abstract final class DynamicTheme {
final corePalette = await DynamicColorPlugin.getCorePalette();
if (corePalette != null) {
final primaryColor = corePalette.toColorScheme().primary;
debugPrint('dynamic_color: Core palette detected.');
dPrint(() => 'dynamic_color: Core palette detected.');
// Some palettes do not generate surface container colors accurately,
// so we regenerate all colors using the primary color
@@ -23,7 +24,7 @@ abstract final class DynamicTheme {
);
}
} catch (error) {
debugPrint('dynamic_color: Failed to obtain core palette: $error');
dPrint(() => 'dynamic_color: Failed to obtain core palette: $error');
}
}

View File

@@ -89,11 +89,17 @@ abstract final class Bootstrap {
return (isar, drift, logDb);
}
static Future<void> initDomain(Isar db, Drift drift, DriftLogger logDb, {bool shouldBufferLogs = true}) async {
static Future<void> initDomain(
Isar db,
Drift drift,
DriftLogger logDb, {
bool listenStoreUpdates = true,
bool shouldBufferLogs = true,
}) async {
final isBeta = await IsarStoreRepository(db).tryGet(StoreKey.betaTimeline) ?? true;
final IStoreRepository storeRepo = isBeta ? DriftStoreRepository(drift) : IsarStoreRepository(db);
await StoreService.init(storeRepository: storeRepo);
await StoreService.init(storeRepository: storeRepo, listenUpdates: listenStoreUpdates);
await LogService.init(
logRepository: LogRepository(logDb),

View File

@@ -0,0 +1,8 @@
import 'package:flutter/foundation.dart';
@pragma('vm:prefer-inline')
void dPrint(String Function() message) {
if (kDebugMode) {
debugPrint(message());
}
}

View File

@@ -1,14 +1,15 @@
import 'dart:async';
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/services/log.service.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/providers/db.provider.dart';
import 'package:immich_mobile/providers/infrastructure/cancel.provider.dart';
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
import 'package:immich_mobile/utils/bootstrap.dart';
import 'package:immich_mobile/utils/debug_print.dart';
import 'package:immich_mobile/utils/http_ssl_options.dart';
import 'package:logging/logging.dart';
import 'package:worker_manager/worker_manager.dart';
@@ -31,55 +32,63 @@ Cancelable<T?> runInIsolateGentle<T>({
}
return workerManager.executeGentle((cancelledChecker) async {
BackgroundIsolateBinaryMessenger.ensureInitialized(token);
DartPluginRegistrant.ensureInitialized();
await runZonedGuarded(
() async {
BackgroundIsolateBinaryMessenger.ensureInitialized(token);
DartPluginRegistrant.ensureInitialized();
final (isar, drift, logDb) = await Bootstrap.initDB();
await Bootstrap.initDomain(isar, drift, logDb, shouldBufferLogs: false);
final ref = ProviderContainer(
overrides: [
// TODO: Remove once isar is removed
dbProvider.overrideWithValue(isar),
isarProvider.overrideWithValue(isar),
cancellationProvider.overrideWithValue(cancelledChecker),
driftProvider.overrideWith(driftOverride(drift)),
],
);
final (isar, drift, logDb) = await Bootstrap.initDB();
await Bootstrap.initDomain(isar, drift, logDb, shouldBufferLogs: false, listenStoreUpdates: false);
final ref = ProviderContainer(
overrides: [
// TODO: Remove once isar is removed
dbProvider.overrideWithValue(isar),
isarProvider.overrideWithValue(isar),
cancellationProvider.overrideWithValue(cancelledChecker),
driftProvider.overrideWith(driftOverride(drift)),
],
);
Logger log = Logger("IsolateLogger");
Logger log = Logger("IsolateLogger");
try {
HttpSSLOptions.apply(applyNative: false);
return await computation(ref);
} on CanceledError {
log.warning("Computation cancelled ${debugLabel == null ? '' : ' for $debugLabel'}");
} catch (error, stack) {
log.severe("Error in runInIsolateGentle ${debugLabel == null ? '' : ' for $debugLabel'}", error, stack);
} finally {
try {
await LogService.I.dispose();
await logDb.close();
await ref.read(driftProvider).close();
// Close Isar safely
try {
final isar = ref.read(isarProvider);
if (isar.isOpen) {
await isar.close();
}
} catch (e) {
debugPrint("Error closing Isar: $e");
}
HttpSSLOptions.apply(applyNative: false);
return await computation(ref);
} on CanceledError {
log.warning("Computation cancelled ${debugLabel == null ? '' : ' for $debugLabel'}");
} catch (error, stack) {
log.severe("Error in runInIsolateGentle ${debugLabel == null ? '' : ' for $debugLabel'}", error, stack);
} finally {
try {
ref.dispose();
ref.dispose();
} catch (error, stack) {
debugPrint("Error closing resources in isolate: $error, $stack");
} finally {
ref.dispose();
// Delay to ensure all resources are released
await Future.delayed(const Duration(seconds: 2));
}
}
await Store.dispose();
await LogService.I.dispose();
await logDb.close();
await drift.close();
// Close Isar safely
try {
if (isar.isOpen) {
await isar.close();
}
} catch (e) {
dPrint(() => "Error closing Isar: $e");
}
} catch (error, stack) {
dPrint(() => "Error closing resources in isolate: $error, $stack");
} finally {
ref.dispose();
// Delay to ensure all resources are released
await Future.delayed(const Duration(seconds: 2));
}
}
return null;
},
(error, stack) {
dPrint(() => "Error in isolate $debugLabel zone: $error, $stack");
},
);
return null;
});
}

View File

@@ -4,7 +4,6 @@ import 'dart:io';
import 'package:collection/collection.dart';
import 'package:drift/drift.dart';
import 'package:flutter/foundation.dart';
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/album.entity.dart';
@@ -22,12 +21,14 @@ import 'package:immich_mobile/infrastructure/entities/store.entity.dart';
import 'package:immich_mobile/infrastructure/entities/store.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart';
import 'package:immich_mobile/utils/debug_print.dart';
import 'package:immich_mobile/utils/diff.dart';
import 'package:isar/isar.dart';
// ignore: import_rule_photo_manager
import 'package:photo_manager/photo_manager.dart';
const int targetVersion = 15;
const int targetVersion = 16;
Future<void> migrateDatabaseIfNeeded(Isar db, Drift drift) async {
final hasVersion = Store.tryGet(StoreKey.version) != null;
@@ -76,11 +77,16 @@ Future<void> migrateDatabaseIfNeeded(Isar db, Drift drift) async {
await Store.put(StoreKey.needBetaMigration, false);
await Store.put(StoreKey.betaTimeline, true);
} else {
await resetDriftDatabase(drift);
await drift.reset();
await Store.put(StoreKey.needBetaMigration, true);
}
}
if (version < 16) {
await SyncStreamRepository(drift).reset();
await Store.put(StoreKey.shouldResetSync, true);
}
if (targetVersion >= 12) {
await Store.put(StoreKey.version, targetVersion);
return;
@@ -117,7 +123,7 @@ Future<bool> _isNewInstallation(Isar db, Drift drift) async {
return true;
} catch (error) {
debugPrint("[MIGRATION] Error checking if new installation: $error");
dPrint(() => "[MIGRATION] Error checking if new installation: $error");
return false;
}
}
@@ -143,10 +149,7 @@ Future<void> _migrateDeviceAsset(Isar db) async {
final PermissionState ps = await PhotoManager.requestPermissionExtend();
if (!ps.hasAccess) {
if (kDebugMode) {
debugPrint("[MIGRATION] Photo library permission not granted. Skipping device asset migration.");
}
dPrint(() => "[MIGRATION] Photo library permission not granted. Skipping device asset migration.");
return;
}
@@ -166,8 +169,8 @@ Future<void> _migrateDeviceAsset(Isar db) async {
localAssets = allDeviceAssets.map((a) => _DeviceAsset(assetId: a.id, dateTime: a.modifiedDateTime)).toList();
}
debugPrint("[MIGRATION] Device Asset Ids length - ${ids.length}");
debugPrint("[MIGRATION] Local Asset Ids length - ${localAssets.length}");
dPrint(() => "[MIGRATION] Device Asset Ids length - ${ids.length}");
dPrint(() => "[MIGRATION] Local Asset Ids length - ${localAssets.length}");
ids.sort((a, b) => a.assetId.compareTo(b.assetId));
localAssets.sort((a, b) => a.assetId.compareTo(b.assetId));
final List<DeviceAssetEntity> toAdd = [];
@@ -182,20 +185,14 @@ Future<void> _migrateDeviceAsset(Isar db) async {
return false;
},
onlyFirst: (deviceAsset) {
if (kDebugMode) {
debugPrint('[MIGRATION] Local asset not found in DeviceAsset: ${deviceAsset.assetId}');
}
dPrint(() => '[MIGRATION] Local asset not found in DeviceAsset: ${deviceAsset.assetId}');
},
onlySecond: (asset) {
if (kDebugMode) {
debugPrint('[MIGRATION] Local asset not found in DeviceAsset: ${asset.assetId}');
}
dPrint(() => '[MIGRATION] Local asset not found in DeviceAsset: ${asset.assetId}');
},
);
if (kDebugMode) {
debugPrint("[MIGRATION] Total number of device assets migrated - ${toAdd.length}");
}
dPrint(() => "[MIGRATION] Total number of device assets migrated - ${toAdd.length}");
await db.writeTxn(() async {
await db.deviceAssetEntitys.putAll(toAdd);
@@ -215,7 +212,7 @@ Future<void> migrateDeviceAssetToSqlite(Isar db, Drift drift) async {
}
});
} catch (error) {
debugPrint("[MIGRATION] Error while migrating device assets to SQLite: $error");
dPrint(() => "[MIGRATION] Error while migrating device assets to SQLite: $error");
}
}
@@ -263,7 +260,7 @@ Future<void> migrateBackupAlbumsToSqlite(Isar db, Drift drift) async {
}
});
} catch (error) {
debugPrint("[MIGRATION] Error while migrating backup albums to SQLite: $error");
dPrint(() => "[MIGRATION] Error while migrating backup albums to SQLite: $error");
}
}
@@ -281,7 +278,7 @@ Future<void> migrateStoreToSqlite(Isar db, Drift drift) async {
}
});
} catch (error) {
debugPrint("[MIGRATION] Error while migrating store values to SQLite: $error");
dPrint(() => "[MIGRATION] Error while migrating store values to SQLite: $error");
}
}
@@ -296,7 +293,7 @@ Future<void> migrateStoreToIsar(Isar db, Drift drift) async {
await db.storeValues.putAll(driftStoreValues);
});
} catch (error) {
debugPrint("[MIGRATION] Error while migrating store values to Isar: $error");
dPrint(() => "[MIGRATION] Error while migrating store values to Isar: $error");
}
}
@@ -307,27 +304,3 @@ class _DeviceAsset {
const _DeviceAsset({required this.assetId, this.hash, this.dateTime});
}
Future<void> resetDriftDatabase(Drift drift) async {
// https://github.com/simolus3/drift/commit/bd80a46264b6dd833ef4fd87fffc03f5a832ab41#diff-3f879e03b4a35779344ef16170b9353608dd9c42385f5402ec6035aac4dd8a04R76-R94
final database = drift.attachedDatabase;
await database.exclusively(() async {
// https://stackoverflow.com/a/65743498/25690041
await database.customStatement('PRAGMA writable_schema = 1;');
await database.customStatement('DELETE FROM sqlite_master;');
await database.customStatement('VACUUM;');
await database.customStatement('PRAGMA writable_schema = 0;');
await database.customStatement('PRAGMA integrity_check');
await database.customStatement('PRAGMA user_version = 0');
await database.beforeOpen(
// ignore: invalid_use_of_internal_member
database.resolvedEngine.executor,
OpeningDetails(null, database.schemaVersion),
);
await database.customStatement('PRAGMA user_version = ${database.schemaVersion}');
// Refresh all stream queries
database.notifyUpdates({for (final table in database.allTables) TableUpdate.onTable(table)});
});
}

View File

@@ -5,6 +5,7 @@ import 'package:immich_mobile/domain/models/exif.model.dart';
import 'package:immich_mobile/widgets/map/map_thumbnail.dart';
import 'package:maplibre_gl/maplibre_gl.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:immich_mobile/utils/debug_print.dart';
class ExifMap extends StatelessWidget {
final ExifInfo exifInfo;
@@ -66,7 +67,7 @@ class ExifMap extends StatelessWidget {
return;
}
debugPrint('Opening Map Uri: $uri');
dPrint(() => 'Opening Map Uri: $uri');
launchUrl(uri);
},
onCreated: onMapCreated,

View File

@@ -14,6 +14,7 @@ import 'package:immich_mobile/repositories/local_files_manager.repository.dart';
import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart';
import 'package:immich_mobile/utils/http_ssl_options.dart';
import 'package:immich_mobile/widgets/settings/beta_timeline_list_tile.dart';
import 'package:immich_mobile/widgets/settings/custom_proxy_headers_settings/custome_proxy_headers_settings.dart';
import 'package:immich_mobile/widgets/settings/local_storage_settings.dart';
import 'package:immich_mobile/widgets/settings/settings_slider_list_tile.dart';
@@ -91,7 +92,7 @@ class AdvancedSettings extends HookConsumerWidget {
title: "advanced_settings_prefer_remote_title".tr(),
subtitle: "advanced_settings_prefer_remote_subtitle".tr(),
),
const LocalStorageSettings(),
if (!Store.isBetaTimelineEnabled) const LocalStorageSettings(),
SettingsSwitchListTile(
enabled: !isLoggedIn,
valueNotifier: allowSelfSignedSSLCert,
@@ -101,12 +102,13 @@ class AdvancedSettings extends HookConsumerWidget {
),
const CustomeProxyHeaderSettings(),
SslClientCertSettings(isLoggedIn: ref.read(currentUserProvider) != null),
SettingsSwitchListTile(
valueNotifier: useAlternatePMFilter,
title: "advanced_settings_enable_alternate_media_filter_title".tr(),
subtitle: "advanced_settings_enable_alternate_media_filter_subtitle".tr(),
),
// TODO: Remove this check when beta timeline goes stable
if (!Store.isBetaTimelineEnabled)
SettingsSwitchListTile(
valueNotifier: useAlternatePMFilter,
title: "advanced_settings_enable_alternate_media_filter_title".tr(),
subtitle: "advanced_settings_enable_alternate_media_filter_subtitle".tr(),
),
const BetaTimelineListTile(),
if (Store.isBetaTimelineEnabled)
SettingsSwitchListTile(
valueNotifier: readonlyModeEnabled,

View File

@@ -5,13 +5,12 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/providers/background_sync.provider.dart';
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
import 'package:immich_mobile/providers/infrastructure/memory.provider.dart';
import 'package:immich_mobile/providers/infrastructure/storage.provider.dart';
import 'package:immich_mobile/providers/sync_status.provider.dart';
import 'package:immich_mobile/utils/migration.dart';
import 'package:immich_mobile/widgets/settings/beta_sync_settings/entity_count_tile.dart';
import 'package:path/path.dart' as path;
import 'package:path_provider/path_provider.dart';
@@ -83,7 +82,7 @@ class SyncStatusAndActions extends HookConsumerWidget {
),
TextButton(
onPressed: () async {
await resetDriftDatabase(ref.read(driftProvider));
await ref.read(driftProvider).reset();
context.pop();
context.scaffoldMessenger.showSnackBar(
SnackBar(content: Text("reset_sqlite_success".t(context: context))),

View File

@@ -1,5 +1,3 @@
import 'dart:math' as math;
import 'package:auto_route/auto_route.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
@@ -12,50 +10,11 @@ import 'package:immich_mobile/providers/server_info.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/services/app_settings.service.dart';
class BetaTimelineListTile extends ConsumerStatefulWidget {
class BetaTimelineListTile extends ConsumerWidget {
const BetaTimelineListTile({super.key});
@override
ConsumerState<BetaTimelineListTile> createState() => _BetaTimelineListTileState();
}
class _BetaTimelineListTileState extends ConsumerState<BetaTimelineListTile> with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _rotationAnimation;
late Animation<double> _pulseAnimation;
late Animation<double> _gradientAnimation;
@override
void initState() {
super.initState();
_animationController = AnimationController(duration: const Duration(seconds: 3), vsync: this);
_rotationAnimation = Tween<double>(
begin: 0,
end: 2 * math.pi,
).animate(CurvedAnimation(parent: _animationController, curve: Curves.linear));
_pulseAnimation = Tween<double>(
begin: 1,
end: 1.1,
).animate(CurvedAnimation(parent: _animationController, curve: Curves.easeInOut));
_gradientAnimation = Tween<double>(
begin: 0,
end: 1,
).animate(CurvedAnimation(parent: _animationController, curve: Curves.easeInOut));
_animationController.repeat(reverse: true);
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
Widget build(BuildContext context, WidgetRef ref) {
final betaTimelineValue = ref.watch(appSettingsServiceProvider).getSetting<bool>(AppSettingsEnum.betaTimeline);
final serverInfo = ref.watch(serverInfoProvider);
final auth = ref.watch(authProvider);
@@ -64,168 +23,50 @@ class _BetaTimelineListTileState extends ConsumerState<BetaTimelineListTile> wit
return const SizedBox.shrink();
}
return AnimatedBuilder(
animation: _animationController,
builder: (context, child) {
void onSwitchChanged(bool value) {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: value ? const Text("Enable Beta Timeline") : const Text("Disable Beta Timeline"),
content: value
? const Text("Are you sure you want to enable the beta timeline?")
: const Text("Are you sure you want to disable the beta timeline?"),
actions: [
TextButton(
onPressed: () {
context.pop();
},
child: Text(
"cancel".t(context: context),
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500, color: context.colorScheme.outline),
),
),
ElevatedButton(
onPressed: () async {
Navigator.of(context).pop();
context.router.replaceAll([ChangeExperienceRoute(switchingToBeta: value)]);
},
child: Text("ok".t(context: context)),
),
],
);
},
);
}
final gradientColors = [
Color.lerp(
context.primaryColor.withValues(alpha: 0.5),
context.primaryColor.withValues(alpha: 0.3),
_gradientAnimation.value,
)!,
Color.lerp(
context.logoPink.withValues(alpha: 0.2),
context.logoPink.withValues(alpha: 0.4),
_gradientAnimation.value,
)!,
Color.lerp(
context.logoRed.withValues(alpha: 0.3),
context.logoRed.withValues(alpha: 0.5),
_gradientAnimation.value,
)!,
];
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(12)),
gradient: LinearGradient(
colors: gradientColors,
stops: const [0.0, 0.5, 1.0],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
transform: GradientRotation(_rotationAnimation.value * 0.5),
),
boxShadow: [
BoxShadow(color: context.primaryColor.withValues(alpha: 0.1), blurRadius: 8, offset: const Offset(0, 2)),
],
),
child: Container(
margin: const EdgeInsets.all(2),
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(10.5)),
color: context.scaffoldBackgroundColor,
),
child: Material(
borderRadius: const BorderRadius.all(Radius.circular(10.5)),
child: InkWell(
borderRadius: const BorderRadius.all(Radius.circular(10.5)),
onTap: () => onSwitchChanged(!betaTimelineValue),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
child: Row(
children: [
Transform.scale(
scale: _pulseAnimation.value,
child: Transform.rotate(
angle: _rotationAnimation.value * 0.02,
child: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: LinearGradient(
colors: [
context.primaryColor.withValues(alpha: 0.2),
context.primaryColor.withValues(alpha: 0.1),
],
),
),
child: Icon(Icons.auto_awesome, color: context.primaryColor, size: 20),
),
),
),
const SizedBox(width: 28),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
"advanced_settings_beta_timeline_title".t(context: context),
style: context.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600),
),
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(8)),
gradient: LinearGradient(
colors: [
context.primaryColor.withValues(alpha: 0.8),
context.primaryColor.withValues(alpha: 0.6),
],
),
),
child: Text(
'NEW',
style: context.textTheme.labelSmall?.copyWith(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 10,
height: 1.2,
),
),
),
],
),
const SizedBox(height: 4),
Text(
"advanced_settings_beta_timeline_subtitle".t(context: context),
style: context.textTheme.labelLarge?.copyWith(
color: context.textTheme.labelLarge?.color?.withValues(alpha: 0.9),
),
maxLines: 2,
),
],
),
),
Switch.adaptive(
value: betaTimelineValue,
onChanged: onSwitchChanged,
activeColor: context.primaryColor,
),
],
),
void onSwitchChanged(bool value) {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: value ? const Text("Enable Beta Timeline") : const Text("Disable Beta Timeline"),
content: value
? const Text("Are you sure you want to enable the beta timeline?")
: const Text("Are you sure you want to disable the beta timeline?"),
actions: [
TextButton(
onPressed: () {
context.pop();
},
child: Text(
"cancel".t(context: context),
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500, color: context.colorScheme.outline),
),
),
),
),
);
},
ElevatedButton(
onPressed: () async {
Navigator.of(context).pop();
context.router.replaceAll([ChangeExperienceRoute(switchingToBeta: value)]);
},
child: Text("ok".t(context: context)),
),
],
);
},
);
}
return Padding(
padding: const EdgeInsets.only(left: 4.0),
child: ListTile(
title: Text("advanced_settings_beta_timeline_title".t(context: context)),
subtitle: Text("advanced_settings_beta_timeline_subtitle".t(context: context)),
trailing: Switch.adaptive(
value: betaTimelineValue,
onChanged: onSwitchChanged,
activeColor: context.primaryColor,
),
onTap: () => onSwitchChanged(!betaTimelineValue),
),
);
}
}

View File

@@ -16,6 +16,7 @@ import 'package:immich_mobile/utils/url_helper.dart';
import 'package:immich_mobile/widgets/common/confirm_dialog.dart';
import 'package:immich_mobile/widgets/common/immich_toast.dart';
import 'package:immich_mobile/widgets/search/thumbnail_with_info.dart';
import 'package:immich_mobile/utils/debug_print.dart';
class SharedLinkItem extends ConsumerWidget {
final SharedLink sharedLink;
@@ -36,7 +37,7 @@ class SharedLinkItem extends ConsumerWidget {
return Text("expired", style: TextStyle(color: Colors.red[300])).tr();
}
final difference = sharedLink.expiresAt!.difference(DateTime.now());
debugPrint("Difference: $difference");
dPrint(() => "Difference: $difference");
if (difference.inDays > 0) {
var dayDifference = difference.inDays;
if (difference.inHours % 24 > 12) {

View File

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

View File

@@ -77,10 +77,10 @@ packages:
dependency: "direct main"
description:
name: background_downloader
sha256: "2d4c2b7438e7643585880f9cc00ace16a52d778088751f1bfbf714627b315462"
sha256: "9ed74c55750932178f6989ba8a659687c2a102e05b70f561a1b3f047a5dda790"
url: "https://pub.dev"
source: hosted
version: "9.2.0"
version: "9.2.5"
bonsoir:
dependency: transitive
description:

View File

@@ -2,7 +2,7 @@ name: immich_mobile
description: Immich - selfhosted backup media file on mobile phone
publish_to: 'none'
version: 1.141.1+3013
version: 1.142.0+3014
environment:
sdk: '>=3.8.0 <4.0.0'
@@ -16,7 +16,7 @@ dependencies:
async: ^2.11.0
auto_route: ^9.2.0
background_downloader: ^9.2.0
background_downloader: ^9.2.5
cached_network_image: ^3.4.1
cancellation_token_http: ^2.1.0
cast: ^2.1.0

View File

@@ -4,12 +4,15 @@ import 'dart:convert';
import 'package:flutter_test/flutter_test.dart';
import 'package:http/http.dart' as http;
import 'package:immich_mobile/domain/models/sync_event.model.dart';
import 'package:immich_mobile/domain/services/store.service.dart';
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/sync_api.repository.dart';
import 'package:mocktail/mocktail.dart';
import 'package:openapi/api.dart';
import '../../api.mocks.dart';
import '../../service.mocks.dart';
import '../../test_utils.dart';
class MockHttpClient extends Mock implements http.Client {}
@@ -33,6 +36,10 @@ void main() {
late StreamController<List<int>> responseStreamController;
late int testBatchSize = 3;
setUpAll(() async {
await StoreService.init(storeRepository: IsarStoreRepository(await TestUtils.initIsar()));
});
setUp(() {
mockApiService = MockApiService();
mockApiClient = MockApiClient();

View File

@@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:immich_mobile/utils/throttle.dart';
import 'package:immich_mobile/utils/debug_print.dart';
class _Counter {
int _count = 0;
@@ -8,7 +8,7 @@ class _Counter {
int get count => _count;
void increment() {
debugPrint("Counter inside increment: $count");
dPrint(() => "Counter inside increment: $count");
_count = _count + 1;
}
}

View File

@@ -9858,7 +9858,7 @@
"info": {
"title": "Immich",
"description": "Immich API",
"version": "1.141.1",
"version": "1.142.0",
"contact": {}
},
"tags": [],

View File

@@ -1,6 +1,6 @@
{
"name": "@immich/sdk",
"version": "1.141.1",
"version": "1.142.0",
"description": "Auto-generated TypeScript SDK for the Immich API",
"type": "module",
"main": "./build/index.js",

View File

@@ -1,6 +1,6 @@
/**
* Immich
* 1.141.1
* 1.142.0
* DO NOT MODIFY - This file has been generated using oazapfts.
* See https://www.npmjs.com/package/oazapfts
*/

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "immich-web",
"version": "1.141.1",
"version": "1.142.0",
"license": "GNU Affero General Public License version 3",
"type": "module",
"scripts": {

View File

@@ -48,7 +48,7 @@
aria-label={$t('show_album_options')}
icon={mdiDotsVertical}
shape="round"
variant="ghost"
variant="filled"
size="medium"
class="icon-white-drop-shadow"
onclick={showAlbumContextMenu}

View File

@@ -45,15 +45,4 @@ describe('Thumbnail component', () => {
const tabbables = getTabbable(container!);
expect(tabbables.length).toBe(0);
});
it('shows thumbhash while image is loading', () => {
const asset = assetFactory.build({ originalPath: 'image.jpg', originalMimeType: 'image/jpeg' });
const sut = render(Thumbnail, {
asset,
selected: true,
});
const thumbhash = sut.getByTestId('thumbhash');
expect(thumbhash).not.toBeFalsy();
});
});

View File

@@ -20,6 +20,7 @@
import { authManager } from '$lib/managers/auth-manager.svelte';
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
import { mobileDevice } from '$lib/stores/mobile-device.svelte';
import { isCached } from '$lib/utils/cache';
import { moveFocus } from '$lib/utils/focus-util';
import { currentUrlReplaceAssetId } from '$lib/utils/navigation';
import { TUNABLES } from '$lib/utils/tunables';
@@ -75,6 +76,12 @@
IMAGE_THUMBNAIL: { THUMBHASH_FADE_DURATION },
} = TUNABLES;
const thumbnailURL = getAssetThumbnailUrl({
id: asset.id,
size: AssetMediaSize.Thumbnail,
cacheKey: asset.thumbhash,
});
let usingMobileDevice = $derived(mobileDevice.pointerCoarse);
let element: HTMLElement | undefined = $state();
let mouseOver = $state(false);
@@ -313,7 +320,7 @@
<ImageThumbnail
class={imageClass}
{brokenAssetClass}
url={getAssetThumbnailUrl({ id: asset.id, size: AssetMediaSize.Thumbnail, cacheKey: asset.thumbhash })}
url={thumbnailURL}
altText={$getAltText(asset)}
widthStyle="{width}px"
heightStyle="{height}px"
@@ -344,17 +351,31 @@
</div>
{/if}
{#if (!loaded || thumbError) && asset.thumbhash}
<canvas
use:thumbhash={{ base64ThumbHash: asset.thumbhash }}
data-testid="thumbhash"
class="absolute top-0 object-cover"
style:width="{width}px"
style:height="{height}px"
class:rounded-xl={selected}
draggable="false"
out:fade={{ duration: THUMBHASH_FADE_DURATION }}
></canvas>
{#if asset.thumbhash}
{#await isCached(new Request(thumbnailURL))}
<canvas
use:thumbhash={{ base64ThumbHash: asset.thumbhash }}
data-testid="thumbhash"
class="absolute top-0 object-cover"
style:width="{width}px"
style:height="{height}px"
class:rounded-xl={selected}
draggable="false"
></canvas>
{:then cached}
{#if !cached && !loaded && !thumbError}
<canvas
use:thumbhash={{ base64ThumbHash: asset.thumbhash }}
data-testid="thumbhash"
class="absolute top-0 object-cover"
style:width="{width}px"
style:height="{height}px"
class:rounded-xl={selected}
draggable="false"
out:fade={{ duration: THUMBHASH_FADE_DURATION }}
></canvas>
{/if}
{/await}
{/if}
</div>

View File

@@ -110,7 +110,7 @@
<svelte:document use:shortcut={{ shortcut: { key: 'Escape' }, onShortcut: onClose }} />
<div
class="fixed top-0 flex h-16 w-full items-center justify-between border-b bg-white p-1 dark:border-immich-dark-gray dark:bg-black dark:text-immich-dark-fg md:p-8"
class="fixed top-0 z-1 flex h-16 w-full items-center justify-between border-b bg-white p-1 dark:border-immich-dark-gray dark:bg-black dark:text-immich-dark-fg md:p-8"
>
<div class="flex items-center">
<IconButton

View File

@@ -64,9 +64,10 @@
{#if showVerticalDots}
<div class="absolute top-2 end-2 z-1">
<ButtonContextMenu
buttonClass="icon-white-drop-shadow focus:opacity-100 {showVerticalDots ? 'opacity-100' : 'opacity-0'}"
color="primary"
buttonClass="icon-white-drop-shadow"
color="secondary"
size="medium"
variant="filled"
icon={mdiDotsVertical}
title={$t('show_person_options')}
>

View File

@@ -68,7 +68,7 @@
<div class="absolute flex h-16 w-full place-items-center justify-between border-b p-2 text-dark">
<div class="flex gap-2 items-center">
{#if title}
<div class="font-medium outline-none" tabindex="-1" id={headerId}>{title}</div>
<div class="font-medium outline-none pe-8" tabindex="-1" id={headerId}>{title}</div>
{/if}
{#if description}
<p class="text-sm text-gray-400 dark:text-gray-600">{description}</p>

View File

@@ -16,7 +16,7 @@
import { fromTimelinePlainDate, getDateLocaleString } from '$lib/utils/timeline-util';
import type { Snippet } from 'svelte';
import { flip } from 'svelte/animate';
import { fly, scale } from 'svelte/transition';
import { scale } from 'svelte/transition';
let { isUploading } = uploadAssetsStore;
@@ -169,10 +169,11 @@
class="flex pt-7 pb-5 max-md:pt-5 max-md:pb-3 h-6 place-items-center text-xs font-medium text-immich-fg dark:text-immich-dark-fg md:text-sm"
style:width={dayGroup.width + 'px'}
>
{#if !singleSelect && ((hoveredDayGroup === dayGroup.groupTitle && isMouseOverGroup) || assetInteraction.selectedGroup.has(dayGroup.groupTitle))}
{#if !singleSelect}
<div
transition:fly={{ x: -24, duration: 200, opacity: 0.5 }}
class="inline-block pe-2 hover:cursor-pointer"
class="hover:cursor-pointer transition-all duration-200 ease-out overflow-hidden w-0"
class:w-8={(hoveredDayGroup === dayGroup.groupTitle && isMouseOverGroup) ||
assetInteraction.selectedGroup.has(dayGroup.groupTitle)}
onclick={() => handleSelectGroup(dayGroup.groupTitle, assetsSnapshot(dayGroup.getAssets()))}
onkeydown={() => handleSelectGroup(dayGroup.groupTitle, assetsSnapshot(dayGroup.getAssets()))}
>

View File

@@ -40,6 +40,8 @@
// of zero when starting the 'slide' animation.
let height: number = $state(0);
let isTransitioned = $state(false);
$effect(() => {
if (menuElement) {
let layoutDirection = direction;
@@ -64,6 +66,12 @@
style:top="{top}px"
transition:slide={{ duration: 250, easing: quintOut }}
use:clickOutside={{ onOutclick: onClose }}
onintroend={() => {
isTransitioned = true;
}}
onoutrostart={() => {
isTransitioned = false;
}}
>
<ul
{id}
@@ -73,7 +81,9 @@
bind:this={menuElement}
class="{isVisible
? 'max-h-dvh'
: 'max-h-0'} flex flex-col transition-all duration-250 ease-in-out outline-none overflow-auto"
: 'max-h-0'} flex flex-col transition-all duration-250 ease-in-out outline-none {isTransitioned
? 'overflow-auto'
: ''}"
role="menu"
tabindex="-1"
>

View File

@@ -353,7 +353,7 @@
<div
class="rounded-full w-[40px] h-[40px] bg-immich-primary text-white flex justify-center items-center font-mono font-bold shadow-lg hover:bg-immich-dark-primary transition-all duration-200 hover:text-immich-dark-bg opacity-90"
>
{feature.properties?.point_count}
{feature.properties?.point_count?.toLocaleString()}
</div>
{/snippet}
</MarkerLayer>

View File

@@ -0,0 +1,18 @@
let cache: Cache | undefined;
const getCache = async () => {
cache ||= await openCache();
return cache;
};
const openCache = async () => {
const [key] = await caches.keys();
if (key) {
return caches.open(key);
}
};
export const isCached = async (req: Request) => {
const cache = await getCache();
return !!(await cache?.match(req));
};

View File

@@ -27,7 +27,7 @@ export class GCastDestination implements ICastDestination {
async initialize(): Promise<boolean> {
const preferencesStore = get(preferences);
if (!preferencesStore.cast.gCastEnabled) {
if (!preferencesStore || !preferencesStore.cast.gCastEnabled) {
this.isAvailable = false;
return false;
}

View File

@@ -1,6 +1,6 @@
import { locale } from '$lib/stores/preferences.store';
import { parseUtcDate } from '$lib/utils/date-time';
import { formatGroupTitle } from '$lib/utils/timeline-util';
import { formatGroupTitle, toISOYearMonthUTC } from '$lib/utils/timeline-util';
import { DateTime } from 'luxon';
describe('formatGroupTitle', () => {
@@ -77,3 +77,13 @@ describe('formatGroupTitle', () => {
expect(formatGroupTitle(date)).toBe('Invalid DateTime');
});
});
describe('toISOYearMonthUTC', () => {
it('should prefix year with 0s', () => {
expect(toISOYearMonthUTC({ year: 28, month: 1 })).toBe('0028-01-01T00:00:00.000Z');
});
it('should prefix month with 0s', () => {
expect(toISOYearMonthUTC({ year: 2025, month: 1 })).toBe('2025-01-01T00:00:00.000Z');
});
});

View File

@@ -94,8 +94,11 @@ export const fromTimelinePlainYearMonth = (timelineYearMonth: TimelineYearMonth)
{ zone: 'local', locale: get(locale) },
) as DateTime<true>;
export const toISOYearMonthUTC = ({ year, month }: TimelineYearMonth): string =>
`${year}-${month.toString().padStart(2, '0')}-01T00:00:00.000Z`;
export const toISOYearMonthUTC = ({ year, month }: TimelineYearMonth): string => {
const yearFull = `${year}`.padStart(4, '0');
const monthFull = `${month}`.padStart(2, '0');
return `${yearFull}-${monthFull}-01T00:00:00.000Z`;
};
export function formatMonthGroupTitle(_date: DateTime): string {
if (!_date.isValid) {

View File

@@ -29,6 +29,6 @@ export const TUNABLES = {
NAVIGATE_ON_ASSET_IN_VIEW: getBoolean(storage.getItem('ASSET_GRID.NAVIGATE_ON_ASSET_IN_VIEW'), false),
},
IMAGE_THUMBNAIL: {
THUMBHASH_FADE_DURATION: getNumber(storage.getItem('THUMBHASH_FADE_DURATION'), 100),
THUMBHASH_FADE_DURATION: getNumber(storage.getItem('THUMBHASH_FADE_DURATION'), 1000),
},
};