Style the file-service #76

Merged
savanni merged 7 commits from file-service/mobile-format into main 2023-10-19 01:53:07 +00:00
7 changed files with 260 additions and 69 deletions

2
Cargo.lock generated
View File

@ -816,7 +816,7 @@ dependencies = [
[[package]] [[package]]
name = "file-service" name = "file-service"
version = "0.1.0" version = "0.1.1"
dependencies = [ dependencies = [
"base64ct", "base64ct",
"build_html", "build_html",

View File

@ -1,6 +1,6 @@
[package] [package]
name = "file-service" name = "file-service"
version = "0.1.0" version = "0.1.1"
authors = ["savanni@luminescent-dreams.com"] authors = ["savanni@luminescent-dreams.com"]
edition = "2018" edition = "2018"

View File

@ -9,6 +9,8 @@ use warp::{filters::multipart::FormData, http::Response, multipart::Part};
use crate::{pages, App, AuthToken, FileId, FileInfo, ReadFileError, SessionToken}; use crate::{pages, App, AuthToken, FileId, FileInfo, ReadFileError, SessionToken};
const CSS: &str = include_str!("../templates/style.css");
pub async fn handle_index( pub async fn handle_index(
app: App, app: App,
token: Option<SessionToken>, 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> { pub fn render_auth_page(message: Option<String>) -> Result<Response<String>, Error> {
Response::builder() Response::builder()
.status(StatusCode::OK) .status(StatusCode::OK)

View File

@ -1,5 +1,38 @@
use build_html::{self, Html, HtmlContainer}; 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)] #[derive(Clone, Debug)]
pub struct Form { pub struct Form {
path: String, path: String,
@ -41,11 +74,11 @@ impl Html for Form {
None => "".to_owned(), None => "".to_owned(),
}; };
format!( 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, path = self.path,
method = self.method, method = self.method,
encoding = encoding, encoding = encoding,
elements = self.elements.to_html_string() elements = self.elements.to_html_string(),
) )
} }
} }
@ -62,7 +95,7 @@ pub struct Input {
name: String, name: String,
id: Option<String>, id: Option<String>,
value: Option<String>, value: Option<String>,
content: Option<String>, attributes: Attributes,
} }
impl Html for Input { impl Html for Input {
@ -75,14 +108,15 @@ impl Html for Input {
Some(ref value) => format!("value=\"{}\"", value), Some(ref value) => format!("value=\"{}\"", value),
None => "".to_owned(), None => "".to_owned(),
}; };
let attrs = self.attributes.to_string();
format!( format!(
"<input type=\"{ty}\" name=\"{name}\" {id} {value}>{content}</input>\n", "<input type=\"{ty}\" name=\"{name}\" {id} {value} {attrs} />\n",
ty = self.ty, ty = self.ty,
name = self.name, name = self.name,
id = id, id = id,
value = value, value = value,
content = self.content.clone().unwrap_or("".to_owned()), attrs = attrs,
) )
} }
} }
@ -94,7 +128,7 @@ impl Input {
name: name.to_owned(), name: name.to_owned(),
id: None, id: None,
value: None, value: None,
content: None, attributes: Attributes::default(),
} }
} }
@ -108,12 +142,18 @@ impl Input {
self self
} }
/* pub fn with_attributes<'a>(
pub fn with_content(mut self, val: &str) -> Self { mut self,
self.content = Some(val.to_owned()); 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 self
} }
*/
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@ -146,6 +186,7 @@ pub struct Button {
ty: Option<String>, ty: Option<String>,
name: Option<String>, name: Option<String>,
label: String, label: String,
attributes: Attributes,
} }
impl Button { impl Button {
@ -154,6 +195,7 @@ impl Button {
ty: None, ty: None,
name: None, name: None,
label: label.to_owned(), label: label.to_owned(),
attributes: Attributes::default(),
} }
} }
@ -161,6 +203,19 @@ impl Button {
self.ty = Some(ty.to_owned()); self.ty = Some(ty.to_owned());
self 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 { impl Html for Button {
@ -174,9 +229,10 @@ impl Html for Button {
None => "".to_owned(), None => "".to_owned(),
}; };
format!( format!(
"<button {ty} {name}>{label}</button>", "<button {ty} {name} {attrs}>{label}</button>",
name = name, name = name,
label = self.label label = self.label,
attrs = self.attributes.to_string()
) )
} }
} }
@ -184,18 +240,37 @@ impl Html for Button {
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct Image { pub struct Image {
path: String, path: String,
attributes: Attributes,
} }
impl Image { impl Image {
pub fn new(path: &str) -> Self { pub fn new(path: &str) -> Self {
Self { Self {
path: path.to_owned(), 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 { impl Html for Image {
fn to_html_string(&self) -> String { 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()
)
} }
} }

View File

@ -1,7 +1,7 @@
extern crate log; extern crate log;
use cookie::Cookie; use cookie::Cookie;
use handlers::{file, handle_auth, handle_upload, thumbnail}; use handlers::{file, handle_auth, handle_css, handle_upload, thumbnail};
use std::{ use std::{
collections::{HashMap, HashSet}, collections::{HashMap, HashSet},
convert::Infallible, convert::Infallible,
@ -119,6 +119,8 @@ pub async fn main() {
.and(maybe_with_session()) .and(maybe_with_session())
.then(handle_index); .then(handle_index);
let styles = warp::path!("css").and(warp::get()).then(handle_css);
let auth = warp::path!("auth") let auth = warp::path!("auth")
.and(warp::post()) .and(warp::post())
.and(with_app(app.clone())) .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)); .then(move |id, old_etags, app: App| file(app, id, old_etags));
let server = warp::serve( let server = warp::serve(
root.or(auth) root.or(styles)
.or(auth)
.or(upload_via_form) .or(upload_via_form)
.or(thumbnail) .or(thumbnail)
.or(file) .or(file)

View File

@ -5,35 +5,48 @@ use file_service::{FileHandle, FileId, ReadFileError};
pub fn auth(_message: Option<String>) -> build_html::HtmlPage { pub fn auth(_message: Option<String>) -> build_html::HtmlPage {
build_html::HtmlPage::new() build_html::HtmlPage::new()
.with_title("Authentication") .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( .with_html(
Form::new() Form::new()
.with_path("/auth") .with_path("/auth")
.with_method("post") .with_method("post")
.with_container( .with_container(
Container::new(ContainerType::Div) Container::new(ContainerType::Div)
.with_html(Input::new("token", "token").with_id("for-token-input")) .with_attributes([("class", "authentication-form__label")])
.with_html(Label::new("for-token-input", "Authentication Token")), .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 { pub fn gallery(handles: Vec<Result<FileHandle, ReadFileError>>) -> build_html::HtmlPage {
let mut page = build_html::HtmlPage::new() let mut page = build_html::HtmlPage::new()
.with_title("Admin list of files") .with_title("Gallery")
.with_header(1, "Admin list of files") .with_stylesheet("/css")
.with_html(
Form::new()
.with_path("/upload")
.with_method("post")
.with_encoding("multipart/form-data")
.with_container( .with_container(
Container::new(ContainerType::Div) Container::new(ContainerType::Div)
.with_html(Input::new("file", "file").with_id("for-selector-input")) .with_attributes([("class", "gallery-page")])
.with_html(Label::new("for-selector-input", "Select a file")), .with_header(1, "Gallery")
) .with_html(upload_form()),
.with_html(Button::new("Upload file").with_type("submit")),
); );
let mut gallery = Container::new(ContainerType::Div).with_attributes([("class", "gallery")]);
for handle in handles { for handle in handles {
let container = match handle { let container = match handle {
Ok(ref handle) => thumbnail(&handle.id).with_html( 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_attributes(vec![("class", "file")])
.with_paragraph(format!("{:?}", err)), .with_paragraph(format!("{:?}", err)),
}; };
page.add_container(container) gallery.add_container(container);
} }
page.add_container(gallery);
page page
} }
pub fn thumbnail(id: &FileId) -> Container { pub fn upload_form() -> Form {
let mut container = Container::new(ContainerType::Div).with_attributes(vec![("class", "file")]); Form::new()
let tn = Container::new(ContainerType::Div) .with_path("/upload")
.with_attributes(vec![("class", "thumbnail")]) .with_method("post")
.with_link( .with_encoding("multipart/form-data")
format!("/{}", **id), .with_container(
Image::new(&format!("{}/tn", **id)).to_html_string(), Container::new(ContainerType::Div)
); .with_attributes([("class", "card upload-form")])
container.add_html(tn); .with_html(Input::new("file", "file").with_attributes([
container ("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(),
),
)
} }

View File

@ -1,29 +1,83 @@
body { :root {
font-family: 'Ariel', sans-serif; --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; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
} }
.file {
display: flex;
margin: 1em;
border: 1px solid #449dfc;
border-radius: 5px;
padding: 1em;
}
.thumbnail { .thumbnail {
max-width: 320px; width: 300px;
margin: 1em; display: flex;
flex-direction: column;
justify-content: space-between;
} }
img { .thumbnail__image {
max-width: 100%; max-width: 100%;
border: none;
} }
/*
[type="submit"] { [type="submit"] {
border-radius: 1em; border-radius: 1em;
margin: 1em; margin: 1em;
@ -31,15 +85,10 @@ img {
} }
.uploadform { .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"] { [type="file"] {
border: 0; border: 0;
clip: rect(0, 0, 0, 0); clip: rect(0, 0, 0, 0);
@ -71,12 +120,30 @@ img {
outline: 1px dotted #000; outline: 1px dotted #000;
outline: -webkit-focus-ring-color auto 5px; 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 { body {
font-size: xx-large; 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"] { [type="submit"] {
font-size: xx-large; font-size: xx-large;
width: 100%; width: 100%;
@ -100,4 +167,5 @@ img {
flex-direction: column; flex-direction: column;
width: 100%; width: 100%;
} }
*/
} }