Mercurial > libervia-web
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() |