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 | 15 | aktuellen Version auf frühere Stände zurück gehen und diese gegebenenfalls |
| 16 | 16 | noch anpassen. Dieser Stand wird wieder ein neuer Patch gegen die gegenwärtige |
| 17 | 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 | 22 | ## Restful API |
| 23 | 23 | [ReadTheDocs apiguide](https://apiguide.readthedocs.io/en/latest/build_and_publish/use_RESTful_urls.html 'ReadTheDocs apiguide') |
| 24 | 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 | 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 | 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 | 49 | .markdown > div:first-child { |
| 16 | - position: fixed; | |
| 50 | + position: fixed; | |
| 17 | 51 | width: inherit; |
| 18 | 52 | z-index: 10; |
| 19 | 53 | } |
| 20 | 54 | |
| 21 | 55 | .markdown > div:first-child > div { |
| 22 | 56 | position: relative; |
| 23 | - left: .5em; | |
| 57 | + left: .5em; | |
| 24 | 58 | width: inherit; |
| 25 | 59 | } |
| 26 | 60 | |
| 27 | 61 | .markdown > div:first-child > pre { |
| 28 | - white-space: pre-wrap; | |
| 29 | - height: 15em; | |
| 62 | + white-space: pre-wrap; | |
| 63 | + height: 15em; | |
| 30 | 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 | 71 | opacity: .95; |
| 38 | 72 | } |
| 39 | 73 | |
| 40 | 74 | .markdown > div:first-child > div > div { |
| 41 | - display: inline; | |
| 75 | + display: inline; | |
| 42 | 76 | } |
| 43 | 77 | |
| 44 | 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 | 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 | 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 | 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 | 36 | [dependencies.web-sys] |
| 37 | 37 | version = "^0.3" |
| 38 | 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 | 62 | [dev-dependencies] |
| 55 | 63 | wasm-bindgen-test = "0.2" | ... | ... |
| ... | ... | @@ -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 | 69 | fn status_error<I: Display>(status :I) -> Error { |
| 81 | 70 | let err_str = format!("Invalid response status: {}", status); |
| 82 | 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 | 1 | use js_sys::JsString; |
| 2 | 2 | use mogwai::prelude::*; |
| 3 | 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 | 5 | use super::error::*; |
| 6 | 6 | |
| 7 | 7 | use std::result::Result as StdResult; |
| ... | ... | @@ -25,7 +25,7 @@ impl Client { |
| 25 | 25 | |
| 26 | 26 | pub async fn get(&self, url :&str) -> Result<(Response, String)> { |
| 27 | 27 | let mut init = RequestInit::new(); |
| 28 | - let request = REQUEST( &url | |
| 28 | + let request = REQUEST( url | |
| 29 | 29 | , init . method("GET") |
| 30 | 30 | . mode(RequestMode::Cors) )?; |
| 31 | 31 | |
| ... | ... | @@ -46,7 +46,7 @@ impl Client { |
| 46 | 46 | |
| 47 | 47 | pub async fn put(&self, url :&str, data :&str) -> Result<Response> { |
| 48 | 48 | let mut init = RequestInit::new(); |
| 49 | - let request = REQUEST( &url | |
| 49 | + let request = REQUEST( url | |
| 50 | 50 | , init . method("PUT") |
| 51 | 51 | . mode(RequestMode::Cors) |
| 52 | 52 | . body(Some(&data.into())) )?; |
| ... | ... | @@ -61,4 +61,25 @@ impl Client { |
| 61 | 61 | |
| 62 | 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 | 6 | |
| 7 | 7 | use std::panic; |
| 8 | 8 | |
| 9 | -use crate::component::markdown; | |
| 9 | +use crate::component::*; | |
| 10 | 10 | use log::Level; |
| 11 | 11 | use mogwai::prelude::*; |
| 12 | 12 | use wasm_bindgen::prelude::*; |
| ... | ... | @@ -16,8 +16,14 @@ pub async fn main() -> Result<(), JsValue> { |
| 16 | 16 | panic::set_hook(Box::new(console_error_panic_hook::hook)); |
| 17 | 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 | 28 | page.build()?.run() |
| 23 | 29 | } | ... | ... |
Please
register
or
login
to post a comment