make hole sidebar/banner uploading and deleting not require page reload

master
Aevann 2024-03-06 05:31:45 +02:00
parent f2b26e3cc3
commit 4f57533d2c
3 changed files with 126 additions and 62 deletions

View File

@ -0,0 +1,58 @@
function upload_banner(t, hole_name) {
postToast(t,
`/h/${hole_name}/settings/banners`,
{
"image": t.files[0]
},
(xhr) => {
const list = document.getElementById('hole-banners')
const url = JSON.parse(xhr.response)['url']
const html = `
<section class="mt-5 d-block hole-settings-subsection">
<img class="mr-3" loading="lazy" alt="/h/${hole_name} banner" src="${url}">
<button class="btn btn-danger hole-banner-delete-button mt-2" data-nonce="${nonce}" data-onclick="areyousure(this)" data-areyousure="delete_image(this, '/h/${hole_name}/settings/banners/delete')">Delete</button>
</section>`
list.insertAdjacentHTML('afterbegin', html);
register_new_elements(list);
const nobanners = document.getElementById('hole-banner-no-banners')
if (nobanners) nobanners.remove()
}
);
}
function upload_sidebar(t, hole_name) {
postToast(t,
`/h/${hole_name}/settings/sidebars`,
{
"image": t.files[0]
},
(xhr) => {
const list = document.getElementById('hole-sidebars')
const url = JSON.parse(xhr.response)['url']
const html = `
<section class="mt-5 d-block hole-settings-subsection">
<img class="mr-3" loading="lazy" alt="/h/${hole_name} sidebar" src="${url}">
<button class="btn btn-danger hole-sidebar-delete-button mt-2" data-nonce="${nonce}" data-onclick="areyousure(this)" data-areyousure="delete_image(this, '/h/${hole_name}/settings/sidebars/delete')">Delete</button>
</section>`
list.insertAdjacentHTML('afterbegin', html);
register_new_elements(list);
const nosidebars = document.getElementById('hole-sidebar-no-sidebars')
if (nosidebars) nosidebars.remove()
}
);
}
function delete_image(t, url) {
postToast(t, url,
{
"url": t.previousElementSibling.src
},
() => {
t.parentElement.remove();
}
);
}

View File

