WIP: improve search #1
|
@ -16,13 +16,13 @@ use actix_web::{middleware, web, App, HttpResponse, HttpServer};
|
|||
use failure::Error;
|
||||
use r2d2_sqlite::SqliteConnectionManager;
|
||||
use rusqlite::params;
|
||||
use std::{env, cmp, io, fmt};
|
||||
use std::{env, cmp, io};
|
||||
use std::ops::Deref;
|
||||
use serde_json::Value;
|
||||
|
||||
const DEFAULT_SIZE: usize = 25;
|
||||
|
||||
#[derive(Copy, Clone, Deserialize)]
|
||||
#[derive(Copy, Clone, Debug, Deserialize)]
|
||||
enum SortKey {
|
||||
Name,
|
||||
Size,
|
||||
|
@ -37,20 +37,7 @@ impl Default for SortKey {
|
|||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for SortKey {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
let s = match &self {
|
||||
Self::Name => "name",
|
||||
Self::Size => "size_bytes",
|
||||
Self::Seeders => "seeders",
|
||||
Self::Leechers => "leechers",
|
||||
Self::Scraped => "scraped_date",
|
||||
};
|
||||
write!(f, "{}", s)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Deserialize)]
|
||||
#[derive(Copy, Clone, Debug, Deserialize)]
|
||||
enum SortDirection {
|
||||
Asc,
|
||||
Desc,
|
||||
|
@ -62,16 +49,6 @@ impl Default for SortDirection {
|
|||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for SortDirection {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
let s = match &self {
|
||||
Self::Asc => "asc",
|
||||
Self::Desc => "desc",
|
||||
};
|
||||
write!(f, "{}", s)
|
||||
}
|
||||
}
|
||||
|
||||
#[actix_rt::main]
|
||||
async fn main() -> io::Result<()> {
|
||||
println!("Access me at {}", endpoint());
|
||||
|
@ -140,6 +117,8 @@ async fn search(
|
|||
Ok(res)
|
||||
}
|
||||
|
||||
// TODO what is NewQuery used for?
|
||||
// it is not a file search, that is done through SearchQuery
|
||||
#[derive(Deserialize)]
|
||||
struct NewQuery {
|
||||
page: Option<usize>,
|
||||
|
@ -167,6 +146,29 @@ async fn new_torrents(
|
|||
Ok(res)
|
||||
}
|
||||
|
||||
fn build_order_clause(type_: &str, sort_key: SortKey, sort_dir: SortDirection) -> String {
|
||||
let dir = match sort_dir {
|
||||
SortDirection::Asc => "asc",
|
||||
SortDirection::Desc => "desc",
|
||||
};
|
||||
|
||||
let column = match sort_key {
|
||||
SortKey::Name => {
|
||||
if type_ == "file" {
|
||||
"path"
|
||||
} else {
|
||||
"name"
|
||||
}
|
||||
},
|
||||
SortKey::Size => "size_bytes",
|
||||
SortKey::Seeders => "seeders",
|
||||
SortKey::Leechers => "leechers",
|
||||
SortKey::Scraped => "scraped_date",
|
||||
};
|
||||
|
||||
format!("{} {}", column, dir)
|
||||
}
|
||||
|
||||
fn search_query(
|
||||
query: web::Query<SearchQuery>,
|
||||
conn: r2d2::PooledConnection<SqliteConnectionManager>,
|
||||
|
@ -185,7 +187,7 @@ fn search_query(
|
|||
let offset = size * (page - 1);
|
||||
|
||||
println!(
|
||||
"query = {}, type = {}, page = {}, size = {}, sort_key = {}, sort_dir = {}",
|
||||
"query = {}, type = {}, page = {}, size = {}, sort_key = {:?}, sort_dir = {:?}",
|
||||
q, type_, page, size, sort_key, sort_dir
|
||||
);
|
||||
|
||||
|
@ -213,7 +215,7 @@ fn new_query(
|
|||
let offset = size * (page - 1);
|
||||
|
||||
println!(
|
||||
"new, type = {}, page = {}, size = {}, sort_key = {}, sort_dir = {}",
|
||||
"new, type = {}, page = {}, size = {}, sort_key = {:?}, sort_dir = {:?}",
|
||||
type_, page, size, sort_key, sort_dir
|
||||
);
|
||||
|
||||
|
@ -248,8 +250,9 @@ fn torrent_search(
|
|||
size: usize,
|
||||
offset: usize,
|
||||
) -> Result<Vec<Torrent>, Error> {
|
||||
// `sort_key` and `sort_dir` are already sanitized and should not be escaped:
|
||||
let stmt_str = format!("select * from torrents where name like '%' || ?1 || '%' order by {} {} limit ?2, ?3", sort_key, sort_dir);
|
||||
let order_clause = build_order_clause("torrent", sort_key, sort_dir);
|
||||
// `order_clause` is already sanitized and should not be escaped:
|
||||
let stmt_str = format!("select * from torrents where name like '%' || ?1 || '%' order by {} limit ?2, ?3", order_clause);
|
||||
let mut stmt = conn.prepare(&stmt_str)?;
|
||||
let torrent_iter = stmt.query_map(
|
||||
params![
|
||||
|
@ -334,8 +337,9 @@ fn torrent_file_search(
|
|||
size: usize,
|
||||
offset: usize,
|
||||
) -> Result<Vec<File>, Error> {
|
||||
// `sort_key` and `sort_dir` are already sanitized and should not be escaped:
|
||||
let stmt_str = format!("select * from files where path like '%' || ?1 || '%' order by {} {} limit ?2, ?3", sort_key, sort_dir);
|
||||
let order_clause = build_order_clause("file", sort_key, sort_dir);
|
||||
// `order_clause` is already sanitized and should not be escaped:
|
||||
let stmt_str = format!("select * from files where path like '%' || ?1 || '%' order by {} limit ?2, ?3", order_clause);
|
||||
let mut stmt = conn.prepare(&stmt_str).unwrap();
|
||||
let file_iter = stmt.query_map(
|
||||
params![
|
||||
|
|
|
@ -8,7 +8,6 @@ interface State {
|
|||
}
|
||||
|
||||
export class Navbar extends Component<any, State> {
|
||||
|
||||
state: State = {
|
||||
searchParams: {
|
||||
page: 1,
|
||||
|
@ -80,20 +79,20 @@ export class Navbar extends Component<any, State> {
|
|||
let searchParams: SearchParams = {
|
||||
q: event.target.value,
|
||||
page: 1,
|
||||
sort_key: 'Name',
|
||||
sort_dir: 'Desc',
|
||||
sort_key: i.state.searchParams.sort_key,
|
||||
sort_dir: i.state.searchParams.sort_dir,
|
||||
type_: i.state.searchParams.type_
|
||||
}
|
||||
i.setState({ searchParams: searchParams });
|
||||
}
|
||||
|
||||
// TODO why does switching from torrent to file search clear the sort_key?
|
||||
searchTypeChange(i: Navbar, event) {
|
||||
console.log("in searchTypeChange");
|
||||
let searchParams: SearchParams = {
|
||||
q: i.state.searchParams.q,
|
||||
page: 1,
|
||||
sort_key: 'Name',
|
||||
sort_dir: 'Desc',
|
||||
sort_key: i.state.searchParams.sort_key,
|
||||
sort_dir: i.state.searchParams.sort_dir,
|
||||
type_: event.target.value
|
||||
}
|
||||
i.setState({ searchParams: searchParams });
|
||||
|
|
|
@ -11,39 +11,6 @@ interface State {
|
|||
searching: Boolean;
|
||||
}
|
||||
|
||||
class SortableLink extends Component {
|
||||
render({ sortKey, state: { searchParams } }) {
|
||||
return <span>
|
||||
<a href={this.buildSearchURL(sortKey, searchParams)}>
|
||||
{sortKey}
|
||||
{this.renderIcon(sortKey, searchParams)}
|
||||
</a>
|
||||
</span>;
|
||||
}
|
||||
|
||||
buildSearchURL(key, searchParams) {
|
||||
let page, direction;
|
||||
if (key === searchParams.sort_key) {
|
||||
page = searchParams.page;
|
||||
direction = searchParams.sort_dir === "Asc" ? "Desc" : "Asc";
|
||||
} else {
|
||||
page = 1;
|
||||
direction = "Desc";
|
||||
}
|
||||
return `/#/search/${searchParams.type_}/${searchParams.q}/${page}/${key}/${direction}`;
|
||||
}
|
||||
|
||||
renderIcon(sortKey, searchParams) {
|
||||
if (searchParams.sort_key !== sortKey) {
|
||||
return "";
|
||||
}
|
||||
if (searchParams.sort_dir === "Asc") {
|
||||
return <svg class="icon icon-arrow-up d-none d-sm-inline mr-1"><use xlinkHref="#icon-arrow-up"></use></svg>;
|
||||
}
|
||||
return <svg class="icon icon-arrow-down mr-1"><use xlinkHref="#icon-arrow-down"></use></svg>;
|
||||
}
|
||||
)
|
||||
|
||||
export class Search extends Component<any, State> {
|
||||
state: State = {
|
||||
results: {
|
||||
|
@ -64,6 +31,7 @@ export class Search extends Component<any, State> {
|
|||
}
|
||||
|
||||
componentDidMount() {
|
||||
console.log("comp did mount");
|
||||
this.state.searchParams = {
|
||||
page: Number(this.props.match.params.page),
|
||||
q: this.props.match.params.q,
|
||||
|
@ -76,6 +44,7 @@ export class Search extends Component<any, State> {
|
|||
|
||||
// Re-do search if the props have changed
|
||||
componentDidUpdate(lastProps: any) {
|
||||
console.log("comp did update");
|
||||
if (lastProps.match && lastProps.match.params !== this.props.match.params) {
|
||||
this.state.searchParams = {
|
||||
page: Number(this.props.match.params.page),
|
||||
|
@ -144,6 +113,48 @@ export class Search extends Component<any, State> {
|
|||
)
|
||||
}
|
||||
|
||||
sortLink(sortKey) {
|
||||
return (
|
||||
<span>
|
||||
<a href={this.buildSortURL(sortKey)}>
|
||||
{sortKey}
|
||||
{this.sortLinkIcon(sortKey)}
|
||||
</a>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
buildSortURL(sortKey) {
|
||||
const searchParams = this.state.searchParams;
|
||||
let page, sortDir;
|
||||
if (sortKey === searchParams.sort_key) {
|
||||
page = searchParams.page;
|
||||
sortDir = searchParams.sort_dir === "Asc" ? "Desc" : "Asc";
|
||||
} else {
|
||||
page = 1;
|
||||
sortDir = "Desc";
|
||||
}
|
||||
return `/#/search/${searchParams.type_}/${searchParams.q}/${page}/${sortKey}/${sortDir}`;
|
||||
}
|
||||
|
||||
sortLinkIcon(sortKey) {
|
||||
const searchParams = this.state.searchParams;
|
||||
|
||||
if (searchParams.sort_key !== sortKey) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (searchParams.sort_dir === "Asc") {
|
||||
return (
|
||||
<svg class="icon icon-arrow-up d-none d-sm-inline mr-1"><use xlinkHref="#icon-arrow-up"></use></svg>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<svg class="icon icon-arrow-down mr-1"><use xlinkHref="#icon-arrow-down"></use></svg>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
torrentsTable() {
|
||||
return (
|
||||
<div>
|
||||
|
@ -151,19 +162,19 @@ export class Search extends Component<any, State> {
|
|||
<thead>
|
||||
<tr>
|
||||
<th class="search-name-col">
|
||||
<SortableLink sortKey="Name" state={this.state}/>
|
||||
{this.sortLink("Name")}
|
||||
</th>
|
||||
<th class="text-right">
|
||||
<SortableLink sortKey="Size" state={this.state}/>
|
||||
{this.sortLink("Size")}
|
||||
</th>
|
||||
<th class="text-right">
|
||||
<SortableLink sortKey="Seeders" state={this.state}/>
|
||||
{this.sortLink("Seeders")}
|
||||
</th>
|
||||
<th class="text-right d-none d-md-table-cell">
|
||||
<SortableLink sortKey="Leechers" state={this.state}/>
|
||||
{this.sortLink("Leechers")}
|
||||
</th>
|
||||
<th class="text-right d-none d-md-table-cell">
|
||||
<SortableLink sortKey="Scraped" state={this.state}/>
|
||||
{this.sortLink("Scraped")}
|
||||
</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
|
|
Loading…
Reference in New Issue