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