mirror of
https://github.com/immich-app/immich.git
synced 2025-12-15 22:44:48 +03:00
Compare commits
23 Commits
v1.25.0_35
...
v1.26.0_36
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fdd9f37abd | ||
|
|
a09bba454c | ||
|
|
4be9aa091b | ||
|
|
33b810de74 | ||
|
|
44ccb1eec1 | ||
|
|
bef38c670c | ||
|
|
025d7bf192 | ||
|
|
5ad2d62039 | ||
|
|
a128833e68 | ||
|
|
87f7b0849a | ||
|
|
4596a8ee01 | ||
|
|
f9b1b12b10 | ||
|
|
68b1655e7f | ||
|
|
658b64df74 | ||
|
|
e344503834 | ||
|
|
bf2760ffef | ||
|
|
db2ed2d881 | ||
|
|
fb0fa742f5 | ||
|
|
3b55cdc0be | ||
|
|
0efcc99f3e | ||
|
|
7a85164a1e | ||
|
|
ba2cda8955 | ||
|
|
9048be4c8e |
134
CODE_OF_CONDUCT.md
Normal file
134
CODE_OF_CONDUCT.md
Normal file
@@ -0,0 +1,134 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation
|
||||
in our community a harassment-free experience for everyone, regardless
|
||||
of age, body size, visible or invisible disability, ethnicity, sex
|
||||
characteristics, gender identity and expression, level of experience,
|
||||
education, socio-economic status, nationality, personal appearance,
|
||||
race, religion, or sexual identity and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open,
|
||||
welcoming, diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for
|
||||
our community include:
|
||||
|
||||
- Demonstrating empathy and kindness toward other people
|
||||
- Being respectful of differing opinions, viewpoints, and experiences
|
||||
- Giving and gracefully accepting constructive feedback
|
||||
- Accepting responsibility and apologizing to those affected by our
|
||||
mistakes, and learning from the experience
|
||||
- Focusing on what is best not just for us as individuals, but for the
|
||||
overall community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
- The use of sexualized language or imagery, and sexual attention or
|
||||
advances of any kind
|
||||
- Trolling, insulting or derogatory comments, and personal or
|
||||
political attacks
|
||||
- Public or private harassment
|
||||
- Publishing others' private information, such as a physical or email
|
||||
address, without their explicit permission
|
||||
- Other conduct which could reasonably be considered inappropriate in
|
||||
a professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our
|
||||
standards of acceptable behavior and will take appropriate and fair
|
||||
corrective action in response to any behavior that they deem
|
||||
inappropriate, threatening, offensive, or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit,
|
||||
or reject comments, commits, code, wiki edits, issues, and other
|
||||
contributions that are not aligned to this Code of Conduct, and will
|
||||
communicate reasons for moderation decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also
|
||||
applies when an individual is officially representing the community in
|
||||
public spaces. Examples of representing our community include using an
|
||||
official e-mail address, posting via an official social media account,
|
||||
or acting as an appointed representative at an online or offline
|
||||
event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior
|
||||
may be reported to the community leaders responsible for enforcement
|
||||
at our Discord channel. All complaints
|
||||
will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and
|
||||
security of the reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in
|
||||
determining the consequences for any action they deem in violation of
|
||||
this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior
|
||||
deemed unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders,
|
||||
providing clarity around the nature of the violation and an
|
||||
explanation of why the behavior was inappropriate. A public apology
|
||||
may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series
|
||||
of actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued
|
||||
behavior. No interaction with the people involved, including
|
||||
unsolicited interaction with those enforcing the Code of Conduct, for
|
||||
a specified period of time. This includes avoiding interactions in
|
||||
community spaces as well as external channels like social
|
||||
media. Violating these terms may lead to a temporary or permanent ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards,
|
||||
including sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or
|
||||
public communication with the community for a specified period of
|
||||
time. No public or private interaction with the people involved,
|
||||
including unsolicited interaction with those enforcing the Code of
|
||||
Conduct, is allowed during this period. Violating these terms may lead
|
||||
to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of
|
||||
community standards, including sustained inappropriate behavior,
|
||||
harassment of an individual, or aggression toward or disparagement of
|
||||
classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction
|
||||
within the community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor
|
||||
Covenant][homepage], version 2.0, available at
|
||||
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||
|
||||
Community Impact Guidelines were inspired by [Mozilla's code of
|
||||
conduct enforcement ladder](https://github.com/mozilla/diversity).
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see the
|
||||
FAQ at https://www.contributor-covenant.org/faq. Translations are
|
||||
available at https://www.contributor-covenant.org/translations.
|
||||
@@ -6,6 +6,7 @@ services:
|
||||
build:
|
||||
context: ../server
|
||||
dockerfile: Dockerfile
|
||||
target: builder
|
||||
command: npm run start:dev immich
|
||||
volumes:
|
||||
- ../server:/usr/src/app
|
||||
@@ -24,6 +25,7 @@ services:
|
||||
build:
|
||||
context: ../machine-learning
|
||||
dockerfile: Dockerfile
|
||||
target: builder
|
||||
command: npm run start:dev
|
||||
volumes:
|
||||
- ../machine-learning:/usr/src/app
|
||||
@@ -41,6 +43,7 @@ services:
|
||||
build:
|
||||
context: ../server
|
||||
dockerfile: Dockerfile
|
||||
target: builder
|
||||
command: npm run start:dev microservices
|
||||
volumes:
|
||||
- ../server:/usr/src/app
|
||||
|
||||
@@ -9,6 +9,8 @@ upload:
|
||||
locale_code: de-DE
|
||||
- file: mobile/assets/i18n/fr-FR.json
|
||||
locale_code: fr-FR
|
||||
- file: mobile/assets/i18n/nl-NL.json
|
||||
locale_code: nl-NL
|
||||
download:
|
||||
files:
|
||||
- file: mobile/assets/i18n/en-US.json
|
||||
@@ -17,3 +19,5 @@ download:
|
||||
locale_code: de-DE
|
||||
- file: mobile/assets/i18n/fr-FR.json
|
||||
locale_code: fr-FR
|
||||
- file: mobile/assets/i18n/nl-NL.json
|
||||
locale_code: nl-NL
|
||||
|
||||
3
mobile/android/.gitignore
vendored
3
mobile/android/.gitignore
vendored
@@ -11,3 +11,6 @@ GeneratedPluginRegistrant.java
|
||||
key.properties
|
||||
**/*.keystore
|
||||
**/*.jks
|
||||
|
||||
# Fastlane
|
||||
/fastlane/report.xml
|
||||
|
||||
@@ -30,8 +30,8 @@ platform :android do
|
||||
task: 'bundle',
|
||||
build_type: 'Release',
|
||||
properties: {
|
||||
"android.injected.version.code" => 35,
|
||||
"android.injected.version.name" => "1.25.0",
|
||||
"android.injected.version.code" => 36,
|
||||
"android.injected.version.name" => "1.26.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')
|
||||
|
||||
@@ -5,12 +5,17 @@
|
||||
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000212">
|
||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000224">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="1: bundleRelease" time="3.608039">
|
||||
<testcase classname="fastlane.lanes" name="1: bundleRelease" time="65.786484">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="36.344276">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
153
mobile/assets/i18n/nl-NL.json
Normal file
153
mobile/assets/i18n/nl-NL.json
Normal file
@@ -0,0 +1,153 @@
|
||||
{
|
||||
"album_info_card_backup_album_excluded": "UITGESLOTEN",
|
||||
"album_info_card_backup_album_included": "INGESLOTEN",
|
||||
"album_viewer_appbar_share_delete": "Verwijder album",
|
||||
"album_viewer_appbar_share_err_delete": "Fout bij verwijderen album",
|
||||
"album_viewer_appbar_share_err_leave": "Fout bij verlaten album",
|
||||
"album_viewer_appbar_share_err_remove": "Er gaat iets mis bij het verwijderen van items uit het album",
|
||||
"album_viewer_appbar_share_err_title": "Fout bij wijzigen album titel",
|
||||
"album_viewer_appbar_share_leave": "Verlaat album",
|
||||
"album_viewer_appbar_share_remove": "Verwijder uit album",
|
||||
"album_viewer_page_share_add_users": "Voeg gebruiker toe",
|
||||
"backup_album_selection_page_albums_device": "Albums op apparaat ({})",
|
||||
"backup_album_selection_page_albums_tap": "Tik om in te voegen, dubbel tik om uit te sluiten",
|
||||
"backup_album_selection_page_assets_scatter": "Items kunnen over verschillende albums verdeeld zijn, dus albums kunnen ingesloten of uitgesloten zijn van het backup proces.",
|
||||
"backup_album_selection_page_select_albums": "Selecteer albums",
|
||||
"backup_album_selection_page_selection_info": "Selectie info",
|
||||
"backup_album_selection_page_total_assets": "Totaal unieke items",
|
||||
"backup_all": "Alle",
|
||||
"backup_background_service_default_notification": "Controleren op nieuw items…",
|
||||
"backup_background_service_disable_battery_optimizations": "Schakel batterij optimalisatie uit voor Immich om achtergrond backup in te schakelen",
|
||||
"backup_background_service_upload_failure_notification": "Fout bij upload {}",
|
||||
"backup_background_service_in_progress_notification": "Backuppen van items…",
|
||||
"backup_background_service_current_upload_notification": "Uploaden {}",
|
||||
"backup_background_service_error_title": "Backup fout",
|
||||
"backup_background_service_connection_failed_message": "Fout bij verbinden server. Opnieuw proberen…",
|
||||
"backup_background_service_backup_failed_message": "Fout bij backuppen items. Opnieuw proberen…",
|
||||
"backup_controller_page_albums": "Backup Albums",
|
||||
"backup_controller_page_backup": "Backup",
|
||||
"backup_controller_page_backup_selected": "Geselecteerd: ",
|
||||
"backup_controller_page_backup_sub": "Foto's en video's gebackupped",
|
||||
"backup_controller_page_background_description": "Gebruik achtergrondservice om automatisch nieuwe items te uploaden naar server zonder de app te openen",
|
||||
"backup_controller_page_background_wifi": "Alleen op WiFi",
|
||||
"backup_controller_page_background_charging": "Alleen tijdens opladen",
|
||||
"backup_controller_page_background_is_on": "Automatische achtergrond backup staat aan",
|
||||
"backup_controller_page_background_is_off": "Automatische achtergrond backup staat uit",
|
||||
"backup_controller_page_background_turn_on": "Zet achtergrondservice aan",
|
||||
"backup_controller_page_background_turn_off": "Zet achtergrondservice uit",
|
||||
"backup_controller_page_background_configure_error": "Achtergrondservice configuratie mislukt",
|
||||
"backup_controller_page_cancel": "Annuleren",
|
||||
"backup_controller_page_created": "Gemaakt op: {}",
|
||||
"backup_controller_page_desc_backup": "Configureer backup om automatisch nieuwe items te uploaden naar server.",
|
||||
"backup_controller_page_excluded": "Uitgezonderd: ",
|
||||
"backup_controller_page_failed": "Mislukt ({})",
|
||||
"backup_controller_page_filename": "Bestandsnaam: {} [{}]",
|
||||
"backup_controller_page_id": "ID: {}",
|
||||
"backup_controller_page_info": "Backup informatie",
|
||||
"backup_controller_page_none_selected": "Geen geselecteerd",
|
||||
"backup_controller_page_remainder": "Rest",
|
||||
"backup_controller_page_remainder_sub": "Overgebleven foto's en video's om te backuppen uit selectie",
|
||||
"backup_controller_page_select": "Selecteer",
|
||||
"backup_controller_page_server_storage": "Server Opslag",
|
||||
"backup_controller_page_start_backup": "Start Backup",
|
||||
"backup_controller_page_status_off": "Backup staat uit",
|
||||
"backup_controller_page_status_on": "Backup staat aan",
|
||||
"backup_controller_page_storage_format": "{} van {} gebruikt",
|
||||
"backup_controller_page_to_backup": "Albums om te backuppen",
|
||||
"backup_controller_page_total": "Totaal",
|
||||
"backup_controller_page_total_sub": "Alle unieke foto's en video's uit geselecteerde albums",
|
||||
"backup_controller_page_turn_off": "Backup uitzetten",
|
||||
"backup_controller_page_turn_on": "Backup aanzetten",
|
||||
"backup_controller_page_uploading_file_info": "Bestandsgegevens uploaden",
|
||||
"backup_err_only_album": "Kan niet alleen het album verwijderen",
|
||||
"backup_info_card_assets": "items",
|
||||
"control_bottom_app_bar_delete": "Verwijderen",
|
||||
"create_shared_album_page_share": "Delen",
|
||||
"create_shared_album_page_create": "Aanmaken",
|
||||
"create_shared_album_page_share_add_assets": "VOEG FOTO'S TOE",
|
||||
"create_shared_album_page_share_select_photos": "Selecteer Foto's",
|
||||
"daily_title_text_date": "E, MMM dd",
|
||||
"daily_title_text_date_year": "E, MMM dd, yyyy",
|
||||
"date_format": "E, LLL d, y • h:mm a",
|
||||
"delete_dialog_alert": "Deze items zullen permanent verwijderd worden van Immich en je apparaat",
|
||||
"delete_dialog_cancel": "Annuleren",
|
||||
"delete_dialog_ok": "Verwijderen",
|
||||
"delete_dialog_title": "Verwijder permanent",
|
||||
"exif_bottom_sheet_description": "Voeg beschrijving toe...",
|
||||
"exif_bottom_sheet_details": "DETAILS",
|
||||
"exif_bottom_sheet_location": "LOCATIE",
|
||||
"login_form_button_text": "Login",
|
||||
"login_form_email_hint": "jouwemail@email.com",
|
||||
"login_form_endpoint_hint": "http://jouw-server-ip:port/api",
|
||||
"login_form_endpoint_url": "Server URL",
|
||||
"login_form_err_http": "Voer http:// of https:// in",
|
||||
"login_form_err_invalid_email": "Ongeldige Email",
|
||||
"login_form_err_leading_whitespace": "Spatie aan het begin",
|
||||
"login_form_err_trailing_whitespace": "Spatie aan het eind",
|
||||
"login_form_failed_login": "Fout bij inloggen, controleer server url, email en wachtwoord",
|
||||
"login_form_label_email": "Email",
|
||||
"login_form_label_password": "Wachtwoord",
|
||||
"login_form_password_hint": "wachtwoord",
|
||||
"login_form_save_login": "Ingelogd blijven",
|
||||
"monthly_title_text_date_format": "MMMM y",
|
||||
"profile_drawer_client_server_up_to_date": "Client en Server zijn up-to-date",
|
||||
"profile_drawer_sign_out": "Uitloggen",
|
||||
"profile_drawer_settings": "Instellingen",
|
||||
"search_bar_hint": "Zoek je foto's",
|
||||
"search_page_no_objects": "Geen object gegevens beschikbaar",
|
||||
"search_page_no_places": "Geen locatie gegevens beschikbaar",
|
||||
"search_page_places": "Plaatsen",
|
||||
"search_page_things": "Dingen",
|
||||
"search_result_page_new_search_hint": "Nieuw resultaat",
|
||||
"select_additional_user_for_sharing_page_suggestions": "Suggesties",
|
||||
"select_user_for_sharing_page_err_album": "Album aanmaken mislukt",
|
||||
"select_user_for_sharing_page_share_suggestions": "Suggesties",
|
||||
"share_add": "Toevoegen",
|
||||
"share_add_photos": "Foto's toevoegen",
|
||||
"share_add_title": "Titel toevoegen",
|
||||
"share_create_album": "Album aanmaken",
|
||||
"share_invite": "Uitnodigen voor album",
|
||||
"sharing_page_album": "Gedeelde albums",
|
||||
"sharing_page_description": "Maak gedeelde albums om foto's en video's te delen met mensen in je netwerk.",
|
||||
"sharing_page_empty_list": "LEGE LIJST",
|
||||
"sharing_silver_appbar_create_shared_album": "Maak gedeeld album",
|
||||
"sharing_silver_appbar_share_partner": "Delen met partner",
|
||||
"tab_controller_nav_photos": "Foto's",
|
||||
"tab_controller_nav_search": "Zoeken",
|
||||
"tab_controller_nav_sharing": "Delen",
|
||||
"tab_controller_nav_library": "Bibliotheek",
|
||||
"version_announcement_overlay_ack": "Bevestig",
|
||||
"version_announcement_overlay_release_notes": "release opmerkingen",
|
||||
"version_announcement_overlay_text_1": "Er is een nieuwe versie beschikbaar van",
|
||||
"version_announcement_overlay_text_2": "neem je tijd en bezoek de ",
|
||||
"version_announcement_overlay_text_3": " controleer of je docker-compose en .env up-to-date zijn om te voorkomen dat er misconfiguraties zijn, in het bijzonder als je gebruik maakt van WatchTower of een ander mechanisme dat je server automatisch configureert.",
|
||||
"version_announcement_overlay_title": "Nieuwe server versie beschikbaar \uD83C\uDF89",
|
||||
"album_thumbnail_card_item": "1 item",
|
||||
"album_thumbnail_card_items": "{} items",
|
||||
"album_thumbnail_card_shared": " · Gedeeld",
|
||||
"library_page_albums": "Albums",
|
||||
"library_page_new_album": "Nieuw album",
|
||||
"create_album_page_untitled": "Naamloos",
|
||||
"share_dialog_preparing": "Voorbereiden...",
|
||||
"control_bottom_app_bar_share": "Delen",
|
||||
"setting_pages_app_bar_settings": "Instellingen",
|
||||
"theme_setting_theme_title": "Thema",
|
||||
"theme_setting_theme_subtitle": "Kies de thema instelling van de app",
|
||||
"theme_setting_system_theme_switch": "Automatisch (volg systeeminstelling)",
|
||||
"theme_setting_dark_mode_switch": "Donkere modus",
|
||||
"theme_setting_image_viewer_quality_title": "Foto weergave kwaliteit",
|
||||
"theme_setting_image_viewer_quality_subtitle": "Pas de kwaliteit aan van de gedetailleerde foto weergave",
|
||||
"theme_setting_three_stage_loading_title": "Drie-laags laden inschakelen",
|
||||
"theme_setting_three_stage_loading_subtitle": "Three-stage loading might increase the loading performance but causes significantly higher network load",
|
||||
"asset_list_settings_title": "Foto Grid",
|
||||
"asset_list_settings_subtitle": "Foto grid layout instellingen",
|
||||
"theme_setting_asset_list_storage_indicator_title": "Laat ruimte indicator zien bij item tegels",
|
||||
"theme_setting_asset_list_tiles_per_row_title": "Aantal items per rij ({})",
|
||||
"setting_notifications_title": "Notificaties",
|
||||
"setting_notifications_subtitle": "Werk je notificatievoorkeuren bij",
|
||||
"setting_notifications_notify_failures_grace_period": "Melding achtergrond backup fouten: {}",
|
||||
"setting_notifications_notify_immediately": "meteen",
|
||||
"setting_notifications_notify_minutes": "{} minuten",
|
||||
"setting_notifications_notify_hours": "{} uur",
|
||||
"setting_notifications_notify_never": "nooit"
|
||||
}
|
||||
@@ -360,7 +360,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 40;
|
||||
CURRENT_PROJECT_VERSION = 51;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
@@ -495,7 +495,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 40;
|
||||
CURRENT_PROJECT_VERSION = 51;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
@@ -522,7 +522,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 40;
|
||||
CURRENT_PROJECT_VERSION = 51;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
|
||||
@@ -17,11 +17,11 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.21.0</string>
|
||||
<string>1.26.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>40</string>
|
||||
<string>51</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true />
|
||||
<key>MGLMapboxMetricsEnabledSettingShownInApp</key>
|
||||
@@ -92,7 +92,9 @@
|
||||
<string>it</string>
|
||||
<string>fi</string>
|
||||
<string>ja</string>
|
||||
<string>nl</string>
|
||||
<string>pl</string>
|
||||
<string>pt</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -19,7 +19,7 @@ platform :ios do
|
||||
desc "iOS Beta"
|
||||
lane :beta do
|
||||
increment_version_number(
|
||||
version_number: "1.25.0"
|
||||
version_number: "1.26.0"
|
||||
)
|
||||
increment_build_number(
|
||||
build_number: latest_testflight_build_number + 1,
|
||||
|
||||
@@ -5,32 +5,32 @@
|
||||
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000205">
|
||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000349">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="1: increment_version_number" time="0.360401">
|
||||
<testcase classname="fastlane.lanes" name="1: increment_version_number" time="0.650297">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="2: latest_testflight_build_number" time="4.012696">
|
||||
<testcase classname="fastlane.lanes" name="2: latest_testflight_build_number" time="7.757602">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="3: increment_build_number" time="0.378836">
|
||||
<testcase classname="fastlane.lanes" name="3: increment_build_number" time="0.421008">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="4: build_app" time="80.023705">
|
||||
<testcase classname="fastlane.lanes" name="4: build_app" time="126.240949">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="5: upload_to_testflight" time="98.18403">
|
||||
<testcase classname="fastlane.lanes" name="5: upload_to_testflight" time="68.206021">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ const List<Locale> locales = [
|
||||
Locale('fr', 'FR'),
|
||||
Locale('it', 'IT'),
|
||||
Locale('ja', 'JP'),
|
||||
Locale('nl', 'NL'),
|
||||
Locale('pl', 'PL'),
|
||||
Locale('pt', 'PR')
|
||||
];
|
||||
|
||||
@@ -65,37 +65,45 @@ class HomePage extends HookConsumerWidget {
|
||||
int? lastMonth;
|
||||
|
||||
assetGroupByDateTime.forEach((dateGroup, immichAssetList) {
|
||||
DateTime parseDateGroup = DateTime.parse(dateGroup);
|
||||
int currentMonth = parseDateGroup.month;
|
||||
try {
|
||||
DateTime parseDateGroup = DateTime.parse(dateGroup);
|
||||
int currentMonth = parseDateGroup.month;
|
||||
|
||||
if (lastMonth != null) {
|
||||
if (currentMonth - lastMonth! != 0) {
|
||||
imageGridGroup.add(
|
||||
MonthlyTitleText(
|
||||
isoDate: dateGroup,
|
||||
),
|
||||
);
|
||||
if (lastMonth != null) {
|
||||
if (currentMonth - lastMonth! != 0) {
|
||||
imageGridGroup.add(
|
||||
MonthlyTitleText(
|
||||
isoDate: dateGroup,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
imageGridGroup.add(
|
||||
DailyTitleText(
|
||||
key: Key('${dateGroup.toString()}title'),
|
||||
isoDate: dateGroup,
|
||||
assetGroup: immichAssetList,
|
||||
),
|
||||
);
|
||||
|
||||
imageGridGroup.add(
|
||||
ImageGrid(
|
||||
assetGroup: immichAssetList,
|
||||
sortedAssetGroup: sortedAssetList,
|
||||
tilesPerRow:
|
||||
appSettingService.getSetting(AppSettingsEnum.tilesPerRow),
|
||||
showStorageIndicator: appSettingService
|
||||
.getSetting(AppSettingsEnum.storageIndicator),
|
||||
),
|
||||
);
|
||||
|
||||
lastMonth = currentMonth;
|
||||
} catch (e) {
|
||||
debugPrint(
|
||||
"[ERROR] Cannot parse $dateGroup - Wrong create date format : ${immichAssetList.map((asset) => asset.createdAt).toList()}",
|
||||
);
|
||||
}
|
||||
|
||||
imageGridGroup.add(
|
||||
DailyTitleText(
|
||||
key: Key('${dateGroup.toString()}title'),
|
||||
isoDate: dateGroup,
|
||||
assetGroup: immichAssetList,
|
||||
),
|
||||
);
|
||||
|
||||
imageGridGroup.add(
|
||||
ImageGrid(
|
||||
assetGroup: immichAssetList,
|
||||
sortedAssetGroup: sortedAssetList,
|
||||
tilesPerRow: appSettingService.getSetting(AppSettingsEnum.tilesPerRow),
|
||||
showStorageIndicator: appSettingService.getSetting(AppSettingsEnum.storageIndicator),
|
||||
),
|
||||
);
|
||||
|
||||
lastMonth = currentMonth;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ name: immich_mobile
|
||||
description: Immich - selfhosted backup media file on mobile phone
|
||||
|
||||
publish_to: "none"
|
||||
version: 1.25.0+35
|
||||
version: 1.26.0+35
|
||||
|
||||
environment:
|
||||
sdk: ">=2.17.0 <3.0.0"
|
||||
|
||||
@@ -5,7 +5,7 @@ WORKDIR /usr/src/app
|
||||
|
||||
COPY package.json package-lock.json ./
|
||||
|
||||
RUN apk add --update-cache build-base python3 libheif vips-dev
|
||||
RUN apk add --update-cache build-base python3 libheif vips-dev ffmpeg
|
||||
RUN npm ci
|
||||
|
||||
COPY . .
|
||||
@@ -22,7 +22,7 @@ COPY package.json package-lock.json ./
|
||||
COPY start-server.sh start-microservices.sh ./
|
||||
|
||||
RUN mkdir -p /usr/src/app/dist \
|
||||
&& apk add --no-cache libheif vips ffmpeg
|
||||
&& apk add --no-cache libheif vips ffmpeg
|
||||
|
||||
COPY --from=builder /usr/src/app/node_modules ./node_modules
|
||||
COPY --from=builder /usr/src/app/dist ./dist
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { APP_UPLOAD_LOCATION } from '@app/common/constants';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ServerInfoResponseDto } from './response-dto/server-info-response.dto';
|
||||
import diskusage from 'diskusage';
|
||||
import { APP_UPLOAD_LOCATION } from '../../constants/upload_location.constant';
|
||||
|
||||
@Injectable()
|
||||
export class ServerInfoService {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { immichAppConfig } from '@app/common/config';
|
||||
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
|
||||
import { UserModule } from './api-v1/user/user.module';
|
||||
import { AssetModule } from './api-v1/asset/asset.module';
|
||||
@@ -5,7 +6,6 @@ import { AuthModule } from './api-v1/auth/auth.module';
|
||||
import { ImmichJwtModule } from './modules/immich-jwt/immich-jwt.module';
|
||||
import { DeviceInfoModule } from './api-v1/device-info/device-info.module';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { immichAppConfig } from './config/app.config';
|
||||
import { BullModule } from '@nestjs/bull';
|
||||
import { ServerInfoModule } from './api-v1/server-info/server-info.module';
|
||||
import { BackgroundTaskModule } from './modules/background-task/background-task.module';
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { APP_UPLOAD_LOCATION } from '@app/common/constants';
|
||||
import { HttpException, HttpStatus } from '@nestjs/common';
|
||||
import { MulterOptions } from '@nestjs/platform-express/multer/interfaces/multer-options.interface';
|
||||
import { existsSync, mkdirSync } from 'fs';
|
||||
import { diskStorage } from 'multer';
|
||||
import { extname } from 'path';
|
||||
import { Request } from 'express';
|
||||
import { APP_UPLOAD_LOCATION } from '../constants/upload_location.constant';
|
||||
import { randomUUID } from 'crypto';
|
||||
|
||||
export const assetUploadOption: MulterOptions = {
|
||||
fileFilter: (req: Request, file: any, cb: any) => {
|
||||
if (file.mimetype.match(/\/(jpg|jpeg|png|gif|mp4|x-msvideo|quicktime|heic|heif|dng|x-adobe-dng|webp)$/)) {
|
||||
if (file.mimetype.match(/\/(jpg|jpeg|png|gif|mp4|x-msvideo|quicktime|heic|heif|dng|x-adobe-dng|webp|tiff)$/)) {
|
||||
cb(null, true);
|
||||
} else {
|
||||
cb(new HttpException(`Unsupported file type ${extname(file.originalname)}`, HttpStatus.BAD_REQUEST), false);
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { APP_UPLOAD_LOCATION } from '@app/common/constants';
|
||||
import { HttpException, HttpStatus } from '@nestjs/common';
|
||||
import { MulterOptions } from '@nestjs/platform-express/multer/interfaces/multer-options.interface';
|
||||
import { existsSync, mkdirSync } from 'fs';
|
||||
import { diskStorage } from 'multer';
|
||||
import { extname } from 'path';
|
||||
import { Request } from 'express';
|
||||
import { APP_UPLOAD_LOCATION } from '../constants/upload_location.constant';
|
||||
|
||||
export const profileImageUploadOption: MulterOptions = {
|
||||
fileFilter: (req: Request, file: any, cb: any) => {
|
||||
|
||||
@@ -10,7 +10,7 @@ export interface IServerVersion {
|
||||
|
||||
export const serverVersion: IServerVersion = {
|
||||
major: 1,
|
||||
minor: 25,
|
||||
minor: 26,
|
||||
patch: 0,
|
||||
build: 0,
|
||||
};
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
"^.+\\.(t|j)s$": "ts-jest"
|
||||
},
|
||||
"moduleNameMapper": {
|
||||
"@app/common/(.*)": "<rootDir>../../../libs/common/src/$1",
|
||||
"@app/database/config/(.*)": "<rootDir>../../../libs/database/src/config/$1",
|
||||
"@app/database/entities/(.*)": "<rootDir>../../../libs/database/src/entities/$1"
|
||||
}
|
||||
|
||||
@@ -1,26 +1,29 @@
|
||||
import { BullModule } from '@nestjs/bull';
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { immichAppConfig } from '@app/common/config';
|
||||
import { DatabaseModule } from '@app/database';
|
||||
import { AssetEntity } from '@app/database/entities/asset.entity';
|
||||
import { ExifEntity } from '@app/database/entities/exif.entity';
|
||||
import { SmartInfoEntity } from '@app/database/entities/smart-info.entity';
|
||||
import { UserEntity } from '@app/database/entities/user.entity';
|
||||
import { MicroservicesService } from './microservices.service';
|
||||
import { AssetUploadedProcessor } from './processors/asset-uploaded.processor';
|
||||
import { ThumbnailGeneratorProcessor } from './processors/thumbnail.processor';
|
||||
import { MetadataExtractionProcessor } from './processors/metadata-extraction.processor';
|
||||
import { VideoTranscodeProcessor } from './processors/video-transcode.processor';
|
||||
import { CommunicationModule } from '../../immich/src/api-v1/communication/communication.module';
|
||||
import {
|
||||
assetUploadedQueueName,
|
||||
metadataExtractionQueueName,
|
||||
thumbnailGeneratorQueueName,
|
||||
videoConversionQueueName,
|
||||
} from '@app/job/constants/queue-name.constant';
|
||||
import { BullModule } from '@nestjs/bull';
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { CommunicationModule } from '../../immich/src/api-v1/communication/communication.module';
|
||||
import { MicroservicesService } from './microservices.service';
|
||||
import { AssetUploadedProcessor } from './processors/asset-uploaded.processor';
|
||||
import { MetadataExtractionProcessor } from './processors/metadata-extraction.processor';
|
||||
import { ThumbnailGeneratorProcessor } from './processors/thumbnail.processor';
|
||||
import { VideoTranscodeProcessor } from './processors/video-transcode.processor';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ConfigModule.forRoot(immichAppConfig),
|
||||
DatabaseModule,
|
||||
TypeOrmModule.forFeature([UserEntity, ExifEntity, AssetEntity, SmartInfoEntity]),
|
||||
BullModule.forRootAsync({
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import { InjectQueue, Process, Processor } from '@nestjs/bull';
|
||||
import { Job, Queue } from 'bull';
|
||||
import { AssetType } from '@app/database/entities/asset.entity';
|
||||
import { randomUUID } from 'crypto';
|
||||
import {
|
||||
IAssetUploadedJob,
|
||||
IMetadataExtractionJob,
|
||||
@@ -17,6 +14,9 @@ import {
|
||||
mp4ConversionProcessorName,
|
||||
videoMetadataExtractionProcessorName,
|
||||
} from '@app/job';
|
||||
import { InjectQueue, Process, Processor } from '@nestjs/bull';
|
||||
import { Job, Queue } from 'bull';
|
||||
import { randomUUID } from 'crypto';
|
||||
|
||||
@Processor(assetUploadedQueueName)
|
||||
export class AssetUploadedProcessor {
|
||||
|
||||
@@ -1,18 +1,6 @@
|
||||
import { Process, Processor } from '@nestjs/bull';
|
||||
import { Job } from 'bull';
|
||||
import { AssetEntity } from '@app/database/entities/asset.entity';
|
||||
import { Repository } from 'typeorm/repository/Repository';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { ExifEntity } from '@app/database/entities/exif.entity';
|
||||
import exifr from 'exifr';
|
||||
import mapboxGeocoding, { GeocodeService } from '@mapbox/mapbox-sdk/services/geocoding';
|
||||
import { MapiResponse } from '@mapbox/mapbox-sdk/lib/classes/mapi-response';
|
||||
import { readFile } from 'fs/promises';
|
||||
import { Logger } from '@nestjs/common';
|
||||
import axios from 'axios';
|
||||
import { SmartInfoEntity } from '@app/database/entities/smart-info.entity';
|
||||
import ffmpeg from 'fluent-ffmpeg';
|
||||
import path from 'path';
|
||||
import {
|
||||
IExifExtractionProcessor,
|
||||
IVideoLengthExtractionProcessor,
|
||||
@@ -24,6 +12,18 @@ import {
|
||||
reverseGeocodingProcessorName,
|
||||
IReverseGeocodingProcessor,
|
||||
} from '@app/job';
|
||||
import { MapiResponse } from '@mapbox/mapbox-sdk/lib/classes/mapi-response';
|
||||
import mapboxGeocoding, { GeocodeService } from '@mapbox/mapbox-sdk/services/geocoding';
|
||||
import { Process, Processor } from '@nestjs/bull';
|
||||
import { Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import axios from 'axios';
|
||||
import { Job } from 'bull';
|
||||
import exifr from 'exifr';
|
||||
import ffmpeg from 'fluent-ffmpeg';
|
||||
import { readFile } from 'fs/promises';
|
||||
import path from 'path';
|
||||
import { Repository } from 'typeorm/repository/Repository';
|
||||
|
||||
@Processor(metadataExtractionQueueName)
|
||||
export class MetadataExtractionProcessor {
|
||||
@@ -60,8 +60,8 @@ export class MetadataExtractionProcessor {
|
||||
newExif.make = exifData['Make'] || null;
|
||||
newExif.model = exifData['Model'] || null;
|
||||
newExif.imageName = path.parse(fileName).name || null;
|
||||
newExif.exifImageHeight = exifData['ExifImageHeight'] || null;
|
||||
newExif.exifImageWidth = exifData['ExifImageWidth'] || null;
|
||||
newExif.exifImageHeight = exifData['ExifImageHeight'] || exifData['ImageHeight'] || null;
|
||||
newExif.exifImageWidth = exifData['ExifImageWidth'] || exifData['ImageWidth'] || null;
|
||||
newExif.fileSizeInByte = fileSize || null;
|
||||
newExif.orientation = exifData['Orientation'] || null;
|
||||
newExif.dateTimeOriginal = exifData['DateTimeOriginal'] || null;
|
||||
|
||||
@@ -1,14 +1,4 @@
|
||||
import { InjectQueue, Process, Processor } from '@nestjs/bull';
|
||||
import { Job, Queue } from 'bull';
|
||||
import { AssetEntity, AssetType } from '@app/database/entities/asset.entity';
|
||||
import { Repository } from 'typeorm/repository/Repository';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import sharp from 'sharp';
|
||||
import { existsSync, mkdirSync } from 'node:fs';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { CommunicationGateway } from '../../../immich/src/api-v1/communication/communication.gateway';
|
||||
import ffmpeg from 'fluent-ffmpeg';
|
||||
import { Logger } from '@nestjs/common';
|
||||
import {
|
||||
WebpGeneratorProcessor,
|
||||
generateJPEGThumbnailProcessorName,
|
||||
@@ -19,7 +9,17 @@ import {
|
||||
thumbnailGeneratorQueueName,
|
||||
JpegGeneratorProcessor,
|
||||
} from '@app/job';
|
||||
import { InjectQueue, Process, Processor } from '@nestjs/bull';
|
||||
import { Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { mapAsset } from 'apps/immich/src/api-v1/asset/response-dto/asset-response.dto';
|
||||
import { Job, Queue } from 'bull';
|
||||
import ffmpeg from 'fluent-ffmpeg';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { existsSync, mkdirSync } from 'node:fs';
|
||||
import sharp from 'sharp';
|
||||
import { Repository } from 'typeorm/repository/Repository';
|
||||
import { CommunicationGateway } from '../../../immich/src/api-v1/communication/communication.gateway';
|
||||
|
||||
@Processor(thumbnailGeneratorQueueName)
|
||||
export class ThumbnailGeneratorProcessor {
|
||||
@@ -51,62 +51,47 @@ export class ThumbnailGeneratorProcessor {
|
||||
const jpegThumbnailPath = resizePath + originalFilename + '.jpeg';
|
||||
|
||||
if (asset.type == AssetType.IMAGE) {
|
||||
sharp(asset.originalPath)
|
||||
.resize(1440, 2560, { fit: 'inside' })
|
||||
.jpeg()
|
||||
.rotate()
|
||||
.toFile(jpegThumbnailPath, async (err) => {
|
||||
if (!err) {
|
||||
await this.assetRepository.update({ id: asset.id }, { resizePath: jpegThumbnailPath });
|
||||
await sharp(asset.originalPath).resize(1440, 2560, { fit: 'inside' }).jpeg().rotate().toFile(jpegThumbnailPath);
|
||||
await this.assetRepository.update({ id: asset.id }, { resizePath: jpegThumbnailPath });
|
||||
|
||||
// Update resize path to send to generate webp queue
|
||||
asset.resizePath = jpegThumbnailPath;
|
||||
// Update resize path to send to generate webp queue
|
||||
asset.resizePath = jpegThumbnailPath;
|
||||
|
||||
await this.thumbnailGeneratorQueue.add(
|
||||
generateWEBPThumbnailProcessorName,
|
||||
{ asset },
|
||||
{ jobId: randomUUID() },
|
||||
);
|
||||
await this.metadataExtractionQueue.add(imageTaggingProcessorName, { asset }, { jobId: randomUUID() });
|
||||
await this.metadataExtractionQueue.add(objectDetectionProcessorName, { asset }, { jobId: randomUUID() });
|
||||
this.wsCommunicationGateway.server
|
||||
.to(asset.userId)
|
||||
.emit('on_upload_success', JSON.stringify(mapAsset(asset)));
|
||||
}
|
||||
});
|
||||
await this.thumbnailGeneratorQueue.add(generateWEBPThumbnailProcessorName, { asset }, { jobId: randomUUID() });
|
||||
await this.metadataExtractionQueue.add(imageTaggingProcessorName, { asset }, { jobId: randomUUID() });
|
||||
await this.metadataExtractionQueue.add(objectDetectionProcessorName, { asset }, { jobId: randomUUID() });
|
||||
this.wsCommunicationGateway.server.to(asset.userId).emit('on_upload_success', JSON.stringify(mapAsset(asset)));
|
||||
}
|
||||
|
||||
if (asset.type == AssetType.VIDEO) {
|
||||
ffmpeg(asset.originalPath)
|
||||
.outputOptions(['-ss 00:00:00.000', '-frames:v 1'])
|
||||
.output(jpegThumbnailPath)
|
||||
.on('start', () => {
|
||||
Logger.log('Start Generating Video Thumbnail', 'generateJPEGThumbnail');
|
||||
})
|
||||
.on('error', (error) => {
|
||||
Logger.error(`Cannot Generate Video Thumbnail ${error}`, 'generateJPEGThumbnail');
|
||||
// reject();
|
||||
})
|
||||
.on('end', async () => {
|
||||
Logger.log(`Generating Video Thumbnail Success ${asset.id}`, 'generateJPEGThumbnail');
|
||||
await this.assetRepository.update({ id: asset.id }, { resizePath: jpegThumbnailPath });
|
||||
await new Promise((resolve, reject) => {
|
||||
ffmpeg(asset.originalPath)
|
||||
.outputOptions(['-ss 00:00:00.000', '-frames:v 1'])
|
||||
.output(jpegThumbnailPath)
|
||||
.on('start', () => {
|
||||
Logger.log('Start Generating Video Thumbnail', 'generateJPEGThumbnail');
|
||||
})
|
||||
.on('error', (error) => {
|
||||
Logger.error(`Cannot Generate Video Thumbnail ${error}`, 'generateJPEGThumbnail');
|
||||
reject(error);
|
||||
})
|
||||
.on('end', async () => {
|
||||
Logger.log(`Generating Video Thumbnail Success ${asset.id}`, 'generateJPEGThumbnail');
|
||||
resolve(asset);
|
||||
})
|
||||
.run();
|
||||
});
|
||||
|
||||
// Update resize path to send to generate webp queue
|
||||
asset.resizePath = jpegThumbnailPath;
|
||||
await this.assetRepository.update({ id: asset.id }, { resizePath: jpegThumbnailPath });
|
||||
|
||||
await this.thumbnailGeneratorQueue.add(
|
||||
generateWEBPThumbnailProcessorName,
|
||||
{ asset },
|
||||
{ jobId: randomUUID() },
|
||||
);
|
||||
await this.metadataExtractionQueue.add(imageTaggingProcessorName, { asset }, { jobId: randomUUID() });
|
||||
await this.metadataExtractionQueue.add(objectDetectionProcessorName, { asset }, { jobId: randomUUID() });
|
||||
// Update resize path to send to generate webp queue
|
||||
asset.resizePath = jpegThumbnailPath;
|
||||
|
||||
this.wsCommunicationGateway.server
|
||||
.to(asset.userId)
|
||||
.emit('on_upload_success', JSON.stringify(mapAsset(asset)));
|
||||
})
|
||||
.run();
|
||||
await this.thumbnailGeneratorQueue.add(generateWEBPThumbnailProcessorName, { asset }, { jobId: randomUUID() });
|
||||
await this.metadataExtractionQueue.add(imageTaggingProcessorName, { asset }, { jobId: randomUUID() });
|
||||
await this.metadataExtractionQueue.add(objectDetectionProcessorName, { asset }, { jobId: randomUUID() });
|
||||
|
||||
this.wsCommunicationGateway.server.to(asset.userId).emit('on_upload_success', JSON.stringify(mapAsset(asset)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,16 +102,10 @@ export class ThumbnailGeneratorProcessor {
|
||||
if (!asset.resizePath) {
|
||||
return;
|
||||
}
|
||||
|
||||
const webpPath = asset.resizePath.replace('jpeg', 'webp');
|
||||
|
||||
sharp(asset.resizePath)
|
||||
.resize(250)
|
||||
.webp()
|
||||
.rotate()
|
||||
.toFile(webpPath, (err) => {
|
||||
if (!err) {
|
||||
this.assetRepository.update({ id: asset.id }, { webpPath: webpPath });
|
||||
}
|
||||
});
|
||||
await sharp(asset.resizePath).resize(250).webp().rotate().toFile(webpPath);
|
||||
await this.assetRepository.update({ id: asset.id }, { webpPath: webpPath });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { APP_UPLOAD_LOCATION } from '@app/common/constants';
|
||||
import { AssetEntity } from '@app/database/entities/asset.entity';
|
||||
import { mp4ConversionProcessorName } from '@app/job/constants/job-name.constant';
|
||||
import { videoConversionQueueName } from '@app/job/constants/queue-name.constant';
|
||||
import { IMp4ConversionProcessor } from '@app/job/interfaces/video-transcode.interface';
|
||||
@@ -8,8 +10,6 @@ import { Job } from 'bull';
|
||||
import ffmpeg from 'fluent-ffmpeg';
|
||||
import { existsSync, mkdirSync } from 'fs';
|
||||
import { Repository } from 'typeorm';
|
||||
import { AssetEntity } from '../../../../libs/database/src/entities/asset.entity';
|
||||
import { APP_UPLOAD_LOCATION } from '../../../immich/src/constants/upload_location.constant';
|
||||
|
||||
@Processor(videoConversionQueueName)
|
||||
export class VideoTranscodeProcessor {
|
||||
|
||||
1
server/libs/common/src/config/index.ts
Normal file
1
server/libs/common/src/config/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './app.config';
|
||||
1
server/libs/common/src/constants/index.ts
Normal file
1
server/libs/common/src/constants/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './upload_location.constant';
|
||||
2
server/libs/common/src/index.ts
Normal file
2
server/libs/common/src/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './config';
|
||||
export * from './constants';
|
||||
9
server/libs/common/tsconfig.lib.json
Normal file
9
server/libs/common/tsconfig.lib.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"declaration": true,
|
||||
"outDir": "../../dist/libs/common"
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist", "test", "**/*spec.ts"]
|
||||
}
|
||||
@@ -23,7 +23,7 @@ export class ExifEntity {
|
||||
@Column({ type: 'integer', nullable: true })
|
||||
exifImageHeight!: number | null;
|
||||
|
||||
@Column({ type: 'integer', nullable: true })
|
||||
@Column({ type: 'bigint', nullable: true })
|
||||
fileSizeInByte!: number | null;
|
||||
|
||||
@Column({ type: 'varchar', nullable: true })
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class AddCaption1661011331242 implements MigrationInterface {
|
||||
name = 'AddCaption1661011331242'
|
||||
name = 'AddCaption1661011331242';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "exif" ADD "description" text DEFAULT ''`);
|
||||
await queryRunner.query(`ALTER TABLE "exif" ADD "fps" double precision`);
|
||||
// await queryRunner.query(`ALTER TABLE "exif" ALTER COLUMN "exifTextSearchableColumn" SET NOT NULL`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
// await queryRunner.query(`ALTER TABLE "exif" ALTER COLUMN "exifTextSearchableColumn" DROP NOT NULL`);
|
||||
await queryRunner.query(`ALTER TABLE "exif" DROP COLUMN "fps"`);
|
||||
await queryRunner.query(`ALTER TABLE "exif" DROP COLUMN "description"`);
|
||||
}
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "exif" ADD "description" text DEFAULT ''`);
|
||||
await queryRunner.query(`ALTER TABLE "exif" ADD "fps" double precision`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "exif" DROP COLUMN "fps"`);
|
||||
await queryRunner.query(`ALTER TABLE "exif" DROP COLUMN "description"`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class ChangeExifFileSizeInByteToBigInt1661528919411 implements MigrationInterface {
|
||||
name = 'ChangeExifFileSizeInByteToBigInt1661528919411';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE exif
|
||||
ALTER COLUMN "fileSizeInByte" type bigint using "fileSizeInByte"::bigint;
|
||||
`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE exif
|
||||
ALTER COLUMN "fileSizeInByte" type integer using "fileSizeInByte"::integer;
|
||||
`);
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,15 @@
|
||||
"tsConfigPath": "apps/microservices/tsconfig.app.json"
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"type": "library",
|
||||
"root": "libs/common",
|
||||
"entryFile": "index",
|
||||
"sourceRoot": "libs/common/src",
|
||||
"compilerOptions": {
|
||||
"tsConfigPath": "libs/common/tsconfig.lib.json"
|
||||
}
|
||||
},
|
||||
"database": {
|
||||
"type": "library",
|
||||
"root": "libs/database",
|
||||
|
||||
@@ -16,6 +16,12 @@
|
||||
"esModuleInterop": true,
|
||||
"baseUrl": "./",
|
||||
"paths": {
|
||||
"@app/common": [
|
||||
"libs/common/src"
|
||||
],
|
||||
"@app/common/*": [
|
||||
"libs/common/src/*"
|
||||
],
|
||||
"@app/database": [
|
||||
"libs/database/src"
|
||||
],
|
||||
|
||||
1550
web/package-lock.json
generated
1550
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -6,7 +6,6 @@
|
||||
"build": "vite build",
|
||||
"package": "svelte-kit package",
|
||||
"preview": "vite preview",
|
||||
"prepare": "svelte-kit sync",
|
||||
"check": "svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"lint": "prettier --check --plugin-search-dir=. . && eslint .",
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from './open-api';
|
||||
export * from './api';
|
||||
export * from './utils';
|
||||
|
||||
12
web/src/api/utils.ts
Normal file
12
web/src/api/utils.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
let _basePath = '/api';
|
||||
|
||||
export function getFileUrl(aid: string, did: string, isThumb?: boolean, isWeb?: boolean) {
|
||||
const urlObj = new URL(`${window.location.origin}${_basePath}/asset/file`);
|
||||
|
||||
urlObj.searchParams.append('aid', aid);
|
||||
urlObj.searchParams.append('did', did);
|
||||
if (isThumb !== undefined && isThumb !== null) urlObj.searchParams.append('isThumb', `${isThumb}`);
|
||||
if (isWeb !== undefined && isWeb !== null) urlObj.searchParams.append('isWeb', `${isWeb}`);
|
||||
|
||||
return urlObj.href;
|
||||
}
|
||||
6
web/src/app.d.ts
vendored
6
web/src/app.d.ts
vendored
@@ -8,10 +8,4 @@ declare namespace App {
|
||||
}
|
||||
|
||||
// interface Platform {}
|
||||
|
||||
interface Session {
|
||||
user?: import('@api').UserResponseDto;
|
||||
}
|
||||
|
||||
// interface Stuff {}
|
||||
}
|
||||
|
||||
@@ -1,31 +1,5 @@
|
||||
import type { ExternalFetch, GetSession, Handle } from '@sveltejs/kit';
|
||||
import * as cookie from 'cookie';
|
||||
import { serverApi } from '@api';
|
||||
import type { Handle } from '@sveltejs/kit';
|
||||
|
||||
export const handle: Handle = async ({ event, resolve }) => {
|
||||
const cookies = cookie.parse(event.request.headers.get('cookie') || '');
|
||||
|
||||
if (!cookies['immich_is_authenticated']) {
|
||||
return await resolve(event);
|
||||
}
|
||||
const accessToken = cookies['immich_access_token'];
|
||||
|
||||
try {
|
||||
serverApi.setAccessToken(accessToken);
|
||||
const { data } = await serverApi.userApi.getMyUserInfo();
|
||||
event.locals.user = data;
|
||||
|
||||
return await resolve(event);
|
||||
} catch (error) {
|
||||
event.locals.user = undefined;
|
||||
return await resolve(event);
|
||||
}
|
||||
};
|
||||
|
||||
export const getSession: GetSession = async ({ locals }) => {
|
||||
if (!locals.user) return {};
|
||||
|
||||
return {
|
||||
user: locals.user
|
||||
};
|
||||
return await resolve(event);
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { AlbumResponseDto, api, ThumbnailFormat } from '@api';
|
||||
import { createEventDispatcher, onMount } from 'svelte';
|
||||
import DotsVertical from 'svelte-material-icons/DotsVertical.svelte';
|
||||
import { fade } from 'svelte/transition';
|
||||
import { fly } from 'svelte/transition';
|
||||
import CircleIconButton from '../shared-components/circle-icon-button.svelte';
|
||||
|
||||
export let album: AlbumResponseDto;
|
||||
|
||||
@@ -22,6 +22,10 @@
|
||||
import MenuOption from '../shared-components/context-menu/menu-option.svelte';
|
||||
import ThumbnailSelection from './thumbnail-selection.svelte';
|
||||
import ControlAppBar from '../shared-components/control-app-bar.svelte';
|
||||
import {
|
||||
notificationController,
|
||||
NotificationType
|
||||
} from '../shared-components/notification/notification';
|
||||
|
||||
export let album: AlbumResponseDto;
|
||||
|
||||
@@ -129,7 +133,11 @@
|
||||
album = data;
|
||||
multiSelectAsset = new Set();
|
||||
} catch (e) {
|
||||
console.log('Error [album-viewer] [removeAssetFromAlbum]', e);
|
||||
console.error('Error [album-viewer] [removeAssetFromAlbum]', e);
|
||||
notificationController.show({
|
||||
type: NotificationType.Error,
|
||||
message: 'Error removing assets from album, check console for more details'
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -179,7 +187,11 @@
|
||||
currentAlbumName = album.albumName;
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log('Error [updateAlbumInfo] ', e);
|
||||
console.error('Error [updateAlbumInfo] ', e);
|
||||
notificationController.show({
|
||||
type: NotificationType.Error,
|
||||
message: "Error updating album's name, check console for more details"
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -193,7 +205,11 @@
|
||||
|
||||
isShowAssetSelection = false;
|
||||
} catch (e) {
|
||||
console.log('Error [createAlbumHandler] ', e);
|
||||
console.error('Error [createAlbumHandler] ', e);
|
||||
notificationController.show({
|
||||
type: NotificationType.Error,
|
||||
message: 'Error creating album, check console for more details'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -209,7 +225,11 @@
|
||||
|
||||
isShowShareUserSelection = false;
|
||||
} catch (e) {
|
||||
console.log('Error [createAlbumHandler] ', e);
|
||||
console.error('Error [addUserHandler] ', e);
|
||||
notificationController.show({
|
||||
type: NotificationType.Error,
|
||||
message: 'Error adding users to album, check console for more details'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -227,7 +247,11 @@
|
||||
album = data;
|
||||
isShowShareInfoModal = false;
|
||||
} catch (e) {
|
||||
console.log('Error [sharedUserDeletedHandler] ', e);
|
||||
console.error('Error [sharedUserDeletedHandler] ', e);
|
||||
notificationController.show({
|
||||
type: NotificationType.Error,
|
||||
message: 'Error deleting share users, check console for more details'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -241,7 +265,11 @@
|
||||
await api.albumApi.deleteAlbum(album.id);
|
||||
goto(backUrl);
|
||||
} catch (e) {
|
||||
console.log('Error [userDeleteMenu] ', e);
|
||||
console.error('Error [userDeleteMenu] ', e);
|
||||
notificationController.show({
|
||||
type: NotificationType.Error,
|
||||
message: 'Error deleting album, check console for more details'
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -262,7 +290,11 @@
|
||||
albumThumbnailAssetId: asset.id
|
||||
});
|
||||
} catch (e) {
|
||||
console.log('Error [setAlbumThumbnailHandler] ', e);
|
||||
console.error('Error [setAlbumThumbnailHandler] ', e);
|
||||
notificationController.show({
|
||||
type: NotificationType.Error,
|
||||
message: 'Error setting album thumbnail, check console for more details'
|
||||
});
|
||||
}
|
||||
|
||||
isShowThumbnailSelection = false;
|
||||
|
||||
@@ -7,6 +7,10 @@
|
||||
import CircleIconButton from '../shared-components/circle-icon-button.svelte';
|
||||
import ContextMenu from '../shared-components/context-menu/context-menu.svelte';
|
||||
import MenuOption from '../shared-components/context-menu/menu-option.svelte';
|
||||
import {
|
||||
notificationController,
|
||||
NotificationType
|
||||
} from '../shared-components/notification/notification';
|
||||
|
||||
export let album: AlbumResponseDto;
|
||||
|
||||
@@ -24,6 +28,10 @@
|
||||
currentUser = data;
|
||||
} catch (e) {
|
||||
console.error('Error [share-info-modal] [getAllUsers]', e);
|
||||
notificationController.show({
|
||||
message: 'Error getting user info, check console for more details',
|
||||
type: NotificationType.Error
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -48,6 +56,10 @@
|
||||
dispatch('user-deleted', { userId });
|
||||
} catch (e) {
|
||||
console.error('Error [share-info-modal] [removeUser]', e);
|
||||
notificationController.show({
|
||||
message: 'Error removing user, check console for more details',
|
||||
type: NotificationType.Error
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,207 +1,215 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher, onMount } from 'svelte';
|
||||
import { fly } from 'svelte/transition';
|
||||
import AsserViewerNavBar from './asser-viewer-nav-bar.svelte';
|
||||
import ChevronRight from 'svelte-material-icons/ChevronRight.svelte';
|
||||
import ChevronLeft from 'svelte-material-icons/ChevronLeft.svelte';
|
||||
import PhotoViewer from './photo-viewer.svelte';
|
||||
import DetailPanel from './detail-panel.svelte';
|
||||
import { downloadAssets } from '$lib/stores/download';
|
||||
import VideoViewer from './video-viewer.svelte';
|
||||
import { api, AssetResponseDto, AssetTypeEnum } from '@api';
|
||||
import { browser } from '$app/env';
|
||||
import { createEventDispatcher, onMount, onDestroy } from 'svelte';
|
||||
import { fly } from 'svelte/transition';
|
||||
import AsserViewerNavBar from './asser-viewer-nav-bar.svelte';
|
||||
import ChevronRight from 'svelte-material-icons/ChevronRight.svelte';
|
||||
import ChevronLeft from 'svelte-material-icons/ChevronLeft.svelte';
|
||||
import PhotoViewer from './photo-viewer.svelte';
|
||||
import DetailPanel from './detail-panel.svelte';
|
||||
import { downloadAssets } from '$lib/stores/download';
|
||||
import VideoViewer from './video-viewer.svelte';
|
||||
import { api, AssetResponseDto, AssetTypeEnum } from '@api';
|
||||
import {
|
||||
notificationController,
|
||||
NotificationType
|
||||
} from '../shared-components/notification/notification';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
export let asset: AssetResponseDto;
|
||||
|
||||
export let asset: AssetResponseDto;
|
||||
const dispatch = createEventDispatcher();
|
||||
let halfLeftHover = false;
|
||||
let halfRightHover = false;
|
||||
let isShowDetail = false;
|
||||
|
||||
let halfLeftHover = false;
|
||||
let halfRightHover = false;
|
||||
let isShowDetail = false;
|
||||
onMount(() => {
|
||||
document.addEventListener('keydown', (keyInfo) => handleKeyboardPress(keyInfo.key));
|
||||
});
|
||||
|
||||
onMount(() => {
|
||||
if (browser) {
|
||||
document.addEventListener('keydown', (keyInfo) => handleKeyboardPress(keyInfo.key));
|
||||
}
|
||||
});
|
||||
onDestroy(() => {
|
||||
document.removeEventListener('keydown', (e) => {});
|
||||
});
|
||||
|
||||
const handleKeyboardPress = (key: string) => {
|
||||
switch (key) {
|
||||
case 'Escape':
|
||||
closeViewer();
|
||||
return;
|
||||
case 'i':
|
||||
isShowDetail = !isShowDetail;
|
||||
return;
|
||||
case 'ArrowLeft':
|
||||
navigateAssetBackward();
|
||||
return;
|
||||
case 'ArrowRight':
|
||||
navigateAssetForward();
|
||||
return;
|
||||
}
|
||||
};
|
||||
const handleKeyboardPress = (key: string) => {
|
||||
switch (key) {
|
||||
case 'Escape':
|
||||
closeViewer();
|
||||
return;
|
||||
case 'i':
|
||||
isShowDetail = !isShowDetail;
|
||||
return;
|
||||
case 'ArrowLeft':
|
||||
navigateAssetBackward();
|
||||
return;
|
||||
case 'ArrowRight':
|
||||
navigateAssetForward();
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const closeViewer = () => {
|
||||
dispatch('close');
|
||||
};
|
||||
const closeViewer = () => {
|
||||
dispatch('close');
|
||||
};
|
||||
|
||||
const navigateAssetForward = (e?: Event) => {
|
||||
e?.stopPropagation();
|
||||
dispatch('navigate-forward');
|
||||
};
|
||||
const navigateAssetForward = (e?: Event) => {
|
||||
e?.stopPropagation();
|
||||
dispatch('navigate-forward');
|
||||
};
|
||||
|
||||
const navigateAssetBackward = (e?: Event) => {
|
||||
e?.stopPropagation();
|
||||
dispatch('navigate-backward');
|
||||
};
|
||||
const navigateAssetBackward = (e?: Event) => {
|
||||
e?.stopPropagation();
|
||||
dispatch('navigate-backward');
|
||||
};
|
||||
|
||||
const showDetailInfoHandler = () => {
|
||||
isShowDetail = !isShowDetail;
|
||||
};
|
||||
const showDetailInfoHandler = () => {
|
||||
isShowDetail = !isShowDetail;
|
||||
};
|
||||
|
||||
const downloadFile = async () => {
|
||||
try {
|
||||
console.log(asset.exifInfo);
|
||||
const imageName = asset.exifInfo?.imageName ? asset.exifInfo?.imageName : asset.id;
|
||||
const imageExtension = asset.originalPath.split('.')[1];
|
||||
const imageFileName = imageName + '.' + imageExtension;
|
||||
const downloadFile = async () => {
|
||||
try {
|
||||
console.log(asset.exifInfo);
|
||||
const imageName = asset.exifInfo?.imageName ? asset.exifInfo?.imageName : asset.id;
|
||||
const imageExtension = asset.originalPath.split('.')[1];
|
||||
const imageFileName = imageName + '.' + imageExtension;
|
||||
|
||||
// If assets is already download -> return;
|
||||
if ($downloadAssets[imageFileName]) {
|
||||
return;
|
||||
}
|
||||
// If assets is already download -> return;
|
||||
if ($downloadAssets[imageFileName]) {
|
||||
return;
|
||||
}
|
||||
|
||||
$downloadAssets[imageFileName] = 0;
|
||||
$downloadAssets[imageFileName] = 0;
|
||||
|
||||
const {data, status} = await api.assetApi.downloadFile(
|
||||
asset.deviceAssetId,
|
||||
asset.deviceId,
|
||||
false,
|
||||
false,
|
||||
{
|
||||
responseType: 'blob',
|
||||
onDownloadProgress: (progressEvent) => {
|
||||
if (progressEvent.lengthComputable) {
|
||||
const total = progressEvent.total;
|
||||
const current = progressEvent.loaded;
|
||||
$downloadAssets[imageFileName] = Math.floor((current / total) * 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
const { data, status } = await api.assetApi.downloadFile(
|
||||
asset.deviceAssetId,
|
||||
asset.deviceId,
|
||||
false,
|
||||
false,
|
||||
{
|
||||
responseType: 'blob',
|
||||
onDownloadProgress: (progressEvent) => {
|
||||
if (progressEvent.lengthComputable) {
|
||||
const total = progressEvent.total;
|
||||
const current = progressEvent.loaded;
|
||||
$downloadAssets[imageFileName] = Math.floor((current / total) * 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (!(data instanceof Blob)) {
|
||||
return;
|
||||
}
|
||||
if (!(data instanceof Blob)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (status === 200) {
|
||||
const fileUrl = URL.createObjectURL(data);
|
||||
const anchor = document.createElement('a');
|
||||
anchor.href = fileUrl;
|
||||
anchor.download = imageFileName;
|
||||
if (status === 200) {
|
||||
const fileUrl = URL.createObjectURL(data);
|
||||
const anchor = document.createElement('a');
|
||||
anchor.href = fileUrl;
|
||||
anchor.download = imageFileName;
|
||||
|
||||
document.body.appendChild(anchor);
|
||||
anchor.click();
|
||||
document.body.removeChild(anchor);
|
||||
document.body.appendChild(anchor);
|
||||
anchor.click();
|
||||
document.body.removeChild(anchor);
|
||||
|
||||
URL.revokeObjectURL(fileUrl);
|
||||
URL.revokeObjectURL(fileUrl);
|
||||
|
||||
// Remove item from download list
|
||||
setTimeout(() => {
|
||||
const copy = $downloadAssets;
|
||||
delete copy[imageFileName];
|
||||
$downloadAssets = copy;
|
||||
}, 2000);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('Error downloading file ', e);
|
||||
}
|
||||
};
|
||||
// Remove item from download list
|
||||
setTimeout(() => {
|
||||
const copy = $downloadAssets;
|
||||
delete copy[imageFileName];
|
||||
$downloadAssets = copy;
|
||||
}, 2000);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error downloading file ', e);
|
||||
notificationController.show({
|
||||
type: NotificationType.Error,
|
||||
message: 'Error downloading file, check console for more details.'
|
||||
});
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<section
|
||||
id="immich-asset-viewer"
|
||||
class="fixed h-screen w-screen top-0 overflow-y-hidden bg-black z-[999] grid grid-rows-[64px_1fr] grid-cols-4 "
|
||||
id="immich-asset-viewer"
|
||||
class="fixed h-screen w-screen top-0 overflow-y-hidden bg-black z-[999] grid grid-rows-[64px_1fr] grid-cols-4 "
|
||||
>
|
||||
<div class="col-start-1 col-span-4 row-start-1 row-span-1 z-[1000] transition-transform">
|
||||
<AsserViewerNavBar
|
||||
on:goBack={closeViewer}
|
||||
on:showDetail={showDetailInfoHandler}
|
||||
on:download={downloadFile}
|
||||
/>
|
||||
</div>
|
||||
<div class="col-start-1 col-span-4 row-start-1 row-span-1 z-[1000] transition-transform">
|
||||
<AsserViewerNavBar
|
||||
on:goBack={closeViewer}
|
||||
on:showDetail={showDetailInfoHandler}
|
||||
on:download={downloadFile}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class={`row-start-2 row-span-end col-start-1 col-span-2 flex place-items-center hover:cursor-pointer w-3/4 ${
|
||||
<div
|
||||
class={`row-start-2 row-span-end col-start-1 col-span-2 flex place-items-center hover:cursor-pointer w-3/4 ${
|
||||
asset.type === AssetTypeEnum.Video ? '' : 'z-[999]'
|
||||
}`}
|
||||
on:mouseenter={() => {
|
||||
on:mouseenter={() => {
|
||||
halfLeftHover = true;
|
||||
halfRightHover = false;
|
||||
}}
|
||||
on:mouseleave={() => {
|
||||
on:mouseleave={() => {
|
||||
halfLeftHover = false;
|
||||
}}
|
||||
on:click={navigateAssetBackward}
|
||||
>
|
||||
<button
|
||||
class="rounded-full p-3 hover:bg-gray-500 hover:text-gray-700 z-[1000] text-gray-500 mx-4"
|
||||
class:navigation-button-hover={halfLeftHover}
|
||||
on:click={navigateAssetBackward}
|
||||
>
|
||||
<ChevronLeft size="36"/>
|
||||
</button>
|
||||
</div>
|
||||
on:click={navigateAssetBackward}
|
||||
>
|
||||
<button
|
||||
class="rounded-full p-3 hover:bg-gray-500 hover:text-gray-700 z-[1000] text-gray-500 mx-4"
|
||||
class:navigation-button-hover={halfLeftHover}
|
||||
on:click={navigateAssetBackward}
|
||||
>
|
||||
<ChevronLeft size="36" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="row-start-1 row-span-full col-start-1 col-span-4">
|
||||
{#key asset.id}
|
||||
{#if asset.type === AssetTypeEnum.Image}
|
||||
<PhotoViewer assetId={asset.id} deviceId={asset.deviceId} on:close={closeViewer}/>
|
||||
{:else}
|
||||
<VideoViewer assetId={asset.id} on:close={closeViewer}/>
|
||||
{/if}
|
||||
{/key}
|
||||
</div>
|
||||
<div class="row-start-1 row-span-full col-start-1 col-span-4">
|
||||
{#key asset.id}
|
||||
{#if asset.type === AssetTypeEnum.Image}
|
||||
<PhotoViewer assetId={asset.id} deviceId={asset.deviceId} on:close={closeViewer} />
|
||||
{:else}
|
||||
<VideoViewer assetId={asset.id} on:close={closeViewer} />
|
||||
{/if}
|
||||
{/key}
|
||||
</div>
|
||||
|
||||
<div
|
||||
class={`row-start-2 row-span-full col-start-3 col-span-2 flex justify-end place-items-center hover:cursor-pointer w-3/4 justify-self-end ${
|
||||
<div
|
||||
class={`row-start-2 row-span-full col-start-3 col-span-2 flex justify-end place-items-center hover:cursor-pointer w-3/4 justify-self-end ${
|
||||
asset.type === AssetTypeEnum.Video ? '' : 'z-[500]'
|
||||
}`}
|
||||
on:click={navigateAssetForward}
|
||||
on:mouseenter={() => {
|
||||
on:click={navigateAssetForward}
|
||||
on:mouseenter={() => {
|
||||
halfLeftHover = false;
|
||||
halfRightHover = true;
|
||||
}}
|
||||
on:mouseleave={() => {
|
||||
on:mouseleave={() => {
|
||||
halfRightHover = false;
|
||||
}}
|
||||
>
|
||||
<button
|
||||
class="rounded-full p-3 hover:bg-gray-500 hover:text-gray-700 text-gray-500 mx-4 z-[1000]"
|
||||
class:navigation-button-hover={halfRightHover}
|
||||
on:click={navigateAssetForward}
|
||||
>
|
||||
<ChevronRight size="36"/>
|
||||
</button>
|
||||
</div>
|
||||
>
|
||||
<button
|
||||
class="rounded-full p-3 hover:bg-gray-500 hover:text-gray-700 text-gray-500 mx-4 z-[1000]"
|
||||
class:navigation-button-hover={halfRightHover}
|
||||
on:click={navigateAssetForward}
|
||||
>
|
||||
<ChevronRight size="36" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if isShowDetail}
|
||||
<div
|
||||
transition:fly={{ duration: 150 }}
|
||||
id="detail-panel"
|
||||
class="bg-immich-bg w-[360px] row-span-full transition-all "
|
||||
translate="yes"
|
||||
>
|
||||
<DetailPanel {asset} on:close={() => (isShowDetail = false)}/>
|
||||
</div>
|
||||
{/if}
|
||||
{#if isShowDetail}
|
||||
<div
|
||||
transition:fly={{ duration: 150 }}
|
||||
id="detail-panel"
|
||||
class="bg-immich-bg w-[360px] row-span-full transition-all "
|
||||
translate="yes"
|
||||
>
|
||||
<DetailPanel {asset} on:close={() => (isShowDetail = false)} />
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.navigation-button-hover {
|
||||
background-color: rgb(107 114 128 / var(--tw-bg-opacity));
|
||||
color: rgb(55 65 81 / var(--tw-text-opacity));
|
||||
transition: all 150ms;
|
||||
}
|
||||
.navigation-button-hover {
|
||||
background-color: rgb(107 114 128 / var(--tw-bg-opacity));
|
||||
color: rgb(55 65 81 / var(--tw-text-opacity));
|
||||
transition: all 150ms;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
import { createEventDispatcher, onMount } from 'svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import LoadingSpinner from '../shared-components/loading-spinner.svelte';
|
||||
import { api, AssetResponseDto } from '@api';
|
||||
|
||||
@@ -10,8 +10,6 @@
|
||||
|
||||
let assetInfo: AssetResponseDto;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
onMount(async () => {
|
||||
const { data } = await api.assetApi.getAssetById(assetId);
|
||||
assetInfo = data;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import { createEventDispatcher, onMount } from 'svelte';
|
||||
import LoadingSpinner from '../shared-components/loading-spinner.svelte';
|
||||
import { api, AssetResponseDto } from '@api';
|
||||
import { api, AssetResponseDto, getFileUrl } from '@api';
|
||||
|
||||
export let assetId: string;
|
||||
|
||||
@@ -13,48 +13,32 @@
|
||||
|
||||
let videoPlayerNode: HTMLVideoElement;
|
||||
let isVideoLoading = true;
|
||||
let videoUrl: string;
|
||||
|
||||
onMount(async () => {
|
||||
const { data: assetInfo } = await api.assetApi.getAssetById(assetId);
|
||||
|
||||
asset = assetInfo;
|
||||
await loadVideoData(assetInfo);
|
||||
|
||||
await loadVideoData();
|
||||
asset = assetInfo;
|
||||
});
|
||||
|
||||
const loadVideoData = async () => {
|
||||
const loadVideoData = async (assetInfo: AssetResponseDto) => {
|
||||
isVideoLoading = true;
|
||||
|
||||
try {
|
||||
const { data } = await api.assetApi.serveFile(
|
||||
asset.deviceAssetId,
|
||||
asset.deviceId,
|
||||
false,
|
||||
true,
|
||||
{
|
||||
responseType: 'blob'
|
||||
}
|
||||
);
|
||||
videoUrl = getFileUrl(assetInfo.deviceAssetId, assetInfo.deviceId, false, true);
|
||||
|
||||
if (!(data instanceof Blob)) {
|
||||
return;
|
||||
}
|
||||
return assetInfo;
|
||||
};
|
||||
|
||||
const videoData = URL.createObjectURL(data);
|
||||
videoPlayerNode.src = videoData;
|
||||
const handleCanPlay = (ev: Event) => {
|
||||
const playerNode = ev.target as HTMLVideoElement;
|
||||
|
||||
videoPlayerNode.load();
|
||||
playerNode.muted = true;
|
||||
playerNode.play();
|
||||
playerNode.muted = false;
|
||||
|
||||
videoPlayerNode.oncanplay = () => {
|
||||
videoPlayerNode.muted = true;
|
||||
videoPlayerNode.play();
|
||||
videoPlayerNode.muted = false;
|
||||
|
||||
isVideoLoading = false;
|
||||
};
|
||||
|
||||
return videoData;
|
||||
} catch (e) {}
|
||||
isVideoLoading = false;
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -63,7 +47,13 @@
|
||||
class="flex place-items-center place-content-center h-full select-none"
|
||||
>
|
||||
{#if asset}
|
||||
<video controls class="h-full object-contain" bind:this={videoPlayerNode}>
|
||||
<video
|
||||
controls
|
||||
class="h-full object-contain"
|
||||
on:canplay={handleCanPlay}
|
||||
bind:this={videoPlayerNode}
|
||||
>
|
||||
<source src={videoUrl} type="video/mp4" />
|
||||
<track kind="captions" />
|
||||
</video>
|
||||
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
import { api, UserResponseDto } from '@api';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import AccountEditOutline from 'svelte-material-icons/AccountEditOutline.svelte';
|
||||
import {
|
||||
notificationController,
|
||||
NotificationType
|
||||
} from '../shared-components/notification/notification';
|
||||
|
||||
export let user: UserResponseDto;
|
||||
|
||||
@@ -29,7 +33,11 @@
|
||||
dispatch('edit-success');
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('Error updating user ', e);
|
||||
console.error('Error updating user ', e);
|
||||
notificationController.show({
|
||||
message: 'Error updating user, check console for more details',
|
||||
type: NotificationType.Error
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -49,7 +57,11 @@
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('Error reseting user password', e);
|
||||
console.error('Error reseting user password', e);
|
||||
notificationController.show({
|
||||
message: 'Error reseting user password, check console for more details',
|
||||
type: NotificationType.Error
|
||||
});
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -8,19 +8,25 @@
|
||||
export let size: number = 48;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
const getUserAvatar = async () => {
|
||||
try {
|
||||
const { data } = await api.userApi.getProfileImage(user.id, {
|
||||
responseType: 'blob'
|
||||
});
|
||||
|
||||
if (data instanceof Blob) {
|
||||
return URL.createObjectURL(data);
|
||||
}
|
||||
} catch (e) {
|
||||
return '/favicon.png';
|
||||
const getUserAvatar = async () => {
|
||||
const { data } = await api.userApi.getProfileImage(user.id, {
|
||||
responseType: 'blob'
|
||||
});
|
||||
|
||||
if (data instanceof Blob) {
|
||||
return URL.createObjectURL(data);
|
||||
}
|
||||
};
|
||||
|
||||
const getFirstLetter = (text?: string) => {
|
||||
return text?.charAt(0).toUpperCase();
|
||||
};
|
||||
|
||||
const getRandomeBackgroundColor = () => {
|
||||
const colors = ['#DE7FB3', '#E64132', '#FFB800', '#4081EF', '#31A452'];
|
||||
return colors[Math.floor(Math.random() * colors.length)];
|
||||
};
|
||||
</script>
|
||||
|
||||
{#await getUserAvatar()}
|
||||
@@ -41,4 +47,17 @@
|
||||
title={user.email}
|
||||
/>
|
||||
</button>
|
||||
{:catch}
|
||||
<button
|
||||
on:click={() => dispatch('click')}
|
||||
style:width={`${size}px`}
|
||||
style:height={`${size}px`}
|
||||
style:background-color={getRandomeBackgroundColor()}
|
||||
alt="profile-img"
|
||||
class="inline rounded-full object-cover shadow-sm text-white font-semibold"
|
||||
>
|
||||
<div title={user.email}>
|
||||
{getFirstLetter(user.firstName)}{getFirstLetter(user.lastName)}
|
||||
</div>
|
||||
</button>
|
||||
{/await}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import PlayCircleOutline from 'svelte-material-icons/PlayCircleOutline.svelte';
|
||||
import PauseCircleOutline from 'svelte-material-icons/PauseCircleOutline.svelte';
|
||||
import LoadingSpinner from './loading-spinner.svelte';
|
||||
import { api, AssetResponseDto, AssetTypeEnum, ThumbnailFormat } from '@api';
|
||||
import { api, AssetResponseDto, AssetTypeEnum, getFileUrl, ThumbnailFormat } from '@api';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
export let isExisted: boolean = false;
|
||||
|
||||
let imageData: string;
|
||||
let videoData: string;
|
||||
// let videoData: string;
|
||||
|
||||
let mouseOver: boolean = false;
|
||||
$: dispatch('mouseEvent', { isMouseOver: mouseOver, selectedGroupIndex: groupIndex });
|
||||
@@ -28,7 +28,8 @@
|
||||
let isThumbnailVideoPlaying = false;
|
||||
let calculateVideoDurationIntervalHandler: NodeJS.Timer;
|
||||
let videoProgress = '00:00';
|
||||
let videoAbortController: AbortController;
|
||||
// let videoAbortController: AbortController;
|
||||
let videoUrl: string;
|
||||
|
||||
const loadImageData = async () => {
|
||||
const { data } = await api.assetApi.getAssetThumbnail(asset.id, format, {
|
||||
@@ -42,51 +43,8 @@
|
||||
|
||||
const loadVideoData = async () => {
|
||||
isThumbnailVideoPlaying = false;
|
||||
videoAbortController = new AbortController();
|
||||
|
||||
try {
|
||||
const { data } = await api.assetApi.serveFile(
|
||||
asset.deviceAssetId,
|
||||
asset.deviceId,
|
||||
false,
|
||||
true,
|
||||
{
|
||||
responseType: 'blob',
|
||||
signal: videoAbortController.signal
|
||||
}
|
||||
);
|
||||
|
||||
if (!(data instanceof Blob)) {
|
||||
return;
|
||||
}
|
||||
|
||||
videoData = URL.createObjectURL(data);
|
||||
|
||||
videoPlayerNode.src = videoData;
|
||||
|
||||
videoPlayerNode.load();
|
||||
|
||||
videoPlayerNode.onloadeddata = () => {
|
||||
console.log('first frame load');
|
||||
};
|
||||
|
||||
videoPlayerNode.oncanplaythrough = () => {
|
||||
console.log('can play through');
|
||||
};
|
||||
|
||||
videoPlayerNode.oncanplay = () => {
|
||||
console.log('can play');
|
||||
videoPlayerNode.muted = true;
|
||||
videoPlayerNode.play();
|
||||
|
||||
isThumbnailVideoPlaying = true;
|
||||
calculateVideoDurationIntervalHandler = setInterval(() => {
|
||||
videoProgress = getVideoDurationInString(Math.round(videoPlayerNode.currentTime));
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
return videoData;
|
||||
} catch (e) {}
|
||||
videoUrl = getFileUrl(asset.deviceAssetId, asset.deviceId, false, true);
|
||||
};
|
||||
|
||||
const getVideoDurationInString = (currentTime: number) => {
|
||||
@@ -136,12 +94,7 @@
|
||||
|
||||
const handleMouseLeaveThumbnail = () => {
|
||||
mouseOver = false;
|
||||
|
||||
// Stop XHR download of video
|
||||
videoAbortController?.abort();
|
||||
|
||||
// Stop video playback
|
||||
URL.revokeObjectURL(videoData);
|
||||
videoUrl = '';
|
||||
|
||||
clearInterval(calculateVideoDurationIntervalHandler);
|
||||
|
||||
@@ -149,6 +102,18 @@
|
||||
videoProgress = '00:00';
|
||||
};
|
||||
|
||||
const handleCanPlay = (ev: Event) => {
|
||||
const playerNode = ev.target as HTMLVideoElement;
|
||||
|
||||
playerNode.muted = true;
|
||||
playerNode.play();
|
||||
|
||||
isThumbnailVideoPlaying = true;
|
||||
calculateVideoDurationIntervalHandler = setInterval(() => {
|
||||
videoProgress = getVideoDurationInString(Math.round(playerNode.currentTime));
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
$: getThumbnailBorderStyle = () => {
|
||||
if (selected) {
|
||||
return 'border-[20px] border-immich-primary/20';
|
||||
@@ -259,17 +224,21 @@
|
||||
|
||||
{#if mouseOver && asset.type === AssetTypeEnum.Video}
|
||||
<div class="absolute w-full h-full top-0" on:mouseenter={loadVideoData}>
|
||||
<video
|
||||
muted
|
||||
autoplay
|
||||
preload="none"
|
||||
class="h-full object-cover"
|
||||
width="250px"
|
||||
style:width={`${thumbnailSize}px`}
|
||||
bind:this={videoPlayerNode}
|
||||
>
|
||||
<track kind="captions" />
|
||||
</video>
|
||||
{#if videoUrl}
|
||||
<video
|
||||
muted
|
||||
autoplay
|
||||
preload="none"
|
||||
class="h-full object-cover"
|
||||
width="250px"
|
||||
style:width={`${thumbnailSize}px`}
|
||||
on:canplay={handleCanPlay}
|
||||
bind:this={videoPlayerNode}
|
||||
>
|
||||
<source src={videoUrl} type="video/mp4" />
|
||||
<track kind="captions" />
|
||||
</video>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import type { ImmichUser } from '$lib/models/immich-user';
|
||||
import { createEventDispatcher, onMount } from 'svelte';
|
||||
import { fade, fly, slide } from 'svelte/transition';
|
||||
import TrayArrowUp from 'svelte-material-icons/TrayArrowUp.svelte';
|
||||
import { clickOutside } from '../../utils/click-outside';
|
||||
import { api } from '@api';
|
||||
import { api, UserResponseDto } from '@api';
|
||||
|
||||
export let user: ImmichUser;
|
||||
export let user: UserResponseDto;
|
||||
export let shouldShowUploadButton = true;
|
||||
|
||||
let shouldShowAccountInfo = false;
|
||||
let shouldShowProfileImage = false;
|
||||
@@ -25,7 +25,6 @@
|
||||
await api.userApi.getProfileImage(user.id);
|
||||
shouldShowProfileImage = true;
|
||||
} catch (e) {
|
||||
console.log('User does not have a profile image');
|
||||
shouldShowProfileImage = false;
|
||||
}
|
||||
};
|
||||
@@ -33,10 +32,6 @@
|
||||
return text?.charAt(0).toUpperCase();
|
||||
};
|
||||
|
||||
const navigateToAdmin = () => {
|
||||
goto('/admin');
|
||||
};
|
||||
|
||||
const showAccountInfoPanel = () => {
|
||||
shouldShowAccountInfoPanel = true;
|
||||
};
|
||||
@@ -60,7 +55,7 @@
|
||||
/>
|
||||
</div>
|
||||
<section class="flex gap-4 place-items-center">
|
||||
{#if $page.url.pathname !== '/admin'}
|
||||
{#if $page.url.pathname !== '/admin' && shouldShowUploadButton}
|
||||
<button
|
||||
in:fly={{ x: 50, duration: 250 }}
|
||||
on:click={() => dispatch('uploadClicked')}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { cubicOut } from 'svelte/easing';
|
||||
import { tweened } from 'svelte/motion';
|
||||
|
||||
const progress = tweened(0, {
|
||||
duration: 1000,
|
||||
easing: cubicOut
|
||||
});
|
||||
|
||||
onMount(() => {
|
||||
progress.set(90);
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="absolute top-0 left-0 w-screen h-[3px] bg-white z-[999999999]">
|
||||
<span class="absolute bg-immich-primary h-[3px]" style:width={`${$progress}%`} />
|
||||
</div>
|
||||
@@ -0,0 +1,70 @@
|
||||
<script lang="ts">
|
||||
import { fade } from 'svelte/transition';
|
||||
import CloseCircleOutline from 'svelte-material-icons/CloseCircleOutline.svelte';
|
||||
import InformationOutline from 'svelte-material-icons/InformationOutline.svelte';
|
||||
|
||||
import {
|
||||
ImmichNotification,
|
||||
notificationController,
|
||||
NotificationType
|
||||
} from '$lib/components/shared-components/notification/notification';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
export let notificationInfo: ImmichNotification;
|
||||
|
||||
let infoPrimaryColor = '#4250AF';
|
||||
let errorPrimaryColor = '#E64132';
|
||||
|
||||
$: icon =
|
||||
notificationInfo.type === NotificationType.Error ? CloseCircleOutline : InformationOutline;
|
||||
|
||||
$: backgroundColor = () => {
|
||||
if (notificationInfo.type === NotificationType.Info) {
|
||||
return '#E0E2F0';
|
||||
}
|
||||
|
||||
if (notificationInfo.type === NotificationType.Error) {
|
||||
return '#FBE8E6';
|
||||
}
|
||||
};
|
||||
|
||||
$: borderStyle = () => {
|
||||
if (notificationInfo.type === NotificationType.Info) {
|
||||
return '1px solid #D8DDFF';
|
||||
}
|
||||
|
||||
if (notificationInfo.type === NotificationType.Error) {
|
||||
return '1px solid #F0E8E7';
|
||||
}
|
||||
};
|
||||
|
||||
$: primaryColor = () => {
|
||||
if (notificationInfo.type === NotificationType.Info) {
|
||||
return infoPrimaryColor;
|
||||
}
|
||||
|
||||
if (notificationInfo.type === NotificationType.Error) {
|
||||
return errorPrimaryColor;
|
||||
}
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
setTimeout(() => {
|
||||
notificationController.removeNotificationById(notificationInfo.id);
|
||||
}, notificationInfo.timeout);
|
||||
});
|
||||
</script>
|
||||
|
||||
<div
|
||||
transition:fade={{ duration: 250 }}
|
||||
style:background-color={backgroundColor()}
|
||||
style:border={borderStyle()}
|
||||
class="min-h-[80px] w-[300px] rounded-2xl z-[999999] shadow-md p-4 mb-4"
|
||||
>
|
||||
<div class="flex gap-2 place-items-center">
|
||||
<svelte:component this={icon} color={primaryColor()} size="20" />
|
||||
<h2 style:color={primaryColor()} class="font-medium">{notificationInfo.type.toString()}</h2>
|
||||
</div>
|
||||
|
||||
<p class="text-sm pl-[28px] pr-[16px]">{notificationInfo.message}</p>
|
||||
</div>
|
||||
@@ -0,0 +1,28 @@
|
||||
<script lang="ts">
|
||||
import { ImmichNotification, notificationController } from './notification';
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
import NotificationCard from './notification-card.svelte';
|
||||
import { flip } from 'svelte/animate';
|
||||
import { quintOut } from 'svelte/easing';
|
||||
|
||||
let notificationList: ImmichNotification[] = [];
|
||||
|
||||
notificationController.notificationList.subscribe((list) => {
|
||||
notificationList = list;
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if notificationList.length > 0}
|
||||
<section
|
||||
transition:fade={{ duration: 250 }}
|
||||
id="notification-list"
|
||||
class="absolute right-5 top-[80px] z-[99999999]"
|
||||
>
|
||||
{#each notificationList as notificationInfo (notificationInfo.id)}
|
||||
<div animate:flip={{ duration: 250, easing: quintOut }}>
|
||||
<NotificationCard {notificationInfo} />
|
||||
</div>
|
||||
{/each}
|
||||
</section>
|
||||
{/if}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
export enum NotificationType {
|
||||
Info = 'Info',
|
||||
Error = 'Error'
|
||||
}
|
||||
|
||||
export class ImmichNotification {
|
||||
id = new Date().getTime();
|
||||
type!: NotificationType;
|
||||
message!: string;
|
||||
timeout = 3000;
|
||||
}
|
||||
|
||||
export class ImmichNotificationDto {
|
||||
/**
|
||||
* Notification type
|
||||
* @type {NotificationType} [Info, Error]
|
||||
*/
|
||||
type: NotificationType = NotificationType.Info;
|
||||
|
||||
/**
|
||||
* Notification message
|
||||
*/
|
||||
message = '';
|
||||
|
||||
/**
|
||||
* Timeout in miliseconds
|
||||
*/
|
||||
timeout?: number;
|
||||
}
|
||||
function createNotificationList() {
|
||||
const notificationList = writable<ImmichNotification[]>([]);
|
||||
|
||||
const show = (notificationInfo: ImmichNotificationDto) => {
|
||||
const newNotification = new ImmichNotification();
|
||||
newNotification.message = notificationInfo.message;
|
||||
newNotification.type = notificationInfo.type;
|
||||
newNotification.timeout = notificationInfo.timeout || 3000;
|
||||
|
||||
notificationList.update((currentList) => [...currentList, newNotification]);
|
||||
};
|
||||
|
||||
const removeNotificationById = (id: number) => {
|
||||
notificationList.update((currentList) => currentList.filter((n) => n.id != id));
|
||||
};
|
||||
|
||||
return {
|
||||
show,
|
||||
removeNotificationById,
|
||||
notificationList
|
||||
};
|
||||
}
|
||||
|
||||
export const notificationController = createNotificationList();
|
||||
@@ -1,6 +1,4 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
import { AppSideBarSelection } from '$lib/models/admin-sidebar-selection';
|
||||
import { onMount } from 'svelte';
|
||||
import { page } from '$app/stores';
|
||||
@@ -24,7 +22,7 @@
|
||||
</script>
|
||||
|
||||
<section id="sidebar" class="flex flex-col gap-1 pt-8 pr-6">
|
||||
<a sveltekit:prefetch href={$page.routeId != 'photos' ? `/photos` : null}>
|
||||
<a sveltekit:prefetch sveltekit:noscroll href={$page.routeId !== 'photos' ? `/photos` : null}>
|
||||
<SideBarButton
|
||||
title="Photos"
|
||||
logo={ImageOutline}
|
||||
@@ -32,7 +30,7 @@
|
||||
isSelected={selectedAction === AppSideBarSelection.PHOTOS}
|
||||
/></a
|
||||
>
|
||||
<a sveltekit:prefetch href={$page.routeId != 'sharing' ? `/sharing` : null}>
|
||||
<a sveltekit:prefetch href={$page.routeId !== 'sharing' ? `/sharing` : null}>
|
||||
<SideBarButton
|
||||
title="Sharing"
|
||||
logo={AccountMultipleOutline}
|
||||
@@ -43,7 +41,7 @@
|
||||
<div class="text-xs ml-5 my-4">
|
||||
<p>LIBRARY</p>
|
||||
</div>
|
||||
<a sveltekit:prefetch href={$page.routeId != 'albums' ? `/albums` : null}>
|
||||
<a sveltekit:prefetch href={$page.routeId !== 'albums' ? `/albums` : null}>
|
||||
<SideBarButton
|
||||
title="Albums"
|
||||
logo={ImageAlbum}
|
||||
@@ -51,8 +49,8 @@
|
||||
isSelected={selectedAction === AppSideBarSelection.ALBUMS}
|
||||
/>
|
||||
</a>
|
||||
<!-- Status Box -->
|
||||
|
||||
<!-- Status Box -->
|
||||
<div class="mb-6 mt-auto">
|
||||
<StatusBox />
|
||||
</div>
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
const { data: serverInfoRes } = await api.serverInfoApi.getServerInfo();
|
||||
serverInfo = serverInfoRes;
|
||||
} catch (e) {
|
||||
console.log('Error [StatusBox] [pingServerInterval]');
|
||||
console.log('Error [StatusBox] [pingServerInterval]', e);
|
||||
isServerOk = false;
|
||||
}
|
||||
}, 10000);
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
import WindowMinimize from 'svelte-material-icons/WindowMinimize.svelte';
|
||||
import type { UploadAsset } from '$lib/models/upload-asset';
|
||||
import { getAssetsInfo } from '$lib/stores/assets';
|
||||
import { session } from '$app/stores';
|
||||
let showDetail = true;
|
||||
|
||||
let uploadLength = 0;
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import {
|
||||
notificationController,
|
||||
NotificationType
|
||||
} from './../components/shared-components/notification/notification';
|
||||
/* @vite-ignore */
|
||||
import * as exifr from 'exifr';
|
||||
import { uploadAssetsStore } from '$lib/stores/upload';
|
||||
import type { UploadAsset } from '../models/upload-asset';
|
||||
import { api, AssetFileUploadResponseDto } from '@api';
|
||||
import { albumUploadAssetStore } from '$lib/stores/album-upload-asset';
|
||||
|
||||
/**
|
||||
* Determine if the upload is for album or for the user general backup
|
||||
* @variant GENERAL - Upload assets to the server for general backup
|
||||
@@ -33,6 +36,17 @@ export const openFileUploadDialog = (uploadType: UploadType) => {
|
||||
fileSelector.onchange = async (e: any) => {
|
||||
const files = Array.from<File>(e.target.files);
|
||||
|
||||
if (files.length > 50) {
|
||||
notificationController.show({
|
||||
type: NotificationType.Error,
|
||||
message: `Cannot upload more than 50 files at a time - you are uploading ${files.length} files.
|
||||
Please use the CLI tool if you need to upload more than 50 files.`,
|
||||
timeout: 5000
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const acceptedFile = files.filter(
|
||||
(e) => e.type.split('/')[0] === 'video' || e.type.split('/')[0] === 'image'
|
||||
);
|
||||
|
||||
29
web/src/routes/+error.svelte
Normal file
29
web/src/routes/+error.svelte
Normal file
@@ -0,0 +1,29 @@
|
||||
<script>
|
||||
import { page } from '$app/stores';
|
||||
</script>
|
||||
|
||||
<div class="h-screen w-screen flex place-items-center place-content-center flex-col">
|
||||
<div class="min-w-[500px] bg-gray-300 rounded-2xl my-4 p-4">
|
||||
<code class="text-xs text-red-500">Error code {$page.status}</code>
|
||||
<br />
|
||||
<code class="text-sm">
|
||||
{$page.error.message}
|
||||
</code>
|
||||
<br />
|
||||
<div class="mt-5">
|
||||
<p class="text-sm font-medium">Verbose</p>
|
||||
<pre class="text-xs">{Object.values($page.error)}</pre>
|
||||
</div>
|
||||
|
||||
<a
|
||||
href="https://github.com/immich-app/immich/issues/new/choose"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<button
|
||||
class="px-5 py-2 rounded-lg text-sm mt-6 bg-immich-primary text-white hover:bg-immich-primary/75"
|
||||
>Get help</button
|
||||
>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
26
web/src/routes/+layout.server.ts
Normal file
26
web/src/routes/+layout.server.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { serverApi } from '@api';
|
||||
import * as cookieParser from 'cookie';
|
||||
|
||||
import type { LayoutServerLoad } from './$types';
|
||||
|
||||
export const load: LayoutServerLoad = async ({ request }) => {
|
||||
try {
|
||||
const cookies = cookieParser.parse(request.headers.get('cookie') || '');
|
||||
const accessToken = cookies['immich_access_token'];
|
||||
|
||||
if (!accessToken) {
|
||||
return {
|
||||
user: undefined
|
||||
};
|
||||
}
|
||||
|
||||
serverApi.setAccessToken(accessToken);
|
||||
const { data: userInfo } = await serverApi.userApi.getMyUserInfo();
|
||||
|
||||
return {
|
||||
user: userInfo
|
||||
};
|
||||
} catch (e) {
|
||||
console.log('[ERROR] layout.server.ts [LayoutServerLoad]: ', e);
|
||||
}
|
||||
};
|
||||
@@ -1,14 +1,3 @@
|
||||
<script context="module" lang="ts">
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
import { checkAppVersion } from '$lib/utils/check-app-version';
|
||||
|
||||
export const load: Load = async ({ url }) => {
|
||||
return {
|
||||
props: { url }
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import '../app.css';
|
||||
|
||||
@@ -17,11 +6,16 @@
|
||||
import AnnouncementBox from '$lib/components/shared-components/announcement-box.svelte';
|
||||
import UploadPanel from '$lib/components/shared-components/upload-panel.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import { checkAppVersion } from '$lib/utils/check-app-version';
|
||||
import { page } from '$app/stores';
|
||||
import { afterNavigate, beforeNavigate } from '$app/navigation';
|
||||
import NavigationLoadingBar from '$lib/components/shared-components/navigation-loading-bar.svelte';
|
||||
import NotificationList from '$lib/components/shared-components/notification/notification-list.svelte';
|
||||
|
||||
export let url: string;
|
||||
let shouldShowAnnouncement: boolean;
|
||||
let localVersion: string;
|
||||
let remoteVersion: string;
|
||||
let showNavigationLoadingBar = false;
|
||||
|
||||
onMount(async () => {
|
||||
const res = await checkAppVersion();
|
||||
@@ -30,16 +24,28 @@
|
||||
localVersion = res.localVersion ?? 'unknown';
|
||||
remoteVersion = res.remoteVersion ?? 'unknown';
|
||||
});
|
||||
|
||||
beforeNavigate(() => {
|
||||
showNavigationLoadingBar = true;
|
||||
});
|
||||
|
||||
afterNavigate(() => {
|
||||
showNavigationLoadingBar = false;
|
||||
});
|
||||
</script>
|
||||
|
||||
<main>
|
||||
{#key url}
|
||||
{#key $page.url}
|
||||
<div in:fade={{ duration: 100 }}>
|
||||
{#if showNavigationLoadingBar}
|
||||
<NavigationLoadingBar />
|
||||
{/if}
|
||||
|
||||
<slot />
|
||||
|
||||
<DownloadPanel />
|
||||
|
||||
<UploadPanel />
|
||||
|
||||
<NotificationList />
|
||||
{#if shouldShowAnnouncement}
|
||||
<AnnouncementBox
|
||||
{localVersion}
|
||||
29
web/src/routes/+page.svelte
Normal file
29
web/src/routes/+page.svelte
Normal file
@@ -0,0 +1,29 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
async function onGettingStartedClicked() {
|
||||
data.isAdminUserExist ? await goto('/auth/login') : await goto('/auth/register');
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Welcome 🎉 - Immich</title>
|
||||
<meta name="description" content="Immich Web Interface" />
|
||||
</svelte:head>
|
||||
|
||||
<section class="h-screen w-screen flex place-items-center place-content-center">
|
||||
<div class="flex flex-col place-items-center gap-8 text-center max-w-[350px]">
|
||||
<div class="flex place-items-center place-content-center ">
|
||||
<img class="text-center" src="immich-logo.svg" height="200" width="200" alt="immich-logo" />
|
||||
</div>
|
||||
<h1 class="text-4xl text-immich-primary font-bold font-immich-title">Welcome to IMMICH Web</h1>
|
||||
<button
|
||||
class="border px-4 py-2 rounded-md bg-immich-primary hover:bg-immich-primary/75 text-white font-bold w-[200px]"
|
||||
on:click={onGettingStartedClicked}
|
||||
>Getting Started
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
20
web/src/routes/+page.ts
Normal file
20
web/src/routes/+page.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
export const prerender = false;
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import { api } from '@api';
|
||||
import { browser } from '$app/env';
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load: PageLoad = async ({ parent }) => {
|
||||
const { user } = await parent();
|
||||
if (user) {
|
||||
throw redirect(302, '/photos');
|
||||
}
|
||||
|
||||
if (browser) {
|
||||
const { data } = await api.userApi.getUserCount();
|
||||
|
||||
return {
|
||||
isAdminUserExist: data.userCount != 0
|
||||
};
|
||||
}
|
||||
};
|
||||
19
web/src/routes/admin/+page.server.ts
Normal file
19
web/src/routes/admin/+page.server.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import { serverApi, UserResponseDto } from '@api';
|
||||
import type { PageServerLoad } from './$types';
|
||||
|
||||
export const load: PageServerLoad = async ({ parent }) => {
|
||||
const { user } = await parent();
|
||||
|
||||
if (!user) {
|
||||
throw redirect(302, '/auth/login');
|
||||
} else if (!user.isAdmin) {
|
||||
throw redirect(302, '/photos');
|
||||
}
|
||||
|
||||
const { data: allUsers } = await serverApi.userApi.getAllUsers(false);
|
||||
return {
|
||||
user: user,
|
||||
allUsers: allUsers
|
||||
};
|
||||
};
|
||||
@@ -1,47 +1,3 @@
|
||||
<script context="module" lang="ts">
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
import { api, UserResponseDto } from '@api';
|
||||
import { browser } from '$app/env';
|
||||
|
||||
export const load: Load = async ({ fetch, session }) => {
|
||||
if (!browser && !session.user) {
|
||||
return {
|
||||
status: 302,
|
||||
redirect: '/auth/login'
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const user: UserResponseDto = await fetch('/data/user/get-my-user-info').then((r) =>
|
||||
r.json()
|
||||
);
|
||||
const allUsers: UserResponseDto[] = await fetch('/data/user/get-all-users?isAll=false').then(
|
||||
(r) => r.json()
|
||||
);
|
||||
|
||||
if (!user.isAdmin) {
|
||||
return {
|
||||
status: 302,
|
||||
redirect: '/photos'
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
props: {
|
||||
user: user,
|
||||
allUsers: allUsers
|
||||
}
|
||||
};
|
||||
} catch (e) {
|
||||
return {
|
||||
status: 302,
|
||||
redirect: '/auth/login'
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
@@ -54,11 +10,12 @@
|
||||
import CreateUserForm from '$lib/components/forms/create-user-form.svelte';
|
||||
import EditUserForm from '$lib/components/forms/edit-user-form.svelte';
|
||||
import StatusBox from '$lib/components/shared-components/status-box.svelte';
|
||||
import type { PageData } from './$types';
|
||||
import { api, UserResponseDto } from '@api';
|
||||
|
||||
let selectedAction: AdminSideBarSelection = AdminSideBarSelection.USER_MANAGEMENT;
|
||||
|
||||
export let user: UserResponseDto;
|
||||
export let allUsers: UserResponseDto[];
|
||||
export let data: PageData;
|
||||
|
||||
let editUser: UserResponseDto;
|
||||
|
||||
@@ -75,8 +32,8 @@
|
||||
});
|
||||
|
||||
const onUserCreated = async () => {
|
||||
const { data } = await api.userApi.getAllUsers(false);
|
||||
allUsers = data;
|
||||
const getAllUsersRes = await api.userApi.getAllUsers(false);
|
||||
data.allUsers = getAllUsersRes.data;
|
||||
shouldShowCreateUserForm = false;
|
||||
};
|
||||
|
||||
@@ -87,14 +44,14 @@
|
||||
};
|
||||
|
||||
const onEditUserSuccess = async () => {
|
||||
const { data } = await api.userApi.getAllUsers(false);
|
||||
allUsers = data;
|
||||
const getAllUsersRes = await api.userApi.getAllUsers(false);
|
||||
data.allUsers = getAllUsersRes.data;
|
||||
shouldShowEditUserForm = false;
|
||||
};
|
||||
|
||||
const onEditPasswordSuccess = async () => {
|
||||
const { data } = await api.userApi.getAllUsers(false);
|
||||
allUsers = data;
|
||||
const getAllUsersRes = await api.userApi.getAllUsers(false);
|
||||
data.allUsers = getAllUsersRes.data;
|
||||
shouldShowEditUserForm = false;
|
||||
shouldShowInfoPanel = true;
|
||||
};
|
||||
@@ -104,7 +61,7 @@
|
||||
<title>Administration - Immich</title>
|
||||
</svelte:head>
|
||||
|
||||
<NavigationBar {user} />
|
||||
<NavigationBar user={data.user} />
|
||||
|
||||
{#if shouldShowCreateUserForm}
|
||||
<FullScreenModal on:clickOutside={() => (shouldShowCreateUserForm = false)}>
|
||||
@@ -125,7 +82,7 @@
|
||||
{#if shouldShowInfoPanel}
|
||||
<FullScreenModal on:clickOutside={() => (shouldShowInfoPanel = false)}>
|
||||
<div class="border bg-white shadow-sm w-[500px] rounded-3xl p-8 text-sm">
|
||||
<h1 class="font-bold text-immich-primary text-lg mb-4">Password reset success</h1>
|
||||
<h1 class="font-medium text-immich-primary text-lg mb-4">Password reset success</h1>
|
||||
|
||||
<p>
|
||||
The user's password has been reset to the default <code
|
||||
@@ -170,7 +127,7 @@
|
||||
<section class="w-[800px] pt-4">
|
||||
{#if selectedAction === AdminSideBarSelection.USER_MANAGEMENT}
|
||||
<UserManagement
|
||||
{allUsers}
|
||||
allUsers={data.allUsers}
|
||||
on:create-user={() => (shouldShowCreateUserForm = true)}
|
||||
on:edit-user={editUserHandler}
|
||||
/>
|
||||
22
web/src/routes/albums/+page.server.ts
Normal file
22
web/src/routes/albums/+page.server.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import type { PageServerLoad } from './$types';
|
||||
import { AlbumResponseDto, serverApi } from '@api';
|
||||
|
||||
export const load: PageServerLoad = async ({ parent }) => {
|
||||
try {
|
||||
const { user } = await parent();
|
||||
|
||||
if (!user) {
|
||||
throw Error('User is not logged in');
|
||||
}
|
||||
|
||||
const { data: albums } = await serverApi.albumApi.getAllAlbums();
|
||||
|
||||
return {
|
||||
user: user,
|
||||
albums: albums
|
||||
};
|
||||
} catch (e) {
|
||||
throw redirect(302, '/auth/login');
|
||||
}
|
||||
};
|
||||
@@ -1,44 +1,3 @@
|
||||
<script context="module" lang="ts">
|
||||
export const prerender = false;
|
||||
|
||||
import PlusBoxOutline from 'svelte-material-icons/PlusBoxOutline.svelte';
|
||||
|
||||
import NavigationBar from '$lib/components/shared-components/navigation-bar.svelte';
|
||||
import { ImmichUser } from '$lib/models/immich-user';
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
import SideBar from '$lib/components/shared-components/side-bar/side-bar.svelte';
|
||||
import { AlbumResponseDto, api } from '@api';
|
||||
|
||||
export const load: Load = async ({ fetch, session }) => {
|
||||
if (!browser && !session.user) {
|
||||
return {
|
||||
status: 302,
|
||||
redirect: '/auth/login'
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const [user, albums] = await Promise.all([
|
||||
fetch('/data/user/get-my-user-info').then((r) => r.json()),
|
||||
fetch('/data/album/get-all-albums').then((r) => r.json())
|
||||
]);
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
props: {
|
||||
user: user,
|
||||
albums: albums
|
||||
}
|
||||
};
|
||||
} catch (e) {
|
||||
return {
|
||||
status: 302,
|
||||
redirect: '/auth/login'
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import AlbumCard from '$lib/components/album-page/album-card.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
@@ -46,27 +5,33 @@
|
||||
import ContextMenu from '$lib/components/shared-components/context-menu/context-menu.svelte';
|
||||
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
|
||||
import DeleteOutline from 'svelte-material-icons/DeleteOutline.svelte';
|
||||
import { browser } from '$app/env';
|
||||
import type { PageData } from './$types';
|
||||
import { AlbumResponseDto, api } from '@api';
|
||||
import NavigationBar from '$lib/components/shared-components/navigation-bar.svelte';
|
||||
import SideBar from '$lib/components/shared-components/side-bar/side-bar.svelte';
|
||||
import PlusBoxOutline from 'svelte-material-icons/PlusBoxOutline.svelte';
|
||||
import {
|
||||
notificationController,
|
||||
NotificationType
|
||||
} from '$lib/components/shared-components/notification/notification';
|
||||
|
||||
export let user: ImmichUser;
|
||||
export let albums: AlbumResponseDto[];
|
||||
export let data: PageData;
|
||||
|
||||
let isShowContextMenu = false;
|
||||
let contextMenuPosition = { x: 0, y: 0 };
|
||||
let targetAlbum: AlbumResponseDto;
|
||||
|
||||
onMount(async () => {
|
||||
const { data } = await api.albumApi.getAllAlbums();
|
||||
albums = data;
|
||||
const getAllAlbumsRes = await api.albumApi.getAllAlbums();
|
||||
data.albums = getAllAlbumsRes.data;
|
||||
|
||||
// Delete album that has no photos and is named 'Untitled'
|
||||
for (const album of albums) {
|
||||
for (const album of data.albums) {
|
||||
if (album.albumName === 'Untitled' && album.assetCount === 0) {
|
||||
const isDeleted = await autoDeleteAlbum(album);
|
||||
|
||||
if (isDeleted) {
|
||||
albums = albums.filter((a) => a.id !== album.id);
|
||||
}
|
||||
setTimeout(async () => {
|
||||
await autoDeleteAlbum(album);
|
||||
data.albums = data.albums.filter((a) => a.id !== album.id);
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -79,7 +44,11 @@
|
||||
|
||||
goto('/albums/' + newAlbum.id);
|
||||
} catch (e) {
|
||||
console.log('Error [createAlbum] ', e);
|
||||
console.error('Error [createAlbum] ', e);
|
||||
notificationController.show({
|
||||
message: 'Error creating album, check console for more details',
|
||||
type: NotificationType.Error
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -88,7 +57,7 @@
|
||||
await api.albumApi.deleteAlbum(album.id);
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.log('Error [autoDeleteAlbum] ', e);
|
||||
console.error('Error [autoDeleteAlbum] ', e);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
@@ -101,9 +70,13 @@
|
||||
) {
|
||||
try {
|
||||
await api.albumApi.deleteAlbum(targetAlbum.id);
|
||||
albums = albums.filter((a) => a.id !== targetAlbum.id);
|
||||
data.albums = data.albums.filter((a) => a.id !== targetAlbum.id);
|
||||
} catch (e) {
|
||||
console.log('Error [userDeleteMenu] ', e);
|
||||
console.error('Error [userDeleteMenu] ', e);
|
||||
notificationController.show({
|
||||
message: 'Error deleting user, check console for more details',
|
||||
type: NotificationType.Error
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,7 +100,7 @@
|
||||
</svelte:head>
|
||||
|
||||
<section>
|
||||
<NavigationBar {user} on:uploadClicked={() => {}} />
|
||||
<NavigationBar user={data.user} shouldShowUploadButton={false} />
|
||||
</section>
|
||||
|
||||
<section class="grid grid-cols-[250px_auto] relative pt-[72px] h-screen bg-immich-bg ">
|
||||
@@ -158,7 +131,7 @@
|
||||
|
||||
<!-- Album Card -->
|
||||
<div class="flex flex-wrap gap-8">
|
||||
{#each albums as album}
|
||||
{#each data.albums as album}
|
||||
{#key album.id}
|
||||
<a sveltekit:prefetch href={`albums/${album.id}`}>
|
||||
<AlbumCard {album} on:showalbumcontextmenu={(e) => showAlbumContextMenu(e, album)} />
|
||||
@@ -168,7 +141,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Empty Message -->
|
||||
{#if albums.length === 0}
|
||||
{#if data.albums.length === 0}
|
||||
<div
|
||||
class="border p-5 w-[50%] m-auto mt-10 bg-gray-50 rounded-3xl flex flex-col place-content-center place-items-center"
|
||||
>
|
||||
23
web/src/routes/albums/[albumId]/+page.server.ts
Normal file
23
web/src/routes/albums/[albumId]/+page.server.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
|
||||
import type { PageServerLoad } from './$types';
|
||||
import { serverApi } from '@api';
|
||||
|
||||
export const load: PageServerLoad = async ({ parent, params }) => {
|
||||
const { user } = await parent();
|
||||
|
||||
if (!user) {
|
||||
throw redirect(302, '/auth/login');
|
||||
}
|
||||
|
||||
const albumId = params['albumId'];
|
||||
|
||||
try {
|
||||
const { data: album } = await serverApi.albumApi.getAlbumInfo(albumId);
|
||||
return {
|
||||
album
|
||||
};
|
||||
} catch (e) {
|
||||
throw redirect(302, '/albums');
|
||||
}
|
||||
};
|
||||
14
web/src/routes/albums/[albumId]/+page.svelte
Normal file
14
web/src/routes/albums/[albumId]/+page.svelte
Normal file
@@ -0,0 +1,14 @@
|
||||
<script lang="ts">
|
||||
import AlbumViewer from '$lib/components/album-page/album-viewer.svelte';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
export let data: PageData;
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{data.album.albumName} - Immich</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="immich-scrollbar">
|
||||
<AlbumViewer album={data.album} />
|
||||
</div>
|
||||
@@ -1,57 +0,0 @@
|
||||
<script context="module" lang="ts">
|
||||
export const prerender = false;
|
||||
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
import { AlbumResponseDto } from '@api';
|
||||
|
||||
export const load: Load = async ({ fetch, params, session }) => {
|
||||
if (!browser && !session.user) {
|
||||
return {
|
||||
status: 302,
|
||||
redirect: '/auth/login'
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const albumId = params['albumId'];
|
||||
|
||||
const albumInfo = await fetch(`/data/album/get-album-info?albumId=${albumId}`).then((r) =>
|
||||
r.json()
|
||||
);
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
props: {
|
||||
album: albumInfo
|
||||
}
|
||||
};
|
||||
} catch (e: any) {
|
||||
if (e.response?.status === 404) {
|
||||
return {
|
||||
status: 302,
|
||||
redirect: '/albums'
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: 302,
|
||||
redirect: '/auth/login'
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import AlbumViewer from '$lib/components/album-page/album-viewer.svelte';
|
||||
import { browser } from '$app/env';
|
||||
|
||||
export let album: AlbumResponseDto;
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{album.albumName} - Immich</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="immich-scrollbar">
|
||||
<AlbumViewer {album} />
|
||||
</div>
|
||||
@@ -1,28 +0,0 @@
|
||||
<script context="module" lang="ts">
|
||||
export const prerender = false;
|
||||
import { browser } from '$app/env';
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
|
||||
export const load: Load = async ({ params, session }) => {
|
||||
if (!browser && !session.user) {
|
||||
return {
|
||||
status: 302,
|
||||
redirect: '/auth/login'
|
||||
};
|
||||
}
|
||||
|
||||
const albumId = params['albumId'];
|
||||
|
||||
if (albumId) {
|
||||
return {
|
||||
status: 302,
|
||||
redirect: `/albums/${albumId}`
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
status: 302,
|
||||
redirect: `/photos`
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
18
web/src/routes/albums/[albumId]/photos/[assetId]/+page.ts
Normal file
18
web/src/routes/albums/[albumId]/photos/[assetId]/+page.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
export const prerender = false;
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load: PageLoad = async ({ params, parent }) => {
|
||||
const { user } = await parent();
|
||||
if (!user) {
|
||||
throw redirect(302, '/auth/login');
|
||||
}
|
||||
|
||||
const albumId = params['albumId'];
|
||||
|
||||
if (albumId) {
|
||||
throw redirect(302, `/albums/${albumId}`);
|
||||
} else {
|
||||
throw redirect(302, `/photos`);
|
||||
}
|
||||
};
|
||||
25
web/src/routes/auth/change-password/+page.svelte
Normal file
25
web/src/routes/auth/change-password/+page.svelte
Normal file
@@ -0,0 +1,25 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
import ChangePasswordForm from '$lib/components/forms/change-password-form.svelte';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
const onSuccessHandler = async () => {
|
||||
await fetch('auth/logout', { method: 'POST' });
|
||||
|
||||
goto('/auth/login');
|
||||
};
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Change Password - Immich</title>
|
||||
</svelte:head>
|
||||
|
||||
<section class="h-screen w-screen flex place-items-center place-content-center">
|
||||
<div in:fade={{ duration: 100 }} out:fade={{ duration: 100 }}>
|
||||
<ChangePasswordForm user={data.user} on:success={onSuccessHandler} />
|
||||
</div>
|
||||
</section>
|
||||
21
web/src/routes/auth/change-password/+page.ts
Normal file
21
web/src/routes/auth/change-password/+page.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { api } from '@api';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
export const prerender = false;
|
||||
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load: PageLoad = async () => {
|
||||
try {
|
||||
const { data: userInfo } = await api.userApi.getMyUserInfo();
|
||||
|
||||
if (userInfo.shouldChangePassword) {
|
||||
return {
|
||||
user: userInfo
|
||||
};
|
||||
} else {
|
||||
throw redirect(302, '/photos');
|
||||
}
|
||||
} catch (e) {
|
||||
throw redirect(302, '/auth/login');
|
||||
}
|
||||
};
|
||||
@@ -1,56 +0,0 @@
|
||||
<script context="module" lang="ts">
|
||||
export const prerender = false;
|
||||
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
|
||||
export const load: Load = async () => {
|
||||
try {
|
||||
const { data: userInfo } = await api.userApi.getMyUserInfo();
|
||||
|
||||
if (userInfo.shouldChangePassword) {
|
||||
return {
|
||||
status: 200,
|
||||
props: {
|
||||
user: userInfo
|
||||
}
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
status: 302,
|
||||
redirect: '/photos'
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
return {
|
||||
status: 302,
|
||||
redirect: '/auth/login'
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
import ChangePasswordForm from '$lib/components/forms/change-password-form.svelte';
|
||||
import { api, UserResponseDto } from '@api';
|
||||
|
||||
export let user: UserResponseDto;
|
||||
|
||||
const onSuccessHandler = async () => {
|
||||
await fetch('auth/logout', { method: 'POST' });
|
||||
|
||||
goto('/auth/login');
|
||||
};
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Change Password - Immich</title>
|
||||
</svelte:head>
|
||||
|
||||
<section class="h-screen w-screen flex place-items-center place-content-center">
|
||||
<div in:fade={{ duration: 100 }} out:fade={{ duration: 100 }}>
|
||||
<ChangePasswordForm {user} on:success={onSuccessHandler} />
|
||||
</div>
|
||||
</section>
|
||||
@@ -3,10 +3,6 @@
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
import LoginForm from '$lib/components/forms/login-form.svelte';
|
||||
|
||||
const onLoginSuccess = async () => {
|
||||
goto('/photos');
|
||||
};
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@@ -15,6 +11,9 @@
|
||||
|
||||
<section class="h-screen w-screen flex place-items-center place-content-center">
|
||||
<div in:fade={{ duration: 100 }} out:fade={{ duration: 100 }}>
|
||||
<LoginForm on:success={onLoginSuccess} on:first-login={() => goto('/auth/change-password')} />
|
||||
<LoginForm
|
||||
on:success={() => goto('/photos')}
|
||||
on:first-login={() => goto('/auth/change-password')}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
@@ -1,19 +0,0 @@
|
||||
import { api, serverApi } from '@api';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
|
||||
export const POST: RequestHandler = async () => {
|
||||
api.removeAccessToken();
|
||||
serverApi.removeAccessToken();
|
||||
|
||||
return {
|
||||
headers: {
|
||||
'Set-Cookie': [
|
||||
'immich_is_authenticated=deleted; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT;',
|
||||
'immich_access_token=delete; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT'
|
||||
]
|
||||
},
|
||||
body: {
|
||||
ok: true
|
||||
}
|
||||
};
|
||||
};
|
||||
27
web/src/routes/auth/logout/+server.ts
Normal file
27
web/src/routes/auth/logout/+server.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import { api, serverApi } from '@api';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
|
||||
export const POST: RequestHandler = async () => {
|
||||
api.removeAccessToken();
|
||||
serverApi.removeAccessToken();
|
||||
|
||||
const headers = new Headers();
|
||||
|
||||
headers.append(
|
||||
'set-cookie',
|
||||
'immich_is_authenticated=deleted; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT;'
|
||||
);
|
||||
headers.append(
|
||||
'set-cookie',
|
||||
'immich_access_token=delete; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT'
|
||||
);
|
||||
return json(
|
||||
{
|
||||
ok: true
|
||||
},
|
||||
{
|
||||
headers
|
||||
}
|
||||
);
|
||||
};
|
||||
13
web/src/routes/auth/register/+page.server.ts
Normal file
13
web/src/routes/auth/register/+page.server.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import type { PageServerLoad } from './$types';
|
||||
import { serverApi } from '@api';
|
||||
|
||||
export const load: PageServerLoad = async () => {
|
||||
const { data } = await serverApi.userApi.getUserCount();
|
||||
if (data.userCount != 0) {
|
||||
// Admin has been registered, redirect to login
|
||||
throw redirect(302, '/auth/login');
|
||||
}
|
||||
|
||||
return;
|
||||
};
|
||||
11
web/src/routes/auth/register/+page.svelte
Normal file
11
web/src/routes/auth/register/+page.svelte
Normal file
@@ -0,0 +1,11 @@
|
||||
<script lang="ts">
|
||||
import AdminRegistrationForm from '$lib/components/forms/admin-registration-form.svelte';
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Admin Registration - Immich</title>
|
||||
</svelte:head>
|
||||
|
||||
<section class="h-screen w-screen flex place-items-center place-content-center">
|
||||
<AdminRegistrationForm />
|
||||
</section>
|
||||
@@ -1,31 +0,0 @@
|
||||
<script context="module" lang="ts">
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
|
||||
export const load: Load = async () => {
|
||||
const { data } = await api.userApi.getUserCount();
|
||||
if (data.userCount != 0) {
|
||||
// Admin has been registered, redirect to login
|
||||
return {
|
||||
status: 302,
|
||||
redirect: '/auth/login'
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: 200
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import AdminRegistrationForm from '$lib/components/forms/admin-registration-form.svelte';
|
||||
import { api } from '@api';
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Admin Registration - Immich</title>
|
||||
</svelte:head>
|
||||
|
||||
<section class="h-screen w-screen flex place-items-center place-content-center">
|
||||
<AdminRegistrationForm />
|
||||
</section>
|
||||
@@ -1 +0,0 @@
|
||||
This directory contain SSR endpoints to user serverApi instance to make request directly to DNS
|
||||
@@ -1,18 +0,0 @@
|
||||
import { AlbumResponseDto, serverApi } from '@api';
|
||||
import type { RequestEvent, RequestHandlerOutput } from '@sveltejs/kit';
|
||||
|
||||
export const GET = async ({
|
||||
url
|
||||
}: RequestEvent): Promise<RequestHandlerOutput<AlbumResponseDto>> => {
|
||||
try {
|
||||
const albumId = url.searchParams.get('albumId') || '';
|
||||
const { data } = await serverApi.albumApi.getAlbumInfo(albumId);
|
||||
return {
|
||||
body: data
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
status: 500
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -1,18 +0,0 @@
|
||||
import { AlbumResponseDto, serverApi } from '@api';
|
||||
import type { RequestEvent, RequestHandler, RequestHandlerOutput } from '@sveltejs/kit';
|
||||
|
||||
export const GET = async ({
|
||||
url
|
||||
}: RequestEvent): Promise<RequestHandlerOutput<AlbumResponseDto[]>> => {
|
||||
try {
|
||||
const isShared = url.searchParams.get('isShared') === 'true' || undefined;
|
||||
const { data } = await serverApi.albumApi.getAllAlbums(isShared);
|
||||
return {
|
||||
body: data
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
status: 500
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -1,15 +0,0 @@
|
||||
import { AssetResponseDto, serverApi } from '@api';
|
||||
import type { RequestHandlerOutput } from '@sveltejs/kit';
|
||||
|
||||
export const GET = async (): Promise<RequestHandlerOutput<AssetResponseDto[]>> => {
|
||||
try {
|
||||
const { data } = await serverApi.assetApi.getAllAssets();
|
||||
return {
|
||||
body: data
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
status: 500
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -1,17 +0,0 @@
|
||||
import { serverApi, UserResponseDto } from '@api';
|
||||
import type { RequestEvent, RequestHandlerOutput } from '@sveltejs/kit';
|
||||
|
||||
export const GET = async ({url} : RequestEvent): Promise<RequestHandlerOutput<UserResponseDto[]>> => {
|
||||
try {
|
||||
const isAll = url.searchParams.get('isAll') === 'true';
|
||||
|
||||
const { data } = await serverApi.userApi.getAllUsers(isAll);
|
||||
return {
|
||||
body: data
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
status: 500
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -1,15 +0,0 @@
|
||||
import { serverApi, UserResponseDto } from '@api';
|
||||
import type { RequestHandlerOutput } from '@sveltejs/kit';
|
||||
|
||||
export const GET = async (): Promise<RequestHandlerOutput<UserResponseDto>> => {
|
||||
try {
|
||||
const { data } = await serverApi.userApi.getMyUserInfo();
|
||||
return {
|
||||
body: data
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
status: 500
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -1,58 +0,0 @@
|
||||
<script context="module" lang="ts">
|
||||
export const prerender = false;
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
import { api } from '@api';
|
||||
import { browser } from '$app/env';
|
||||
|
||||
export const load: Load = async () => {
|
||||
if (browser) {
|
||||
try {
|
||||
const {data: user} = await api.userApi.getMyUserInfo();
|
||||
|
||||
return {
|
||||
status: 302,
|
||||
redirect: '/photos'
|
||||
};
|
||||
} catch (e) {
|
||||
}
|
||||
|
||||
const {data} = await api.userApi.getUserCount();
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
props: {
|
||||
isAdminUserExist: data.userCount != 0
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
export let isAdminUserExist: boolean;
|
||||
|
||||
async function onGettingStartedClicked() {
|
||||
isAdminUserExist ? await goto('/auth/login') : await goto('/auth/register');
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Welcome 🎉 - Immich</title>
|
||||
<meta name="description" content="Immich Web Interface"/>
|
||||
</svelte:head>
|
||||
|
||||
<section class="h-screen w-screen flex place-items-center place-content-center">
|
||||
<div class="flex flex-col place-items-center gap-8 text-center max-w-[350px]">
|
||||
<div class="flex place-items-center place-content-center ">
|
||||
<img class="text-center" src="immich-logo.svg" height="200" width="200" alt="immich-logo"/>
|
||||
</div>
|
||||
<h1 class="text-4xl text-immich-primary font-bold font-immich-title">Welcome to IMMICH Web</h1>
|
||||
<button
|
||||
class="border px-4 py-2 rounded-md bg-immich-primary hover:bg-immich-primary/75 text-white font-bold w-[200px]"
|
||||
on:click={onGettingStartedClicked}>Getting Started
|
||||
</button
|
||||
>
|
||||
</div>
|
||||
</section>
|
||||
21
web/src/routes/photos/+page.server.ts
Normal file
21
web/src/routes/photos/+page.server.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { serverApi } from './../../api/api';
|
||||
import type { PageServerLoad } from './$types';
|
||||
import { redirect, error } from '@sveltejs/kit';
|
||||
|
||||
export const load: PageServerLoad = async ({ parent }) => {
|
||||
try {
|
||||
const { user } = await parent();
|
||||
if (!user) {
|
||||
throw error(400, 'Not logged in');
|
||||
}
|
||||
|
||||
const { data: assets } = await serverApi.assetApi.getAllAssets();
|
||||
|
||||
return {
|
||||
user,
|
||||
assets
|
||||
};
|
||||
} catch (e) {
|
||||
throw redirect(302, '/auth/login');
|
||||
}
|
||||
};
|
||||
@@ -1,60 +1,32 @@
|
||||
<script context="module" lang="ts">
|
||||
export const prerender = false;
|
||||
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
import { setAssetInfo } from '$lib/stores/assets';
|
||||
|
||||
export const load: Load = async ({ fetch, session }) => {
|
||||
if (!browser && !session.user) {
|
||||
return {
|
||||
status: 302,
|
||||
redirect: '/auth/login'
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const [userInfo, assets] = await Promise.all([
|
||||
fetch('/data/user/get-my-user-info').then((r) => r.json()),
|
||||
fetch('/data/asset/get-all-assets').then((r) => r.json())
|
||||
]);
|
||||
|
||||
setAssetInfo(assets);
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
props: {
|
||||
user: userInfo
|
||||
}
|
||||
};
|
||||
} catch (e) {
|
||||
console.log('ERROR load photos index');
|
||||
return {
|
||||
status: 302,
|
||||
redirect: '/auth/login'
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import NavigationBar from '$lib/components/shared-components/navigation-bar.svelte';
|
||||
import CheckCircle from 'svelte-material-icons/CheckCircle.svelte';
|
||||
import { fly } from 'svelte/transition';
|
||||
import { assetsGroupByDate, flattenAssetGroupByDate, assets } from '$lib/stores/assets';
|
||||
import {
|
||||
assetsGroupByDate,
|
||||
flattenAssetGroupByDate,
|
||||
assets,
|
||||
setAssetInfo
|
||||
} from '$lib/stores/assets';
|
||||
import ImmichThumbnail from '$lib/components/shared-components/immich-thumbnail.svelte';
|
||||
import moment from 'moment';
|
||||
import AssetViewer from '$lib/components/asset-viewer/asset-viewer.svelte';
|
||||
import { openFileUploadDialog, UploadType } from '$lib/utils/file-uploader';
|
||||
import { api, AssetResponseDto, UserResponseDto } from '@api';
|
||||
import { api, AssetResponseDto } from '@api';
|
||||
import SideBar from '$lib/components/shared-components/side-bar/side-bar.svelte';
|
||||
import CircleOutline from 'svelte-material-icons/CircleOutline.svelte';
|
||||
import CircleIconButton from '$lib/components/shared-components/circle-icon-button.svelte';
|
||||
import DeleteOutline from 'svelte-material-icons/DeleteOutline.svelte';
|
||||
import Close from 'svelte-material-icons/Close.svelte';
|
||||
import { browser } from '$app/env';
|
||||
import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte';
|
||||
import type { PageData } from './$types';
|
||||
import { onMount } from 'svelte';
|
||||
import {
|
||||
notificationController,
|
||||
NotificationType
|
||||
} from '$lib/components/shared-components/notification/notification';
|
||||
|
||||
export let user: UserResponseDto;
|
||||
export let data: PageData;
|
||||
|
||||
let selectedGroupThumbnail: number | null;
|
||||
let isMouseOverGroup: boolean;
|
||||
@@ -73,6 +45,10 @@
|
||||
let currentViewAssetIndex = 0;
|
||||
let selectedAsset: AssetResponseDto;
|
||||
|
||||
onMount(() => {
|
||||
setAssetInfo(data.assets);
|
||||
});
|
||||
|
||||
const thumbnailMouseEventHandler = (event: CustomEvent) => {
|
||||
const { selectedGroupIndex }: { selectedGroupIndex: number } = event.detail;
|
||||
|
||||
@@ -96,7 +72,10 @@
|
||||
pushState(selectedAsset.id);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('Error navigating asset forward', e);
|
||||
notificationController.show({
|
||||
type: NotificationType.Info,
|
||||
message: 'You have reached the end'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -108,7 +87,10 @@
|
||||
pushState(selectedAsset.id);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('Error navigating asset backward', e);
|
||||
notificationController.show({
|
||||
type: NotificationType.Info,
|
||||
message: 'You have reached the end'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -204,7 +186,11 @@
|
||||
clearMultiSelectAssetAssetHandler();
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('Error deleteSelectedAssetHandler', e);
|
||||
notificationController.show({
|
||||
type: NotificationType.Error,
|
||||
message: 'Error deleting assets, check console for more details'
|
||||
});
|
||||
console.error('Error deleteSelectedAssetHandler', e);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -234,7 +220,10 @@
|
||||
{/if}
|
||||
|
||||
{#if !isMultiSelectionMode}
|
||||
<NavigationBar {user} on:uploadClicked={() => openFileUploadDialog(UploadType.GENERAL)} />
|
||||
<NavigationBar
|
||||
user={data.user}
|
||||
on:uploadClicked={() => openFileUploadDialog(UploadType.GENERAL)}
|
||||
/>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
<script context="module" lang="ts">
|
||||
export const prerender = false;
|
||||
|
||||
import { browser } from '$app/env';
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
|
||||
export const load: Load = async ({ session }) => {
|
||||
if (!browser && !session.user) {
|
||||
return {
|
||||
status: 302,
|
||||
redirect: '/auth/login'
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
status: 302,
|
||||
redirect: '/photos'
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
14
web/src/routes/photos/[assetId]/+page.server.ts
Normal file
14
web/src/routes/photos/[assetId]/+page.server.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
export const prerender = false;
|
||||
|
||||
import type { PageServerLoad } from './$types';
|
||||
|
||||
export const load: PageServerLoad = async ({ parent }) => {
|
||||
const { user } = await parent();
|
||||
|
||||
if (!user) {
|
||||
throw redirect(302, '/auth/login');
|
||||
} else {
|
||||
throw redirect(302, '/photos');
|
||||
}
|
||||
};
|
||||
0
web/src/routes/photos/[assetId]/+page.svelte
Normal file
0
web/src/routes/photos/[assetId]/+page.svelte
Normal file
23
web/src/routes/sharing/+page.server.ts
Normal file
23
web/src/routes/sharing/+page.server.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
export const prerender = false;
|
||||
|
||||
import { serverApi } from '@api';
|
||||
import type { PageServerLoad } from './$types';
|
||||
|
||||
export const load: PageServerLoad = async ({ parent }) => {
|
||||
try {
|
||||
const { user } = await parent();
|
||||
if (!user) {
|
||||
throw redirect(302, '/auth/login');
|
||||
}
|
||||
|
||||
const { data: sharedAlbums } = await serverApi.albumApi.getAllAlbums(true);
|
||||
|
||||
return {
|
||||
user: user,
|
||||
sharedAlbums: sharedAlbums
|
||||
};
|
||||
} catch (e) {
|
||||
throw redirect(302, '/auth/login');
|
||||
}
|
||||
};
|
||||
92
web/src/routes/sharing/+page.svelte
Normal file
92
web/src/routes/sharing/+page.svelte
Normal file
@@ -0,0 +1,92 @@
|
||||
<script lang="ts">
|
||||
import NavigationBar from '$lib/components/shared-components/navigation-bar.svelte';
|
||||
import SideBar from '$lib/components/shared-components/side-bar/side-bar.svelte';
|
||||
import PlusBoxOutline from 'svelte-material-icons/PlusBoxOutline.svelte';
|
||||
import SharedAlbumListTile from '$lib/components/sharing-page/shared-album-list-tile.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { api } from '@api';
|
||||
import type { PageData } from './$types';
|
||||
import {
|
||||
notificationController,
|
||||
NotificationType
|
||||
} from '$lib/components/shared-components/notification/notification';
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
const createSharedAlbum = async () => {
|
||||
try {
|
||||
const { data: newAlbum } = await api.albumApi.createAlbum({
|
||||
albumName: 'Untitled'
|
||||
});
|
||||
|
||||
goto('/albums/' + newAlbum.id);
|
||||
} catch (e) {
|
||||
notificationController.show({
|
||||
message: 'Error creating album, check console for more details',
|
||||
type: NotificationType.Error
|
||||
});
|
||||
|
||||
console.log('Error [createAlbum] ', e);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Albums - Immich</title>
|
||||
</svelte:head>
|
||||
|
||||
<section>
|
||||
<NavigationBar user={data.user} shouldShowUploadButton={false} />
|
||||
</section>
|
||||
|
||||
<section class="grid grid-cols-[250px_auto] relative pt-[72px] h-screen bg-immich-bg">
|
||||
<SideBar />
|
||||
|
||||
<section class="overflow-y-auto relative">
|
||||
<section id="album-content" class="relative pt-8 pl-4 mb-12 bg-immich-bg">
|
||||
<!-- Main Section -->
|
||||
<div class="px-4 flex justify-between place-items-center">
|
||||
<div>
|
||||
<p class="font-medium">Sharing</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button
|
||||
on:click={createSharedAlbum}
|
||||
class="flex place-items-center gap-1 text-sm hover:bg-immich-primary/5 p-2 rounded-lg font-medium hover:text-gray-700"
|
||||
>
|
||||
<span>
|
||||
<PlusBoxOutline size="18" />
|
||||
</span>
|
||||
<p>Create shared album</p>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="my-4">
|
||||
<hr />
|
||||
</div>
|
||||
|
||||
<!-- Share Album List -->
|
||||
<div class="w-full flex flex-col place-items-center">
|
||||
{#each data.sharedAlbums as album}
|
||||
<a sveltekit:prefetch href={`albums/${album.id}`}>
|
||||
<SharedAlbumListTile {album} user={data.user} />
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<!-- Empty List -->
|
||||
{#if data.sharedAlbums.length === 0}
|
||||
<div
|
||||
class="border p-5 w-[50%] m-auto mt-10 bg-gray-50 rounded-3xl flex flex-col place-content-center place-items-center"
|
||||
>
|
||||
<img src="/empty-2.svg" alt="Empty shared album" width="500" />
|
||||
<p class="text-center text-immich-text-gray-500">
|
||||
Create a shared album to share photos and videos with people in your network
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
</section>
|
||||
</section>
|
||||
@@ -1,120 +0,0 @@
|
||||
<script context="module" lang="ts">
|
||||
export const prerender = false;
|
||||
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
import { AlbumResponseDto, api, UserResponseDto } from '@api';
|
||||
import { browser } from '$app/env';
|
||||
|
||||
export const load: Load = async ({fetch, session}) => {
|
||||
if (!browser && !session.user) {
|
||||
return {
|
||||
status: 302,
|
||||
redirect: '/auth/login'
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const [user, sharedAlbums] = await Promise.all([
|
||||
fetch('/data/user/get-my-user-info').then((r) => r.json()),
|
||||
fetch('/data/album/get-all-albums?isShared=true').then((r) => r.json())
|
||||
]);
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
props: {
|
||||
user: user,
|
||||
sharedAlbums: sharedAlbums
|
||||
}
|
||||
};
|
||||
} catch (e) {
|
||||
return {
|
||||
status: 302,
|
||||
redirect: '/auth/login'
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import NavigationBar from '$lib/components/shared-components/navigation-bar.svelte';
|
||||
import SideBar from '$lib/components/shared-components/side-bar/side-bar.svelte';
|
||||
import PlusBoxOutline from 'svelte-material-icons/PlusBoxOutline.svelte';
|
||||
import SharedAlbumListTile from '$lib/components/sharing-page/shared-album-list-tile.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
export let user: UserResponseDto;
|
||||
export let sharedAlbums: AlbumResponseDto[];
|
||||
|
||||
const createSharedAlbum = async () => {
|
||||
try {
|
||||
const {data: newAlbum} = await api.albumApi.createAlbum({
|
||||
albumName: 'Untitled'
|
||||
});
|
||||
|
||||
goto('/albums/' + newAlbum.id);
|
||||
} catch (e) {
|
||||
console.log('Error [createAlbum] ', e);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Albums - Immich</title>
|
||||
</svelte:head>
|
||||
|
||||
<section>
|
||||
<NavigationBar {user} on:uploadClicked={() => {}}/>
|
||||
</section>
|
||||
|
||||
<section class="grid grid-cols-[250px_auto] relative pt-[72px] h-screen bg-immich-bg">
|
||||
<SideBar/>
|
||||
|
||||
<section class="overflow-y-auto relative">
|
||||
<section id="album-content" class="relative pt-8 pl-4 mb-12 bg-immich-bg">
|
||||
<!-- Main Section -->
|
||||
<div class="px-4 flex justify-between place-items-center">
|
||||
<div>
|
||||
<p class="font-medium">Sharing</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button
|
||||
on:click={createSharedAlbum}
|
||||
class="flex place-items-center gap-1 text-sm hover:bg-immich-primary/5 p-2 rounded-lg font-medium hover:text-gray-700"
|
||||
>
|
||||
<span>
|
||||
<PlusBoxOutline size="18"/>
|
||||
</span>
|
||||
<p>Create shared album</p>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="my-4">
|
||||
<hr/>
|
||||
</div>
|
||||
|
||||
<!-- Share Album List -->
|
||||
<div class="w-full flex flex-col place-items-center">
|
||||
{#each sharedAlbums as album}
|
||||
<a sveltekit:prefetch href={`albums/${album.id}`}>
|
||||
<SharedAlbumListTile {album} {user}/>
|
||||
</a
|
||||
>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<!-- Empty List -->
|
||||
{#if sharedAlbums.length === 0}
|
||||
<div
|
||||
class="border p-5 w-[50%] m-auto mt-10 bg-gray-50 rounded-3xl flex flex-col place-content-center place-items-center"
|
||||
>
|
||||
<img src="/empty-2.svg" alt="Empty shared album" width="500"/>
|
||||
<p class="text-center text-immich-text-gray-500">
|
||||
Create a shared album to share photos and videos with people in your network
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
</section>
|
||||
</section>
|
||||
Reference in New Issue
Block a user