add image upload feature in dev mode
This commit is contained in:
		
							parent
							
								
									47c2b74ecb
								
							
						
					
					
						commit
						0ce7f50285
					
				
					 11 changed files with 369 additions and 15 deletions
				
			
		
							
								
								
									
										13
									
								
								Cargo.lock
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										13
									
								
								Cargo.lock
									
										
									
										generated
									
									
									
								
							| 
						 | 
				
			
			@ -179,6 +179,7 @@ checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f"
 | 
			
		|||
dependencies = [
 | 
			
		||||
 "async-trait",
 | 
			
		||||
 "axum-core",
 | 
			
		||||
 "axum-macros",
 | 
			
		||||
 "bytes",
 | 
			
		||||
 "futures-util",
 | 
			
		||||
 "http",
 | 
			
		||||
| 
						 | 
				
			
			@ -226,6 +227,17 @@ dependencies = [
 | 
			
		|||
 "tracing",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "axum-macros"
 | 
			
		||||
version = "0.4.2"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "57d123550fa8d071b7255cb0cc04dc302baa6c8c4a79f55701552684d8399bce"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "proc-macro2",
 | 
			
		||||
 "quote",
 | 
			
		||||
 "syn",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "backtrace"
 | 
			
		||||
version = "0.3.74"
 | 
			
		||||
| 
						 | 
				
			
			@ -2131,6 +2143,7 @@ version = "0.1.0"
 | 
			
		|||
dependencies = [
 | 
			
		||||
 "anyhow",
 | 
			
		||||
 "axum",
 | 
			
		||||
 "axum-macros",
 | 
			
		||||
 "base64",
 | 
			
		||||
 "blake3",
 | 
			
		||||
 "chrono",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										40
									
								
								content/treehouse/dev/tools.tree
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								content/treehouse/dev/tools.tree
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,40 @@
 | 
			
		|||
%% title = "developer tools"
 | 
			
		||||
styles = ["page/treehouse/dev/tools.css"]
 | 
			
		||||
scripts = ["treehouse/dev/picture-upload.js"]
 | 
			
		||||
 | 
			
		||||
% id = "01JEHDJSJP282VCTRKYHNFM4N7"
 | 
			
		||||
- welcome! if you stumbled upon this page at random, know these tools are available in *debug builds only* (which <https://liquidex.house> is not.)
 | 
			
		||||
 | 
			
		||||
    % id = "01JEHDJSJP7FT74RB92VRA14F2"
 | 
			
		||||
    - I don't currently have an option to disable generating a page in release builds, so here you are.
 | 
			
		||||
 | 
			
		||||
% id = "01JEHDJSJPPJP2HFY4DFW0G3Z0"
 | 
			
		||||
- #### picture upload
 | 
			
		||||
 | 
			
		||||
    % id = "01JEHDJSJP78Y3FFAVJ9EZPPAQ"
 | 
			
		||||
    - ``` =html
 | 
			
		||||
    <th-picture-upload></th-picture-upload>
 | 
			
		||||
    ```
 | 
			
		||||
 | 
			
		||||
        % id = "01JEHF34KV8NMSHBWN6KJ2JGWT"
 | 
			
		||||
        + label tags
 | 
			
		||||
 | 
			
		||||
            % id = "01JEHF34KVXJM3PW32CCTG9MJ9"
 | 
			
		||||
            - `+pixel` - `image-rendering: pixelated; border-radius: 0;`
 | 
			
		||||
 | 
			
		||||
            % id = "01JEHF34KVCS6HPEPW30RCTE2Q"
 | 
			
		||||
            - `+width72`
 | 
			
		||||
 | 
			
		||||
            % id = "01JEHF34KV1J9SWV2PRYJK1PEM"
 | 
			
		||||
            - `+width160`
 | 
			
		||||
 | 
			
		||||
            % id = "01JEHF34KVEDE5YFXRZ544X1FH"
 | 
			
		||||
            - `+width640`
 | 
			
		||||
 | 
			
		||||
            % id = "01JEHF34KVRMK1DVEM5QGV1FB3"
 | 
			
		||||
            - `+width752`
 | 
			
		||||
 | 
			
		||||
        + compression levels
 | 
			
		||||
 | 
			
		||||
            - `lossless` - use for screenshots and other pictures that should not lose quality
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -8,7 +8,8 @@ edition = "2021"
 | 
			
		|||
treehouse-format = { workspace = true }
 | 
			
		||||
 | 
			
		||||
anyhow = "1.0.75"
 | 
			
		||||
axum = "0.7.4"
 | 
			
		||||
axum = { version = "0.7.9", features = ["macros"] }
 | 
			
		||||
axum-macros = "0.4.2"
 | 
			
		||||
base64 = "0.21.7"
 | 
			
		||||
blake3 = "1.5.3"
 | 
			
		||||
chrono = { version = "0.4.35", features = ["serde"] }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -166,12 +166,12 @@ pub fn fix_file_cli(fix_args: FixArgs, root: &dyn Dir) -> anyhow::Result<Edit> {
 | 
			
		|||
                    Edit::Seq(vec![
 | 
			
		||||
                        Edit::Write(
 | 
			
		||||
                            backup_edit_path,
 | 
			
		||||
                            treehouse.source(file_id).input().to_owned(),
 | 
			
		||||
                            treehouse.source(file_id).input().to_owned().into(),
 | 
			
		||||
                        ),
 | 
			
		||||
                        Edit::Write(edit_path, fixed),
 | 
			
		||||
                        Edit::Write(edit_path, fixed.into()),
 | 
			
		||||
                    ])
 | 
			
		||||
                } else {
 | 
			
		||||
                    Edit::Write(edit_path, fixed)
 | 
			
		||||
                    Edit::Write(edit_path, fixed.into())
 | 
			
		||||
                }
 | 
			
		||||
            } else {
 | 
			
		||||
                println!("{fixed}");
 | 
			
		||||
| 
						 | 
				
			
			@ -201,7 +201,7 @@ pub fn fix_all_cli(fix_all_args: FixAllArgs, dir: &dyn Dir) -> anyhow::Result<Ed
 | 
			
		|||
 | 
			
		||||
            if let Ok(fixed) = fix_file(&mut treehouse, &mut diagnostics, file_id) {
 | 
			
		||||
                if fixed != treehouse.source(file_id).input() {
 | 
			
		||||
                    return Ok(Edit::Write(edit_path, fixed));
 | 
			
		||||
                    return Ok(Edit::Write(edit_path, fixed.into()));
 | 
			
		||||
                }
 | 
			
		||||
            } else {
 | 
			
		||||
                report_diagnostics(&treehouse, &diagnostics)?;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,6 @@
 | 
			
		|||
#[cfg(debug_assertions)]
 | 
			
		||||
mod live_reload;
 | 
			
		||||
mod picture_upload;
 | 
			
		||||
 | 
			
		||||
use std::fmt::Write;
 | 
			
		||||
use std::{net::Ipv4Addr, sync::Arc};
 | 
			
		||||
| 
						 | 
				
			
			@ -18,6 +19,7 @@ use serde::Deserialize;
 | 
			
		|||
use tokio::net::TcpListener;
 | 
			
		||||
use tracing::{info, instrument};
 | 
			
		||||
 | 
			
		||||
use crate::dirs::Dirs;
 | 
			
		||||
use crate::sources::Sources;
 | 
			
		||||
use crate::vfs::asynch::AsyncDir;
 | 
			
		||||
use crate::vfs::VPath;
 | 
			
		||||
| 
						 | 
				
			
			@ -36,17 +38,27 @@ struct Server {
 | 
			
		|||
    target: AsyncDir,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[instrument(skip(sources, target))]
 | 
			
		||||
pub async fn serve(sources: Arc<Sources>, target: AsyncDir, port: u16) -> anyhow::Result<()> {
 | 
			
		||||
#[instrument(skip(sources, dirs, target))]
 | 
			
		||||
pub async fn serve(
 | 
			
		||||
    sources: Arc<Sources>,
 | 
			
		||||
    dirs: Arc<Dirs>,
 | 
			
		||||
    target: AsyncDir,
 | 
			
		||||
    port: u16,
 | 
			
		||||
) -> anyhow::Result<()> {
 | 
			
		||||
    let app = Router::new()
 | 
			
		||||
        .route("/", get(index)) // needed explicitly because * does not match empty paths
 | 
			
		||||
        .route("/*path", get(vfs_entry))
 | 
			
		||||
        .route("/b", get(branch))
 | 
			
		||||
        .fallback(get(four_oh_four))
 | 
			
		||||
        .with_state(Arc::new(Server { sources, target }));
 | 
			
		||||
        .with_state(Arc::new(Server {
 | 
			
		||||
            sources: sources.clone(),
 | 
			
		||||
            target,
 | 
			
		||||
        }));
 | 
			
		||||
 | 
			
		||||
    #[cfg(debug_assertions)]
 | 
			
		||||
    let app = app.nest("/dev/live-reload", live_reload::router());
 | 
			
		||||
    let app = app
 | 
			
		||||
        .nest("/dev/live-reload", live_reload::router())
 | 
			
		||||
        .nest("/dev/picture-upload", picture_upload::router(dirs));
 | 
			
		||||
 | 
			
		||||
    info!("serving on port {port}");
 | 
			
		||||
    let listener = TcpListener::bind((Ipv4Addr::from([0u8, 0, 0, 0]), port)).await?;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										84
									
								
								crates/treehouse/src/cli/serve/picture_upload.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								crates/treehouse/src/cli/serve/picture_upload.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,84 @@
 | 
			
		|||
use std::sync::Arc;
 | 
			
		||||
 | 
			
		||||
use axum::{
 | 
			
		||||
    body::Bytes,
 | 
			
		||||
    debug_handler,
 | 
			
		||||
    extract::{Query, State},
 | 
			
		||||
    response::IntoResponse,
 | 
			
		||||
    routing::post,
 | 
			
		||||
    Json, Router,
 | 
			
		||||
};
 | 
			
		||||
use serde::{Deserialize, Serialize};
 | 
			
		||||
 | 
			
		||||
use crate::{
 | 
			
		||||
    dirs::Dirs,
 | 
			
		||||
    vfs::{self, Edit, EditPath, VPathBuf},
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
pub fn router<S>(dirs: Arc<Dirs>) -> Router<S> {
 | 
			
		||||
    Router::new()
 | 
			
		||||
        .route("/", post(picture_upload))
 | 
			
		||||
        .with_state(dirs)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Deserialize)]
 | 
			
		||||
struct PictureUpload {
 | 
			
		||||
    label: String,
 | 
			
		||||
    format: String,
 | 
			
		||||
    compression: Compression,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
 | 
			
		||||
enum Compression {
 | 
			
		||||
    Lossless,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Serialize)]
 | 
			
		||||
#[serde(rename_all = "camelCase")]
 | 
			
		||||
enum Response {
 | 
			
		||||
    Ulid(String),
 | 
			
		||||
    Error(String),
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[debug_handler]
 | 
			
		||||
async fn picture_upload(
 | 
			
		||||
    State(dirs): State<Arc<Dirs>>,
 | 
			
		||||
    Query(mut params): Query<PictureUpload>,
 | 
			
		||||
    image: Bytes,
 | 
			
		||||
) -> impl IntoResponse {
 | 
			
		||||
    let ulid = ulid::Generator::new()
 | 
			
		||||
        .generate_with_source(&mut rand::thread_rng())
 | 
			
		||||
        .expect("failed to generate ulid");
 | 
			
		||||
 | 
			
		||||
    if params.label.is_empty() {
 | 
			
		||||
        params.label = "untitled".into();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let file_name = VPathBuf::new(format!(
 | 
			
		||||
        "{ulid}-{}.{}",
 | 
			
		||||
        params.label,
 | 
			
		||||
        get_extension(¶ms.format).unwrap_or("unknown")
 | 
			
		||||
    ));
 | 
			
		||||
    let Some(edit_path) = vfs::query::<EditPath>(&dirs.pic, &file_name) else {
 | 
			
		||||
        return Json(Response::Error(format!("{file_name} is not editable")));
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    let result = match params.compression {
 | 
			
		||||
        Compression::Lossless => Edit::Write(edit_path, image.to_vec()).apply().await,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    Json(match result {
 | 
			
		||||
        Ok(()) => Response::Ulid(ulid.to_string()),
 | 
			
		||||
        Err(error) => Response::Error(error.to_string()),
 | 
			
		||||
    })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn get_extension(content_type: &str) -> Option<&'static str> {
 | 
			
		||||
    match content_type {
 | 
			
		||||
        "image/png" => Some("png"),
 | 
			
		||||
        "image/jpeg" => Some("jpg"),
 | 
			
		||||
        "image/svg+xml" => Some("svg"),
 | 
			
		||||
        "image/webp" => Some("webp"),
 | 
			
		||||
        _ => None,
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -75,11 +75,11 @@ async fn fallible_main(
 | 
			
		|||
        } => {
 | 
			
		||||
            let _span = info_span!("load").entered();
 | 
			
		||||
            let sources = Arc::new(Sources::load(&dirs).context("failed to load sources")?);
 | 
			
		||||
            let target = generate::target(dirs, sources.clone());
 | 
			
		||||
            let target = generate::target(dirs.clone(), sources.clone());
 | 
			
		||||
            drop(_span);
 | 
			
		||||
            drop(flush_guard);
 | 
			
		||||
 | 
			
		||||
            serve(sources, AsyncDir::new(target), serve_args.port).await?;
 | 
			
		||||
            serve(sources, dirs, AsyncDir::new(target), serve_args.port).await?;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        Command::Fix(fix_args) => fix_file_cli(fix_args, &*dirs.content)?.apply().await?,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -14,8 +14,8 @@ pub enum Edit {
 | 
			
		|||
    /// An edit that doesn't do anything.
 | 
			
		||||
    NoOp,
 | 
			
		||||
 | 
			
		||||
    /// Write the given string to a file.
 | 
			
		||||
    Write(EditPath, String),
 | 
			
		||||
    /// Write the given content to a file.
 | 
			
		||||
    Write(EditPath, Vec<u8>),
 | 
			
		||||
 | 
			
		||||
    /// Execute a sequence of edits in order.
 | 
			
		||||
    Seq(Vec<Edit>),
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -130,7 +130,8 @@ body,
 | 
			
		|||
pre,
 | 
			
		||||
code,
 | 
			
		||||
kbd,
 | 
			
		||||
button {
 | 
			
		||||
button,
 | 
			
		||||
select {
 | 
			
		||||
    font-family: "RecVar", sans-serif;
 | 
			
		||||
    line-height: 1.5;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -142,7 +143,8 @@ body {
 | 
			
		|||
pre,
 | 
			
		||||
code,
 | 
			
		||||
kbd,
 | 
			
		||||
button {
 | 
			
		||||
button,
 | 
			
		||||
select {
 | 
			
		||||
    font-size: 100%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										53
									
								
								static/css/page/treehouse/dev/tools.css
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								static/css/page/treehouse/dev/tools.css
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,53 @@
 | 
			
		|||
th-picture-upload {
 | 
			
		||||
    display: block;
 | 
			
		||||
 | 
			
		||||
    border: 1px solid var(--border-1);
 | 
			
		||||
    padding: 8px 12px;
 | 
			
		||||
    margin-right: 8px;
 | 
			
		||||
    border-radius: 8px;
 | 
			
		||||
 | 
			
		||||
    cursor: default;
 | 
			
		||||
 | 
			
		||||
    &:focus {
 | 
			
		||||
        border-color: var(--liquidex-brand-blue);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    & > .nothing-pasted {
 | 
			
		||||
        text-align: center;
 | 
			
		||||
        opacity: 50%;
 | 
			
		||||
        padding: 16px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    & > .have-picture {
 | 
			
		||||
        & > p {
 | 
			
		||||
            padding-bottom: 8px;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        & > img {
 | 
			
		||||
            max-height: 480px;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    & > .copied-to-clipboard {
 | 
			
		||||
        text-align: center;
 | 
			
		||||
        padding: 16px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /* State display */
 | 
			
		||||
 | 
			
		||||
    & > * {
 | 
			
		||||
        display: none;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &[data-state="init"] > .nothing-pasted {
 | 
			
		||||
        display: block;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &[data-state="have-picture"] > .have-picture {
 | 
			
		||||
        display: block;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &[data-state="copied-to-clipboard"] > .copied-to-clipboard {
 | 
			
		||||
        display: block;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										149
									
								
								static/js/dev/picture-upload.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										149
									
								
								static/js/dev/picture-upload.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,149 @@
 | 
			
		|||
class PictureUpload extends HTMLElement {
 | 
			
		||||
    constructor() {
 | 
			
		||||
        super();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    connectedCallback() {
 | 
			
		||||
        this.tabIndex = 0;
 | 
			
		||||
        this.gotoInit();
 | 
			
		||||
 | 
			
		||||
        this.preview = this.querySelector("img[name='preview']");
 | 
			
		||||
 | 
			
		||||
        this.addEventListener("click", (event) => {
 | 
			
		||||
            if (event.target == this || event.target.parentElement == this) {
 | 
			
		||||
                event.preventDefault();
 | 
			
		||||
                this.focus();
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        this.addEventListener("paste", async (event) => {
 | 
			
		||||
            if (event.clipboardData.items.length != 1) {
 | 
			
		||||
                console.error("only one item is supported");
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            let item = event.clipboardData.items[0];
 | 
			
		||||
            await this.paste(item);
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    gotoInit() {
 | 
			
		||||
        this.setState("init");
 | 
			
		||||
        this.innerHTML = `
 | 
			
		||||
            <div class="nothing-pasted">
 | 
			
		||||
                paste or drop an image here to make a picture out of it
 | 
			
		||||
            </div>
 | 
			
		||||
        `;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async gotoHavePicture(imageType, imageFile) {
 | 
			
		||||
        this.setState("have-picture");
 | 
			
		||||
        this.innerHTML = `
 | 
			
		||||
            <div class="have-picture">
 | 
			
		||||
                <img name="preview" class="pic" alt="preview">
 | 
			
		||||
                <p>
 | 
			
		||||
                    <span name="preview-width"></span> × <span name="preview-height"></span> px (<span name="file-size"></span>)
 | 
			
		||||
                </p>
 | 
			
		||||
 | 
			
		||||
                <p>
 | 
			
		||||
                    <label for="label">label</label>
 | 
			
		||||
                    <input name="label" type="text" placeholder="untitled"></input>
 | 
			
		||||
                </p>
 | 
			
		||||
 | 
			
		||||
                <p>
 | 
			
		||||
                    <label for="compression">compression</label>
 | 
			
		||||
                    <select name="compression">
 | 
			
		||||
                        <option value="Lossless">lossless</option>
 | 
			
		||||
                    </select>
 | 
			
		||||
                </p>
 | 
			
		||||
 | 
			
		||||
                <button name="upload">upload</button>
 | 
			
		||||
            </div>
 | 
			
		||||
        `;
 | 
			
		||||
 | 
			
		||||
        let preview = this.querySelector("img[name='preview']");
 | 
			
		||||
        let previewWidth = this.querySelector("[name='preview-width']");
 | 
			
		||||
        let previewHeight = this.querySelector("[name='preview-height']");
 | 
			
		||||
        let fileSize = this.querySelector("[name='file-size']");
 | 
			
		||||
        let label = this.querySelector("[name='label']");
 | 
			
		||||
        let compression = this.querySelector("[name='compression']");
 | 
			
		||||
        let upload = this.querySelector("button[name='upload']");
 | 
			
		||||
 | 
			
		||||
        fileSize.textContent = formatSizeSI(imageFile.size);
 | 
			
		||||
 | 
			
		||||
        let url = URL.createObjectURL(imageFile);
 | 
			
		||||
        preview.src = url;
 | 
			
		||||
 | 
			
		||||
        createImageBitmap(imageFile).then((bitmap) => {
 | 
			
		||||
            console.log(bitmap);
 | 
			
		||||
            previewWidth.textContent = bitmap.width.toString();
 | 
			
		||||
            previewHeight.textContent = bitmap.height.toString();
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        upload.addEventListener("click", async () => {
 | 
			
		||||
            let params = new URLSearchParams({
 | 
			
		||||
                label: label.value,
 | 
			
		||||
                format: imageType,
 | 
			
		||||
                compression: compression.value,
 | 
			
		||||
            });
 | 
			
		||||
            let response = await fetch(`/dev/picture-upload?${params}`, {
 | 
			
		||||
                method: "POST",
 | 
			
		||||
                body: imageFile,
 | 
			
		||||
            });
 | 
			
		||||
            let json = await response.json();
 | 
			
		||||
            if (json.error != null) {
 | 
			
		||||
                console.error(json.error);
 | 
			
		||||
            } else {
 | 
			
		||||
                await navigator.clipboard.writeText(json.ulid);
 | 
			
		||||
                await this.gotoCopiedToClipboard();
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async gotoCopiedToClipboard() {
 | 
			
		||||
        this.setState("copied-to-clipboard");
 | 
			
		||||
        this.innerHTML = `
 | 
			
		||||
            <div class="copied-to-clipboard">ulid copied to clipboard; the window will now refresh</div>
 | 
			
		||||
        `;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    setState(name) {
 | 
			
		||||
        this.setAttribute("data-state", name);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async paste(item) {
 | 
			
		||||
        console.log(item);
 | 
			
		||||
        if (!isSupportedImageType(item.type)) {
 | 
			
		||||
            console.error("unsupported mime type", item.type);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let file = item.getAsFile();
 | 
			
		||||
        if (file == null) {
 | 
			
		||||
            console.error("data transfer does not contain a file");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await this.gotoHavePicture(item.type, file);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
customElements.define("th-picture-upload", PictureUpload);
 | 
			
		||||
 | 
			
		||||
function isSupportedImageType(mime) {
 | 
			
		||||
    return (
 | 
			
		||||
        mime == "image/png" ||
 | 
			
		||||
        mime == "image/jpeg" ||
 | 
			
		||||
        mime == "image/svg+xml" ||
 | 
			
		||||
        mime == "image/webp"
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function formatSizeSI(bytes) {
 | 
			
		||||
    return new Intl.NumberFormat(undefined, {
 | 
			
		||||
        style: "unit",
 | 
			
		||||
        unit: "byte",
 | 
			
		||||
        notation: "compact",
 | 
			
		||||
        unitDisplay: "narrow",
 | 
			
		||||
    }).format(bytes);
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue