From 071e375cc246f9bf95d618c0a16d9e2edd4eda27 Mon Sep 17 00:00:00 2001 From: Joakim Hulthe Date: Mon, 6 Nov 2023 22:28:56 +0100 Subject: [PATCH] Add alternate bulb list view --- frontend/src/page/lights.rs | 165 +++++++++++++++++------- frontend/static/images/circle-arrow.svg | 5 + frontend/static/styles/common.scss | 139 ++++++++++++++++++++ 3 files changed, 259 insertions(+), 50 deletions(-) create mode 100644 frontend/static/images/circle-arrow.svg diff --git a/frontend/src/page/lights.rs b/frontend/src/page/lights.rs index cbacb75..f7f0f3a 100644 --- a/frontend/src/page/lights.rs +++ b/frontend/src/page/lights.rs @@ -3,7 +3,7 @@ use crate::css::C; use chrono::{NaiveTime, Weekday}; use common::{BulbGroup, BulbGroupShape, BulbMap, ClientMessage, ServerMessage}; use lighter_lib::{BulbId, BulbMode}; -use seed::{attrs, button, div, h2, input, table, td, tr, C}; +use seed::{attrs, button, div, h1, h2, h3, input, label, span, table, td, tr, C}; use seed::{prelude::*, IF}; use seed_router::Page; use std::collections::{BTreeMap, HashMap, HashSet}; @@ -14,11 +14,18 @@ use std::fmt::Write; pub struct Model { bulb_states: BTreeMap, + select_mode: SelectMode, + + bulb_groups: BTreeMap>, + bulb_map: BulbMap, - /// the currently selected bulb map groups + /// the currently selected bulbs on the map selected_groups: HashSet, + /// the currently selected bulbs on the list + selected_bulbs: HashSet, + /// whether the currently selected map groups have been interacted with groups_interacted: bool, @@ -37,9 +44,20 @@ pub enum Msg { SelectGroup(usize), DeselectGroups, + + SelectBulb(BulbId), + ColorPicker(ColorPickerMsg), SetBulbPower(bool), LightTime(String, Weekday), + SetSelectMode(SelectMode), +} + +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub enum SelectMode { + #[default] + Map, + List, } impl Page for Model { @@ -64,8 +82,6 @@ impl Page for Model { mode: new_mode, wake_schedule, }; - - //color_picker.set_color(mode.color); } ServerMessage::BulbMap(bulb_map) => { self.bulb_map = bulb_map; @@ -100,6 +116,11 @@ impl Page for Model { } } } + Msg::SelectBulb(bulb) => { + if !self.selected_bulbs.remove(&bulb) { + self.selected_bulbs.insert(bulb); + } + } Msg::ColorPicker(ColorPickerMsg::SetColor(color)) => { self.groups_interacted = true; self.for_selected_bulbs(|id, _| { @@ -145,36 +166,13 @@ impl Page for Model { }); } } + Msg::SetSelectMode(mode) => { + self.select_mode = mode; + } } } fn view(&self) -> Node { - //let view_bulb = |(id, (mode, color_picker)): (&BulbId, &(BulbMode, ColorPicker))| { - // div![ - // C![C.bulb_box], - // h1![id], - // div![ - // C![C.bulb_controls], - // { - // let id = id.clone(); - // color_picker.view().map_msg(|msg| Msg::ColorPicker(id, msg)) - // }, - // button![ - // if mode.power { - // C![C.bulb_power_button, C.bulb_power_button_on] - // } else { - // C![C.bulb_power_button] - // }, - // { - // let id = id.clone(); - // let power = !mode.power; - // ev(Ev::Click, move |_| Msg::SetBulbPower(id, power)) - // }, - // ], - // ], - // ] - //}; - let bulb_map_width = self .bulb_map .groups @@ -191,7 +189,7 @@ impl Page for Model { .max() .unwrap_or(0); - let view_bulb_group = |(i, group): (usize, &BulbGroup)| { + let bulb_group_map = |(i, group): (usize, &BulbGroup)| { let (w, h) = (group.shape.width(), group.shape.height()); let mut style = String::new(); write!( @@ -222,13 +220,51 @@ impl Page for Model { ] }; - let selected_bulb = self - .selected_groups - .iter() - .next() - .and_then(|&index| self.bulb_map.groups.get(index)) - .and_then(|group| group.bulbs.first()) - .and_then(|id| self.bulb_states.get(id)); + let bulb_group_list = |(_i, group): (usize, &BulbGroup)| { + div![ + C![C.bulb_list_group], + h1![&group.name], + group.bulbs.iter().map(|bulb| { + let select_ev = || { + let bulb = bulb.clone(); + ev(Ev::Click, |_| Msg::SelectBulb(bulb)) + }; + + button![ + C![C.bulb_list_bulb], + select_ev(), + div![ + C![C.bulb_list_checkbox], + label![ + C![C.container], + input![ + attrs! { At::Type => "checkbox" }, + IF!(self.selected_bulbs.contains(bulb) => attrs! { At::Checked => true }), + select_ev(), + ], + div![C![C.checkmark]], + ], + ], + span![bulb], + ] + }), + ] + }; + + // pick one (arbitrary) selected bulb to pull values for the controls from + let selected_bulb = if let SelectMode::Map = self.select_mode { + self.selected_groups + .iter() + .next() + .and_then(|&index| self.bulb_map.groups.get(index)) + .and_then(|group| group.bulbs.first()) + .and_then(|id| self.bulb_states.get(id)) + } else { + self.selected_bulbs + .iter() + .next() + .and_then(|id| self.bulb_states.get(id)) + }; let calendar_day = |day: Weekday| { let time = selected_bulb @@ -249,13 +285,35 @@ impl Page for Model { div![ C![C.bulb_box], div![ - C![C.bulb_map], - attrs! { - At::Style => format!("min-width: {}rem; height: {}rem;", bulb_map_width, bulb_map_height), - }, - ev(Ev::Click, |_| Msg::DeselectGroups), - self.bulb_map.groups.iter().enumerate().map(view_bulb_group), + C![C.selector_selector], + button![ + "Map", + ev(Ev::Click, move |_| Msg::SetSelectMode(SelectMode::Map)) + ], + div![ + C![C.selector_selector_arrow], + IF!(self.select_mode == SelectMode::Map => + attrs! { At::Style => "transform: rotateY(180deg);"}), + ], + button![ + "List", + ev(Ev::Click, move |_| Msg::SetSelectMode(SelectMode::List)) + ], ], + match self.select_mode { + SelectMode::Map => div![ + C![C.bulb_map], + attrs! { + At::Style => format!("min-width: {}rem; height: {}rem;", bulb_map_width, bulb_map_height), + }, + ev(Ev::Click, |_| Msg::DeselectGroups), + self.bulb_map.groups.iter().enumerate().map(bulb_group_map), + ], + SelectMode::List => div![ + C![C.bulb_list], + self.bulb_map.groups.iter().enumerate().map(bulb_group_list), + ], + }, div![ C![C.bulb_controls], IF!(selected_bulb.is_none() => C![C.cross_out]), @@ -297,11 +355,18 @@ impl Page for Model { impl Model { fn for_selected_bulbs(&self, mut f: impl FnMut(&BulbId, &BulbState)) { - self.selected_groups - .iter() - .filter_map(|&index| self.bulb_map.groups.get(index)) - .flat_map(|group| group.bulbs.iter()) - .filter_map(|id| self.bulb_states.get(id).map(|bulb| (id, bulb))) - .for_each(|(id, bulb)| f(id, bulb)); + if let SelectMode::Map = self.select_mode { + self.selected_groups + .iter() + .filter_map(|&index| self.bulb_map.groups.get(index)) + .flat_map(|group| group.bulbs.iter()) + .filter_map(|id| self.bulb_states.get(id).map(|bulb| (id, bulb))) + .for_each(|(id, bulb)| f(id, bulb)); + } else { + self.selected_bulbs + .iter() + .filter_map(|id| self.bulb_states.get(id).map(|bulb| (id, bulb))) + .for_each(|(id, bulb)| f(id, bulb)); + } } } diff --git a/frontend/static/images/circle-arrow.svg b/frontend/static/images/circle-arrow.svg new file mode 100644 index 0000000..ec74310 --- /dev/null +++ b/frontend/static/images/circle-arrow.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/frontend/static/styles/common.scss b/frontend/static/styles/common.scss index fef7729..3911383 100644 --- a/frontend/static/styles/common.scss +++ b/frontend/static/styles/common.scss @@ -102,6 +102,42 @@ body { margin-bottom: 1.5rem; } +/* lol i'm so funny */ +.selector_selector { + margin-bottom: 0; + display: flex; + justify-content: center; + //background: #534f44; +} +.selector_selector > button { + width: 10rem; + border: solid 0.1rem wheat; + background-color: #3a3743; + color: white; + font-size: 1.5rem; + border-left: solid white 0.1rem; + border-right: solid white 0.1rem; +} +.selector_selector > button:hover { + background-color: #4c4858; +} +.selector_selector > button:active { + background-color: #312e38; +} +.selector_selector > button:first-child { + border-top-left-radius: 0.5rem; +} +.selector_selector > button:last-child { + border-top-right-radius: 0.5rem; +} +.selector_selector_arrow { + position: absolute; + width: 2.15rem; + height: 2.15rem; + background: url("/images/circle-arrow.svg"); + transition: transform 0.5s ease-in-out; +} + .bulb_controls { display: flex; flex-direction: row; @@ -150,6 +186,47 @@ body { } @keyframes to_width_90 { to { width: 90%; } } +.bulb_list { + display: flex; + flex-direction: column; +} + +.bulb_list_group { + display: flex; + flex-direction: column; +} + +.bulb_list_group > h1 { + margin-top: 0.6em; + margin-bottom: 0; + font-size: 1.3em; + border-bottom: solid 1px; +} + +.bulb_list_bulb { + font-family: Ubuntu Mono; + display: flex; + font-size: 0.8em; + padding-top: 0.5em; + padding-bottom: 0.5em; + margin-top: 0.3em; + background-color: #3a3743; + border: solid 0.2em #45374f; + border-radius: 0.5em; + color: wheat; +} +.bulb_list_bulb:hover { + background-color: #4c4858; +} +.bulb_list_bulb:active { + background-color: #312e38; +} +.bulb_list_bulb > span { + margin: auto; + flex-grow: 1; + padding-right: 1em; +} + .bulb_map { background: url(images/blueprint_bg.png); background-size: auto; @@ -337,3 +414,65 @@ body { flex-shrink: 1; } +.bulb_list_checkbox {} +.bulb_list_checkbox input[type="checkbox"] { + visibility: hidden; + display: none; +} + +.bulb_list_checkbox *, +.bulb_list_checkbox ::after, +.bulb_list_checkbox ::before { + box-sizing: border-box; +} + +.bulb_list_checkbox .container { + display: block; + position: relative; + cursor: pointer; + font-size: 25px; + user-select: none; +} + + /* Create a custom checkbox */ +.bulb_list_checkbox .checkmark { + position: relative; + top: 0; + left: 0; + height: 1.3em; + width: 1.3em; + background: black; + border-radius: 50px; + transition: all 0.7s; + --spread: 10px; +} + + /* When the checkbox is checked, add a blue background */ +.bulb_list_checkbox .container input:checked ~ .checkmark { + background: black; + /* spawn a bunch of colored balls and blur them together */ + box-shadow: -5px -5px var(--spread) 0px #5B51D8, 0 -5px var(--spread) 0px #833AB4, 5px -5px var(--spread) 0px #E1306C, 5px 0 var(--spread) 0px #FD1D1D, 5px 5px var(--spread) 0px #F77737, 0 5px var(--spread) 0px #FCAF45, -5px 5px var(--spread) 0px #FFDC80; +} + + /* Create the checkmark/indicator (hidden when not checked) */ +.bulb_list_checkbox .checkmark::after { + content: ""; + position: absolute; + display: none; +} + + /* Show the checkmark when checked */ +.bulb_list_checkbox .container input:checked ~ .checkmark::after { + display: block; +} + + /* Style the checkmark/indicator */ +.bulb_list_checkbox .container .checkmark::after { + left: 0.5em; + top: 0.34em; + width: 0.25em; + height: 0.5em; + border: solid wheat; + border-width: 0 0.15em 0.15em 0; + transform: rotate(45deg); +}