Showing
15 changed files
with
498 additions
and
65 deletions
README.md
0 → 100644
| 1 | +# Steffie's Artshop (Arbeitstitel) | ||
| 2 | + | ||
| 3 | +A pure rust implementation of a small shop system. This is | ||
| 4 | +ment to host the Arts created by my wife Stefanie and give | ||
| 5 | +the opportunity to sell some of these of offer art services. | ||
| 6 | + | ||
| 7 | +## Description | ||
| 8 | + | ||
| 9 | +[:Description:] | ||
| 10 | + | ||
| 11 | +## Requirements | ||
| 12 | + | ||
| 13 | +This will be mostly self sufficient. It comes with its own web | ||
| 14 | +server. One needs a browser to see the results. | ||
| 15 | + | ||
| 16 | +## Dependencies | ||
| 17 | + | ||
| 18 | +- rust | ||
| 19 | +- mogwai | ||
| 20 | +- actix-web | ||
| 21 | +- actix-file | ||
| 22 | +- serde | ||
| 23 | +- serde-json | ||
| 24 | +- diffy | ||
| 25 | + | ||
| 26 | +## Contributing | ||
| 27 | + | ||
| 28 | +Currently this is a lot of lerning for myself and I prefer to | ||
| 29 | +work on this alone as long as I am not settled with it. | ||
| 30 | + | ||
| 31 | +Anyway, I will look into suggestions when they arrive me. | ||
| 32 | + | ||
| 33 | +## License | ||
| 34 | + | ||
| 35 | +Copyright © 2022 Georg Hopp | ||
| 36 | + | ||
| 37 | +This program is free software: you can redistribute it and/or modify | ||
| 38 | +it under the terms of the GNU General Public License as published by | ||
| 39 | +the Free Software Foundation, either version 3 of the License, or | ||
| 40 | +(at your option) any later version. | ||
| 41 | + | ||
| 42 | +This program is distributed in the hope that it will be useful, | ||
| 43 | +but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 44 | +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 45 | +GNU General Public License for more details. | ||
| 46 | + | ||
| 47 | +You should have received a copy of the GNU General Public License | ||
| 48 | +along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
| 49 | + | ||
| 50 | +## Author | ||
| 51 | + | ||
| 52 | +Georg Hopp <georg@steffers.org> |
| @@ -15,3 +15,24 @@ Statt also ein **undo** auf eine frühere Version zuzulassen kann man in der | @@ -15,3 +15,24 @@ Statt also ein **undo** auf eine frühere Version zuzulassen kann man in der | ||
| 15 | aktuellen Version auf frühere Stände zurück gehen und diese gegebenenfalls | 15 | aktuellen Version auf frühere Stände zurück gehen und diese gegebenenfalls |
| 16 | noch anpassen. Dieser Stand wird wieder ein neuer Patch gegen die gegenwärtige | 16 | noch anpassen. Dieser Stand wird wieder ein neuer Patch gegen die gegenwärtige |
| 17 | Version. | 17 | Version. |
| 18 | + | ||
| 19 | +# Gedanken zum file upload. | ||
| 20 | +Der finale Dateiname sollte ein eindeutiger Hashwert des content sein. | ||
| 21 | +Um die erzeugung des Hash schnell zu halten sollte es ausreichen nur einen | ||
| 22 | +Teil vom Anfang und von Ende und der Mitte zu hashen. Das habe ich schon mal | ||
| 23 | +in einem anderen Projekt gemacht... [SUCHE] | ||
| 24 | + | ||
| 25 | +Die Dateien sollten dann in einem zweistufigem Verzeichnisbaum gespeichert | ||
| 26 | +werden bei dem die Verzeichnisse mit den ersten Zeichen des Hashs beginnnen. | ||
| 27 | + | ||
| 28 | +Weitere Metainformationen wie Dateiname, Mime-Type, etc. werden in der | ||
| 29 | +Datenbank gespeichert. | ||
| 30 | + | ||
| 31 | +Die ausgelieferten Bilder sollen eine steganographisch eingebettet Copyright | ||
| 32 | +Information enthalten oder aber mit einem Wassermark versehen sein. Es gibt | ||
| 33 | +ein rust crate steganography das für die erste Anwendung geeignet scheint. | ||
| 34 | +Für die zweite Anwendung ist evtl. photon_rs geeignet. | ||
| 35 | + | ||
| 36 | +Uploads ueber die fetch API koennen zur Zeit scheinbar nicht wirklich einen | ||
| 37 | +Progressbar haben. Daher werde ich wohl einen spinner anzeigen und dann Error | ||
| 38 | +oder Ok... |
| @@ -22,3 +22,14 @@ | @@ -22,3 +22,14 @@ | ||
| 22 | ## Restful API | 22 | ## Restful API |
| 23 | [ReadTheDocs apiguide](https://apiguide.readthedocs.io/en/latest/build_and_publish/use_RESTful_urls.html 'ReadTheDocs apiguide') | 23 | [ReadTheDocs apiguide](https://apiguide.readthedocs.io/en/latest/build_and_publish/use_RESTful_urls.html 'ReadTheDocs apiguide') |
| 24 | [RestfulApi](https://restfulapi.net/versioning/ 'RestfulApi') | 24 | [RestfulApi](https://restfulapi.net/versioning/ 'RestfulApi') |
| 25 | + | ||
| 26 | +## HTML5 file upload | ||
| 27 | +[Pure HTML5 file upload](https://www.script-tutorials.com/pure-html5-file-upload/, 'Pure HTML5 file upload') | ||
| 28 | +[HTML5 File Upload Example](https://www.webcodegeeks.com/html5/html5-file-upload-example/, 'HTML5 File Upload Example') | ||
| 29 | +[How to upload files on the server using Fetch API](https://attacomsian.com/blog/uploading-files-using-fetch-api, 'How to upload files on the server using Fetch API') | ||
| 30 | +[Stackoverflow 1](https://stackoverflow.com/questions/36453950/upload-file-with-fetch-api-in-javascript-and-show-progress 'Stackoverflow 1') | ||
| 31 | +[GitHub Fetch Issues](https://github.com/github/fetch/issues/89 'GitHub Fetch Issues') | ||
| 32 | + | ||
| 33 | +## CSS spinner... | ||
| 34 | +[How to Create Loading Spinner With CSS](https://www.w3docs.com/snippets/css/how-to-create-loading-spinner-with-css.html 'How to Create Loading Spinner With CSS') | ||
| 35 | +[Loading.io - Pure CSS Loader](https://loading.io/css/ 'Loading.io - Pure CSS Loader') |
| 1 | +.upload { | ||
| 2 | + float: left; | ||
| 3 | + width: 400px; | ||
| 4 | + padding: 1em; | ||
| 5 | + border-radius: .5em; | ||
| 6 | + border: 1px solid #ddd; | ||
| 7 | + background: #f7f7f7; | ||
| 8 | +} | ||
| 9 | + | ||
| 10 | +.upload ul { | ||
| 11 | + padding-left: 5px; | ||
| 12 | +} | ||
| 13 | + | ||
| 14 | +.upload > ul { | ||
| 15 | + max-height: 15em; | ||
| 16 | + width: calc(100% - 10px); | ||
| 17 | + overflow-y: auto; | ||
| 18 | + overflow-x: hidden; | ||
| 19 | + background: #ffffff; | ||
| 20 | + border-radius: .35em; | ||
| 21 | + border: 2px solid #bbb; | ||
| 22 | + cursor: default; | ||
| 23 | +} | ||
| 24 | + | ||
| 25 | +.upload > ul > li { | ||
| 26 | + display: flex; | ||
| 27 | + border: 1px solid black; | ||
| 28 | + width: fit-content; | ||
| 29 | +} | ||
| 30 | + | ||
| 31 | +.upload > ul > li > ul > li { | ||
| 32 | + display: block; | ||
| 33 | +} | ||
| 34 | + | ||
| 1 | .markdown { | 35 | .markdown { |
| 2 | - float: left; | ||
| 3 | - padding: 1em; | ||
| 4 | - border-radius: .5em; | ||
| 5 | - border: 1px solid #ddd; | ||
| 6 | - background: #f7f7f7; | 36 | + float: left; |
| 37 | + padding: 1em; | ||
| 38 | + border-radius: .5em; | ||
| 39 | + border: 1px solid #ddd; | ||
| 40 | + background: #f7f7f7; | ||
| 7 | } | 41 | } |
| 8 | 42 | ||
| 9 | .markdown p { | 43 | .markdown p { |
| 10 | - text-align: justify; | ||
| 11 | - text-indent: .5em; | ||
| 12 | - margin-block: .5em; | 44 | + text-align: justify; |
| 45 | + text-indent: .5em; | ||
| 46 | + margin-block: .5em; | ||
| 13 | } | 47 | } |
| 14 | 48 | ||
| 15 | .markdown > div:first-child { | 49 | .markdown > div:first-child { |
| 16 | - position: fixed; | 50 | + position: fixed; |
| 17 | width: inherit; | 51 | width: inherit; |
| 18 | z-index: 10; | 52 | z-index: 10; |
| 19 | } | 53 | } |
| 20 | 54 | ||
| 21 | .markdown > div:first-child > div { | 55 | .markdown > div:first-child > div { |
| 22 | position: relative; | 56 | position: relative; |
| 23 | - left: .5em; | 57 | + left: .5em; |
| 24 | width: inherit; | 58 | width: inherit; |
| 25 | } | 59 | } |
| 26 | 60 | ||
| 27 | .markdown > div:first-child > pre { | 61 | .markdown > div:first-child > pre { |
| 28 | - white-space: pre-wrap; | ||
| 29 | - height: 15em; | 62 | + white-space: pre-wrap; |
| 63 | + height: 15em; | ||
| 30 | width: calc(100% - 10px); | 64 | width: calc(100% - 10px); |
| 31 | - overflow-y: auto; | ||
| 32 | - overflow-x: hidden; | ||
| 33 | - background: #ffffff; | ||
| 34 | - border-radius: .35em; | ||
| 35 | - border: 2px solid #bbb; | ||
| 36 | - margin: 2px 0px; | 65 | + overflow-y: auto; |
| 66 | + overflow-x: hidden; | ||
| 67 | + background: #ffffff; | ||
| 68 | + border-radius: .35em; | ||
| 69 | + border: 2px solid #bbb; | ||
| 70 | + margin: 2px 0px; | ||
| 37 | opacity: .95; | 71 | opacity: .95; |
| 38 | } | 72 | } |
| 39 | 73 | ||
| 40 | .markdown > div:first-child > div > div { | 74 | .markdown > div:first-child > div > div { |
| 41 | - display: inline; | 75 | + display: inline; |
| 42 | } | 76 | } |
| 43 | 77 | ||
| 44 | .markdown > div:first-child > div ul { | 78 | .markdown > div:first-child > div ul { |
| 45 | - height: 15em; | ||
| 46 | - list-style-type: none; | ||
| 47 | - margin: 2px 0px; | ||
| 48 | - overflow-y: auto; | ||
| 49 | - padding-left: 0px; | ||
| 50 | - position: absolute; | ||
| 51 | - scroll-behavior: auto; | ||
| 52 | - top: 1.8em; | 79 | + height: 15em; |
| 80 | + list-style-type: none; | ||
| 81 | + margin: 2px 0px; | ||
| 82 | + overflow-y: auto; | ||
| 83 | + padding-left: 0px; | ||
| 84 | + position: absolute; | ||
| 85 | + scroll-behavior: auto; | ||
| 86 | + top: 1.8em; | ||
| 53 | } | 87 | } |
| 54 | 88 | ||
| 55 | .markdown blockquote { | 89 | .markdown blockquote { |
| 56 | - border-left: 5px solid #ccc; | ||
| 57 | - margin: .5em 10px; | ||
| 58 | - padding: .5em 10px; | 90 | + border-left: 5px solid #ccc; |
| 91 | + margin: .5em 10px; | ||
| 92 | + padding: .5em 10px; | ||
| 59 | } | 93 | } |
| 60 | 94 | ||
| 61 | .markdown blockquote p { | 95 | .markdown blockquote p { |
| 62 | - text-align: left; | ||
| 63 | - text-indent: 0; | ||
| 64 | - margin-block: 0; | 96 | + text-align: left; |
| 97 | + text-indent: 0; | ||
| 98 | + margin-block: 0; | ||
| 65 | } | 99 | } |
| 66 | 100 | ||
| 67 | .markdown .footnote-definition > * { | 101 | .markdown .footnote-definition > * { |
| 68 | - display: inline; | 102 | + display: inline; |
| 103 | +} | ||
| 104 | + | ||
| 105 | +@keyframes spinner { | ||
| 106 | + 0% { | ||
| 107 | + transform: translate3d(-50%, -50%, 0) rotate(0deg); | ||
| 108 | + } | ||
| 109 | + 100% { | ||
| 110 | + transform: translate3d(-50%, -50%, 0) rotate(360deg); | ||
| 111 | + } | ||
| 112 | +} | ||
| 113 | + | ||
| 114 | +.spin::before { | ||
| 115 | + animation: 1.5s linear infinite spinner; | ||
| 116 | + animation-play-state: inherit; | ||
| 117 | + border: solid 5px #cfd0d1; | ||
| 118 | + border-bottom-color: #1c87c9; | ||
| 119 | + border-radius: 50%; | ||
| 120 | + content: ""; | ||
| 121 | + height: 1.5em; | ||
| 122 | + width: 1.5em; | ||
| 123 | + position: absolute; | ||
| 124 | + top: 10%; | ||
| 125 | + left: 1.5em; | ||
| 126 | + transform: translate3d(-50%, -50%, 0); | ||
| 127 | + will-change: transform; | ||
| 69 | } | 128 | } |
| @@ -36,20 +36,28 @@ version = "^0.5" | @@ -36,20 +36,28 @@ version = "^0.5" | ||
| 36 | [dependencies.web-sys] | 36 | [dependencies.web-sys] |
| 37 | version = "^0.3" | 37 | version = "^0.3" |
| 38 | features = [ | 38 | features = [ |
| 39 | - "Document", | ||
| 40 | - "DomParser", | ||
| 41 | - "Headers", | ||
| 42 | - "HtmlElement", | ||
| 43 | - "HtmlInputElement", | ||
| 44 | - "MouseEvent", | ||
| 45 | - "Node", | ||
| 46 | - "Request", | ||
| 47 | - "RequestInit", | ||
| 48 | - "RequestMode", | ||
| 49 | - "Response", | ||
| 50 | - "SupportedType", | ||
| 51 | - "Window", | ||
| 52 | -] | 39 | + "Blob", |
| 40 | + "CanvasRenderingContext2d", | ||
| 41 | + "Document", | ||
| 42 | + "DomParser", | ||
| 43 | + "File", | ||
| 44 | + "FileList", | ||
| 45 | + "FileReader", | ||
| 46 | + "ImageBitmap", | ||
| 47 | + "Headers", | ||
| 48 | + "HtmlCanvasElement", | ||
| 49 | + "HtmlElement", | ||
| 50 | + "HtmlInputElement", | ||
| 51 | + "MouseEvent", | ||
| 52 | + "Node", | ||
| 53 | + "ReadableStream", | ||
| 54 | + "Request", | ||
| 55 | + "RequestInit", | ||
| 56 | + "RequestMode", | ||
| 57 | + "Response", | ||
| 58 | + "SupportedType", | ||
| 59 | + "Window", | ||
| 60 | + ] | ||
| 53 | 61 | ||
| 54 | [dev-dependencies] | 62 | [dev-dependencies] |
| 55 | wasm-bindgen-test = "0.2" | 63 | wasm-bindgen-test = "0.2" |
| @@ -66,17 +66,6 @@ impl Markdown { | @@ -66,17 +66,6 @@ impl Markdown { | ||
| 66 | } | 66 | } |
| 67 | } | 67 | } |
| 68 | 68 | ||
| 69 | - pub(crate) fn _to_html_string(&self) -> String { | ||
| 70 | - use pulldown_cmark::{Parser, Options, html}; | ||
| 71 | - | ||
| 72 | - let mut html_out = String::new(); | ||
| 73 | - let parser = Parser::new_ext(&self.json.content, Options::all()); | ||
| 74 | - | ||
| 75 | - html::push_html(&mut html_out, parser); | ||
| 76 | - html_out | ||
| 77 | - } | ||
| 78 | - | ||
| 79 | - | ||
| 80 | fn status_error<I: Display>(status :I) -> Error { | 69 | fn status_error<I: Display>(status :I) -> Error { |
| 81 | let err_str = format!("Invalid response status: {}", status); | 70 | let err_str = format!("Invalid response status: {}", status); |
| 82 | Error::from(err_str.as_str()) | 71 | Error::from(err_str.as_str()) |
ui/src/api/upload.rs
0 → 100644
| 1 | +use std::fmt::Display; | ||
| 2 | + | ||
| 3 | +use super::super::error::*; | ||
| 4 | +use super::super::client::Client; | ||
| 5 | +use crate::upload::upload::Upload; | ||
| 6 | + | ||
| 7 | +#[derive(Debug, Clone)] | ||
| 8 | +pub struct UploadApi { | ||
| 9 | + client :Client, | ||
| 10 | +} | ||
| 11 | + | ||
| 12 | +impl UploadApi { | ||
| 13 | + pub(crate) async fn new() -> Result<UploadApi> { | ||
| 14 | + let client = Client::new()?; | ||
| 15 | + Ok(UploadApi { client }) | ||
| 16 | + } | ||
| 17 | + | ||
| 18 | + pub(crate) async fn store(&self, upload :&Upload) -> Result<&UploadApi> { | ||
| 19 | + let response = self.client.post_stream( "/api/v0/upload" | ||
| 20 | + , &upload.mime_type() | ||
| 21 | + , upload.data() ).await?; | ||
| 22 | + match response.status() { | ||
| 23 | + 200 => Ok(self), | ||
| 24 | + status => Err(Self::status_error(status)), | ||
| 25 | + } | ||
| 26 | + } | ||
| 27 | + | ||
| 28 | + fn status_error<I: Display>(status :I) -> Error { | ||
| 29 | + let err_str = format!("Invalid response status: {}", status); | ||
| 30 | + Error::from(err_str.as_str()) | ||
| 31 | + } | ||
| 32 | +} |
| 1 | use js_sys::JsString; | 1 | use js_sys::JsString; |
| 2 | use mogwai::prelude::*; | 2 | use mogwai::prelude::*; |
| 3 | use wasm_bindgen::prelude::*; | 3 | use wasm_bindgen::prelude::*; |
| 4 | -use web_sys::{Window, window, Response, Request, RequestInit, RequestMode}; | 4 | +use web_sys::{Window, window, Response, Request, RequestInit, RequestMode, ReadableStream}; |
| 5 | use super::error::*; | 5 | use super::error::*; |
| 6 | 6 | ||
| 7 | use std::result::Result as StdResult; | 7 | use std::result::Result as StdResult; |
| @@ -25,7 +25,7 @@ impl Client { | @@ -25,7 +25,7 @@ impl Client { | ||
| 25 | 25 | ||
| 26 | pub async fn get(&self, url :&str) -> Result<(Response, String)> { | 26 | pub async fn get(&self, url :&str) -> Result<(Response, String)> { |
| 27 | let mut init = RequestInit::new(); | 27 | let mut init = RequestInit::new(); |
| 28 | - let request = REQUEST( &url | 28 | + let request = REQUEST( url |
| 29 | , init . method("GET") | 29 | , init . method("GET") |
| 30 | . mode(RequestMode::Cors) )?; | 30 | . mode(RequestMode::Cors) )?; |
| 31 | 31 | ||
| @@ -46,7 +46,7 @@ impl Client { | @@ -46,7 +46,7 @@ impl Client { | ||
| 46 | 46 | ||
| 47 | pub async fn put(&self, url :&str, data :&str) -> Result<Response> { | 47 | pub async fn put(&self, url :&str, data :&str) -> Result<Response> { |
| 48 | let mut init = RequestInit::new(); | 48 | let mut init = RequestInit::new(); |
| 49 | - let request = REQUEST( &url | 49 | + let request = REQUEST( url |
| 50 | , init . method("PUT") | 50 | , init . method("PUT") |
| 51 | . mode(RequestMode::Cors) | 51 | . mode(RequestMode::Cors) |
| 52 | . body(Some(&data.into())) )?; | 52 | . body(Some(&data.into())) )?; |
| @@ -61,4 +61,25 @@ impl Client { | @@ -61,4 +61,25 @@ impl Client { | ||
| 61 | 61 | ||
| 62 | Ok(response) | 62 | Ok(response) |
| 63 | } | 63 | } |
| 64 | + | ||
| 65 | + pub async fn post_stream( &self | ||
| 66 | + , url :&str | ||
| 67 | + , mime_type :&str | ||
| 68 | + , data :ReadableStream) -> Result<Response> { | ||
| 69 | + let mut init = RequestInit::new(); | ||
| 70 | + let request = REQUEST( url | ||
| 71 | + , init . method("POST") | ||
| 72 | + . mode(RequestMode::Cors) | ||
| 73 | + . body(Some(&data.into())) )?; | ||
| 74 | + | ||
| 75 | + request . headers() | ||
| 76 | + . set("Content-Type", mime_type)?; | ||
| 77 | + | ||
| 78 | + let response = JsFuture::from( self.window | ||
| 79 | + . fetch_with_request(&request)) | ||
| 80 | + . await? | ||
| 81 | + . dyn_into::<Response>()?; | ||
| 82 | + | ||
| 83 | + Ok(response) | ||
| 84 | + } | ||
| 64 | } | 85 | } |
ui/src/component/upload/logic.rs
0 → 100644
| 1 | +use mogwai::prelude::*; | ||
| 2 | +use web_sys::{HtmlInputElement, HtmlCanvasElement, CanvasRenderingContext2d}; | ||
| 3 | + | ||
| 4 | +use crate::api::upload::UploadApi; | ||
| 5 | + | ||
| 6 | +use super::upload::Upload; | ||
| 7 | + | ||
| 8 | +#[derive(Clone, Debug)] | ||
| 9 | +pub(super) enum UploadLogic { | ||
| 10 | + Add(DomEvent), | ||
| 11 | + Remove(usize), | ||
| 12 | + Upload, | ||
| 13 | +} | ||
| 14 | + | ||
| 15 | +pub(super) async fn upload_preview_logic( mut rx_canvas :broadcast::Receiver<Dom> | ||
| 16 | + , mut rx_click :broadcast::Receiver<DomEvent> | ||
| 17 | + , upload :Upload ) { | ||
| 18 | + if let Some(dom) = rx_canvas.next().await { | ||
| 19 | + if let Either::Left(c) = dom.inner_read() { | ||
| 20 | + let canvas = c.to_owned().dyn_into::<HtmlCanvasElement>().unwrap(); | ||
| 21 | + let context = canvas | ||
| 22 | + . get_context("2d").unwrap().unwrap() | ||
| 23 | + . dyn_into::<CanvasRenderingContext2d>().unwrap(); | ||
| 24 | + context | ||
| 25 | + . draw_image_with_image_bitmap_and_dw_and_dh( | ||
| 26 | + &upload.bitmap() | ||
| 27 | + , 0.0, 0.0 | ||
| 28 | + , canvas.width() as f64, canvas.height() as f64 ) | ||
| 29 | + . unwrap(); | ||
| 30 | + } | ||
| 31 | + } | ||
| 32 | + | ||
| 33 | + while let Some(_) = rx_click.next().await { | ||
| 34 | + upload.tx_logic . try_broadcast(UploadLogic::Remove(upload.id)) | ||
| 35 | + . unwrap(); | ||
| 36 | + } | ||
| 37 | +} | ||
| 38 | + | ||
| 39 | +pub(super) async fn upload_logic( mut rx_logic :broadcast::Receiver<UploadLogic> | ||
| 40 | + , tx_logic :broadcast::Sender<UploadLogic> | ||
| 41 | + , tx_previews: mpmc::Sender<ListPatch<ViewBuilder<Dom>>> | ||
| 42 | + ) { | ||
| 43 | + let mut uploads: ListPatchModel<Upload> = ListPatchModel::new(); | ||
| 44 | + let api = UploadApi::new().await.unwrap(); | ||
| 45 | + | ||
| 46 | + mogwai::spawn(uploads.stream().for_each(move |patch| { | ||
| 47 | + let patch = patch.map(|u| u.into()); | ||
| 48 | + let tx_previews = tx_previews.clone(); | ||
| 49 | + async move { | ||
| 50 | + tx_previews.send(patch).await.unwrap(); | ||
| 51 | + } | ||
| 52 | + })); | ||
| 53 | + | ||
| 54 | + let mut next_id = 0; | ||
| 55 | + | ||
| 56 | + while let Some(msg) = rx_logic.next().await { | ||
| 57 | + match msg { | ||
| 58 | + UploadLogic::Add(event) => | ||
| 59 | + if let Either::Left(inner) = event.clone_inner() { | ||
| 60 | + let filelist = inner.dyn_into::<Event>().unwrap() | ||
| 61 | + . target().unwrap() | ||
| 62 | + . dyn_into::<HtmlInputElement>().unwrap() | ||
| 63 | + . files().unwrap(); | ||
| 64 | + | ||
| 65 | + for index in 0..filelist.length() { | ||
| 66 | + let file = filelist.item(index).unwrap(); | ||
| 67 | + let tx_logic = tx_logic.clone(); | ||
| 68 | + uploads.list_patch_push( Upload::new(next_id, file, tx_logic) | ||
| 69 | + . await); | ||
| 70 | + next_id += 1; | ||
| 71 | + } | ||
| 72 | + }, | ||
| 73 | + UploadLogic::Remove(id) => { | ||
| 74 | + let mut found = None; | ||
| 75 | + | ||
| 76 | + for (upload, index) in uploads.read().await.iter().zip(0..) { | ||
| 77 | + if upload.id == id { | ||
| 78 | + found = Some(index); | ||
| 79 | + break; | ||
| 80 | + } | ||
| 81 | + } | ||
| 82 | + | ||
| 83 | + if let Some(index) = found { | ||
| 84 | + uploads.list_patch_remove(index).unwrap(); | ||
| 85 | + } | ||
| 86 | + }, | ||
| 87 | + UploadLogic::Upload => { | ||
| 88 | + for upload in uploads.read().await.iter() { | ||
| 89 | + match api.store(upload).await { | ||
| 90 | + Ok(_) => (), | ||
| 91 | + Err(e) => log::error!("{:?}", e), | ||
| 92 | + } | ||
| 93 | + } | ||
| 94 | + } | ||
| 95 | + } | ||
| 96 | + } | ||
| 97 | +} |
ui/src/component/upload/mod.rs
0 → 100644
| 1 | +mod logic; | ||
| 2 | +pub(crate) mod upload; | ||
| 3 | +mod view; | ||
| 4 | + | ||
| 5 | +use mogwai::prelude::*; | ||
| 6 | + | ||
| 7 | +use self::{view::upload_view, logic::upload_logic}; | ||
| 8 | + | ||
| 9 | +pub(crate) async fn new() -> Component<Dom> { | ||
| 10 | + let (tx_logic, rx_logic) = broadcast::bounded(1); | ||
| 11 | + let (tx_previews, rx_previews) = mpmc::bounded(1); | ||
| 12 | + | ||
| 13 | + let view = upload_view(tx_logic.clone(), rx_previews); | ||
| 14 | + let logic = upload_logic(rx_logic, tx_logic, tx_previews); | ||
| 15 | + | ||
| 16 | + Component::from(view).with_logic(logic) | ||
| 17 | +} |
ui/src/component/upload/upload.rs
0 → 100644
| 1 | +use mogwai::{prelude::*, utils::window}; | ||
| 2 | +use web_sys::{File, ImageBitmap, ReadableStream}; | ||
| 3 | + | ||
| 4 | +use super::{view::upload_preview_view, logic::{upload_preview_logic, UploadLogic}}; | ||
| 5 | + | ||
| 6 | +#[derive(Clone, Debug)] | ||
| 7 | +pub(crate) struct Upload { | ||
| 8 | + pub(super) id :usize, | ||
| 9 | + file :File, | ||
| 10 | + bitmap :ImageBitmap, | ||
| 11 | + pub(super) tx_logic :broadcast::Sender<UploadLogic>, | ||
| 12 | +} | ||
| 13 | + | ||
| 14 | +impl Upload { | ||
| 15 | + pub(super) async fn new( id :usize | ||
| 16 | + , file :File | ||
| 17 | + , tx_logic :broadcast::Sender<UploadLogic> | ||
| 18 | + ) -> Upload { | ||
| 19 | + let bitmap = window() | ||
| 20 | + . create_image_bitmap_with_blob(&file.clone().into()) | ||
| 21 | + . unwrap(); | ||
| 22 | + let bitmap = JsFuture::from(bitmap) | ||
| 23 | + . await.unwrap() | ||
| 24 | + . dyn_into::<ImageBitmap>().unwrap(); | ||
| 25 | + | ||
| 26 | + Self { id, file, bitmap, tx_logic } | ||
| 27 | + } | ||
| 28 | + | ||
| 29 | + pub(crate) fn mime_type(&self) -> String { | ||
| 30 | + self.file.type_() | ||
| 31 | + } | ||
| 32 | + | ||
| 33 | + pub(crate) fn data(&self) -> ReadableStream { | ||
| 34 | + self.file.stream() | ||
| 35 | + } | ||
| 36 | + | ||
| 37 | + pub(super) fn bitmap(&self) -> ImageBitmap { | ||
| 38 | + self.to_owned().bitmap | ||
| 39 | + } | ||
| 40 | +} | ||
| 41 | + | ||
| 42 | +impl From<Upload> for Component<Dom> { | ||
| 43 | + fn from(upload: Upload) -> Self { | ||
| 44 | + let (tx_canvas, rx_canvas) = broadcast::bounded(1); | ||
| 45 | + let (tx_click, rx_click) = broadcast::bounded(1); | ||
| 46 | + | ||
| 47 | + let view = upload_preview_view( tx_canvas | ||
| 48 | + , tx_click | ||
| 49 | + , upload.file.name() | ||
| 50 | + , upload.file.size() | ||
| 51 | + , upload.file.type_() | ||
| 52 | + , upload.file.last_modified() ); | ||
| 53 | + let logic = upload_preview_logic(rx_canvas, rx_click, upload); | ||
| 54 | + | ||
| 55 | + Component::from(view).with_logic(logic) | ||
| 56 | + } | ||
| 57 | +} | ||
| 58 | + | ||
| 59 | +impl From<Upload> for ViewBuilder<Dom> { | ||
| 60 | + fn from(upload: Upload) -> Self { | ||
| 61 | + let component :Component<Dom> = upload.into(); | ||
| 62 | + component.into() | ||
| 63 | + } | ||
| 64 | +} |
ui/src/component/upload/view.rs
0 → 100644
| 1 | +use mogwai::prelude::*; | ||
| 2 | + | ||
| 3 | +use crate::component::upload::logic::UploadLogic; | ||
| 4 | + | ||
| 5 | +pub(super) fn upload_preview_view( tx_canvas :broadcast::Sender<Dom> | ||
| 6 | + , tx_click :broadcast::Sender<DomEvent> | ||
| 7 | + , filename :String | ||
| 8 | + , size :f64 | ||
| 9 | + , mime_type :String | ||
| 10 | + , mtime :f64 | ||
| 11 | + ) -> ViewBuilder<Dom> { | ||
| 12 | + let post_build = move |dom: &mut Dom| { | ||
| 13 | + tx_canvas.try_broadcast(dom.clone()).unwrap(); | ||
| 14 | + }; | ||
| 15 | + | ||
| 16 | + builder! { | ||
| 17 | + <li style:display="flex" | ||
| 18 | + on:click=tx_click.sink()> | ||
| 19 | + <canvas width="75px" | ||
| 20 | + height="75px" | ||
| 21 | + post:build=post_build /> | ||
| 22 | + <ul> | ||
| 23 | + <li>{format!("filename: {}", filename)}</li> | ||
| 24 | + <li>{format!("size: {}", size)}</li> | ||
| 25 | + <li>{format!("mime type: {}", mime_type)}</li> | ||
| 26 | + <li>{format!("modification time: {}", mtime)}</li> | ||
| 27 | + </ul> | ||
| 28 | + </li> | ||
| 29 | + } | ||
| 30 | +} | ||
| 31 | + | ||
| 32 | +pub(super) fn upload_view( tx_logic: broadcast::Sender<UploadLogic> | ||
| 33 | + , rx_previews: mpmc::Receiver<ListPatch<ViewBuilder<Dom>>> | ||
| 34 | + ) -> ViewBuilder<Dom> { | ||
| 35 | + let select_filter = tx_logic.sink() | ||
| 36 | + . contra_map(|e| UploadLogic::Add(e)); | ||
| 37 | + let upload_filter = tx_logic.sink() | ||
| 38 | + . contra_map(|_| UploadLogic::Upload); | ||
| 39 | + | ||
| 40 | +// <div class="spin"></div> | ||
| 41 | + builder! { | ||
| 42 | + <div class="upload"> | ||
| 43 | + <div> | ||
| 44 | + <input type="file" | ||
| 45 | + multiple="multiple" | ||
| 46 | + accept="image/*" | ||
| 47 | + on:change=select_filter /> | ||
| 48 | + <button on:click=upload_filter>"Upload"</button> | ||
| 49 | + </div> | ||
| 50 | + <ul patch:children=rx_previews> | ||
| 51 | + </ul> | ||
| 52 | + </div> | ||
| 53 | + } | ||
| 54 | +} |
| @@ -6,7 +6,7 @@ mod component; | @@ -6,7 +6,7 @@ mod component; | ||
| 6 | 6 | ||
| 7 | use std::panic; | 7 | use std::panic; |
| 8 | 8 | ||
| 9 | -use crate::component::markdown; | 9 | +use crate::component::*; |
| 10 | use log::Level; | 10 | use log::Level; |
| 11 | use mogwai::prelude::*; | 11 | use mogwai::prelude::*; |
| 12 | use wasm_bindgen::prelude::*; | 12 | use wasm_bindgen::prelude::*; |
| @@ -16,8 +16,14 @@ pub async fn main() -> Result<(), JsValue> { | @@ -16,8 +16,14 @@ pub async fn main() -> Result<(), JsValue> { | ||
| 16 | panic::set_hook(Box::new(console_error_panic_hook::hook)); | 16 | panic::set_hook(Box::new(console_error_panic_hook::hook)); |
| 17 | console_log::init_with_level(Level::Trace).unwrap(); | 17 | console_log::init_with_level(Level::Trace).unwrap(); |
| 18 | 18 | ||
| 19 | - let comp = markdown::new().await; | 19 | + let md = markdown::new().await; |
| 20 | + let comp = upload::new().await; | ||
| 20 | 21 | ||
| 21 | - let page = Component::from(builder! {{comp}}); | 22 | + let page = Component::from(builder! { |
| 23 | + <div> | ||
| 24 | + {comp} | ||
| 25 | + {md} | ||
| 26 | + </div> | ||
| 27 | + }); | ||
| 22 | page.build()?.run() | 28 | page.build()?.run() |
| 23 | } | 29 | } |
Please
register
or
login
to post a comment