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