don't refresh on adding intro, swap widget

pull/11/head
Patrick Cleavelin 2023-08-05 15:21:15 -05:00
parent ff7e608f9a
commit 5da57545e2
3 changed files with 175 additions and 115 deletions

View File

@ -42,19 +42,15 @@ impl Database {
guilds guilds
} }
pub fn get_all_user_intros(&self, username: &str, guild_id: u64) -> Result<Vec<Intro>> { pub fn get_guild_intros(&self, guild_id: u64) -> Result<Vec<Intro>> {
let mut query = self.conn.prepare( let mut query = self.conn.prepare(
" "
SELECT SELECT
Intro.id, Intro.id,
Intro.name, Intro.name
UI.channel_name
FROM Intro FROM Intro
LEFT JOIN UserIntro UI ON UI.intro_id = Intro.id
WHERE WHERE
UI.username = :username Intro.guild_id = :guild_id
AND UI.guild_id = :guild_id
ORDER BY UI.channel_name DESC, UI.intro_id;
", ",
)?; )?;
@ -63,7 +59,6 @@ impl Database {
let intros = query let intros = query
.query_map( .query_map(
&[ &[
(":username", username),
// :vomit: // :vomit:
(":guild_id", &guild_id.to_string()), (":guild_id", &guild_id.to_string()),
], ],
@ -71,7 +66,6 @@ impl Database {
Ok(Intro { Ok(Intro {
id: row.get(0)?, id: row.get(0)?,
name: row.get(1)?, name: row.get(1)?,
channel_name: row.get(2)?,
}) })
}, },
)? )?
@ -81,6 +75,47 @@ impl Database {
intros intros
} }
pub fn get_all_user_intros(&self, guild_id: u64) -> Result<Vec<UserIntro>> {
let mut query = self.conn.prepare(
"
SELECT
Intro.id,
Intro.name,
UI.channel_name,
UI.username
FROM Intro
LEFT JOIN UserIntro UI ON UI.intro_id = Intro.id
WHERE
UI.guild_id = :guild_id
ORDER BY UI.username DESC, UI.channel_name DESC, UI.intro_id;
",
)?;
// NOTE(pcleavelin): for some reason this needs to be a let-binding or else
// the compiler complains about it being dropped too early (maybe I should update the compiler version)
let intros = query
.query_map(
&[
// :vomit:
(":guild_id", &guild_id.to_string()),
],
|row| {
Ok(UserIntro {
intro: Intro {
id: row.get(0)?,
name: row.get(1)?,
},
channel_name: row.get(2)?,
username: row.get(3)?,
})
},
)?
.into_iter()
.collect::<Result<Vec<UserIntro>>>();
intros
}
pub(crate) fn get_user_permissions( pub(crate) fn get_user_permissions(
&self, &self,
username: &str, username: &str,
@ -109,5 +144,10 @@ pub struct Guild {
pub struct Intro { pub struct Intro {
pub id: i32, pub id: i32,
pub name: String, pub name: String,
pub channel_name: String, }
pub struct UserIntro {
pub intro: Intro,
pub channel_name: String,
pub username: String,
} }

View File

@ -77,6 +77,7 @@ fn intro_list<'a>(
HtmxBuilder::new(Tag::Empty).form(|b| { HtmxBuilder::new(Tag::Empty).form(|b| {
b.attribute("class", "container") b.attribute("class", "container")
.hx_post(post) .hx_post(post)
.hx_target("closest #channel-intro-selector")
.attribute("hx-encoding", "multipart/form-data") .attribute("hx-encoding", "multipart/form-data")
.builder(Tag::FieldSet, |b| { .builder(Tag::FieldSet, |b| {
let mut b = b let mut b = b
@ -105,18 +106,24 @@ pub(crate) async fn guild_dashboard(
) -> Result<Html<String>, Redirect> { ) -> Result<Html<String>, Redirect> {
let db = state.db.lock().await; let db = state.db.lock().await;
let user_intros = db let guild_intros = db.get_guild_intros(guild_id).map_err(|err| {
.get_all_user_intros(&user.name, guild_id) error!(?err, %guild_id, "couldn't get guild intros");
.map_err(|err| { // TODO: change to actual error
error!(?err, user = %user.name, %guild_id, "couldn't get user's intros"); Redirect::to("/login")
// TODO: change to actual error })?;
Redirect::to("/login") let all_user_intros = db.get_all_user_intros(guild_id).map_err(|err| {
})?; error!(?err, %guild_id, "couldn't get user intros");
// TODO: change to actual error
Redirect::to("/login")
})?;
let user_permissions = db let user_permissions = db
.get_user_permissions(&user.name, guild_id) .get_user_permissions(&user.name, guild_id)
.unwrap_or_default(); .unwrap_or_default();
let channel_user_intros = user_intros.iter().group_by(|intro| &intro.channel_name); let grouped_intros = all_user_intros.iter().group_by(|intro| &intro.username);
let user_intros = grouped_intros
.into_iter()
.filter(|(username, _)| username == &&user.name);
let can_upload = user_permissions.can(auth::Permission::UploadSounds); let can_upload = user_permissions.can(auth::Permission::UploadSounds);
let is_moderator = user_permissions.can(auth::Permission::DeleteSounds); let is_moderator = user_permissions.can(auth::Permission::DeleteSounds);
@ -168,74 +175,28 @@ pub(crate) async fn guild_dashboard(
.builder(Tag::Article, |b| { .builder(Tag::Article, |b| {
let mut b = b.builder_text(Tag::Header, "Guild Intros"); let mut b = b.builder_text(Tag::Header, "Guild Intros");
for (channel_name, intros) in &channel_user_intros { for (_, intros) in user_intros {
b = b.builder(Tag::Article, |b| { for (channel_name, intros) in
b.builder_text(Tag::Header, &channel_name).builder( intros.group_by(|intro| &intro.channel_name).into_iter()
Tag::Div, {
|b| { b = b.builder(Tag::Article, |b| {
b.builder_text(Tag::Strong, "Your Current Intros") b.builder_text(Tag::Header, &channel_name).builder(
.push_builder(intro_list( Tag::Div,
intros, |b| {
"Remove Intro", b.attribute("id", "channel-intro-selector")
&format!( .push_builder(channel_intro_selector(
"{}/v2/intros/remove/{}/{}", &state.origin,
state.origin, guild_id, channel_name guild_id,
), channel_name,
)) intros.map(|intro| &intro.intro),
}, guild_intros.iter(),
) ))
}); },
)
});
}
} }
// for (channel_name, channel_settings) in &guild.channels {
// if let Some(channel_user) = channel_settings.users.get(&user.name) {
// let current_intros =
// channel_user.intros.iter().filter_map(|intro_index| {
// Some((
// &intro_index.index,
// guild.intros.get(&intro_index.index)?,
// ))
// });
// let available_intros =
// guild.intros.iter().filter_map(|intro| {
// if !channel_user
// .intros
// .iter()
// .any(|intro_index| intro.0 == &intro_index.index)
// {
// Some((intro.0, intro.1))
// } else {
// None
// }
// });
// b = b.builder(Tag::Article, |b| {
// b.builder_text(Tag::Header, channel_name).builder(
// Tag::Div,
// |b| {
// b.builder_text(Tag::Strong, "Your Current Intros")
// .push_builder(intro_list(
// current_intros,
// "Remove Intro",
// &format!(
// "{}/v2/intros/remove/{}/{}",
// state.origin, guild_id, channel_name
// ),
// ))
// .builder_text(Tag::Strong, "Select Intros")
// .push_builder(intro_list(
// available_intros,
// "Add Intro",
// &format!(
// "{}/v2/intros/add/{}/{}",
// state.origin, guild_id, channel_name
// ),
// ))
// },
// )
// });
// }
// }
b b
}) })
}) })
@ -244,6 +205,28 @@ pub(crate) async fn guild_dashboard(
)) ))
} }
pub fn channel_intro_selector<'a>(
origin: &str,
guild_id: u64,
channel_name: &String,
intros: impl Iterator<Item = &'a db::Intro>,
guild_intros: impl Iterator<Item = &'a db::Intro>,
) -> HtmxBuilder {
HtmxBuilder::new(Tag::Empty)
.builder_text(Tag::Strong, "Your Current Intros")
.push_builder(intro_list(
intros,
"Remove Intro",
&format!("{}/v2/intros/remove/{}/{}", origin, guild_id, &channel_name),
))
.builder_text(Tag::Strong, "Select Intros")
.push_builder(intro_list(
guild_intros,
"Add Intro",
&format!("{}/v2/intros/add/{}/{}", origin, guild_id, channel_name),
))
}
fn upload_form(origin: &str, guild_id: u64) -> HtmxBuilder { fn upload_form(origin: &str, guild_id: u64) -> HtmxBuilder {
HtmxBuilder::new(Tag::Empty).form(|b| { HtmxBuilder::new(Tag::Empty).form(|b| {
b.attribute("class", "container") b.attribute("class", "container")

View File

@ -4,11 +4,12 @@ use axum::{
body::Bytes, body::Bytes,
extract::{Multipart, Path, Query, State}, extract::{Multipart, Path, Query, State},
http::{HeaderMap, HeaderValue}, http::{HeaderMap, HeaderValue},
response::{IntoResponse, Redirect}, response::{Html, IntoResponse, Redirect},
Form, Json, Form, Json,
}; };
use axum_extra::extract::{cookie::Cookie, CookieJar}; use axum_extra::extract::{cookie::Cookie, CookieJar};
use iter_tools::Itertools;
use reqwest::{Proxy, StatusCode, Url}; use reqwest::{Proxy, StatusCode, Url};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::{json, Value}; use serde_json::{json, Value};
@ -17,6 +18,8 @@ use uuid::Uuid;
use crate::{ use crate::{
auth::{self, User}, auth::{self, User},
htmx::Build,
page,
settings::FileIntro, settings::FileIntro,
}; };
use crate::{ use crate::{
@ -89,6 +92,9 @@ pub(crate) enum Error {
YtdlTerminated, YtdlTerminated,
#[error("ffmpeg terminated unsuccessfully")] #[error("ffmpeg terminated unsuccessfully")]
FfmpegTerminated, FfmpegTerminated,
#[error("database error: {0}")]
Database(#[from] rusqlite::Error),
} }
impl IntoResponse for Error { impl IntoResponse for Error {
@ -111,6 +117,10 @@ impl IntoResponse for Error {
Self::YtdlTerminated | Self::FfmpegTerminated => { Self::YtdlTerminated | Self::FfmpegTerminated => {
(StatusCode::INTERNAL_SERVER_ERROR, self.to_string()).into_response() (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()).into_response()
} }
Self::Database(error) => {
(StatusCode::INTERNAL_SERVER_ERROR, error.to_string()).into_response()
}
} }
} }
} }
@ -328,45 +338,72 @@ pub(crate) async fn v2_add_intro_to_user(
Path((guild_id, channel)): Path<(u64, String)>, Path((guild_id, channel)): Path<(u64, String)>,
user: User, user: User,
mut form_data: Multipart, mut form_data: Multipart,
) -> HeaderMap { ) -> Result<Html<String>, Redirect> {
let mut headers = HeaderMap::new(); let db = state.db.lock().await;
headers.insert("HX-Refresh", HeaderValue::from_static("true"));
let mut settings = state.settings.lock().await;
let Some(guild) = settings.guilds.get_mut(&guild_id) else {
return headers;
};
let Some(channel) = guild.channels.get_mut(&channel) else {
return headers;
};
let Some(channel_user) = channel.users.get_mut(&user.name) else {
return headers;
};
while let Ok(Some(field)) = form_data.next_field().await { while let Ok(Some(field)) = form_data.next_field().await {
let Some(field_name) = field.name() else { let Some(field_name) = field.name() else {
continue; continue;
}; };
if !channel_user // TODO: insert into database
.intros //if !channel_user
.iter() // .intros
.any(|intro| intro.index == field_name) // .iter()
{ // .any(|intro| intro.index == field_name)
channel_user.intros.push(IntroIndex { //{
index: field_name.to_string(), // channel_user.intros.push(IntroIndex {
volume: 20, // index: field_name.to_string(),
}); // volume: 20,
} // });
//}
} }
// TODO: don't save on every change let guild_intros = db.get_guild_intros(guild_id).map_err(|err| {
if let Err(err) = settings.save() { error!(?err, %guild_id, "couldn't get guild intros");
error!("Failed to save config: {err:?}"); // TODO: change to actual error
} Redirect::to("/login")
})?;
let all_user_intros = db.get_all_user_intros(guild_id).map_err(|err| {
error!(?err, %guild_id, "couldn't get user intros");
// TODO: change to actual error
Redirect::to("/login")
})?;
headers let grouped_intros = all_user_intros.iter().group_by(|intro| &intro.username);
let user_intros = grouped_intros
.into_iter()
.filter_map(|(username, intro)| {
if username == &user.name {
Some(intro)
} else {
None
}
})
.flatten();
let grouped_user_intros = user_intros.group_by(|intro| &intro.channel_name);
let intros = grouped_user_intros
.into_iter()
.filter_map(|(channel_name, intros)| {
if channel_name == &channel {
Some(intros.map(|intro| &intro.intro))
} else {
None
}
})
.flatten();
Ok(Html(
page::channel_intro_selector(
&state.origin,
guild_id,
&channel,
intros,
guild_intros.iter(),
)
.build(),
))
} }
pub(crate) async fn v2_remove_intro_from_user( pub(crate) async fn v2_remove_intro_from_user(