@ -537,7 +537,7 @@ def get_hole_css(hole):
resp.headers.add("Content-Type", "text/css") resp.headers.add("Content-Type", "text/css")
return resp return resp
@app.post("/h/<hole>/settings/sidebars/") @app.post("/h/<hole>/settings/sidebars")
@limiter.limit('1/second', scope=rpath) @limiter.limit('1/second', scope=rpath)
@limiter.limit('1/second', scope=rpath, key_func=get_ID) @limiter.limit('1/second', scope=rpath, key_func=get_ID)
@limiter.limit("50/day", deduct_when=lambda response: response.status_code < 400) @limiter.limit("50/day", deduct_when=lambda response: response.status_code < 400)
@ -550,7 +550,7 @@ def upload_hole_sidebar(v, hole):
if not v.mods_hole(hole.name): abort(403) if not v.mods_hole(hole.name): abort(403)
if v.shadowbanned: abort(500) if v.shadowbanned: abort(500)
file = request.files["sidebar"] file = request.files["image"]
name = f'/images/{time.time()}'.replace('.','') + '.webp' name = f'/images/{time.time()}'.replace('.','') + '.webp'
file.save(name) file.save(name)
@ -568,26 +568,28 @@ def upload_hole_sidebar(v, hole):
) )
g.db.add(ma) g.db.add(ma)
return redirect(f'/h/{hole}/settings') return {"message": "Sidebar image uploaded successfully!", "url": sidebarurl}
@app.post("/h/<hole>/settings/sidebars/delete/<int:index>") @app.post("/h/<hole>/settings/sidebars/delete")
@limiter.limit("1/second", deduct_when=lambda response: response.status_code < 400) @limiter.limit("1/second", deduct_when=lambda response: response.status_code < 400)
@limiter.limit("1/second", deduct_when=lambda response: response.status_code < 400, key_func=get_ID) @limiter.limit("1/second", deduct_when=lambda response: response.status_code < 400, key_func=get_ID)
@auth_required @auth_required
def delete_hole_sidebar(v, hole, index): def delete_hole_sidebar(v, hole):
hole = get_hole(hole) hole = get_hole(hole)
if not v.mods_hole(hole.name): abort(403) if not v.mods_hole(hole.name): abort(403)
if not hole.sidebarurls: if not hole.sidebarurls:
abort(404, f"Sidebar image not found (/h/{hole.name} has no sidebar images)") abort(404, f"Sidebar image not found (/h/{hole.name} has no sidebar images)")
if index < 0 or index >= len(hole.sidebarurls):
abort(404, f'Sidebar image not found (sidebar index {index} is not between 0 and {len(hole.sidebarurls)})') sidebar = request.values["url"]
sidebar = hole.sidebarurls[index]
try: if sidebar not in hole.sidebarurls:
remove_media_using_link(sidebar) abort(404, "Sidebar image not found!")
except FileNotFoundError:
pass try: remove_media_using_link(sidebar)
del hole.sidebarurls[index] except FileNotFoundError: pass
hole.sidebarurls.remove(sidebar)
g.db.add(hole) g.db.add(hole)
ma = HoleAction( ma = HoleAction(
@ -598,7 +600,7 @@ def delete_hole_sidebar(v, hole, index):
) )
g.db.add(ma) g.db.add(ma)
return {"message": f"Deleted sidebar {index} from /h/{hole} successfully"} return {"message": "Sidebar image deleted successfully!"}
@app.post("/h/<hole>/settings/sidebars/delete_all") @app.post("/h/<hole>/settings/sidebars/delete_all")
@limiter.limit("1/10 second;30/day", deduct_when=lambda response: response.status_code < 400) @limiter.limit("1/10 second;30/day", deduct_when=lambda response: response.status_code < 400)
@ -626,7 +628,7 @@ def delete_all_hole_sidebars(v, hole):
return {"message": f"Deleted all sidebar images from /h/{hole} successfully"} return {"message": f"Deleted all sidebar images from /h/{hole} successfully"}
@app.post("/h/<hole>/settings/banners/") @app.post("/h/<hole>/settings/banners")
@limiter.limit('1/second', scope=rpath) @limiter.limit('1/second', scope=rpath)
@limiter.limit('1/second', scope=rpath, key_func=get_ID) @limiter.limit('1/second', scope=rpath, key_func=get_ID)
@limiter.limit("50/day", deduct_when=lambda response: response.status_code < 400) @limiter.limit("50/day", deduct_when=lambda response: response.status_code < 400)
@ -639,7 +641,7 @@ def upload_hole_banner(v, hole):
if not v.mods_hole(hole.name): abort(403) if not v.mods_hole(hole.name): abort(403)
if v.shadowbanned: abort(500) if v.shadowbanned: abort(500)
file = request.files["banner"] file = request.files["image"]
name = f'/images/{time.time()}'.replace('.','') + '.webp' name = f'/images/{time.time()}'.replace('.','') + '.webp'
file.save(name) file.save(name)
@ -657,37 +659,39 @@ def upload_hole_banner(v, hole):
) )
g.db.add(ma) g.db.add(ma)
return redirect(f'/h/{hole}/settings') return {"message": "Banner uploaded successfully!", "url": bannerurl}
@app.post("/h/<hole>/settings/banners/delete/<int:index>") @app.post("/h/<hole>/settings/banners/delete")
@limiter.limit("1/second", deduct_when=lambda response: response.status_code < 400) @limiter.limit("1/second", deduct_when=lambda response: response.status_code < 400)
@limiter.limit("1/second", deduct_when=lambda response: response.status_code < 400, key_func=get_ID) @limiter.limit("1/second", deduct_when=lambda response: response.status_code < 400, key_func=get_ID)
@auth_required @auth_required
def delete_hole_banner(v, hole, index): def delete_hole_banner(v, hole):
hole = get_hole(hole) hole = get_hole(hole)
if not v.mods_hole(hole.name): abort(403) if not v.mods_hole(hole.name): abort(403)
if not hole.bannerurls: if not hole.bannerurls:
abort(404, f"Banner not found (/h/{hole.name} has no banners)") abort(404, f"Banner not found (/h/{hole.name} has no banner images)")
if index < 0 or index >= len(hole.bannerurls):
abort(404, f'Banner not found (banner index {index} is not between 0 and {len(hole.bannerurls)})') banner = request.values["url"]
banner = hole.bannerurls[index]
try: if banner not in hole.bannerurls:
remove_media_using_link(banner) abort(404, "Banner not found!")
except FileNotFoundError:
pass try: remove_media_using_link(banner)
del hole.bannerurls[index] except FileNotFoundError: pass
hole.bannerurls.remove(banner)
g.db.add(hole) g.db.add(hole)
ma = HoleAction( ma = HoleAction(
hole=hole.name, hole=hole.name,
kind='delete_banner', kind='delete_banner_image',
_note=f'<a href="{banner}">{banner}</a>', _note=f'<a href="{banner}">{banner}</a>',
user_id=v.id user_id=v.id
) )
g.db.add(ma) g.db.add(ma)
return {"message": f"Deleted banner {index} from /h/{hole} successfully"} return {"message": "Banner deleted successfully!"}
@app.post("/h/<hole>/settings/banners/delete_all") @app.post("/h/<hole>/settings/banners/delete_all")
@limiter.limit("1/10 second;30/day", deduct_when=lambda response: response.status_code < 400) @limiter.limit("1/10 second;30/day", deduct_when=lambda response: response.status_code < 400)

