Add Sortable to pools
This commit is contained in:
parent
2b6b1f30f4
commit
239258e324
5 changed files with 141 additions and 29 deletions
|
|
@ -4,7 +4,6 @@ Sam's small image board. Currently a WIP.
|
||||||
|
|
||||||
## TODO
|
## TODO
|
||||||
|
|
||||||
- [ ] Pools
|
|
||||||
- [ ] Config
|
- [ ] Config
|
||||||
- [ ] Video support
|
- [ ] Video support
|
||||||
- [ ] Cleanup/fixup background tasks
|
- [ ] Cleanup/fixup background tasks
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ use crate::views::{
|
||||||
add_post_source, add_post_to_pool, change_pool_visibility, create_pool, delete_post,
|
add_post_source, add_post_to_pool, change_pool_visibility, create_pool, delete_post,
|
||||||
edit_post_details, get_full_media, get_media, get_pools, get_pools_page, index, login, logout,
|
edit_post_details, get_full_media, get_media, get_pools, get_pools_page, index, login, logout,
|
||||||
post_details, posts, posts_page, remove_field, remove_pool_post, search_tags, select_tag,
|
post_details, posts, posts_page, remove_field, remove_pool_post, search_tags, select_tag,
|
||||||
submit_post_details, upload, view_pool, view_post,
|
sort_pool, submit_post_details, upload, view_pool, view_post,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub(crate) const NEGATIVE_PREFIX: &str = "-";
|
pub(crate) const NEGATIVE_PREFIX: &str = "-";
|
||||||
|
|
@ -94,6 +94,7 @@ pub async fn get_router(db: DatabaseConnection, files_dir: &str) -> Result<Route
|
||||||
.route("/pool/{pool_id}", get(view_pool))
|
.route("/pool/{pool_id}", get(view_pool))
|
||||||
.route("/pool/{pool_id}/public", put(change_pool_visibility))
|
.route("/pool/{pool_id}/public", put(change_pool_visibility))
|
||||||
.route("/pool/{pool_id}/post", post(add_post_to_pool))
|
.route("/pool/{pool_id}/post", post(add_post_to_pool))
|
||||||
|
.route("/pool/{pool_id}/sort", put(sort_pool))
|
||||||
.route("/pool_post/{pool_post_id}", delete(remove_pool_post))
|
.route("/pool_post/{pool_post_id}", delete(remove_pool_post))
|
||||||
// Search routes
|
// Search routes
|
||||||
.route("/posts", get(posts))
|
.route("/posts", get(posts))
|
||||||
|
|
|
||||||
125
src/views.rs
125
src/views.rs
|
|
@ -512,9 +512,8 @@ struct ViewPoolTemplate {
|
||||||
pub(crate) async fn view_pool(
|
pub(crate) async fn view_pool(
|
||||||
State(AppState { db, .. }): State<AppState>,
|
State(AppState { db, .. }): State<AppState>,
|
||||||
auth_session: AuthSession,
|
auth_session: AuthSession,
|
||||||
Path(pool_id): Path<u32>,
|
Path(pool_id): Path<i32>,
|
||||||
) -> Result<impl IntoResponse, SameyError> {
|
) -> Result<impl IntoResponse, SameyError> {
|
||||||
let pool_id = pool_id as i32;
|
|
||||||
let pool = SameyPool::find_by_id(pool_id)
|
let pool = SameyPool::find_by_id(pool_id)
|
||||||
.one(&db)
|
.one(&db)
|
||||||
.await?
|
.await?
|
||||||
|
|
@ -551,10 +550,9 @@ pub(crate) struct ChangePoolVisibilityForm {
|
||||||
pub(crate) async fn change_pool_visibility(
|
pub(crate) async fn change_pool_visibility(
|
||||||
State(AppState { db, .. }): State<AppState>,
|
State(AppState { db, .. }): State<AppState>,
|
||||||
auth_session: AuthSession,
|
auth_session: AuthSession,
|
||||||
Path(pool_id): Path<u32>,
|
Path(pool_id): Path<i32>,
|
||||||
Form(body): Form<ChangePoolVisibilityForm>,
|
Form(body): Form<ChangePoolVisibilityForm>,
|
||||||
) -> Result<impl IntoResponse, SameyError> {
|
) -> Result<impl IntoResponse, SameyError> {
|
||||||
let pool_id = pool_id as i32;
|
|
||||||
let pool = SameyPool::find_by_id(pool_id)
|
let pool = SameyPool::find_by_id(pool_id)
|
||||||
.one(&db)
|
.one(&db)
|
||||||
.await?
|
.await?
|
||||||
|
|
@ -595,6 +593,7 @@ pub(crate) struct PoolWithMaxPosition {
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
#[template(path = "add_post_to_pool.html")]
|
#[template(path = "add_post_to_pool.html")]
|
||||||
struct AddPostToPoolTemplate {
|
struct AddPostToPoolTemplate {
|
||||||
|
pool: PoolWithMaxPosition,
|
||||||
posts: Vec<PoolPost>,
|
posts: Vec<PoolPost>,
|
||||||
can_edit: bool,
|
can_edit: bool,
|
||||||
}
|
}
|
||||||
|
|
@ -602,10 +601,10 @@ struct AddPostToPoolTemplate {
|
||||||
pub(crate) async fn add_post_to_pool(
|
pub(crate) async fn add_post_to_pool(
|
||||||
State(AppState { db, .. }): State<AppState>,
|
State(AppState { db, .. }): State<AppState>,
|
||||||
auth_session: AuthSession,
|
auth_session: AuthSession,
|
||||||
Path(pool_id): Path<u32>,
|
Path(pool_id): Path<i32>,
|
||||||
Form(body): Form<AddPostToPoolForm>,
|
Form(body): Form<AddPostToPoolForm>,
|
||||||
) -> Result<impl IntoResponse, SameyError> {
|
) -> Result<impl IntoResponse, SameyError> {
|
||||||
let pool = SameyPool::find_by_id(pool_id as i32)
|
let pool = SameyPool::find_by_id(pool_id)
|
||||||
.select_only()
|
.select_only()
|
||||||
.column(samey_pool::Column::Id)
|
.column(samey_pool::Column::Id)
|
||||||
.column(samey_pool::Column::UploaderId)
|
.column(samey_pool::Column::UploaderId)
|
||||||
|
|
@ -637,7 +636,7 @@ pub(crate) async fn add_post_to_pool(
|
||||||
SameyPoolPost::insert(samey_pool_post::ActiveModel {
|
SameyPoolPost::insert(samey_pool_post::ActiveModel {
|
||||||
pool_id: Set(pool.id),
|
pool_id: Set(pool.id),
|
||||||
post_id: Set(post.id),
|
post_id: Set(post.id),
|
||||||
position: Set(pool.max_position.unwrap_or(-1.0).floor() + 1.0),
|
position: Set(pool.max_position.unwrap_or(0.0).floor() + 1.0),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
})
|
})
|
||||||
.exec(&db)
|
.exec(&db)
|
||||||
|
|
@ -649,6 +648,7 @@ pub(crate) async fn add_post_to_pool(
|
||||||
|
|
||||||
Ok(Html(
|
Ok(Html(
|
||||||
AddPostToPoolTemplate {
|
AddPostToPoolTemplate {
|
||||||
|
pool,
|
||||||
posts,
|
posts,
|
||||||
can_edit: true,
|
can_edit: true,
|
||||||
}
|
}
|
||||||
|
|
@ -659,9 +659,8 @@ pub(crate) async fn add_post_to_pool(
|
||||||
pub(crate) async fn remove_pool_post(
|
pub(crate) async fn remove_pool_post(
|
||||||
State(AppState { db, .. }): State<AppState>,
|
State(AppState { db, .. }): State<AppState>,
|
||||||
auth_session: AuthSession,
|
auth_session: AuthSession,
|
||||||
Path(pool_post_id): Path<u32>,
|
Path(pool_post_id): Path<i32>,
|
||||||
) -> Result<impl IntoResponse, SameyError> {
|
) -> Result<impl IntoResponse, SameyError> {
|
||||||
let pool_post_id = pool_post_id as i32;
|
|
||||||
let pool_post = SameyPoolPost::find_by_id(pool_post_id)
|
let pool_post = SameyPoolPost::find_by_id(pool_post_id)
|
||||||
.one(&db)
|
.one(&db)
|
||||||
.await?
|
.await?
|
||||||
|
|
@ -685,6 +684,85 @@ pub(crate) async fn remove_pool_post(
|
||||||
Ok("")
|
Ok("")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub(crate) struct SortPoolForm {
|
||||||
|
old_index: usize,
|
||||||
|
new_index: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Template)]
|
||||||
|
#[template(path = "pool_posts.html")]
|
||||||
|
struct PoolPostsTemplate {
|
||||||
|
pool: samey_pool::Model,
|
||||||
|
posts: Vec<PoolPost>,
|
||||||
|
can_edit: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn sort_pool(
|
||||||
|
State(AppState { db, .. }): State<AppState>,
|
||||||
|
auth_session: AuthSession,
|
||||||
|
Path(pool_id): Path<i32>,
|
||||||
|
Form(body): Form<SortPoolForm>,
|
||||||
|
) -> Result<impl IntoResponse, SameyError> {
|
||||||
|
let pool = SameyPool::find_by_id(pool_id)
|
||||||
|
.one(&db)
|
||||||
|
.await?
|
||||||
|
.ok_or(SameyError::NotFound)?;
|
||||||
|
|
||||||
|
let can_edit = match auth_session.user.as_ref() {
|
||||||
|
None => false,
|
||||||
|
Some(user) => user.is_admin || pool.uploader_id == user.id,
|
||||||
|
};
|
||||||
|
|
||||||
|
if !can_edit {
|
||||||
|
return Err(SameyError::Forbidden);
|
||||||
|
}
|
||||||
|
|
||||||
|
if body.old_index != body.new_index {
|
||||||
|
let posts = get_posts_in_pool(pool_id, auth_session.user.as_ref())
|
||||||
|
.all(&db)
|
||||||
|
.await?;
|
||||||
|
let changed_post = posts.get(body.old_index).ok_or(SameyError::NotFound)?;
|
||||||
|
let min_index = if body.new_index < body.old_index {
|
||||||
|
body.new_index.checked_sub(1)
|
||||||
|
} else {
|
||||||
|
Some(body.new_index)
|
||||||
|
};
|
||||||
|
let max_index = if body.new_index == posts.len().saturating_sub(1) {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
if body.new_index < body.old_index {
|
||||||
|
Some(body.new_index)
|
||||||
|
} else {
|
||||||
|
Some(body.new_index + 1)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let min = min_index.map(|index| posts[index].position).unwrap_or(0.0);
|
||||||
|
let max = max_index
|
||||||
|
.map(|index| posts[index].position)
|
||||||
|
.unwrap_or_else(|| posts.last().map(|post| post.position).unwrap_or(min) + 2.0);
|
||||||
|
SameyPoolPost::update(samey_pool_post::ActiveModel {
|
||||||
|
id: Set(changed_post.pool_post_id),
|
||||||
|
position: Set((min + max) / 2.0),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.exec(&db)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let posts = get_posts_in_pool(pool_id, auth_session.user.as_ref())
|
||||||
|
.all(&db)
|
||||||
|
.await?;
|
||||||
|
Ok(Html(
|
||||||
|
PoolPostsTemplate {
|
||||||
|
pool,
|
||||||
|
posts,
|
||||||
|
can_edit: true,
|
||||||
|
}
|
||||||
|
.render()?,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
// Single post views
|
// Single post views
|
||||||
|
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
|
|
@ -702,9 +780,9 @@ struct ViewPostTemplate {
|
||||||
pub(crate) async fn view_post(
|
pub(crate) async fn view_post(
|
||||||
State(AppState { db, .. }): State<AppState>,
|
State(AppState { db, .. }): State<AppState>,
|
||||||
auth_session: AuthSession,
|
auth_session: AuthSession,
|
||||||
Path(post_id): Path<u32>,
|
Path(post_id): Path<i32>,
|
||||||
) -> Result<impl IntoResponse, SameyError> {
|
) -> Result<impl IntoResponse, SameyError> {
|
||||||
let post_id = post_id as i32;
|
let post_id = post_id;
|
||||||
let tags = get_tags_for_post(post_id).all(&db).await?;
|
let tags = get_tags_for_post(post_id).all(&db).await?;
|
||||||
let tags_text = tags.iter().map(|tag| &tag.name).join(" ");
|
let tags_text = tags.iter().map(|tag| &tag.name).join(" ");
|
||||||
|
|
||||||
|
|
@ -796,9 +874,8 @@ struct PostDetailsTemplate {
|
||||||
pub(crate) async fn post_details(
|
pub(crate) async fn post_details(
|
||||||
State(AppState { db, .. }): State<AppState>,
|
State(AppState { db, .. }): State<AppState>,
|
||||||
auth_session: AuthSession,
|
auth_session: AuthSession,
|
||||||
Path(post_id): Path<u32>,
|
Path(post_id): Path<i32>,
|
||||||
) -> Result<impl IntoResponse, SameyError> {
|
) -> Result<impl IntoResponse, SameyError> {
|
||||||
let post_id = post_id as i32;
|
|
||||||
let sources = SameyPostSource::find()
|
let sources = SameyPostSource::find()
|
||||||
.filter(samey_post_source::Column::PostId.eq(post_id))
|
.filter(samey_post_source::Column::PostId.eq(post_id))
|
||||||
.all(&db)
|
.all(&db)
|
||||||
|
|
@ -853,11 +930,9 @@ struct SubmitPostDetailsTemplate {
|
||||||
pub(crate) async fn submit_post_details(
|
pub(crate) async fn submit_post_details(
|
||||||
State(AppState { db, .. }): State<AppState>,
|
State(AppState { db, .. }): State<AppState>,
|
||||||
auth_session: AuthSession,
|
auth_session: AuthSession,
|
||||||
Path(post_id): Path<u32>,
|
Path(post_id): Path<i32>,
|
||||||
Form(body): Form<SubmitPostDetailsForm>,
|
Form(body): Form<SubmitPostDetailsForm>,
|
||||||
) -> Result<impl IntoResponse, SameyError> {
|
) -> Result<impl IntoResponse, SameyError> {
|
||||||
let post_id = post_id as i32;
|
|
||||||
|
|
||||||
let post = SameyPost::find_by_id(post_id)
|
let post = SameyPost::find_by_id(post_id)
|
||||||
.one(&db)
|
.one(&db)
|
||||||
.await?
|
.await?
|
||||||
|
|
@ -1000,9 +1075,9 @@ struct EditDetailsTemplate {
|
||||||
pub(crate) async fn edit_post_details(
|
pub(crate) async fn edit_post_details(
|
||||||
State(AppState { db, .. }): State<AppState>,
|
State(AppState { db, .. }): State<AppState>,
|
||||||
auth_session: AuthSession,
|
auth_session: AuthSession,
|
||||||
Path(post_id): Path<u32>,
|
Path(post_id): Path<i32>,
|
||||||
) -> Result<impl IntoResponse, SameyError> {
|
) -> Result<impl IntoResponse, SameyError> {
|
||||||
let post = SameyPost::find_by_id(post_id as i32)
|
let post = SameyPost::find_by_id(post_id)
|
||||||
.one(&db)
|
.one(&db)
|
||||||
.await?
|
.await?
|
||||||
.ok_or(SameyError::NotFound)?;
|
.ok_or(SameyError::NotFound)?;
|
||||||
|
|
@ -1026,7 +1101,7 @@ pub(crate) async fn edit_post_details(
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let tags = get_tags_for_post(post_id as i32)
|
let tags = get_tags_for_post(post_id)
|
||||||
.select_only()
|
.select_only()
|
||||||
.column(samey_tag::Column::Name)
|
.column(samey_tag::Column::Name)
|
||||||
.into_tuple::<String>()
|
.into_tuple::<String>()
|
||||||
|
|
@ -1072,9 +1147,9 @@ struct GetMediaTemplate {
|
||||||
pub(crate) async fn get_media(
|
pub(crate) async fn get_media(
|
||||||
State(AppState { db, .. }): State<AppState>,
|
State(AppState { db, .. }): State<AppState>,
|
||||||
auth_session: AuthSession,
|
auth_session: AuthSession,
|
||||||
Path(post_id): Path<u32>,
|
Path(post_id): Path<i32>,
|
||||||
) -> Result<impl IntoResponse, SameyError> {
|
) -> Result<impl IntoResponse, SameyError> {
|
||||||
let post = SameyPost::find_by_id(post_id as i32)
|
let post = SameyPost::find_by_id(post_id)
|
||||||
.one(&db)
|
.one(&db)
|
||||||
.await?
|
.await?
|
||||||
.ok_or(SameyError::NotFound)?;
|
.ok_or(SameyError::NotFound)?;
|
||||||
|
|
@ -1100,9 +1175,9 @@ struct GetFullMediaTemplate {
|
||||||
pub(crate) async fn get_full_media(
|
pub(crate) async fn get_full_media(
|
||||||
State(AppState { db, .. }): State<AppState>,
|
State(AppState { db, .. }): State<AppState>,
|
||||||
auth_session: AuthSession,
|
auth_session: AuthSession,
|
||||||
Path(post_id): Path<u32>,
|
Path(post_id): Path<i32>,
|
||||||
) -> Result<impl IntoResponse, SameyError> {
|
) -> Result<impl IntoResponse, SameyError> {
|
||||||
let post = SameyPost::find_by_id(post_id as i32)
|
let post = SameyPost::find_by_id(post_id)
|
||||||
.one(&db)
|
.one(&db)
|
||||||
.await?
|
.await?
|
||||||
.ok_or(SameyError::NotFound)?;
|
.ok_or(SameyError::NotFound)?;
|
||||||
|
|
@ -1122,9 +1197,9 @@ pub(crate) async fn get_full_media(
|
||||||
pub(crate) async fn delete_post(
|
pub(crate) async fn delete_post(
|
||||||
State(AppState { db, files_dir }): State<AppState>,
|
State(AppState { db, files_dir }): State<AppState>,
|
||||||
auth_session: AuthSession,
|
auth_session: AuthSession,
|
||||||
Path(post_id): Path<u32>,
|
Path(post_id): Path<i32>,
|
||||||
) -> Result<impl IntoResponse, SameyError> {
|
) -> Result<impl IntoResponse, SameyError> {
|
||||||
let post = SameyPost::find_by_id(post_id as i32)
|
let post = SameyPost::find_by_id(post_id)
|
||||||
.one(&db)
|
.one(&db)
|
||||||
.await?
|
.await?
|
||||||
.ok_or(SameyError::NotFound)?;
|
.ok_or(SameyError::NotFound)?;
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,36 @@
|
||||||
{% if let Some(post) = posts.first() %}
|
{% if let Some(post) = posts.first() %}
|
||||||
<meta property="og:image" content="/files/{{ post.thumbnail }}" />
|
<meta property="og:image" content="/files/{{ post.thumbnail }}" />
|
||||||
<meta property="twitter:image:src" content="/files/{{ post.thumbnail }}" />
|
<meta property="twitter:image:src" content="/files/{{ post.thumbnail }}" />
|
||||||
|
{% endif %} {% if can_edit %}
|
||||||
|
<script>
|
||||||
|
htmx.onLoad(function (content) {
|
||||||
|
var sortables = content.querySelectorAll(".sortable");
|
||||||
|
for (var i = 0; i < sortables.length; i++) {
|
||||||
|
var sortable = sortables[i];
|
||||||
|
var sortableInstance = new Sortable(sortable, {
|
||||||
|
animation: 150,
|
||||||
|
ghostClass: "blue-background-class",
|
||||||
|
|
||||||
|
// Make the `.htmx-indicator` unsortable
|
||||||
|
filter: ".htmx-indicator",
|
||||||
|
onMove: function (evt) {
|
||||||
|
return evt.related.className.indexOf("htmx-indicator") === -1;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Disable sorting on the `end` event
|
||||||
|
onEnd: function (evt) {
|
||||||
|
console.log(evt);
|
||||||
|
this.option("disabled", true);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Re-enable sorting on the `htmx:afterSwap` event
|
||||||
|
sortable.addEventListener("htmx:afterSwap", function () {
|
||||||
|
sortableInstance.option("disabled", false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,16 @@
|
||||||
{% if posts.is_empty() %}
|
{% if posts.is_empty() %}
|
||||||
<span>No posts in pool.</span>
|
<span>No posts in pool.</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
<ul>
|
<ul
|
||||||
|
class="sortable"
|
||||||
|
hx-put="/pool/{{ pool.id }}/sort"
|
||||||
|
hx-trigger="end"
|
||||||
|
hx-vals="js:{old_index: event.oldIndex, new_index: event.newIndex}"
|
||||||
|
hx-target="#pool-posts"
|
||||||
|
hx-swap="outerHTML"
|
||||||
|
>
|
||||||
{% for post in posts %}
|
{% for post in posts %}
|
||||||
<li class="pool-post" data-position="{{ post.position }}">
|
<li class="pool-post">
|
||||||
<a href="/post/{{ post.id }}" title="{{ post.tags }}">
|
<a href="/post/{{ post.id }}" title="{{ post.tags }}">
|
||||||
<img src="/files/{{ post.thumbnail }}" />
|
<img src="/files/{{ post.thumbnail }}" />
|
||||||
<div>{{ post.rating | upper }}</div>
|
<div>{{ post.rating | upper }}</div>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue