Style the file-service #76
|
@ -816,7 +816,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "file-service"
|
||||
version = "0.1.0"
|
||||
version = "0.1.1"
|
||||
dependencies = [
|
||||
"base64ct",
|
||||
"build_html",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "file-service"
|
||||
version = "0.1.0"
|
||||
version = "0.1.1"
|
||||
authors = ["savanni@luminescent-dreams.com"]
|
||||
edition = "2018"
|
||||
|
||||
|
|
|
@ -9,6 +9,8 @@ use warp::{filters::multipart::FormData, http::Response, multipart::Part};
|
|||
|
||||
use crate::{pages, App, AuthToken, FileId, FileInfo, ReadFileError, SessionToken};
|
||||
|
||||
const CSS: &str = include_str!("../templates/style.css");
|
||||
|
||||
pub async fn handle_index(
|
||||
app: App,
|
||||
token: Option<SessionToken>,
|
||||
|
@ -22,6 +24,13 @@ pub async fn handle_index(
|
|||
}
|
||||
}
|
||||
|
||||
pub async fn handle_css() -> Result<Response<String>, Error> {
|
||||
Response::builder()
|
||||
.header("content-type", "text/css")
|
||||
.status(StatusCode::OK)
|
||||
.body(CSS.to_owned())
|
||||
}
|
||||
|
||||
pub fn render_auth_page(message: Option<String>) -> Result<Response<String>, Error> {
|
||||
Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
|
|
|
@ -1,5 +1,38 @@
|
|||
use build_html::{self, Html, HtmlContainer};
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Attributes(Vec<(String, String)>);
|
||||
|
||||
/*
|
||||
impl FromIterator<(String, String)> for Attributes {
|
||||
fn from_iter<T>(iter: T) -> Self
|
||||
where
|
||||
T: IntoIterator<Item = (String, String)>,
|
||||
{
|
||||
Attributes(iter.collect::<Vec<(String, String)>>())
|
||||
}
|
||||
}
|
||||
|
||||
impl FromIterator<(&str, &str)> for Attributes {
|
||||
fn from_iter<T>(iter: T) -> Self
|
||||
where
|
||||
T: IntoIterator<Item = (&str, &str)>,
|
||||
{
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
impl ToString for Attributes {
|
||||
fn to_string(&self) -> String {
|
||||
self.0
|
||||
.iter()
|
||||
.map(|(key, value)| format!("{}=\"{}\"", key, value))
|
||||
.collect::<Vec<String>>()
|
||||
.join(" ")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Form {
|
||||
path: String,
|
||||
|
@ -41,11 +74,11 @@ impl Html for Form {
|
|||
None => "".to_owned(),
|
||||
};
|
||||
format!(
|
||||
"<form action=\"{path}\" method=\"{method}\" {encoding}>\n{elements}\n</form>\n",
|
||||
"<form action=\"{path}\" method=\"{method}\" {encoding}\n{elements}\n</form>\n",
|
||||
path = self.path,
|
||||
method = self.method,
|
||||
encoding = encoding,
|
||||
elements = self.elements.to_html_string()
|
||||
elements = self.elements.to_html_string(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -62,7 +95,7 @@ pub struct Input {
|
|||
name: String,
|
||||
id: Option<String>,
|
||||
value: Option<String>,
|
||||
content: Option<String>,
|
||||
attributes: Attributes,
|
||||
}
|
||||
|
||||
impl Html for Input {
|
||||
|
@ -75,14 +108,15 @@ impl Html for Input {
|
|||
Some(ref value) => format!("value=\"{}\"", value),
|
||||
None => "".to_owned(),
|
||||
};
|
||||
let attrs = self.attributes.to_string();
|
||||
|
||||
format!(
|
||||
"<input type=\"{ty}\" name=\"{name}\" {id} {value}>{content}</input>\n",
|
||||
"<input type=\"{ty}\" name=\"{name}\" {id} {value} {attrs} />\n",
|
||||
ty = self.ty,
|
||||
name = self.name,
|
||||
id = id,
|
||||
value = value,
|
||||
content = self.content.clone().unwrap_or("".to_owned()),
|
||||
attrs = attrs,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -94,7 +128,7 @@ impl Input {
|
|||
name: name.to_owned(),
|
||||
id: None,
|
||||
value: None,
|
||||
content: None,
|
||||
attributes: Attributes::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -108,12 +142,18 @@ impl Input {
|
|||
self
|
||||
}
|
||||
|
||||
/*
|
||||
pub fn with_content(mut self, val: &str) -> Self {
|
||||
self.content = Some(val.to_owned());
|
||||
pub fn with_attributes<'a>(
|
||||
mut self,
|
||||
values: impl IntoIterator<Item = (&'a str, &'a str)>,
|
||||
) -> Self {
|
||||
self.attributes = Attributes(
|
||||
values
|
||||
.into_iter()
|
||||
.map(|(a, b)| (a.to_owned(), b.to_owned()))
|
||||
.collect::<Vec<(String, String)>>(),
|
||||
);
|
||||
self
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
|
@ -146,6 +186,7 @@ pub struct Button {
|
|||
ty: Option<String>,
|
||||
name: Option<String>,
|
||||
label: String,
|
||||
attributes: Attributes,
|
||||
}
|
||||
|
||||
impl Button {
|
||||
|
@ -154,6 +195,7 @@ impl Button {
|
|||
ty: None,
|
||||
name: None,
|
||||
label: label.to_owned(),
|
||||
attributes: Attributes::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -161,6 +203,19 @@ impl Button {
|
|||
self.ty = Some(ty.to_owned());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_attributes<'a>(
|
||||
mut self,
|
||||
values: impl IntoIterator<Item = (&'a str, &'a str)>,
|
||||
) -> Self {
|
||||
self.attributes = Attributes(
|
||||
values
|
||||
.into_iter()
|
||||
.map(|(a, b)| (a.to_owned(), b.to_owned()))
|
||||
.collect::<Vec<(String, String)>>(),
|
||||
);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Html for Button {
|
||||
|
@ -174,9 +229,10 @@ impl Html for Button {
|
|||
None => "".to_owned(),
|
||||
};
|
||||
format!(
|
||||
"<button {ty} {name}>{label}</button>",
|
||||
"<button {ty} {name} {attrs}>{label}</button>",
|
||||
name = name,
|
||||
label = self.label
|
||||
label = self.label,
|
||||
attrs = self.attributes.to_string()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -184,18 +240,37 @@ impl Html for Button {
|
|||
#[derive(Clone, Debug)]
|
||||
pub struct Image {
|
||||
path: String,
|
||||
attributes: Attributes,
|
||||
}
|
||||
|
||||
impl Image {
|
||||
pub fn new(path: &str) -> Self {
|
||||
Self {
|
||||
path: path.to_owned(),
|
||||
attributes: Attributes::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_attributes<'a>(
|
||||
mut self,
|
||||
values: impl IntoIterator<Item = (&'a str, &'a str)>,
|
||||
) -> Self {
|
||||
self.attributes = Attributes(
|
||||
values
|
||||
.into_iter()
|
||||
.map(|(a, b)| (a.to_owned(), b.to_owned()))
|
||||
.collect::<Vec<(String, String)>>(),
|
||||
);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Html for Image {
|
||||
fn to_html_string(&self) -> String {
|
||||
format!("<img src={path} />", path = self.path,)
|
||||
format!(
|
||||
"<img src={path} {attrs} />",
|
||||
path = self.path,
|
||||
attrs = self.attributes.to_string()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
extern crate log;
|
||||
|
||||
use cookie::Cookie;
|
||||
use handlers::{file, handle_auth, handle_upload, thumbnail};
|
||||
use handlers::{file, handle_auth, handle_css, handle_upload, thumbnail};
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
convert::Infallible,
|
||||
|
@ -119,6 +119,8 @@ pub async fn main() {
|
|||
.and(maybe_with_session())
|
||||
.then(handle_index);
|
||||
|
||||
let styles = warp::path!("css").and(warp::get()).then(handle_css);
|
||||
|
||||
let auth = warp::path!("auth")
|
||||
.and(warp::post())
|
||||
.and(with_app(app.clone()))
|
||||
|
@ -145,7 +147,8 @@ pub async fn main() {
|
|||
.then(move |id, old_etags, app: App| file(app, id, old_etags));
|
||||
|
||||
let server = warp::serve(
|
||||
root.or(auth)
|
||||
root.or(styles)
|
||||
.or(auth)
|
||||
.or(upload_via_form)
|
||||
.or(thumbnail)
|
||||
.or(file)
|
||||
|
|
|
@ -5,35 +5,48 @@ use file_service::{FileHandle, FileId, ReadFileError};
|
|||
pub fn auth(_message: Option<String>) -> build_html::HtmlPage {
|
||||
build_html::HtmlPage::new()
|
||||
.with_title("Authentication")
|
||||
.with_stylesheet("/css")
|
||||
.with_container(
|
||||
Container::new(ContainerType::Div)
|
||||
.with_attributes([("class", "authentication-page")])
|
||||
.with_container(
|
||||
Container::new(ContainerType::Div)
|
||||
.with_attributes([("class", "card authentication-form")])
|
||||
.with_html(
|
||||
Form::new()
|
||||
.with_path("/auth")
|
||||
.with_method("post")
|
||||
.with_container(
|
||||
Container::new(ContainerType::Div)
|
||||
.with_html(Input::new("token", "token").with_id("for-token-input"))
|
||||
.with_html(Label::new("for-token-input", "Authentication Token")),
|
||||
.with_attributes([("class", "authentication-form__label")])
|
||||
.with_html(Label::new("for-token-input", "Authentication")),
|
||||
)
|
||||
.with_container(
|
||||
Container::new(ContainerType::Div)
|
||||
.with_attributes([("class", "authentication-form__input")])
|
||||
.with_html(
|
||||
Input::new("token", "token")
|
||||
.with_id("for-token-input")
|
||||
.with_attributes([("size", "50")]),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn gallery(handles: Vec<Result<FileHandle, ReadFileError>>) -> build_html::HtmlPage {
|
||||
let mut page = build_html::HtmlPage::new()
|
||||
.with_title("Admin list of files")
|
||||
.with_header(1, "Admin list of files")
|
||||
.with_html(
|
||||
Form::new()
|
||||
.with_path("/upload")
|
||||
.with_method("post")
|
||||
.with_encoding("multipart/form-data")
|
||||
.with_title("Gallery")
|
||||
.with_stylesheet("/css")
|
||||
.with_container(
|
||||
Container::new(ContainerType::Div)
|
||||
.with_html(Input::new("file", "file").with_id("for-selector-input"))
|
||||
.with_html(Label::new("for-selector-input", "Select a file")),
|
||||
)
|
||||
.with_html(Button::new("Upload file").with_type("submit")),
|
||||
.with_attributes([("class", "gallery-page")])
|
||||
.with_header(1, "Gallery")
|
||||
.with_html(upload_form()),
|
||||
);
|
||||
|
||||
let mut gallery = Container::new(ContainerType::Div).with_attributes([("class", "gallery")]);
|
||||
for handle in handles {
|
||||
let container = match handle {
|
||||
Ok(ref handle) => thumbnail(&handle.id).with_html(
|
||||
|
@ -48,19 +61,42 @@ pub fn gallery(handles: Vec<Result<FileHandle, ReadFileError>>) -> build_html::H
|
|||
.with_attributes(vec![("class", "file")])
|
||||
.with_paragraph(format!("{:?}", err)),
|
||||
};
|
||||
page.add_container(container)
|
||||
gallery.add_container(container);
|
||||
}
|
||||
page.add_container(gallery);
|
||||
page
|
||||
}
|
||||
|
||||
pub fn thumbnail(id: &FileId) -> Container {
|
||||
let mut container = Container::new(ContainerType::Div).with_attributes(vec![("class", "file")]);
|
||||
let tn = Container::new(ContainerType::Div)
|
||||
.with_attributes(vec![("class", "thumbnail")])
|
||||
.with_link(
|
||||
format!("/{}", **id),
|
||||
Image::new(&format!("{}/tn", **id)).to_html_string(),
|
||||
);
|
||||
container.add_html(tn);
|
||||
container
|
||||
pub fn upload_form() -> Form {
|
||||
Form::new()
|
||||
.with_path("/upload")
|
||||
.with_method("post")
|
||||
.with_encoding("multipart/form-data")
|
||||
.with_container(
|
||||
Container::new(ContainerType::Div)
|
||||
.with_attributes([("class", "card upload-form")])
|
||||
.with_html(Input::new("file", "file").with_attributes([
|
||||
("id", "for-selector-input"),
|
||||
("placeholder", "select file"),
|
||||
("class", "upload-form__selector"),
|
||||
]))
|
||||
.with_html(
|
||||
Button::new("Upload file")
|
||||
.with_attributes([("class", "upload-form__button")])
|
||||
.with_type("submit"),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn thumbnail(id: &FileId) -> Container {
|
||||
Container::new(ContainerType::Div)
|
||||
.with_attributes(vec![("class", "card thumbnail")])
|
||||
.with_html(
|
||||
Container::new(ContainerType::Div).with_link(
|
||||
format!("/{}", **id),
|
||||
Image::new(&format!("{}/tn", **id))
|
||||
.with_attributes([("class", "thumbnail__image")])
|
||||
.to_html_string(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,29 +1,83 @@
|
|||
body {
|
||||
font-family: 'Ariel', sans-serif;
|
||||
:root {
|
||||
--main-bg-color: #e5f0fc;
|
||||
--fg-color: #449dfc;
|
||||
|
||||
--space-small: 4px;
|
||||
--space-medium: 8px;
|
||||
--space-large: 12px;
|
||||
|
||||
--hover-low: 4px 4px 4px gray;
|
||||
}
|
||||
|
||||
.files {
|
||||
body {
|
||||
font-family: 'Ariel', sans-serif;
|
||||
background-color: var(--main-bg-color);
|
||||
}
|
||||
|
||||
.card {
|
||||
border: 1px solid black;
|
||||
border-radius: 5px;
|
||||
box-shadow: var(--hover-low);
|
||||
margin: var(--space-large);
|
||||
padding: var(--space-medium);
|
||||
|
||||
}
|
||||
|
||||
.authentication-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.authentication-form {
|
||||
}
|
||||
|
||||
.authentication-form__label {
|
||||
margin: var(--space-small);
|
||||
}
|
||||
|
||||
.authentication-form__input {
|
||||
margin: var(--space-small);
|
||||
}
|
||||
|
||||
.gallery-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.upload-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.upload-form__selector {
|
||||
margin: var(--space-small);
|
||||
}
|
||||
|
||||
.upload-form__button {
|
||||
margin: var(--space-small);
|
||||
}
|
||||
|
||||
.gallery {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.file {
|
||||
display: flex;
|
||||
margin: 1em;
|
||||
border: 1px solid #449dfc;
|
||||
border-radius: 5px;
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
.thumbnail {
|
||||
max-width: 320px;
|
||||
margin: 1em;
|
||||
width: 300px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
img {
|
||||
.thumbnail__image {
|
||||
max-width: 100%;
|
||||
border: none;
|
||||
}
|
||||
|
||||
/*
|
||||
[type="submit"] {
|
||||
border-radius: 1em;
|
||||
margin: 1em;
|
||||
|
@ -31,15 +85,10 @@ img {
|
|||
}
|
||||
|
||||
.uploadform {
|
||||
display: flex;
|
||||
margin: 1em;
|
||||
background-color: #e5f0fc;
|
||||
border: 1px solid #449dfc;
|
||||
border-radius: 5px;
|
||||
padding: 1em;
|
||||
}
|
||||
*/
|
||||
|
||||
/* https://benmarshall.me/styling-file-inputs/ */
|
||||
/*
|
||||
[type="file"] {
|
||||
border: 0;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
|
@ -71,12 +120,30 @@ img {
|
|||
outline: 1px dotted #000;
|
||||
outline: -webkit-focus-ring-color auto 5px;
|
||||
}
|
||||
*/
|
||||
|
||||
@media screen and (max-width: 980px) { /* This is the screen width of a OnePlus 5t */
|
||||
@media screen and (max-width: 1080px) { /* This is the screen width of a OnePlus 8 */
|
||||
body {
|
||||
font-size: xx-large;
|
||||
}
|
||||
|
||||
.authentication-form {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.upload-form__selector {
|
||||
font-size: larger;
|
||||
}
|
||||
|
||||
.upload-form__button {
|
||||
font-size: larger;
|
||||
}
|
||||
|
||||
.thumbnail {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/*
|
||||
[type="submit"] {
|
||||
font-size: xx-large;
|
||||
width: 100%;
|
||||
|
@ -100,4 +167,5 @@ img {
|
|||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue