view libervia/pages/photos/album/_browser/__init__.py @ 1330:b525fdcb393b

browser (photos/album): used biggest thumbnails instead of original image for slideshow: biggest thumbnail should be adapted to fullscreen slideshow, and it's better to use than original image which may have a size of several megabytes.
author Goffi <goffi@goffi.org>
date Fri, 14 Aug 2020 09:31:32 +0200
parents 0cbf86b1dcca
children fe353fceec38
line wrap: on
line source

from browser import document, window, bind, html, DOMNode, timer
from javascript import JSON
from bridge import Bridge
from template import Template
import dialog
from slideshow import SlideShow

files_service = window.files_service
files_path = window.files_path
bridge = Bridge()

# 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):
    # TODO: cleaner error notification
    window.alert(f"can't upload {file_.name}: failure")


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):
    breakpoint()


def upload_files(files):
    print(f"uploading {len(files)} files")
    photo_tpl = Template('photo/item.html')
    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.fileHTTPUploadGetSlot(
            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):
    # TODO: cleaner error notification
    window.alert(f"error while deleting {item['name']}: failure")


def delete_ok(evt, notif_elt, item_elt, item):
    file_path = f"{files_path.rstrip('/')}/{item['name']}"
    bridge.fileSharingDelete(
        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.elt.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",
    ).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),
    )

# slideshow

@bind(".photo_thumb_click", "click")
def photo_click(evt):
    evt.stopPropagation()
    evt.preventDefault()
    slideshow = SlideShow()
    target = evt.currentTarget
    clicked_item_elt = DOMNode(target.elt.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 = item['url']
        slideshow.add_slide(html.IMG(src=thumb_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)

# manage

def on_manager_close(manager_panel_elt):
    side_panel = manager_panel_elt.select_one('.invitation_manager_side_panel')
    side_panel.classList.remove('open')
    side_panel.bind("transitionend", lambda evt: manager_panel_elt.remove())

def _on_invitation_cb(field_elt, entity):
    print(f"invitation for {entity!r} sent successfully")
    submit_elt = document['invitation_submit']
    submit_elt.disabled = False
    form_elt = document['invitation_form']
    form_elt.disabled = False
    jids_elt = form_elt.select_one('*[name="jids"]')
    emails_elt = form_elt.select_one('*[name="emails"]')
    new = [d.strip() for d in field_elt.value.split('\n') if d and d.strip() != entity]
    field_elt.value = '\n'.join(new)
    if not jids_elt.value.strip() and not emails_elt.value.strip():
        # FIXME: Q&D notification, needs to do this properly in a separated module with
        #   some animations
        notifs_elt = document['invitation_notifications']
        notification_elt = html.DIV(Class="notification is-success has-text-centered")
        notification_elt <= "invitations sent successfully"
        notifs_elt <= notification_elt
        timer.set_timeout(lambda: notification_elt.remove(), 5000)


def invitationSimpleCreateCb(invitation_data, email):
    invitee_jid = invitation_data['jid']
    album_name = files_path.rsplit('/')[-1]
    form_elt = document['invitation_form']
    emails_elt = form_elt.select_one('*[name="emails"]')
    bridge.FISInvite(
        invitee_jid,
        files_service,
        "photos",
        "",
        files_path,
        album_name,
        '',
        callback=lambda: _on_invitation_cb(emails_elt, email),
        errback=lambda e: window.alert(f"invitation failed for {email}: {e}")
    )


def invite_by_email(email):
    guest_url_tpl = f'{window.URL.new("/g", document.baseURI).href}/{{uuid}}'
    bridge.invitationSimpleCreate(
        email,
        guest_url_tpl,
        '',
        callback=lambda data: invitationSimpleCreateCb(data, email),
        errback=lambda e: window.alert(f"can't send email invitation: {e}")
    )


def on_invitation_submit(evt, manager_panel_elt):
    evt.stopPropagation()
    evt.preventDefault()
    submit_elt = document['invitation_submit']
    submit_elt.disabled = True
    form_elt = document['invitation_form']
    form_elt.disabled = True
    jids_elt = form_elt.select_one('*[name="jids"]')
    emails_elt = form_elt.select_one('*[name="emails"]')
    jids = [j.strip() for j in jids_elt.value.split('\n') if j.strip()]
    emails = [e.strip() for e in emails_elt.value.split('\n') if e.strip()]
    album_name = files_path.rsplit('/')[-1]
    for entity_jid in jids:
        print(f"inviting {entity_jid}")
        bridge.FISInvite(
            entity_jid,
            files_service,
            "photos",
            "",
            files_path,
            album_name,
            '',
            callback=lambda entity=entity_jid: _on_invitation_cb(jids_elt, entity),
            errback=lambda e: window.alert(f"invitation failed: {e}")
        )

    for email in emails:
        invite_by_email(email)

    print(f"{jids=}, {emails=}")


def _FISAffiliationSetCb(affiliation_elt):
    affiliation_elt.remove()


def on_affiliation_remove(entity_jid, affiliation_elt):
    bridge.FISAffiliationsSet(
        files_service,
        "",
        files_path,
        {entity_jid: "none"},
        callback=lambda: _FISAffiliationSetCb(affiliation_elt),
        errback=lambda e: window.alert(f"can't remove affiliation: {e}")
    )


@bind("#button_manage", "click")
def manage_click(evt):
    evt.stopPropagation()
    evt.preventDefault()
    manager_panel_tpl = Template('invitation/manager.html')
    manager_panel_elt = manager_panel_tpl.get_elt()
    document.body <= manager_panel_elt
    document['invitation_submit'].bind(
        "click", lambda evt: on_invitation_submit(evt, manager_panel_elt))
    side_panel = manager_panel_elt.select_one('.invitation_manager_side_panel')
    timer.set_timeout(lambda: side_panel.classList.add("open"), 0)
    for close_elt in manager_panel_elt.select('.click_to_close'):
        close_elt.bind("click", lambda evt: on_manager_close(manager_panel_elt))
    side_panel.bind("click", lambda evt: evt.stopPropagation())
    affiliations = window.affiliations.to_dict()
    affiliation_tpl = Template('invitation/affiliation_item.html')
    for entity_jid, affiliation in affiliations.items():
        affiliation_elt = affiliation_tpl.get_elt({
            "entity_jid": entity_jid,
            "affiliation": affiliation,
        })
        document['affiliations'] <= affiliation_elt
        for elt in affiliation_elt.select(".click_to_delete"):
            elt.bind(
                "click",
                lambda evt,
                       entity_jid=entity_jid,
                       affiliation_elt=affiliation_elt: on_affiliation_remove(
                    entity_jid,
                    affiliation_elt
                )
            )