From 3c6f2e5ce392d9523fd9b7d59b4b98d4ad1bd03f Mon Sep 17 00:00:00 2001 From: Eugene Burmakin Date: Tue, 26 Nov 2024 17:36:22 +0100 Subject: [PATCH] Add loading spinner and checkmark --- app/assets/stylesheets/application.css | 35 +++++++++++++++ app/controllers/api/v1/photos_controller.rb | 8 +++- app/javascript/controllers/maps_controller.js | 45 ++++++++++++++++++- app/services/immich/request_photos.rb | 20 +++++++-- 4 files changed, 101 insertions(+), 7 deletions(-) diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css index 08196e09..982d94b0 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css @@ -72,3 +72,38 @@ width: 48px; height: 48px; } + +.leaflet-loading-control { + padding: 5px; + border-radius: 4px; + box-shadow: 0 1px 5px rgba(0,0,0,0.2); + margin: 10px; + width: 32px; + height: 32px; + background: white; +} + +.loading-spinner { + display: flex; + align-items: center; + gap: 8px; + font-size: 18px; + color: gray; +} + +.loading-spinner::before { + content: '🔵'; + font-size: 18px; + animation: spinner 1s linear infinite; +} + +.loading-spinner.done::before { + content: '✅'; + animation: none; +} + +@keyframes spinner { + to { + transform: rotate(360deg); + } +} diff --git a/app/controllers/api/v1/photos_controller.rb b/app/controllers/api/v1/photos_controller.rb index 93f17208..df042494 100644 --- a/app/controllers/api/v1/photos_controller.rb +++ b/app/controllers/api/v1/photos_controller.rb @@ -3,8 +3,12 @@ class Api::V1::PhotosController < ApiController def index @photos = Rails.cache.fetch("photos_#{params[:start_date]}_#{params[:end_date]}", expires_in: 1.day) do - Immich::RequestPhotos.new(current_api_user, start_date: params[:start_date], end_date: params[:end_date]).call - end.reject { |photo| photo['type'].downcase == 'video' } + Immich::RequestPhotos.new( + current_api_user, + start_date: params[:start_date], + end_date: params[:end_date] + ).call.reject { |asset| asset['type'].downcase == 'video' } + end render json: @photos, status: :ok end diff --git a/app/javascript/controllers/maps_controller.js b/app/javascript/controllers/maps_controller.js index 8a54ea99..70d42258 100644 --- a/app/javascript/controllers/maps_controller.js +++ b/app/javascript/controllers/maps_controller.js @@ -786,6 +786,18 @@ export default class extends Controller { const MAX_RETRIES = 3; const RETRY_DELAY = 3000; // 3 seconds + // Create loading control + const LoadingControl = L.Control.extend({ + onAdd: (map) => { + const container = L.DomUtil.create('div', 'leaflet-loading-control'); + container.innerHTML = '
'; + return container; + } + }); + + const loadingControl = new LoadingControl({ position: 'topleft' }); + this.map.addControl(loadingControl); + try { const params = new URLSearchParams({ api_key: this.apiKey, @@ -801,14 +813,42 @@ export default class extends Controller { const photos = await response.json(); this.photoMarkers.clearLayers(); - photos.forEach(photo => this.createPhotoMarker(photo)); + // Create a promise for each photo to track when it's fully loaded + const photoLoadPromises = photos.map(photo => { + return new Promise((resolve) => { + const img = new Image(); + const thumbnailUrl = `/api/v1/photos/${photo.id}/thumbnail.jpg?api_key=${this.apiKey}`; + + img.onload = () => { + this.createPhotoMarker(photo); + resolve(); + }; + + img.onerror = () => { + console.error(`Failed to load photo ${photo.id}`); + resolve(); // Resolve anyway to not block other photos + }; + + img.src = thumbnailUrl; + }); + }); + + // Wait for all photos to be loaded and rendered + await Promise.all(photoLoadPromises); if (!this.map.hasLayer(this.photoMarkers)) { this.photoMarkers.addTo(this.map); } + // Show checkmark for 1 second before removing + const loadingSpinner = document.querySelector('.loading-spinner'); + loadingSpinner.classList.add('done'); + + await new Promise(resolve => setTimeout(resolve, 1000)); + } catch (error) { console.error('Error fetching photos:', error); + showFlashMessage('error', 'Failed to fetch photos'); if (retryCount < MAX_RETRIES) { console.log(`Retrying in ${RETRY_DELAY/1000} seconds... (Attempt ${retryCount + 1}/${MAX_RETRIES})`); @@ -818,6 +858,9 @@ export default class extends Controller { } else { showFlashMessage('error', 'Failed to fetch photos after multiple attempts'); } + } finally { + // Remove loading control after the delay + this.map.removeControl(loadingControl); } } diff --git a/app/services/immich/request_photos.rb b/app/services/immich/request_photos.rb index 78be8197..9073ad9e 100644 --- a/app/services/immich/request_photos.rb +++ b/app/services/immich/request_photos.rb @@ -12,7 +12,9 @@ class Immich::RequestPhotos end def call - retrieve_immich_data + data = retrieve_immich_data + + time_framed_data(data) end private @@ -20,11 +22,14 @@ class Immich::RequestPhotos def retrieve_immich_data page = 1 data = [] - max_pages = 100_000 # Prevent infinite loop + max_pages = 10_000 # Prevent infinite loop while page <= max_pages - body = request_body(page) - response = JSON.parse(HTTParty.post(immich_api_base_url, headers: headers, body: body).body) + response = JSON.parse( + HTTParty.post( + immich_api_base_url, headers: headers, body: request_body(page) + ).body + ) items = response.dig('assets', 'items') @@ -58,4 +63,11 @@ class Immich::RequestPhotos body.merge(createdBefore: end_date) end + + def time_framed_data(data) + data.select do |photo| + photo['localDateTime'] >= start_date && + (end_date.nil? || photo['localDateTime'] <= end_date) + end + end end