use fission::prelude::*;
pub struct AttachmentPicker;
impl From<AttachmentPicker> for Widget {
fn from(_: AttachmentPicker) -> Widget {
let (ctx, view) = fission::build::current::<AppState>();
FileUpload {
label: "Choose attachment".into(),
selected_file: view.state().attachment_name.clone(),
on_browse: Some(with_reducer!(ctx, PickAttachment, on_pick_attachment)),
}
.into()
}
}
#[derive(Debug, Clone, Default)]
pub struct AppState {
pub attachment_name: Option<String>,
pub pending_attachment: Option<PickedAttachment>,
pub upload_status: Option<String>,
}
#[derive(Debug, Clone)]
pub struct PickedAttachment {
pub name: String,
pub content_type: Option<String>,
pub byte_len: Option<u64>,
pub stream: DataStreamId,
}
use fission::prelude::*;
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct PickAttachment;
fn on_pick_attachment(
state: &mut AppState,
_: PickAttachment,
ctx: &mut ReducerContext<AppState>,
) {
state.upload_status = Some("Waiting for file selection".into());
ctx.effects
.capability(
PICK_OPEN_FILES,
PickOpenFilesRequest {
allow_multiple: false,
mime_types: vec!["application/pdf".into(), "image/png".into()],
extensions: vec!["pdf".into(), "png".into()],
},
)
.on_ok(ctx.effects.bind(
AttachmentPicked,
reduce_with!(on_attachment_picked),
))
.on_err(ctx.effects.bind(
AttachmentPickFailed,
reduce_with!(on_attachment_pick_failed),
));
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct AttachmentPicked;
fn on_attachment_picked(
state: &mut AppState,
_: AttachmentPicked,
ctx: &mut ReducerContext<AppState>,
) {
let Some(result) = ctx.input.capability_ok(PICK_OPEN_FILES) else {
return;
};
let Some(file) = result.files.into_iter().next() else {
state.upload_status = Some("No file selected".into());
return;
};
state.attachment_name = Some(file.name.clone());
state.pending_attachment = Some(PickedAttachment {
name: file.name,
content_type: file.content_type,
byte_len: file.byte_len,
stream: file.stream,
});
state.upload_status = Some("Ready to upload".into());
}
#[derive(Debug)]
pub struct UploadAttachmentJob;
impl JobSpec for UploadAttachmentJob {
type Request = UploadAttachmentRequest;
type Ok = UploadAttachmentResult;
type Err = String;
const NAME: &'static str = "app::upload-attachment";
}
pub const UPLOAD_ATTACHMENT_JOB: JobRef<UploadAttachmentJob> =
JobRef::new(UploadAttachmentJob::NAME);
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct UploadAttachmentRequest {
pub name: String,
pub content_type: Option<String>,
pub byte_len: Option<u64>,
pub stream: DataStreamId,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct UploadAttachmentResult {
pub object_id: String,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct UploadAttachment;
fn on_upload_attachment(
state: &mut AppState,
_: UploadAttachment,
ctx: &mut ReducerContext<AppState>,
) {
let Some(file) = state.pending_attachment.clone() else {
state.upload_status = Some("Choose a file first".into());
return;
};
state.upload_status = Some("Uploading".into());
ctx.effects
.app(
UPLOAD_ATTACHMENT_JOB,
UploadAttachmentRequest {
name: file.name,
content_type: file.content_type,
byte_len: file.byte_len,
stream: file.stream,
},
)
.on_ok(ctx.effects.bind(
AttachmentUploaded,
reduce_with!(on_attachment_uploaded),
))
.on_err(ctx.effects.bind(
AttachmentUploadFailed,
reduce_with!(on_attachment_upload_failed),
));
}
use futures_util::StreamExt;
DesktopApp::<AppState, _>::new(App)
.with_async(|asyncs| {
asyncs.register_job(
UPLOAD_ATTACHMENT_JOB,
|request: UploadAttachmentRequest, ctx: JobCtx| async move {
let mut stream = ctx
.open_data_stream(request.stream)
.map_err(|error| error.to_string())?;
let mut uploaded = 0_u64;
while let Some(chunk) = stream.next().await {
let chunk = chunk.map_err(|error| error.to_string())?;
uploaded += chunk.len() as u64;
// Send `chunk` to object storage, hash it, parse it, or write it
// to a temporary file. Do not buffer the whole stream unless the
// target API forces you to.
}
Ok(UploadAttachmentResult {
object_id: format!("attachment:{uploaded}"),
})
},
);
})
.run()
// Shape only: the stream boundary is the important part.
let stream = ctx.open_data_stream(command.stream)?;
while let Some(chunk) = stream.next().await {
let bytes = chunk?;
upload_part(bytes).await?;
ctx.emit(UploadProgress { uploaded_bytes }).await?;
}
Target | Default behavior |
|---|---|
macOS, Windows, Linux | Native open-file picker with file contents streamed from disk. |
Web | Shell/provider-specific. Browser file APIs can register streams for picked files. |
Android, iOS | Shell/provider-specific. Native pickers should return scoped streams or copied sandbox streams. |
Server, SSR, Static site, Terminal | Usually unsupported unless the host app registers an appropriate provider. |