View File

@ -99,56 +99,58 @@
<h4 class="mb-4 pb-2">Banners</h4> <h4 class="mb-4 pb-2">Banners</h4>
{% if not g.is_tor %} {% if not g.is_tor %}
<section id="hole-banner-upload-new" class="mb-5 hole-settings-subsection"> <section id="hole-banner-upload-new" class="mb-5 hole-settings-subsection">
<form class="d-inline-block" action="/h/{{hole.name}}/settings/banners/" method="post" enctype="multipart/form-data"> <input hidden name="formkey" value="{{v|formkey}}" class="notranslate" translate="no">
<input hidden name="formkey" value="{{v|formkey}}" class="notranslate" translate="no"> <label class="btn btn-secondary text-capitalize mr-2 mb-0">
<label class="btn btn-secondary text-capitalize mr-2 mb-0"> Upload New Banner<input id="upload_banner" autocomplete="off" type="file" accept="image/*" hidden name="banner" data-nonce="{{g.nonce}}" data-onchange="upload_banner(this, '{{hole.name}}')">
Upload New Banner<input autocomplete="off" type="file" accept="image/*" hidden name="banner" data-nonce="{{g.nonce}}" onchange_submit> </label>
</label>
</form>
<button type="button" class="btn btn-danger hole-banner-delete-button" id="hole-banner-delete-all" data-nonce="{{g.nonce}}" data-onclick="areyousure(this)" data-areyousure="postToastReload(this, '/h/{{hole.name}}/settings/banners/delete_all')">Delete All Banners</button> <button type="button" class="btn btn-danger hole-banner-delete-button" id="hole-banner-delete-all" data-nonce="{{g.nonce}}" data-onclick="areyousure(this)" data-areyousure="postToastReload(this, '/h/{{hole.name}}/settings/banners/delete_all')">Delete All Banners</button>
<div class="text-small text-muted mt-3"> <div class="text-small text-muted mt-3">
All image files are supported. Max file size is {% if v and v.patron %}16{% else %}8{% endif %} MB. All image files are supported. Max file size is {% if v and v.patron %}16{% else %}8{% endif %} MB.
</div> </div>
</section> </section>
{% endif %} {% endif %}
{% for banner in hole.bannerurls %} <div id="hole-banners">
<section id="hole-banner-update-{{loop.index - 1}}" class="mt-5 d-block hole-settings-subsection"> {% for banner in hole.bannerurls %}
<img class="mr-3" loading="lazy" alt="/h/{{hole.name}} banner" src="{{banner}}"> <section class="mt-5 d-block hole-settings-subsection">
<button class="btn btn-danger hole-banner-delete-button mt-2" id="hole-banner-delete-{{loop.index}}" data-nonce="{{g.nonce}}" data-onclick="areyousure(this)" data-areyousure="postToastReload(this, '/h/{{hole.name}}/settings/banners/delete/{{loop.index - 1}}')">Delete</button> <img class="mr-3" loading="lazy" alt="/h/{{hole.name}} banner" src="{{banner}}">
</section> <button class="btn btn-danger hole-banner-delete-button mt-2" data-nonce="{{g.nonce}}" data-onclick="areyousure(this)" data-areyousure="delete_image(this, '/h/{{hole.name}}/settings/banners/delete')">Delete</button>
{% else %} </section>
<section id="hole-banner-no-banners" class="d-block hole-settings-subsection"> {% else %}
{{macros.ghost_box("No banners uploaded", "", 2, "flex:1")}} <section id="hole-banner-no-banners" class="d-block hole-settings-subsection">
</section> {{macros.ghost_box("No banners uploaded", "", 2, "flex:1")}}
{% endfor %} </section>
{% endfor %}
</div>
</div> </div>
<div class="hole-settings-section hole-sidebar-update-section"> <div class="hole-settings-section hole-sidebar-update-section">
<h4 class="mb-4 pb-2">Sidebar Images</h4> <h4 class="mb-4 pb-2">Sidebar Images</h4>
{% if not g.is_tor %} {% if not g.is_tor %}
<section id="hole-sidebar-upload-new" class="mb-5 hole-settings-subsection"> <section id="hole-sidebar-upload-new" class="mb-5 hole-settings-subsection">
<form class="d-inline-block" action="/h/{{hole.name}}/settings/sidebars/" method="post" enctype="multipart/form-data"> <input hidden name="formkey" value="{{v|formkey}}" class="notranslate" translate="no">
<input hidden name="formkey" value="{{v|formkey}}" class="notranslate" translate="no"> <label class="btn btn-secondary text-capitalize mr-2 mb-0">
<label class="btn btn-secondary text-capitalize mr-2 mb-0"> Upload New Sidebar Image<input id="upload_sidebar" autocomplete="off" type="file" accept="image/*" hidden name="sidebar" data-nonce="{{g.nonce}}" data-onchange="upload_sidebar(this, '{{hole.name}}')">
Upload New Sidebar Image<input autocomplete="off" type="file" accept="image/*" hidden name="sidebar" data-nonce="{{g.nonce}}" onchange_submit> </label>
</label>
</form>
<button type="button" class="btn btn-danger hole-sidebar-delete-button" id="hole-sidebar-delete-all" data-nonce="{{g.nonce}}" data-onclick="areyousure(this)" data-areyousure="postToastReload(this, '/h/{{hole.name}}/settings/sidebars/delete_all')">Delete All Sidebar Images</button> <button type="button" class="btn btn-danger hole-sidebar-delete-button" id="hole-sidebar-delete-all" data-nonce="{{g.nonce}}" data-onclick="areyousure(this)" data-areyousure="postToastReload(this, '/h/{{hole.name}}/settings/sidebars/delete_all')">Delete All Sidebar Images</button>
<div class="text-small text-muted mt-3"> <div class="text-small text-muted mt-3">
All image files are supported. Max file size is {% if v and v.patron %}16{% else %}8{% endif %} MB. All image files are supported. Max file size is {% if v and v.patron %}16{% else %}8{% endif %} MB.
</div> </div>
</section> </section>
{% endif %} {% endif %}
{% for sidebar in hole.sidebarurls %} <div id="hole-sidebars">
<section id="hole-sidebar-update-{{loop.index - 1}}" class="mt-5 d-block hole-settings-subsection"> {% for sidebar in hole.sidebarurls %}
<img class="mr-3" loading="lazy" alt="/h/{{hole.name}} sidebar" src="{{sidebar}}"> <section class="mt-5 d-block hole-settings-subsection">
<button class="btn btn-danger hole-sidebar-delete-button mt-2" id="hole-sidebar-delete-{{loop.index}}" data-nonce="{{g.nonce}}" data-onclick="areyousure(this)" data-areyousure="postToastReload(this, '/h/{{hole.name}}/settings/sidebars/delete/{{loop.index - 1}}')">Delete</button> <img class="mr-3" loading="lazy" alt="/h/{{hole.name}} sidebar" src="{{sidebar}}">
</section> <button class="btn btn-danger hole-sidebar-delete-button mt-2" data-nonce="{{g.nonce}}" data-onclick="areyousure(this)" data-areyousure="delete_image(this, '/h/{{hole.name}}/settings/sidebars/delete')">Delete</button>
{% else %} </section>
<section id="hole-sidebar-no-sidebars" class="d-block hole-settings-subsection"> {% else %}
{{macros.ghost_box("No sidebar images uploaded", "", 2, "flex:1")}} <section id="hole-sidebar-no-sidebars" class="d-block hole-settings-subsection">
</section> {{macros.ghost_box("No sidebar images uploaded", "", 2, "flex:1")}}
{% endfor %} </section>
{% endfor %}
</div>
</div> </div>
</div> </div>
<script defer src="{{'js/hole_settings.js' | asset}}"></script>
{% endblock %} {% endblock %}