comparison libervia/web/pages/photos/album/_browser/__init__.py @ 1518:eb00d593801d

refactoring: rename `libervia` to `libervia.web` + update imports following backend changes
author Goffi <goffi@goffi.org>
date Fri, 02 Jun 2023 16:49:28 +0200
parents libervia/pages/photos/album/_browser/__init__.py@5ea06e8b06ed
children d7c78722e4f8
comparison
equal deleted inserted replaced
1517:b8ed9726525b 1518:eb00d593801d
1 from browser import document, window, bind, html, DOMNode, aio
2 from javascript import JSON
3 from bridge import Bridge, AsyncBridge
4 from template import Template
5 import dialog
6 from slideshow import SlideShow
7 from invitation import InvitationManager
8 import alt_media_player
9 # we use tmp_aio because `blob` is not handled in Brython's aio
10 import tmp_aio
11 import loading
12
13
14 cache_path = window.cache_path
15 files_service = window.files_service
16 files_path = window.files_path
17 try:
18 affiliations = window.affiliations.to_dict()
19 except AttributeError:
20 pass
21 bridge = Bridge()
22 async_bridge = AsyncBridge()
23
24 alt_media_player.install_if_needed()
25
26 photo_tpl = Template('photo/item.html')
27 player_tpl = Template('components/media_player.html')
28
29 # file upload
30
31 def on_progress(ev, photo_elt):
32 if ev.lengthComputable:
33 percent = int(ev.loaded/ev.total*100)
34 update_progress(photo_elt, percent)
35
36
37 def on_load(file_, photo_elt):
38 update_progress(photo_elt, 100)
39 photo_elt.classList.add("progress_finished")
40 photo_elt.classList.remove("progress_started")
41 photo_elt.select_one('.action_delete').bind("click", on_delete)
42 print(f"file {file_.name} uploaded correctly")
43
44
45 def on_error(failure, file_, photo_elt):
46 dialog.notification.show(
47 f"can't upload {file_.name}: {failure}",
48 level="error"
49 )
50
51
52 def update_progress(photo_elt, new_value):
53 progress_elt = photo_elt.select_one("progress")
54 progress_elt.value = new_value
55 progress_elt.text = f"{new_value}%"
56
57
58 def on_slot_cb(file_, upload_slot, photo_elt):
59 put_url, get_url, headers = upload_slot
60 xhr = window.XMLHttpRequest.new()
61 xhr.open("PUT", put_url, True)
62 xhr.upload.bind('progress', lambda ev: on_progress(ev, photo_elt))
63 xhr.upload.bind('load', lambda ev: on_load(file_, photo_elt))
64 xhr.upload.bind('error', lambda ev: on_error(xhr.response, file_, photo_elt))
65 xhr.setRequestHeader('Xmpp-File-Path', window.encodeURIComponent(files_path))
66 xhr.setRequestHeader('Xmpp-File-No-Http', "true")
67 xhr.send(file_)
68
69
70 def on_slot_eb(file_, failure, photo_elt):
71 dialog.notification.show(
72 f"Can't get upload slot: {failure['message']}",
73 level="error"
74 )
75 photo_elt.remove()
76
77
78 def upload_files(files):
79 print(f"uploading {len(files)} files")
80 album_items = document['album_items']
81 for file_ in files:
82 url = window.URL.createObjectURL(file_)
83 photo_elt = photo_tpl.get_elt({
84 "file": {
85 "name": file_.name,
86 # we don't want to open the file on click, it's not yet the
87 # uploaded URL
88 "url": url,
89 # we have no thumb yet, so we use the whole image
90 # TODO: reduce image for preview
91 "thumb_url": url,
92 },
93 "uploading": True,
94 })
95 photo_elt.classList.add("progress_started")
96 album_items <= photo_elt
97
98 bridge.file_http_upload_get_slot(
99 file_.name,
100 file_.size,
101 file_.type or '',
102 files_service,
103 callback=lambda upload_slot, file_=file_, photo_elt=photo_elt:
104 on_slot_cb(file_, upload_slot, photo_elt),
105 errback=lambda failure, file_=file_, photo_elt=photo_elt:
106 on_slot_eb(file_, failure, photo_elt),
107 )
108
109
110 @bind("#file_drop", "drop")
111 def on_file_select(evt):
112 evt.stopPropagation()
113 evt.preventDefault()
114 files = evt.dataTransfer.files
115 upload_files(files)
116
117
118 @bind("#file_drop", "dragover")
119 def on_drag_over(evt):
120 evt.stopPropagation()
121 evt.preventDefault()
122 evt.dataTransfer.dropEffect = 'copy'
123
124
125 @bind("#file_input", "change")
126 def on_file_input_change(evt):
127 files = evt.currentTarget.files
128 upload_files(files)
129
130 # delete
131
132 def file_delete_cb(item_elt, item):
133 item_elt.classList.add("state_deleted")
134 item_elt.bind("transitionend", lambda evt: item_elt.remove())
135 print(f"deleted {item['name']}")
136
137
138 def file_delete_eb(failure, item_elt, item):
139 dialog.notification.show(
140 f"error while deleting {item['name']}: failure",
141 level="error"
142 )
143
144
145 def delete_ok(evt, notif_elt, item_elt, item):
146 file_path = f"{files_path.rstrip('/')}/{item['name']}"
147 bridge.file_sharing_delete(
148 files_service,
149 file_path,
150 "",
151 callback=lambda : file_delete_cb(item_elt, item),
152 errback=lambda failure: file_delete_eb(failure, item_elt, item),
153 )
154
155
156 def delete_cancel(evt, notif_elt, item_elt, item):
157 notif_elt.remove()
158 item_elt.classList.remove("selected_for_deletion")
159
160
161 def on_delete(evt):
162 evt.stopPropagation()
163 target = evt.currentTarget
164 item_elt = DOMNode(target.closest('.item'))
165 item_elt.classList.add("selected_for_deletion")
166 item = JSON.parse(item_elt.dataset.item)
167 dialog.Confirm(
168 f"{item['name']!r} will be deleted, are you sure?",
169 ok_label="delete",
170 ok_color="danger",
171 ).show(
172 ok_cb=lambda evt, notif_elt: delete_ok(evt, notif_elt, item_elt, item),
173 cancel_cb=lambda evt, notif_elt: delete_cancel(evt, notif_elt, item_elt, item),
174 )
175
176 # cover
177
178 async def cover_ok(evt, notif_elt, item_elt, item):
179 # we first need to get a blob of the image
180 img_elt = item_elt.select_one("img")
181 # the simplest way is to download it
182 r = await tmp_aio.ajax("GET", img_elt.src, "blob")
183 if r.status != 200:
184 dialog.notification.show(
185 f"can't retrieve cover: {r.status}: {r.statusText}",
186 level="error"
187 )
188 return
189 img_blob = r.response
190 # now we'll upload it via HTTP Upload, we need a slow
191 img_name = img_elt.src.rsplit('/', 1)[-1]
192 img_size = img_blob.size
193
194 slot = await async_bridge.file_http_upload_get_slot(
195 img_name,
196 img_size,
197 '',
198 files_service
199 )
200 get_url, put_url, headers = slot
201 # we have the slot, we can upload image
202 r = await tmp_aio.ajax("PUT", put_url, "", img_blob)
203 if r.status != 201:
204 dialog.notification.show(
205 f"can't upload cover: {r.status}: {r.statusText}",
206 level="error"
207 )
208 return
209 extra = {"thumb_url": get_url}
210 album_name = files_path.rsplit('/', 1)[-1]
211 await async_bridge.interests_file_sharing_register(
212 files_service,
213 "photos",
214 "",
215 files_path,
216 album_name,
217 JSON.stringify(extra),
218 )
219 dialog.notification.show("Album cover has been changed")
220
221
222 def cover_cancel(evt, notif_elt, item_elt, item):
223 notif_elt.remove()
224 item_elt.classList.remove("selected_for_action")
225
226
227 def on_cover(evt):
228 evt.stopPropagation()
229 target = evt.currentTarget
230 item_elt = DOMNode(target.closest('.item'))
231 item_elt.classList.add("selected_for_action")
232 item = JSON.parse(item_elt.dataset.item)
233 dialog.Confirm(
234 f"use {item['name']!r} for this album cover?",
235 ok_label="use as cover",
236 ).show(
237 ok_cb=lambda evt, notif_elt: aio.run(cover_ok(evt, notif_elt, item_elt, item)),
238 cancel_cb=lambda evt, notif_elt: cover_cancel(evt, notif_elt, item_elt, item),
239 )
240
241
242 # slideshow
243
244 @bind(".photo_thumb_click", "click")
245 def photo_click(evt):
246 evt.stopPropagation()
247 evt.preventDefault()
248 slideshow = SlideShow()
249 target = evt.currentTarget
250 clicked_item_elt = DOMNode(target.closest('.item'))
251
252 slideshow.attach()
253 for idx, item_elt in enumerate(document.select('.item')):
254 item = JSON.parse(item_elt.dataset.item)
255 try:
256 biggest_thumb = item['extra']['thumbnails'][-1]
257 thumb_url = f"{cache_path}{biggest_thumb['filename']}"
258 except (KeyError, IndexError) as e:
259 print(f"Can't get full screen thumbnail URL: {e}")
260 thumb_url = None
261 if item.get("mime_type", "")[:5] == "video":
262 player = alt_media_player.MediaPlayer(
263 [item['url']],
264 poster = thumb_url,
265 reduce_click_area = True
266 )
267 elt = player.elt
268 elt.classList.add("slide_video", "no_fullscreen")
269 slideshow.add_slide(
270 elt,
271 item,
272 options={
273 "flags": (alt_media_player.NO_PAGINATION, alt_media_player.NO_SCROLLBAR),
274 "exit_callback": player.reset,
275 }
276 )
277 else:
278 slideshow.add_slide(html.IMG(src=thumb_url or item['url'], Class="slide_img"), item)
279 if item_elt == clicked_item_elt:
280 slideshow.index = idx
281
282
283 for elt in document.select('.action_delete'):
284 elt.bind("click", on_delete)
285 for elt in document.select('.action_cover'):
286 elt.bind("click", on_cover)
287
288 # manage
289
290
291 @bind("#button_manage", "click")
292 def manage_click(evt):
293 evt.stopPropagation()
294 evt.preventDefault()
295 manager = InvitationManager("photos", {"service": files_service, "path": files_path})
296 manager.attach(affiliations=affiliations)
297
298
299 # hint
300 @bind("#hint .click_to_delete", "click")
301 def remove_hint(evt):
302 document['hint'].remove()
303
304
305 loading.remove_loading_screen()