Edit tags in the middle of input

This commit is contained in:
Bad Manners 2025-04-09 20:33:26 -03:00
parent 54379b98e0
commit fe7edb93ad
9 changed files with 42 additions and 19 deletions

View file

@ -11,7 +11,7 @@ readme = "README.md"
members = [".", "migration"]
[dependencies]
askama = "0.13.0"
askama = { version = "0.13.0", features = ["serde_json"] }
async-trait = "0.1.88"
axum = { version = "0.8.3", features = ["multipart", "macros"] }
axum-extra = { version = "0.10.1", features = ["form"] }

View file

@ -4,8 +4,6 @@ Sam's small image board. Currently a WIP.
## TODO
- [ ] Parent posts (including tags and stuff)
- [ ] Edit tags in the middle of input
- [ ] Pools
- [ ] Video support
- [ ] Cleanup/fixup background tasks

View file

@ -74,7 +74,7 @@ pub async fn get_router(db: DatabaseConnection, files_dir: &str) -> Result<Route
post(upload).layer(DefaultBodyLimit::max(100_000_000)),
)
.route("/search_tags", post(search_tags))
.route("/select_tag/{new_tag}", post(select_tag))
.route("/select_tag", post(select_tag))
.route("/posts", get(posts))
.route("/posts/{page}", get(posts_page))
.route("/view/{post_id}", get(view_post))

View file

@ -235,20 +235,25 @@ struct SearchTag {
#[template(path = "search_tags.html")]
struct SearchTagsTemplate {
tags: Vec<SearchTag>,
selection_end: usize,
}
#[derive(Debug, Deserialize)]
pub(crate) struct SearchTagsForm {
tags: String,
selection_end: usize,
}
pub(crate) async fn search_tags(
State(AppState { db, .. }): State<AppState>,
Form(body): Form<SearchTagsForm>,
) -> Result<impl IntoResponse, SameyError> {
let tags = match body.tags.split(' ').last() {
Some(tag) => {
if tag.starts_with(NEGATIVE_PREFIX) {
let tags = match body.tags[..body.selection_end].split(' ').last() {
Some(mut tag) => {
tag = tag.trim();
if tag.is_empty() {
vec![]
} else if tag.starts_with(NEGATIVE_PREFIX) {
if tag[NEGATIVE_PREFIX.len()..].starts_with(RATING_PREFIX) {
[
format!("{}u", RATING_PREFIX),
@ -269,6 +274,7 @@ pub(crate) async fn search_tags(
"LOWER(\"samey_tag\".\"name\") LIKE CONCAT(?, '%')",
tag[NEGATIVE_PREFIX.len()..].to_lowercase(),
))
.limit(10)
.all(&db)
.await?
.into_iter()
@ -298,6 +304,7 @@ pub(crate) async fn search_tags(
"LOWER(\"samey_tag\".\"name\") LIKE CONCAT(?, '%')",
tag.to_lowercase(),
))
.limit(10)
.all(&db)
.await?
.into_iter()
@ -310,7 +317,13 @@ pub(crate) async fn search_tags(
}
_ => vec![],
};
Ok(Html(SearchTagsTemplate { tags }.render()?))
Ok(Html(
SearchTagsTemplate {
tags,
selection_end: body.selection_end,
}
.render()?,
))
}
#[derive(Template)]
@ -322,14 +335,15 @@ struct SelectTagTemplate {
#[derive(Debug, Deserialize)]
pub(crate) struct SelectTagForm {
tags: String,
new_tag: String,
selection_end: usize,
}
pub(crate) async fn select_tag(
Path(new_tag): Path<String>,
Form(body): Form<SelectTagForm>,
) -> Result<impl IntoResponse, SameyError> {
let mut tags = String::new();
for (tag, _) in body.tags.split_whitespace().tuple_windows() {
for (tag, _) in body.tags[..body.selection_end].split(' ').tuple_windows() {
if !tags.is_empty() {
tags.push(' ');
}
@ -338,7 +352,13 @@ pub(crate) async fn select_tag(
if !tags.is_empty() {
tags.push(' ');
}
tags.push_str(&new_tag);
tags.push_str(&body.new_tag);
for tag in body.tags[body.selection_end..].split(' ') {
if !tags.is_empty() {
tags.push(' ');
}
tags.push_str(tag);
}
tags.push(' ');
Ok(Html(SelectTagTemplate { tags }.render()?))
}

View file

@ -17,8 +17,9 @@
id="search-tags"
name="tags"
hx-post="/search_tags"
hx-trigger="input changed delay:400ms"
hx-trigger="input changed"
hx-target="next .tags-autocomplete"
hx-vals="js:{selection_end: event.target.selectionEnd}"
hx-on::after-settle="this.focus(); this.setSelectionRange(-1, -1);"
autofocus
/>

View file

@ -16,10 +16,11 @@
id="search-tags"
name="tags"
hx-post="/search_tags"
hx-trigger="input changed delay:400ms"
hx-trigger="input changed"
hx-target="next .tags-autocomplete"
hx-vals="js:{selection_end: event.target.selectionEnd}"
hx-on::after-settle="this.focus(); this.setSelectionRange(-1, -1);"
value="{% if let Some(tags_text) = tags_text %}{{ tags_text }}{% endif %}"
autofocus
/>
<ul class="tags-autocomplete" id="search-autocomplete"></ul>
<button type="submit">Search</button>
@ -46,7 +47,7 @@
title="{{ post.tags }}"
>
<img src="/files/{{ post.thumbnail }}" />
<div>{{ post.rating }}</div>
<div>{{ post.rating | upper }}</div>
</a>
</li>
{% endfor %}

View file

@ -1,9 +1,10 @@
{% for tag in tags %}
<li>
<button
hx-post="/select_tag/{{ tag.value }}"
hx-post="/select_tag"
hx-target="previous .tags"
hx-swap="outerHTML"
hx-vals='{"selection_end": {{ selection_end }}, "new_tag": {{ tag.value | json | safe }}}'
>
{{ tag.name }}
</button>

View file

@ -1,12 +1,14 @@
<input
class="tags"
type="text"
id="search-tags"
name="tags"
hx-post="/search_tags"
hx-trigger="input changed delay:400ms"
hx-trigger="input changed"
hx-target="next .tags-autocomplete"
hx-swap="innerHTML"
hx-vals="js:{selection_end: event.target.selectionEnd}"
hx-on::after-settle="this.focus(); this.setSelectionRange(-1, -1);"
value="{{ tags }}"
autofocus
/>
<ul class="tags-autocomplete" hx-swap-oob="outerHTML:.tags-autocomplete"></ul>

View file

@ -52,7 +52,7 @@
<li>
<a href="/view/{{ child_post.id }}" title="{{ child_post.tags }}">
<img src="/files/{{ child_post.thumbnail }}" />
<div>{{ child_post.rating }}</div>
<div>{{ child_post.rating | upper }}</div>
</a>
</li>
{% endfor %}