use std::io;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use day_core::{BuildCx, Flex, Piece, RNode, with_tree};
use day_part_permissions::{self as perms, Permission, Status};
use day_pieces::{Decorated, IntoReactive, Reactive};
use day_reactive::{AnyBinding, Binding, Signal, Trigger, on_main, watch};
use day_spec::Event;
pub const KIND: &str = "day.piece.camera";
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum Facing {
#[default]
Back,
Front,
}
impl Facing {
pub fn code(self) -> i32 {
match self {
Facing::Back => 0,
Facing::Front => 1,
}
}
pub fn flipped(self) -> Facing {
match self {
Facing::Back => Facing::Front,
Facing::Front => Facing::Back,
}
}
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct CameraProps {
pub facing: Facing,
pub active: bool,
}
#[derive(Clone, Debug, PartialEq)]
pub enum CameraPatch {
Start,
Stop,
Capture,
Facing(Facing),
Surface(String),
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub enum CameraState {
#[default]
Idle,
Starting,
Running,
Unavailable,
Denied,
Error(String),
}
impl CameraState {
pub fn label(&self) -> &'static str {
match self {
CameraState::Idle => "idle",
CameraState::Starting => "starting",
CameraState::Running => "running",
CameraState::Unavailable => "unavailable",
CameraState::Denied => "denied",
CameraState::Error(_) => "error",
}
}
pub fn from_report(num: f64, text: &str) -> Option<CameraState> {
Some(match num as i32 {
report::STOPPED => CameraState::Idle,
report::STARTING => CameraState::Starting,
report::RUNNING => CameraState::Running,
report::UNAVAILABLE => CameraState::Unavailable,
report::DENIED => CameraState::Denied,
report::ERROR => CameraState::Error(text.to_string()),
_ => return None,
})
}
}
pub mod report {
pub const STOPPED: i32 = 0;
pub const STARTING: i32 = 1;
pub const RUNNING: i32 = 2;
pub const UNAVAILABLE: i32 = 3;
pub const DENIED: i32 = 4;
pub const ERROR: i32 = 5;
pub const PHOTO: i32 = 6;
pub const SURFACE: i32 = 7;
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Photo {
pub path: PathBuf,
pub width: u32,
pub height: u32,
}
impl Photo {
pub fn from_report(text: &str) -> Option<Photo> {
let mut parts = text.split('\u{1f}');
let path = parts.next().filter(|p| !p.is_empty())?;
let width = parts
.next()
.and_then(|w| w.trim().parse().ok())
.unwrap_or(0);
let height = parts
.next()
.and_then(|h| h.trim().parse().ok())
.unwrap_or(0);
Some(Photo {
path: PathBuf::from(path),
width,
height,
})
}
pub fn bytes(&self) -> io::Result<Arc<Vec<u8>>> {
std::fs::read(&self.path).map(Arc::new)
}
pub fn file_name(&self) -> String {
Path::new(&self.path)
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_default()
}
}
pub fn support() -> day_spec::Support {
if cfg!(any(
all(feature = "uikit", target_os = "ios"),
all(feature = "material", target_os = "android"),
all(feature = "arkui", target_env = "ohos"),
)) {
day_spec::Support::Native
} else {
day_spec::Support::Unsupported
}
}
pub struct Camera {
facing: Reactive<Facing>,
active: Reactive<bool>,
auto_request: bool,
capture: Option<Trigger>,
state: Option<AnyBinding<CameraState>>,
photo: Option<AnyBinding<Option<Photo>>>,
}
pub fn camera() -> Camera {
Camera {
facing: Reactive::Const(Facing::Back),
active: Reactive::Const(true),
auto_request: false,
capture: None,
state: None,
photo: None,
}
}
#[day_pieces::builder(CameraBuilder)]
impl Camera {
pub fn facing<M>(mut self, facing: impl IntoReactive<Facing, M>) -> Self {
self.facing = facing.into_reactive();
self
}
pub fn active<M>(mut self, active: impl IntoReactive<bool, M>) -> Self {
self.active = active.into_reactive();
self
}
pub fn auto_request(mut self) -> Self {
self.auto_request = true;
self
}
pub fn capture(mut self, trigger: Trigger) -> Self {
self.capture = Some(trigger);
self
}
pub fn state(mut self, state: impl Binding<CameraState>) -> Self {
self.state = Some(AnyBinding::new(state));
self
}
pub fn photo(mut self, photo: impl Binding<Option<Photo>>) -> Self {
self.photo = Some(AnyBinding::new(photo));
self
}
}
impl Piece for Camera {
fn build(self, cx: &mut BuildCx) -> RNode {
let Camera {
facing,
active,
auto_request,
capture,
state,
photo,
} = self;
let set_state = move |next: CameraState| {
if let Some(state) = state
&& state.get_untracked() != next
{
state.set(next);
}
};
let wanted = active.get_untracked();
let held = perms::status(Permission::Camera) == Status::Granted;
let props = CameraProps {
facing: facing.get_untracked(),
active: wanted && held,
};
let node = cx.leaf(
KIND,
&props,
Flex {
grow_w: true,
grow_h: true,
..Default::default()
},
);
let send = move |patch: CameraPatch| {
with_tree(|t| t.patch(node, Box::new(patch), false));
};
let denied_relay: Signal<Option<CameraState>> = Signal::new(None);
watch(
move || denied_relay.get(),
move |next, _| {
if let Some(next) = next {
set_state(next.clone());
}
},
);
let start = move || match perms::status(Permission::Camera) {
Status::Granted => send(CameraPatch::Start),
Status::Prompt if auto_request => {
set_state(CameraState::Starting);
let denied = denied_relay.setter();
perms::request(Permission::Camera, move |status| {
on_main(move || {
if status == Status::Granted {
with_tree(|t| t.patch(node, Box::new(CameraPatch::Start), false));
} else {
denied.set(Some(CameraState::Denied));
}
});
});
}
Status::Prompt => set_state(CameraState::Idle),
Status::Denied | Status::Restricted => set_state(CameraState::Denied),
_ => set_state(CameraState::Unavailable),
};
if wanted && !held {
start();
}
if let Reactive::Dyn(read) = active {
let last = std::rc::Rc::new(std::cell::Cell::new(wanted));
watch(
move || read(),
move |now, _| {
if *now == last.get() {
return;
}
last.set(*now);
if *now {
start();
} else {
send(CameraPatch::Stop);
}
},
);
}
if let Reactive::Dyn(read) = facing {
let last = std::rc::Rc::new(std::cell::Cell::new(props.facing));
watch(
move || read(),
move |f, _| {
if *f != last.get() {
last.set(*f);
send(CameraPatch::Facing(*f));
}
},
);
}
if let Some(capture) = capture {
watch(
move || capture.track(),
move |_, _| send(CameraPatch::Capture),
);
}
cx.on(node, move |ev| {
if let Event::Custom { num, text, .. } = ev {
if *num as i32 == report::SURFACE {
send(CameraPatch::Surface(text.clone()));
return;
}
if *num as i32 == report::PHOTO {
if let Some(photo) = photo
&& let Some(shot) = Photo::from_report(text)
{
photo.set(Some(shot));
}
return;
}
if let Some(next) = CameraState::from_report(*num, text) {
set_state(next);
}
}
});
node
}
}
day_pieces::glue_modules!(uikit, material, arkui);
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn photo_report_parses_path_and_size() {
let p = Photo::from_report("/tmp/day-camera/a.jpg\u{1f}4032\u{1f}3024").unwrap();
assert_eq!(p.path, PathBuf::from("/tmp/day-camera/a.jpg"));
assert_eq!((p.width, p.height), (4032, 3024));
assert_eq!(p.file_name(), "a.jpg");
}
#[test]
fn photo_report_without_size_is_still_a_photo() {
let p = Photo::from_report("/tmp/x.jpg").unwrap();
assert_eq!((p.width, p.height), (0, 0));
assert!(Photo::from_report("").is_none());
}
#[test]
fn state_reports_map_and_photo_is_not_a_state() {
assert_eq!(
CameraState::from_report(report::RUNNING as f64, ""),
Some(CameraState::Running)
);
assert_eq!(
CameraState::from_report(report::ERROR as f64, "boom"),
Some(CameraState::Error("boom".into()))
);
assert_eq!(CameraState::from_report(report::PHOTO as f64, "x"), None);
assert_eq!(CameraState::Unavailable.label(), "unavailable");
}
#[test]
fn facing_codes_round_trip() {
assert_eq!(Facing::Back.code(), 0);
assert_eq!(Facing::Front.code(), 1);
assert_eq!(Facing::Back.flipped(), Facing::Front);
}
}