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"] members = [".", "migration"]
[dependencies] [dependencies]
askama = "0.13.0" askama = { version = "0.13.0", features = ["serde_json"] }
async-trait = "0.1.88" async-trait = "0.1.88"
axum = { version = "0.8.3", features = ["multipart", "macros"] } axum = { version = "0.8.3", features = ["multipart", "macros"] }
axum-extra = { version = "0.10.1", features = ["form"] } axum-extra = { version = "0.10.1", features = ["form"] }

View file

@ -4,8 +4,6 @@ Sam's small image board. Currently a WIP.
## TODO ## TODO
- [ ] Parent posts (including tags and stuff)
- [ ] Edit tags in the middle of input
- [ ] Pools - [ ] Pools
- [ ] Video support - [ ] Video support
- [ ] Cleanup/fixup background tasks - [ ] 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)), post(upload).layer(DefaultBodyLimit::max(100_000_000)),
) )
.route("/search_tags", post(search_tags)) .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", get(posts))
.route("/posts/{page}", get(posts_page)) .route("/posts/{page}", get(posts_page))
.route("/view/{post_id}", get(view_post)) .route("/view/{post_id}", get(view_post))

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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