tangled
alpha
login
or
join now
ericwood.org
/
photos-site
1
fork
atom
A little app to serve my photography from my personal website
1
fork
atom
overview
issues
pulls
pipelines
better pagination
ericwood.org
5 months ago
ca706986
f54c6946
+94
-45
5 changed files
expand all
collapse all
unified
split
Cargo.lock
Cargo.toml
src
db.rs
routes
photos.rs
templates
photos
index.jinja
+1
Cargo.lock
···
1070
1070
"dotenv",
1071
1071
"minijinja",
1072
1072
"serde",
1073
1073
+
"serde_urlencoded",
1073
1074
"sqlx",
1074
1075
"tokio",
1075
1076
"tower-http",
+1
Cargo.toml
···
10
10
dotenv = "0.15.0"
11
11
minijinja = { version = "2.12.0", features = ["loader"] }
12
12
serde = "1.0.228"
13
13
+
serde_urlencoded = "0.7.1"
13
14
sqlx = { version = "0.8.6", features = ["runtime-tokio", "sqlite"] }
14
15
tokio = { version = "1.47.1", features = ["rt-multi-thread"] }
15
16
tower-http = { version = "0.6.6", features = ["fs"] }
+25
-7
src/db.rs
···
1
1
+
use std::convert;
2
2
+
1
3
use crate::{Photo, Tag};
2
4
use serde::{Deserialize, Serialize};
3
5
use sqlx::{QueryBuilder, Sqlite, SqlitePool, query::QueryAs};
4
6
5
5
-
#[derive(Deserialize, Serialize, Clone, Copy)]
7
7
+
#[derive(Deserialize, Serialize, Clone, Copy, PartialEq, Eq)]
6
8
#[serde(rename_all = "snake_case")]
7
9
pub enum SortDirection {
8
10
Asc,
9
11
Desc,
10
12
}
11
13
14
14
+
impl convert::From<SortDirection> for String {
15
15
+
fn from(value: SortDirection) -> String {
16
16
+
match value {
17
17
+
SortDirection::Asc => "asc",
18
18
+
SortDirection::Desc => "desc",
19
19
+
}
20
20
+
.to_string()
21
21
+
}
22
22
+
}
23
23
+
12
24
impl SortDirection {
13
25
pub fn to_sql(self) -> String {
14
26
match self {
···
19
31
}
20
32
}
21
33
22
22
-
#[derive(Deserialize, Serialize, Clone, Copy)]
34
34
+
#[derive(Deserialize, Serialize, Clone, Copy, PartialEq, Eq)]
23
35
#[serde(rename_all = "snake_case")]
24
36
pub enum SortField {
25
37
TakenAt,
26
38
CreatedAt,
27
39
}
28
40
29
29
-
impl SortField {
30
30
-
pub fn to_sql(self) -> String {
31
31
-
match self {
32
32
-
Self::TakenAt => "taken_at",
33
33
-
Self::CreatedAt => "created_at",
41
41
+
impl convert::From<SortField> for String {
42
42
+
fn from(value: SortField) -> String {
43
43
+
match value {
44
44
+
SortField::TakenAt => "taken_at",
45
45
+
SortField::CreatedAt => "created_at",
34
46
}
35
47
.to_string()
48
48
+
}
49
49
+
}
50
50
+
51
51
+
impl SortField {
52
52
+
pub fn to_sql(self) -> String {
53
53
+
self.into()
36
54
}
37
55
}
38
56
+62
-25
src/routes/photos.rs
···
16
16
pub struct Pagination {
17
17
page: u32,
18
18
num_pages: u32,
19
19
+
prev_query: Option<String>,
20
20
+
next_query: Option<String>,
19
21
}
20
22
21
21
-
#[derive(Deserialize)]
22
22
-
#[serde(default)]
23
23
+
#[derive(Deserialize, Serialize, Clone)]
23
24
pub struct IndexParams {
24
24
-
page: u32,
25
25
-
limit: u32,
25
25
+
page: Option<u32>,
26
26
+
limit: Option<u32>,
26
27
tag: Option<String>,
27
27
-
sort: SortField,
28
28
-
dir: SortDirection,
28
28
+
sort: Option<SortField>,
29
29
+
dir: Option<SortDirection>,
29
30
}
30
31
31
32
impl Default for IndexParams {
32
33
fn default() -> Self {
33
34
Self {
34
34
-
page: 1,
35
35
-
limit: 10,
35
35
+
page: Some(1),
36
36
+
limit: Some(10),
36
37
tag: None,
37
37
-
sort: SortField::TakenAt,
38
38
-
dir: SortDirection::Desc,
38
38
+
sort: Some(SortField::TakenAt),
39
39
+
dir: Some(SortDirection::Desc),
39
40
}
40
41
}
41
42
}
···
46
47
query: Query<IndexParams>,
47
48
State(state): State<Arc<AppState>>,
48
49
) -> Result<Html<String>, AppError> {
50
50
+
let query = query.0;
51
51
+
let page = query.page.unwrap_or(1);
52
52
+
let limit = query.limit.unwrap_or(10);
53
53
+
let tag = query.tag.clone();
54
54
+
let sort = query.sort.unwrap_or(SortField::TakenAt);
55
55
+
let dir = query.dir.unwrap_or(SortDirection::Desc);
49
56
let photos = db::get_photos(
50
57
&state.pool,
51
58
PhotoQuery {
52
59
sort: Sort {
53
53
-
field: query.sort,
54
54
-
direction: query.dir,
55
55
-
},
56
56
-
pagination: QueryPagination {
57
57
-
limit: query.limit,
58
58
-
page: query.page,
60
60
+
field: sort,
61
61
+
direction: dir,
59
62
},
60
60
-
tag: query.tag.to_owned(),
63
63
+
pagination: QueryPagination { limit, page },
64
64
+
tag: tag.to_owned(),
61
65
},
62
66
)
63
67
.await?;
64
68
65
69
let total_photos = db::get_photo_count(&state.pool).await?;
66
70
67
67
-
let current_tag = query.tag.clone();
71
71
+
let current_tag = tag.clone();
68
72
let mut tags = db::get_tags(&state.pool).await?;
69
69
-
if let Some(tag) = &query.tag {
70
70
-
tags.retain(|t| t.name != *tag);
73
73
+
if let Some(tag) = tag {
74
74
+
tags.retain(|t| t.name != tag);
71
75
}
72
76
73
77
let sort_dir = query.dir;
74
78
let sort_field = query.sort;
75
79
let template = state.template_env.get_template("photos/index")?;
76
76
-
let pagination = Pagination {
77
77
-
page: query.page,
78
78
-
num_pages: total_photos / query.limit,
79
79
-
};
80
80
+
let num_pages = total_photos / limit;
81
81
+
let pagination = get_pagination(query, num_pages)?;
80
82
let rendered = template.render(context! {
81
83
photos => photos,
82
84
tags => tags,
83
83
-
current_tag => current_tag,
84
85
current_tag => current_tag,
85
86
sort_dir => sort_dir,
86
87
sort_field => sort_field,
···
103
104
104
105
Ok(Html(rendered))
105
106
}
107
107
+
108
108
+
fn get_pagination(query: IndexParams, num_pages: u32) -> anyhow::Result<Pagination> {
109
109
+
let page = query.page.unwrap_or(1);
110
110
+
let prev_query = if page > 1 {
111
111
+
let prev_page = if page == 1 { None } else { Some(page - 1) };
112
112
+
Some(serde_urlencoded::to_string(IndexParams {
113
113
+
page: prev_page,
114
114
+
tag: query.tag.clone(),
115
115
+
..query
116
116
+
})?)
117
117
+
} else {
118
118
+
None
119
119
+
};
120
120
+
121
121
+
let next_query = if page < num_pages {
122
122
+
let next_page = if page < num_pages {
123
123
+
Some(page + 1)
124
124
+
} else {
125
125
+
None
126
126
+
};
127
127
+
Some(serde_urlencoded::to_string(IndexParams {
128
128
+
page: next_page,
129
129
+
tag: query.tag.clone(),
130
130
+
..query
131
131
+
})?)
132
132
+
} else {
133
133
+
None
134
134
+
};
135
135
+
136
136
+
Ok(Pagination {
137
137
+
page,
138
138
+
num_pages,
139
139
+
prev_query,
140
140
+
next_query,
141
141
+
})
142
142
+
}
+5
-13
templates/photos/index.jinja
···
47
47
</div>
48
48
49
49
<ul class="pagination">
50
50
-
{% if pagination.page > 1 %}
51
51
-
<a href="?page={{ pagination.page - 1 }}">←</a>
50
50
+
{% if pagination.prev_query %}
51
51
+
<a href="?{{ pagination.prev_query }}">←</a>
52
52
{% endif %}
53
53
-
{% for i in range(1, pagination.num_pages+1) %}
54
54
-
<li>
55
55
-
{% if pagination.page == i %}
56
56
-
{{ i }}
57
57
-
{% else %}
58
58
-
<a href="?page={{ i }}">{{ i }}</a>
59
59
-
{% endif %}
60
60
-
</li>
61
61
-
{% endfor %}
62
62
-
{% if pagination.page != pagination.num_pages %}
63
63
-
<a href="?page={{ pagination.page + 1 }}">→</a>
53
53
+
<li>{{ pagination.page }} / {{ pagination.num_pages }}</li>
54
54
+
{% if pagination.next_query %}
55
55
+
<a href="?{{ pagination.next_query }}">→</a>
64
56
{% endif %}
65
57
</ul>
66
58
{% endblock %}