-
Notifications
You must be signed in to change notification settings - Fork 5
/
Copy pathrenderer.ts
227 lines (210 loc) · 9.8 KB
/
renderer.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
import { moment, Notice } from 'obsidian'
import GooglePhotos from './main'
import { Moment } from 'moment'
import { GooglePhotosMediaItem, GooglePhotosSearchParams } from 'photosApi'
export class ThumbnailImage extends Image {
photoId: string
baseUrl: string
productUrl: string
filename: string
description?: string
creationTime: Moment
}
type ThumbnailClick = (event: MouseEvent) => Promise<void>
export default class Renderer {
plugin: GooglePhotos
thumbnailWidth: number
thumbnailHeight: number
spinner: HTMLElement
constructor (plugin: GooglePhotos) {
this.plugin = plugin
this.thumbnailWidth = this.plugin.settings.thumbnailWidth
this.thumbnailHeight = this.plugin.settings.thumbnailHeight
// Create a nice Google-themed loading spinner
this.spinner = document.createElement('div')
this.spinner.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" style="margin: auto; background: none; display: block; shape-rendering: auto;" width="161px" height="161px" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid">
<circle cx="84" cy="50" r="10" fill="#c5523f"><animate attributeName="r" repeatCount="indefinite" dur="0.4807692307692307s" calcMode="spline" keyTimes="0;1" values="10;0" keySplines="0 0.5 0.5 1" begin="0s"></animate><animate attributeName="fill" repeatCount="indefinite" dur="1.923076923076923s" calcMode="discrete" keyTimes="0;0.25;0.5;0.75;1" values="#c5523f;#1875e5;#499255;#f2b736;#c5523f" begin="0s"></animate></circle>
<circle cx="16" cy="50" r="10" fill="#c5523f"><animate attributeName="r" repeatCount="indefinite" dur="1.923076923076923s" calcMode="spline" keyTimes="0;0.25;0.5;0.75;1" values="0;0;10;10;10" keySplines="0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1" begin="0s"></animate><animate attributeName="cx" repeatCount="indefinite" dur="1.923076923076923s" calcMode="spline" keyTimes="0;0.25;0.5;0.75;1" values="16;16;16;50;84" keySplines="0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1" begin="0s"></animate></circle>
<circle cx="50" cy="50" r="10" fill="#f2b736"><animate attributeName="r" repeatCount="indefinite" dur="1.923076923076923s" calcMode="spline" keyTimes="0;0.25;0.5;0.75;1" values="0;0;10;10;10" keySplines="0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1" begin="-0.4807692307692307s"></animate><animate attributeName="cx" repeatCount="indefinite" dur="1.923076923076923s" calcMode="spline" keyTimes="0;0.25;0.5;0.75;1" values="16;16;16;50;84" keySplines="0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1" begin="-0.4807692307692307s"></animate></circle>
<circle cx="84" cy="50" r="10" fill="#499255"><animate attributeName="r" repeatCount="indefinite" dur="1.923076923076923s" calcMode="spline" keyTimes="0;0.25;0.5;0.75;1" values="0;0;10;10;10" keySplines="0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1" begin="-0.9615384615384615s"></animate><animate attributeName="cx" repeatCount="indefinite" dur="1.923076923076923s" calcMode="spline" keyTimes="0;0.25;0.5;0.75;1" values="16;16;16;50;84" keySplines="0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1" begin="-0.9615384615384615s"></animate></circle>
<circle cx="16" cy="50" r="10" fill="#1875e5"><animate attributeName="r" repeatCount="indefinite" dur="1.923076923076923s" calcMode="spline" keyTimes="0;0.25;0.5;0.75;1" values="0;0;10;10;10" keySplines="0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1" begin="-1.4423076923076923s"></animate><animate attributeName="cx" repeatCount="indefinite" dur="1.923076923076923s" calcMode="spline" keyTimes="0;0.25;0.5;0.75;1" values="16;16;16;50;84" keySplines="0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1" begin="-1.4423076923076923s"></animate></circle></svg>`
this.spinner.style.display = 'none'
this.spinner.style.transform = 'scale(0.5)'
}
isVisible (el: HTMLElement) {
return new Promise(resolve => {
const o = new IntersectionObserver(([entry]) => {
resolve(entry.intersectionRatio > 0.3)
o.disconnect()
})
o.observe(el)
})
}
/**
* Append an array of mediaItems to an HTML element
* @param {HTMLElement} el
* @param {array} thumbnails
* @param {function} onclick
*/
appendThumbnailsToElement (el: HTMLElement, thumbnails: GooglePhotosMediaItem[], onclick: (event: MouseEvent) => void) {
(thumbnails || []).forEach((mediaItem: GooglePhotosMediaItem) => {
// Image element
const img = new ThumbnailImage()
const settings = this.plugin.settings
img.src = `${mediaItem.baseUrl}=w${this.thumbnailWidth}-h${this.thumbnailHeight}`
img.photoId = mediaItem.id
img.baseUrl = mediaItem.baseUrl
img.productUrl = mediaItem.productUrl
img.description = mediaItem.description // Optional caption
img.creationTime = moment(mediaItem.mediaMetadata.creationTime)
img.filename = img.creationTime.format(settings.filename)
img.onclick = onclick
img.classList.add('google-photos-grid-thumbnail')
// Output to Obsidian
el.appendChild(img)
})
}
}
export class GridView extends Renderer {
scrollEl: HTMLElement
containerEl: HTMLElement
gridEl: HTMLElement
title: string
searchParams: GooglePhotosSearchParams = {}
plugin: GooglePhotos
onThumbnailClick: ThumbnailClick
nextPageToken: string
fetching = false
moreResults = true
active = true
constructor ({ scrollEl, plugin, onThumbnailClick, title }: {
plugin: GooglePhotos,
scrollEl?: HTMLElement,
onThumbnailClick?: ThumbnailClick,
title?: string
}) {
super(plugin)
if (onThumbnailClick) {
// Add an event handler if provided
this.onThumbnailClick = onThumbnailClick
}
// Add the photo-grid container
this.containerEl = document.createElement('div')
if (title) {
const titleEl = this.containerEl.createEl('div')
titleEl.className = 'google-photos-album-title'
titleEl.innerText = title
}
this.gridEl = this.containerEl.createEl('div')
// Add the loading spinner
this.containerEl.appendChild(this.spinner)
this.spinner.style.display = 'block'
// Watch for a scroll event
this.scrollEl = scrollEl || this.containerEl
this.scrollEl.addEventListener('scroll', () => this.getThumbnails())
}
/**
* Reset the photo-grid view to a blank state
*/
async resetGrid () {
this.spinner.style.display = 'block'
const oldGrid = this.gridEl
oldGrid.empty()
this.gridEl = document.createElement('div')
this.containerEl.replaceChild(this.gridEl, oldGrid)
this.active = true
this.fetching = false
this.nextPageToken = ''
this.moreResults = true
}
setTitle (title: string) {
this.title = title
}
setSearchParams (searchParams: GooglePhotosSearchParams) {
this.searchParams = searchParams
}
clearSearchParams () {
this.searchParams = {}
}
/**
* Load more thumbnails if both of these checks are true:
* 1. There are more results to return from Google Photos API
* 2. The scrolled height is within 8 thumbnails height from the bottom
*
* @returns {Promise<void>}
*/
getThumbnails = async (): Promise<void> => {
if (this.fetching) {
// An instance is already in the process of fetching more thumbnails
return
}
this.fetching = true
const targetEl = this.gridEl
/*
While:
+ the active flag is enabled, and there are more results to fetch from Photos API, and there is a scrollable element
+ the user is within 5 thumbnails distance from the bottom of the scrollable element
+ the scrollable element is visible in the viewport OR we have not yet loaded any thumbnails
*/
while (
this.active && this.moreResults && this.scrollEl &&
this.scrollEl.scrollHeight - this.scrollEl.scrollTop < this.scrollEl.clientHeight + (5 * this.thumbnailHeight) &&
(!targetEl.innerHTML || await this.isVisible(this.scrollEl)) // Element is visible in the viewport
) {
// Perform the search with Photos API and output the result
try {
const localOptions = Object.assign({}, this.searchParams)
if (this.nextPageToken) Object.assign(localOptions, { pageToken: this.nextPageToken })
const searchResult = await this.plugin.photosApi.mediaItemsSearch(localOptions)
if (searchResult.mediaItems) {
this.appendThumbnailsToElement(targetEl, searchResult.mediaItems, event => this.onThumbnailClick(event))
} else if (!targetEl.childElementCount) {
targetEl.createEl('p', {
text: 'No photos found for this query.'
})
}
this.moreResults = !!searchResult.nextPageToken
if (this.moreResults) {
this.spinner.style.display = 'block'
} else {
// Remove the loading spinner after a short timeout, to give thumbnails a chance to load
setTimeout(() => {
this.spinner.style.display = 'none'
}, targetEl.childElementCount ? 1000 : 0)
}
this.nextPageToken = searchResult.nextPageToken
} catch (e) {
// Unable to fetch results from Photos API
console.log(e)
if (e === 'Retry') {
// Re-authenticated, so try getting the thumbnails again
console.log('Google Photos: Retrying authentication')
} else {
// Unable to authenticate or process the query
this.moreResults = false
this.nextPageToken = ''
if (e === 'Unauthenticated') {
this.active = false
new Notice('Failed to authenticate')
} else {
// Add the error message to the photos grid
targetEl.createEl('p', {
text: e
})
}
this.spinner.style.display = 'none'
break
}
}
}
this.fetching = false
}
destroy () {
try {
this.scrollEl.removeEventListener('scroll', () => this.getThumbnails())
} catch (e) {
// nothing
}
this.active = false
}
}