diff 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
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libervia/web/pages/photos/album/_browser/__init__.py	Fri Jun 02 16:49:28 2023 +0200
@@ -0,0 +1,305 @@
+from browser import document, window, bind, html, DOMNode, aio
+from javascript import JSON
+from bridge import Bridge, AsyncBridge
+from template import Template
+import dialog
+from slideshow import SlideShow
+from invitation import InvitationManager
+import alt_media_player
+# we use tmp_aio because `blob` is not handled in Brython's aio
+import tmp_aio
+import loading
+
+
+cache_path = window.cache_path
+files_service = window.files_service
+files_path = window.files_path
+try:
+    affiliations = window.affiliations.to_dict()
+except AttributeError:
+    pass
+bridge = Bridge()
+async_bridge = AsyncBridge()
+
+alt_media_player.install_if_needed()
+
+photo_tpl = Template('photo/item.html')
+player_tpl = Template('components/media_player.html')
+
+# file upload
+
+def on_progress(ev, photo_elt):
+    if ev.lengthComputable:
+        percent = int(ev.loaded/ev.total*100)
+        update_progress(photo_elt, percent)
+
+
+def on_load(file_, photo_elt):
+    update_progress(photo_elt, 100)
+    photo_elt.classList.add("progress_finished")
+    photo_elt.classList.remove("progress_started")
+    photo_elt.select_one('.action_delete').bind("click", on_delete)
+    print(f"file {file_.name} uploaded correctly")
+
+
+def on_error(failure, file_, photo_elt):
+    dialog.notification.show(
+        f"can't upload {file_.name}: {failure}",
+        level="error"
+    )
+
+
+def update_progress(photo_elt, new_value):
+    progress_elt = photo_elt.select_one("progress")
+    progress_elt.value = new_value
+    progress_elt.text = f"{new_value}%"
+
+
+def on_slot_cb(file_, upload_slot, photo_elt):
+    put_url, get_url, headers = upload_slot
+    xhr = window.XMLHttpRequest.new()
+    xhr.open("PUT", put_url, True)
+    xhr.upload.bind('progress', lambda ev: on_progress(ev, photo_elt))
+    xhr.upload.bind('load', lambda ev: on_load(file_, photo_elt))
+    xhr.upload.bind('error', lambda ev: on_error(xhr.response, file_, photo_elt))
+    xhr.setRequestHeader('Xmpp-File-Path', window.encodeURIComponent(files_path))
+    xhr.setRequestHeader('Xmpp-File-No-Http', "true")
+    xhr.send(file_)
+
+
+def on_slot_eb(file_, failure, photo_elt):
+    dialog.notification.show(
+        f"Can't get upload slot: {failure['message']}",
+        level="error"
+    )
+    photo_elt.remove()
+
+
+def upload_files(files):
+    print(f"uploading {len(files)} files")
+    album_items = document['album_items']
+    for file_ in files:
+        url = window.URL.createObjectURL(file_)
+        photo_elt = photo_tpl.get_elt({
+            "file": {
+                "name": file_.name,
+                # we don't want to open the file on click, it's not yet the
+                # uploaded URL
+                "url": url,
+                # we have no thumb yet, so we use the whole image
+                # TODO: reduce image for preview
+                "thumb_url": url,
+            },
+            "uploading": True,
+        })
+        photo_elt.classList.add("progress_started")
+        album_items <= photo_elt
+
+        bridge.file_http_upload_get_slot(
+            file_.name,
+            file_.size,
+            file_.type or '',
+            files_service,
+            callback=lambda upload_slot, file_=file_, photo_elt=photo_elt:
+                on_slot_cb(file_, upload_slot, photo_elt),
+            errback=lambda failure, file_=file_, photo_elt=photo_elt:
+                on_slot_eb(file_, failure, photo_elt),
+        )
+
+
+@bind("#file_drop", "drop")
+def on_file_select(evt):
+    evt.stopPropagation()
+    evt.preventDefault()
+    files = evt.dataTransfer.files
+    upload_files(files)
+
+
+@bind("#file_drop", "dragover")
+def on_drag_over(evt):
+    evt.stopPropagation()
+    evt.preventDefault()
+    evt.dataTransfer.dropEffect = 'copy'
+
+
+@bind("#file_input", "change")
+def on_file_input_change(evt):
+    files = evt.currentTarget.files
+    upload_files(files)
+
+# delete
+
+def file_delete_cb(item_elt, item):
+    item_elt.classList.add("state_deleted")
+    item_elt.bind("transitionend", lambda evt: item_elt.remove())
+    print(f"deleted {item['name']}")
+
+
+def file_delete_eb(failure, item_elt, item):
+    dialog.notification.show(
+        f"error while deleting {item['name']}: failure",
+        level="error"
+    )
+
+
+def delete_ok(evt, notif_elt, item_elt, item):
+    file_path = f"{files_path.rstrip('/')}/{item['name']}"
+    bridge.file_sharing_delete(
+        files_service,
+        file_path,
+        "",
+        callback=lambda : file_delete_cb(item_elt, item),
+        errback=lambda failure: file_delete_eb(failure, item_elt, item),
+    )
+
+
+def delete_cancel(evt, notif_elt, item_elt, item):
+    notif_elt.remove()
+    item_elt.classList.remove("selected_for_deletion")
+
+
+def on_delete(evt):
+    evt.stopPropagation()
+    target = evt.currentTarget
+    item_elt = DOMNode(target.closest('.item'))
+    item_elt.classList.add("selected_for_deletion")
+    item = JSON.parse(item_elt.dataset.item)
+    dialog.Confirm(
+        f"{item['name']!r} will be deleted, are you sure?",
+        ok_label="delete",
+        ok_color="danger",
+    ).show(
+        ok_cb=lambda evt, notif_elt: delete_ok(evt, notif_elt, item_elt, item),
+        cancel_cb=lambda evt, notif_elt: delete_cancel(evt, notif_elt, item_elt, item),
+    )
+
+# cover
+
+async def cover_ok(evt, notif_elt, item_elt, item):
+    # we first need to get a blob of the image
+    img_elt = item_elt.select_one("img")
+    # the simplest way is to download it
+    r = await tmp_aio.ajax("GET", img_elt.src, "blob")
+    if r.status != 200:
+        dialog.notification.show(
+            f"can't retrieve cover: {r.status}: {r.statusText}",
+            level="error"
+        )
+        return
+    img_blob = r.response
+    # now we'll upload it via HTTP Upload, we need a slow
+    img_name = img_elt.src.rsplit('/', 1)[-1]
+    img_size = img_blob.size
+
+    slot = await async_bridge.file_http_upload_get_slot(
+        img_name,
+        img_size,
+        '',
+        files_service
+    )
+    get_url, put_url, headers = slot
+    # we have the slot, we can upload image
+    r = await tmp_aio.ajax("PUT", put_url, "", img_blob)
+    if r.status != 201:
+        dialog.notification.show(
+            f"can't upload cover: {r.status}: {r.statusText}",
+            level="error"
+        )
+        return
+    extra = {"thumb_url": get_url}
+    album_name = files_path.rsplit('/', 1)[-1]
+    await async_bridge.interests_file_sharing_register(
+        files_service,
+        "photos",
+        "",
+        files_path,
+        album_name,
+        JSON.stringify(extra),
+    )
+    dialog.notification.show("Album cover has been changed")
+
+
+def cover_cancel(evt, notif_elt, item_elt, item):
+    notif_elt.remove()
+    item_elt.classList.remove("selected_for_action")
+
+
+def on_cover(evt):
+    evt.stopPropagation()
+    target = evt.currentTarget
+    item_elt = DOMNode(target.closest('.item'))
+    item_elt.classList.add("selected_for_action")
+    item = JSON.parse(item_elt.dataset.item)
+    dialog.Confirm(
+        f"use {item['name']!r} for this album cover?",
+        ok_label="use as cover",
+    ).show(
+        ok_cb=lambda evt, notif_elt: aio.run(cover_ok(evt, notif_elt, item_elt, item)),
+        cancel_cb=lambda evt, notif_elt: cover_cancel(evt, notif_elt, item_elt, item),
+    )
+
+
+# slideshow
+
+@bind(".photo_thumb_click", "click")
+def photo_click(evt):
+    evt.stopPropagation()
+    evt.preventDefault()
+    slideshow = SlideShow()
+    target = evt.currentTarget
+    clicked_item_elt = DOMNode(target.closest('.item'))
+
+    slideshow.attach()
+    for idx, item_elt in enumerate(document.select('.item')):
+        item = JSON.parse(item_elt.dataset.item)
+        try:
+            biggest_thumb = item['extra']['thumbnails'][-1]
+            thumb_url = f"{cache_path}{biggest_thumb['filename']}"
+        except (KeyError, IndexError) as e:
+            print(f"Can't get full screen thumbnail URL: {e}")
+            thumb_url = None
+        if item.get("mime_type", "")[:5] == "video":
+            player = alt_media_player.MediaPlayer(
+                [item['url']],
+                poster = thumb_url,
+                reduce_click_area =  True
+            )
+            elt = player.elt
+            elt.classList.add("slide_video", "no_fullscreen")
+            slideshow.add_slide(
+                elt,
+                item,
+                options={
+                    "flags": (alt_media_player.NO_PAGINATION, alt_media_player.NO_SCROLLBAR),
+                    "exit_callback": player.reset,
+                }
+            )
+        else:
+            slideshow.add_slide(html.IMG(src=thumb_url or item['url'], Class="slide_img"), item)
+        if item_elt == clicked_item_elt:
+            slideshow.index = idx
+
+
+for elt in document.select('.action_delete'):
+    elt.bind("click", on_delete)
+for elt in document.select('.action_cover'):
+    elt.bind("click", on_cover)
+
+# manage
+
+
+@bind("#button_manage", "click")
+def manage_click(evt):
+    evt.stopPropagation()
+    evt.preventDefault()
+    manager = InvitationManager("photos", {"service": files_service, "path": files_path})
+    manager.attach(affiliations=affiliations)
+
+
+# hint
+@bind("#hint .click_to_delete", "click")
+def remove_hint(evt):
+    document['hint'].remove()
+
+
+loading.remove_loading_screen()