Remove yewprint dependency
This commit is contained in:
parent
23f23c4936
commit
eda5b62650
30
Cargo.lock
generated
30
Cargo.lock
generated
@ -748,15 +748,6 @@ dependencies = [
|
|||||||
"rayon",
|
"rayon",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "id_tree"
|
|
||||||
version = "1.8.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "bcd9db8dd5be8bde5a2624ed4b2dfb74368fe7999eb9c4940fd3ca344b61071a"
|
|
||||||
dependencies = [
|
|
||||||
"snowflake",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "idna"
|
name = "idna"
|
||||||
version = "0.3.0"
|
version = "0.3.0"
|
||||||
@ -1460,12 +1451,6 @@ dependencies = [
|
|||||||
"autocfg",
|
"autocfg",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "snowflake"
|
|
||||||
version = "1.3.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "27207bb65232eda1f588cf46db2fee75c0808d557f6b3cf19a75f5d6d7c94df1"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "spin"
|
name = "spin"
|
||||||
version = "0.5.2"
|
version = "0.5.2"
|
||||||
@ -2306,20 +2291,6 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "yewprint"
|
|
||||||
version = "0.4.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "29de9263e650aab7a0ccf49c53ae6b122082b1451762ca2d0e64367e6c8448b2"
|
|
||||||
dependencies = [
|
|
||||||
"gloo",
|
|
||||||
"id_tree",
|
|
||||||
"implicit-clone",
|
|
||||||
"wasm-bindgen",
|
|
||||||
"web-sys",
|
|
||||||
"yew",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "yewprint-app"
|
name = "yewprint-app"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@ -2338,7 +2309,6 @@ dependencies = [
|
|||||||
"wee_alloc",
|
"wee_alloc",
|
||||||
"yew",
|
"yew",
|
||||||
"yew-router",
|
"yew-router",
|
||||||
"yewprint",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -17,7 +17,7 @@ crate-type = ["cdylib"]
|
|||||||
console_error_panic_hook = { version = "0.1.6", optional = true }
|
console_error_panic_hook = { version = "0.1.6", optional = true }
|
||||||
|
|
||||||
wasm-bindgen = "0.2"
|
wasm-bindgen = "0.2"
|
||||||
web-sys = { version = "0.3", features = ["Window", "MediaQueryList"] }
|
web-sys = { version = "0.3", features = ["Window", "MediaQueryList", "DomTokenList"] }
|
||||||
js-sys = "0.3"
|
js-sys = "0.3"
|
||||||
|
|
||||||
# `wee_alloc` is a tiny allocator for wasm that is only ~1K in code size
|
# `wee_alloc` is a tiny allocator for wasm that is only ~1K in code size
|
||||||
@ -29,7 +29,7 @@ wee_alloc = { version = "0.4.5", optional = false }
|
|||||||
|
|
||||||
yew = { version = "0.20.0", features = ["csr"] }
|
yew = { version = "0.20.0", features = ["csr"] }
|
||||||
yew-router = { version = "0.17.0" }
|
yew-router = { version = "0.17.0" }
|
||||||
yewprint = "0.4.0"
|
#yewprint = "0.4.0"
|
||||||
gloo = "0.8.0"
|
gloo = "0.8.0"
|
||||||
gloo-events = "0.1"
|
gloo-events = "0.1"
|
||||||
futures = "0.3.25"
|
futures = "0.3.25"
|
||||||
|
48
src/app.rs
48
src/app.rs
@ -3,7 +3,7 @@ use yew::prelude::*;
|
|||||||
use yew_router::Routable;
|
use yew_router::Routable;
|
||||||
use yew_router::prelude::Navigator;
|
use yew_router::prelude::Navigator;
|
||||||
use yew_router::prelude::use_navigator;
|
use yew_router::prelude::use_navigator;
|
||||||
use yewprint::Icon;
|
use crate::component::fa_icon::FontawesomeIcon;
|
||||||
use crate::page::Page;
|
use crate::page::Page;
|
||||||
use crate::page::PageRouter;
|
use crate::page::PageRouter;
|
||||||
use crate::page::Pages;
|
use crate::page::Pages;
|
||||||
@ -11,6 +11,7 @@ use crate::theme::ThemeContext;
|
|||||||
use crate::theme::ThemeMsg;
|
use crate::theme::ThemeMsg;
|
||||||
use crate::theme::ThemeProvider;
|
use crate::theme::ThemeProvider;
|
||||||
use crate::component::actionbar::{Actionbar, ActionbarOption};
|
use crate::component::actionbar::{Actionbar, ActionbarOption};
|
||||||
|
use crate::theme::ThemeState;
|
||||||
|
|
||||||
|
|
||||||
#[function_component]
|
#[function_component]
|
||||||
@ -29,16 +30,16 @@ fn ThemedApp() -> Html {
|
|||||||
let navigator = use_navigator().unwrap();
|
let navigator = use_navigator().unwrap();
|
||||||
|
|
||||||
// Helper function for generating tabs
|
// Helper function for generating tabs
|
||||||
fn page_option(current: &Page, page: Page, navigator: Navigator, state: UseStateHandle<Page>) -> ActionbarOption {
|
fn page_option(current: &Page, page: Page, navigator: Navigator, state: UseStateSetter<Page>) -> ActionbarOption {
|
||||||
let is_current = *current == page;
|
let is_current = *current == page;
|
||||||
ActionbarOption::new(
|
ActionbarOption::new(
|
||||||
page.name(),
|
page.name(),
|
||||||
match page {
|
match page {
|
||||||
Page::Home => Icon::Home,
|
Page::Home => FontawesomeIcon::House,
|
||||||
Page::Projects => Icon::Code,
|
Page::Projects => FontawesomeIcon::Code,
|
||||||
Page::Socials => Icon::SocialMedia,
|
Page::Socials => FontawesomeIcon::ShareNodes,
|
||||||
Page::Gallery => Icon::Camera,
|
Page::Gallery => FontawesomeIcon::Camera,
|
||||||
Page::Contact => Icon::Envelope,
|
Page::Contact => FontawesomeIcon::Envelope,
|
||||||
},
|
},
|
||||||
Callback::from(move |_| {
|
Callback::from(move |_| {
|
||||||
navigator.push(&page);
|
navigator.push(&page);
|
||||||
@ -49,6 +50,7 @@ fn ThemedApp() -> Html {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let ctx = use_context::<ThemeContext>().expect("No theme context supplied");
|
let ctx = use_context::<ThemeContext>().expect("No theme context supplied");
|
||||||
|
let theme_icon = if ctx.theme == ThemeState::Dark { FontawesomeIcon::Sun } else { FontawesomeIcon::Moon };
|
||||||
let page_state = use_state_eq(||
|
let page_state = use_state_eq(||
|
||||||
window()
|
window()
|
||||||
.location()
|
.location()
|
||||||
@ -65,23 +67,21 @@ fn ThemedApp() -> Html {
|
|||||||
<div class="main">
|
<div class="main">
|
||||||
<Pages />
|
<Pages />
|
||||||
</div>
|
</div>
|
||||||
<Actionbar>
|
<Actionbar mobile_title="Menu" items={vec![
|
||||||
{vec![
|
page_option(¤t_page, Page::Home, navigator.clone(), page_state.setter()),
|
||||||
page_option(¤t_page, Page::Home, navigator.clone(), page_state.clone()),
|
page_option(¤t_page, Page::Projects, navigator.clone(), page_state.setter()),
|
||||||
page_option(¤t_page, Page::Projects, navigator.clone(), page_state.clone()),
|
page_option(¤t_page, Page::Socials, navigator.clone(), page_state.setter()),
|
||||||
page_option(¤t_page, Page::Socials, navigator.clone(), page_state.clone()),
|
page_option(¤t_page, Page::Gallery, navigator.clone(), page_state.setter()),
|
||||||
page_option(¤t_page, Page::Gallery, navigator.clone(), page_state.clone()),
|
page_option(¤t_page, Page::Contact, navigator, page_state.setter()),
|
||||||
page_option(¤t_page, Page::Contact, navigator, page_state),
|
ActionbarOption::new_opt(
|
||||||
ActionbarOption::new_opt(
|
None,
|
||||||
None,
|
Some(theme_icon),
|
||||||
Some(Icon::Flash),
|
Some(Callback::from(move |_| ctx.dispatch(ThemeMsg::Toggle))),
|
||||||
Some(Callback::from(move |_| ctx.dispatch(ThemeMsg::Toggle))),
|
true,
|
||||||
true,
|
false,
|
||||||
false,
|
false
|
||||||
false
|
)
|
||||||
)
|
]}/>
|
||||||
]}
|
|
||||||
</Actionbar>
|
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,14 +1,12 @@
|
|||||||
use yew::html::ChildrenRenderer;
|
|
||||||
use yew::prelude::*;
|
use yew::prelude::*;
|
||||||
use yew::virtual_dom::VNode;
|
use yew::virtual_dom::VNode;
|
||||||
use yewprint::{Collapse, Icon, ButtonGroup};
|
|
||||||
use yewprint::Button;
|
use super::fa_icon::{FAIcon, FontawesomeIcon};
|
||||||
use yewprint::Icon::{MenuClosed, MenuOpen};
|
|
||||||
|
|
||||||
#[derive(PartialEq, Clone)]
|
#[derive(PartialEq, Clone)]
|
||||||
pub struct ActionbarOption {
|
pub struct ActionbarOption {
|
||||||
name: Option<String>,
|
name: Option<String>,
|
||||||
icon: Option<Icon>,
|
icon: Option<FontawesomeIcon>,
|
||||||
onclick: Option<Callback<MouseEvent>>,
|
onclick: Option<Callback<MouseEvent>>,
|
||||||
center_content: bool,
|
center_content: bool,
|
||||||
selected: bool,
|
selected: bool,
|
||||||
@ -18,7 +16,7 @@ pub struct ActionbarOption {
|
|||||||
#[derive(PartialEq, Clone)]
|
#[derive(PartialEq, Clone)]
|
||||||
struct ActionbarOptionState {
|
struct ActionbarOptionState {
|
||||||
inner: ActionbarOption,
|
inner: ActionbarOption,
|
||||||
callback: UseStateHandle<bool>
|
callback: UseStateSetter<bool>
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Into<VNode> for ActionbarOption {
|
impl Into<VNode> for ActionbarOption {
|
||||||
@ -34,57 +32,69 @@ impl Into<VNode> for ActionbarOption {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ActionbarOption {
|
impl ActionbarOption {
|
||||||
pub fn new_opt(name: Option<String>, icon: Option<Icon>, onclick: Option<Callback<MouseEvent>>, center_content: bool, selected: bool, fill: bool) -> ActionbarOption {
|
pub fn new_opt(name: Option<String>, icon: Option<FontawesomeIcon>, onclick: Option<Callback<MouseEvent>>, center_content: bool, selected: bool, fill: bool) -> ActionbarOption {
|
||||||
ActionbarOption { name, icon, onclick, center_content, selected, fill }
|
ActionbarOption { name, icon, onclick, center_content, selected, fill }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new(name: &str, icon: Icon, onclick: Callback<MouseEvent>, selected: bool) -> ActionbarOption {
|
pub fn new(name: &str, icon: FontawesomeIcon, onclick: Callback<MouseEvent>, selected: bool) -> ActionbarOption {
|
||||||
ActionbarOption::new_opt(Some(name.to_owned()), Some(icon), Some(onclick), false, selected, true)
|
ActionbarOption::new_opt(Some(name.to_owned()), Some(icon), Some(onclick), false, selected, true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Into<VNode> for ActionbarOptionState {
|
impl Into<VNode> for ActionbarOptionState {
|
||||||
fn into(self) -> VNode {
|
fn into(self) -> VNode {
|
||||||
let classes = if self.inner.center_content { classes!() } else { classes!("ab-navbutton") };
|
|
||||||
let onclick = self.inner.onclick.unwrap_or_default();
|
let onclick = self.inner.onclick.unwrap_or_default();
|
||||||
html! {
|
html! {
|
||||||
<Button
|
<div
|
||||||
fill={self.inner.fill}
|
class={classes!(
|
||||||
large=true
|
if self.inner.selected { "current" } else { "" },
|
||||||
class={classes}
|
if self.inner.icon.is_some() && self.inner.name.is_some() { "" } else { "small" }
|
||||||
icon={self.inner.icon}
|
)}
|
||||||
onclick={Callback::from(move |e| { self.callback.set(false); onclick.emit(e) })}
|
onclick={Callback::from(move |e| {
|
||||||
active={self.inner.selected}>
|
self.callback.set(false);
|
||||||
{self.inner.name}
|
onclick.emit(e)
|
||||||
</Button>
|
})}>
|
||||||
|
<div>
|
||||||
|
{if let Some(icon) = self.inner.icon { html!{ <FAIcon icon={icon} /> } } else { html!{} }}
|
||||||
|
<span>{self.inner.name}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Properties, PartialEq)]
|
#[derive(Properties, PartialEq)]
|
||||||
pub struct ActionbarProps {
|
pub struct ActionbarProps {
|
||||||
pub children: ChildrenRenderer<ActionbarOption>
|
#[prop_or_default]
|
||||||
|
pub mobile_title: Option<String>,
|
||||||
|
pub items: Vec<ActionbarOption>
|
||||||
}
|
}
|
||||||
|
|
||||||
#[function_component]
|
#[function_component]
|
||||||
pub fn Actionbar(props: &ActionbarProps) -> Html {
|
pub fn Actionbar(props: &ActionbarProps) -> Html {
|
||||||
let expanded = use_state_eq(|| false);
|
let expanded = use_state_eq(|| false);
|
||||||
|
let expanded_setter = expanded.setter();
|
||||||
let is_expanded = *expanded;
|
let is_expanded = *expanded;
|
||||||
let children = props.children.iter().map(|opt| ActionbarOptionState { inner: opt, callback: expanded.clone() }).collect::<Vec<ActionbarOptionState>>();
|
|
||||||
|
|
||||||
html! {
|
html! {
|
||||||
<div class="actionbar">
|
<div class="navbar">
|
||||||
<div class="ab-portrait">
|
<div style={if props.mobile_title.is_some() { "gap: 5px;" } else { "" }} onclick={Callback::from(move |_| expanded.set(!*expanded))}>
|
||||||
<Button onclick={Callback::from(move |_| expanded.set(!is_expanded))} icon={if is_expanded { MenuOpen } else { MenuClosed }} large=true />
|
<FAIcon
|
||||||
<Collapse is_open={is_expanded}>
|
icon={if is_expanded { FontawesomeIcon::ChevronUp } else { FontawesomeIcon::ChevronDown }} />
|
||||||
<ButtonGroup class="ab-portrait-content" vertical=true>
|
{
|
||||||
{children.clone()}
|
if let Some(ref title) = props.mobile_title {
|
||||||
</ButtonGroup>
|
html! {
|
||||||
</Collapse>
|
<span>{title}</span>
|
||||||
|
}
|
||||||
|
} else { html! {} }
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class={if is_expanded { "open" } else { "" }}></div>
|
||||||
|
<div>
|
||||||
|
{props.items.iter()
|
||||||
|
.map(move |opt| ActionbarOptionState { inner: (*opt).clone(), callback: expanded_setter.clone() })
|
||||||
|
.collect::<Html>()}
|
||||||
</div>
|
</div>
|
||||||
<ButtonGroup class="ab-landscape" fill=true>
|
|
||||||
{children}
|
|
||||||
</ButtonGroup>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
0
src/component/cache.rs
Normal file
0
src/component/cache.rs
Normal file
30
src/component/card.rs
Normal file
30
src/component/card.rs
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
use web_sys::MouseEvent;
|
||||||
|
use yew::{Html, function_component, html, Children, Properties, Classes, Callback, classes};
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Properties, PartialEq)]
|
||||||
|
pub struct CardProps {
|
||||||
|
#[prop_or_default]
|
||||||
|
pub children: Children,
|
||||||
|
|
||||||
|
#[prop_or_default]
|
||||||
|
pub style: String,
|
||||||
|
|
||||||
|
#[prop_or_default]
|
||||||
|
pub class: Classes,
|
||||||
|
|
||||||
|
#[prop_or_default]
|
||||||
|
pub onclick: Callback<MouseEvent>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[function_component]
|
||||||
|
pub fn Card(props: &CardProps) -> Html {
|
||||||
|
html! {
|
||||||
|
<div
|
||||||
|
class={classes!("card", props.class.clone())}
|
||||||
|
style={props.style.clone()}
|
||||||
|
onclick={props.onclick.clone()}>
|
||||||
|
{props.children.clone()}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
377
src/component/fa_icon.rs
Normal file
377
src/component/fa_icon.rs
Normal file
@ -0,0 +1,377 @@
|
|||||||
|
use std::fmt::Display;
|
||||||
|
|
||||||
|
use web_sys::MouseEvent;
|
||||||
|
use yew::{Properties, function_component, Html, html, Callback, Classes, classes};
|
||||||
|
|
||||||
|
#[derive(PartialEq, Clone)]
|
||||||
|
pub enum FontawesomeIcon {
|
||||||
|
Rust,
|
||||||
|
GitHub,
|
||||||
|
Camera,
|
||||||
|
ShareNodes,
|
||||||
|
Sun,
|
||||||
|
Moon,
|
||||||
|
House,
|
||||||
|
Code,
|
||||||
|
Envelope,
|
||||||
|
ChevronDown,
|
||||||
|
ChevronUp,
|
||||||
|
ChevronLeft,
|
||||||
|
ChevronRight,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for FontawesomeIcon {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_str(match self {
|
||||||
|
Self::Rust => "fa-brands fa-rust",
|
||||||
|
Self::GitHub => "fa-brands fa-github",
|
||||||
|
Self::Camera => "fa-camera",
|
||||||
|
Self::ShareNodes => "fa-share-nodes",
|
||||||
|
Self::Sun => "fa-sun",
|
||||||
|
Self::Moon => "fa-moon",
|
||||||
|
Self::House => "fa-house",
|
||||||
|
Self::Code => "fa-code",
|
||||||
|
Self::Envelope => "fa-envelope",
|
||||||
|
Self::ChevronDown => "fa-chevron-down",
|
||||||
|
Self::ChevronUp => "fa-chevron-up",
|
||||||
|
Self::ChevronLeft => "fa-chevron-left",
|
||||||
|
Self::ChevronRight => "fa-chevron-right",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq, Default)]
|
||||||
|
pub enum FontawesomeStyle {
|
||||||
|
#[default]
|
||||||
|
//Regular,
|
||||||
|
Solid,
|
||||||
|
Light,
|
||||||
|
DuoTone,
|
||||||
|
Thin,
|
||||||
|
Sharp,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for FontawesomeStyle {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_str(match self {
|
||||||
|
//Self::Regular => "fa-regular",
|
||||||
|
Self::Solid => "fa-solid",
|
||||||
|
Self::Light => "fa-light",
|
||||||
|
Self::DuoTone => "fa-duotone",
|
||||||
|
Self::Thin => "fa-thin",
|
||||||
|
Self::Sharp => "fa-sharp",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq, Default)]
|
||||||
|
pub enum FontawesomeSize {
|
||||||
|
XXS,
|
||||||
|
XS,
|
||||||
|
S,
|
||||||
|
#[default]
|
||||||
|
Regular,
|
||||||
|
L,
|
||||||
|
XL,
|
||||||
|
XXL
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for FontawesomeSize {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_str(match self {
|
||||||
|
Self::XXS => "fa-2xs",
|
||||||
|
Self::XS => "fa-xs",
|
||||||
|
Self::S => "fa-sm",
|
||||||
|
Self::Regular => "",
|
||||||
|
Self::L => "fa-lg",
|
||||||
|
Self::XL => "fa-xl",
|
||||||
|
Self::XXL => "fa-2xl"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq, Default)]
|
||||||
|
pub enum FontawesomeSpinType {
|
||||||
|
#[default]
|
||||||
|
Regular,
|
||||||
|
Pulse
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq, Default)]
|
||||||
|
pub enum FontawesomeSpinDirection {
|
||||||
|
#[default]
|
||||||
|
Clockwise,
|
||||||
|
CounterClockwise
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq)]
|
||||||
|
pub enum FontawesomeAnimationType {
|
||||||
|
Beat {
|
||||||
|
scale: f32,
|
||||||
|
},
|
||||||
|
Fade {
|
||||||
|
opacity: f32,
|
||||||
|
},
|
||||||
|
BeatFade {
|
||||||
|
scale: f32,
|
||||||
|
opacity: f32,
|
||||||
|
},
|
||||||
|
Bounce {
|
||||||
|
rebound: f32,
|
||||||
|
height: f32,
|
||||||
|
squish_scale_x: f32,
|
||||||
|
squish_scale_y: f32,
|
||||||
|
jump_scale_x: f32,
|
||||||
|
jump_scale_y: f32,
|
||||||
|
land_scale_x: f32,
|
||||||
|
land_scale_y: f32,
|
||||||
|
},
|
||||||
|
Flip {
|
||||||
|
x: f32,
|
||||||
|
y: f32,
|
||||||
|
z: f32,
|
||||||
|
angle: f32,
|
||||||
|
},
|
||||||
|
Shake,
|
||||||
|
Spin {
|
||||||
|
spin_type: FontawesomeSpinType,
|
||||||
|
direction: FontawesomeSpinDirection
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FontawesomeAnimationType {
|
||||||
|
fn class(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Beat { scale: _ } => "fa-beat",
|
||||||
|
Self::Fade { opacity: _ } => "fa-fade",
|
||||||
|
Self::BeatFade { scale: _, opacity: _ } => "fa-beat-fade",
|
||||||
|
Self::Bounce { rebound: _, height: _, squish_scale_x: _, squish_scale_y: _, jump_scale_x: _, jump_scale_y: _, land_scale_x: _, land_scale_y: _ } => "fa-bounce",
|
||||||
|
Self::Flip { x: _, y: _, z: _, angle: _ } => "fa-flip",
|
||||||
|
Self::Shake => "fa-shake",
|
||||||
|
Self::Spin { spin_type, direction } =>
|
||||||
|
match direction {
|
||||||
|
FontawesomeSpinDirection::Clockwise =>
|
||||||
|
match spin_type {
|
||||||
|
FontawesomeSpinType::Regular => "fa-spin",
|
||||||
|
FontawesomeSpinType::Pulse => "fa-spin-pulse",
|
||||||
|
},
|
||||||
|
FontawesomeSpinDirection::CounterClockwise =>
|
||||||
|
match spin_type {
|
||||||
|
FontawesomeSpinType::Regular => "fa-spin fa-spin-reverse",
|
||||||
|
FontawesomeSpinType::Pulse => "fa-spin-pulse fa-spin-reverse",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn style(&self) -> String {
|
||||||
|
match self {
|
||||||
|
Self::Beat { scale } => format!("--fa-beat-scale: {scale};"),
|
||||||
|
Self::Fade { opacity } => format!("--fa-fade-opacity: {opacity};"),
|
||||||
|
Self::BeatFade { scale, opacity } => format!("--fa-beat-fade-opacity: {opacity}; --fa-beat-fade-scale: {scale};"),
|
||||||
|
Self::Bounce {
|
||||||
|
rebound,
|
||||||
|
height,
|
||||||
|
squish_scale_x,
|
||||||
|
squish_scale_y,
|
||||||
|
jump_scale_x,
|
||||||
|
jump_scale_y,
|
||||||
|
land_scale_x,
|
||||||
|
land_scale_y
|
||||||
|
} => format!("--fa-bounce-rebound: {rebound}; --fa-bounce-height: {height}; --fa-bounce-start-scale-x: {squish_scale_x}; --fa-bounce-start-scale-y: {squish_scale_y}; --fa-bounce-jump-scale-x: {jump_scale_x}; --fa-bounce-jump-scale-y: {jump_scale_y}; --fa-bounce-land-scale-x: {land_scale_x}; --fa-bounce-land-scale-y: {land_scale_y};"),
|
||||||
|
Self::Flip { x, y, z, angle } => format!("--fa-flip-x: {x}; --fa-flip-y: {y}; --fa-flip-z: {z}; --fa-flip-angle: {angle};"),
|
||||||
|
_ => "".to_owned(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq)]
|
||||||
|
pub enum AnimationTime {
|
||||||
|
Seconds(f32),
|
||||||
|
Milliseconds(f32),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AnimationTime {
|
||||||
|
fn style_unit(&self) -> String {
|
||||||
|
match self {
|
||||||
|
Self::Seconds(seconds) => format!("{seconds}s"),
|
||||||
|
Self::Milliseconds(millis) => format!("{millis}ms"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for AnimationTime {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Seconds(0.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for AnimationTime {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_str(&self.style_unit())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq)]
|
||||||
|
pub enum AnimationDuration {
|
||||||
|
Time(AnimationTime),
|
||||||
|
Infinite
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for AnimationDuration {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Time(Default::default())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for AnimationDuration {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_str(&match self {
|
||||||
|
Self::Time(time) => time.style_unit(),
|
||||||
|
Self::Infinite => "infinite".to_owned(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq, Default)]
|
||||||
|
pub enum AnimationDirection {
|
||||||
|
#[default]
|
||||||
|
Normal,
|
||||||
|
Reverse,
|
||||||
|
Alternate,
|
||||||
|
AlternateReverse,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for AnimationDirection {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_str(match self {
|
||||||
|
Self::Normal => "normal",
|
||||||
|
Self::Reverse => "reverse",
|
||||||
|
Self::Alternate => "alternate",
|
||||||
|
Self::AlternateReverse => "alternate-reverse"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq)]
|
||||||
|
pub enum AnimationStepType {
|
||||||
|
JumpStart,
|
||||||
|
JumpEnd,
|
||||||
|
JumpNone,
|
||||||
|
JumpBoth,
|
||||||
|
Start,
|
||||||
|
End
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for AnimationStepType {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_str(match self {
|
||||||
|
Self::JumpStart => "jump-start",
|
||||||
|
Self::JumpEnd => "jump-end",
|
||||||
|
Self::JumpNone => "jump-none",
|
||||||
|
Self::JumpBoth => "jump-both",
|
||||||
|
Self::Start => "start",
|
||||||
|
Self::End => "end",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq, Default)]
|
||||||
|
pub enum AnimationTiming {
|
||||||
|
#[default]
|
||||||
|
Ease,
|
||||||
|
Linear,
|
||||||
|
EaseIn,
|
||||||
|
EaseOut,
|
||||||
|
EaseInOut,
|
||||||
|
CubicBezier(f32, f32, f32, f32),
|
||||||
|
Steps(i32, AnimationStepType),
|
||||||
|
StepStart,
|
||||||
|
StepEnd,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for AnimationTiming {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_str(&
|
||||||
|
match self {
|
||||||
|
Self::Ease => "ease".to_owned(),
|
||||||
|
Self::Linear => "linear".to_owned(),
|
||||||
|
Self::EaseIn => "ease-in".to_owned(),
|
||||||
|
Self::EaseOut => "ease-out".to_owned(),
|
||||||
|
Self::EaseInOut => "ease-in-out".to_owned(),
|
||||||
|
Self::CubicBezier(p1, p2, p3, p4) => format!("cubic-bezier({p1}, {p2}, {p3}, {p4})"),
|
||||||
|
Self::Steps(count, step_type) => format!("steps({count}, {step_type})"),
|
||||||
|
Self::StepStart => "step-start".to_owned(),
|
||||||
|
Self::StepEnd => "step-end".to_owned(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq)]
|
||||||
|
pub struct FontawesomeAnimation {
|
||||||
|
duration: AnimationDuration,
|
||||||
|
delay: AnimationTime,
|
||||||
|
direction: AnimationDirection,
|
||||||
|
iterations: AnimationDuration,
|
||||||
|
timing: AnimationTiming,
|
||||||
|
anim_type: FontawesomeAnimationType
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FontawesomeAnimation {
|
||||||
|
fn class(&self) -> &'static str {
|
||||||
|
self.anim_type.class()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn style(&self) -> String {
|
||||||
|
format!(
|
||||||
|
"--fa-animation-delay: {}; --fa-animation-direction: {}; --fa-animation-duration: {}; --fa-animation-iteration-count: {}; --fa-animation-timing: {}; {}",
|
||||||
|
self.delay,
|
||||||
|
self.direction,
|
||||||
|
self.duration,
|
||||||
|
self.iterations,
|
||||||
|
self.timing,
|
||||||
|
self.anim_type.style()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Properties, PartialEq)]
|
||||||
|
pub struct FontawesomeProperties {
|
||||||
|
pub icon: FontawesomeIcon,
|
||||||
|
#[prop_or_default]
|
||||||
|
pub style: FontawesomeStyle,
|
||||||
|
#[prop_or_default]
|
||||||
|
pub size: FontawesomeSize,
|
||||||
|
#[prop_or_default]
|
||||||
|
pub animation: Option<FontawesomeAnimation>,
|
||||||
|
#[prop_or_default]
|
||||||
|
pub onclick: Callback<MouseEvent>,
|
||||||
|
#[prop_or_default]
|
||||||
|
pub class: Classes,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[function_component]
|
||||||
|
pub fn FAIcon(props: &FontawesomeProperties) -> Html {
|
||||||
|
html! {
|
||||||
|
<i
|
||||||
|
onclick={props.onclick.clone()}
|
||||||
|
class={classes!(format!(
|
||||||
|
"{} {} {} {}",
|
||||||
|
props.icon,
|
||||||
|
props.style,
|
||||||
|
props.size,
|
||||||
|
match props.animation {
|
||||||
|
None => "",
|
||||||
|
Some(ref animation) => animation.class()
|
||||||
|
}
|
||||||
|
), props.class.clone())}
|
||||||
|
style={format!(
|
||||||
|
"{}",
|
||||||
|
match props.animation {
|
||||||
|
None => "".to_owned(),
|
||||||
|
Some(ref animation) => animation.style()
|
||||||
|
}
|
||||||
|
)}></i>
|
||||||
|
}
|
||||||
|
}
|
@ -1,9 +1,7 @@
|
|||||||
use web_sys::MouseEvent;
|
use web_sys::MouseEvent;
|
||||||
use yew::{function_component, Html, html, Callback, Properties, use_state};
|
use yew::{function_component, Html, html, Callback, Properties, Component};
|
||||||
use yewprint::{Overlay, Icon, IconSize, Intent};
|
|
||||||
|
|
||||||
const CHEVRON_SIZE: f64 = 40.0;
|
use crate::component::{overlay::Overlay, fa_icon::{FAIcon, FontawesomeIcon, FontawesomeSize}};
|
||||||
const CHEVRON_TYPE: Intent = Intent::Danger;
|
|
||||||
|
|
||||||
#[derive(PartialEq, Clone)]
|
#[derive(PartialEq, Clone)]
|
||||||
pub struct ImageDescription {
|
pub struct ImageDescription {
|
||||||
@ -36,20 +34,24 @@ pub struct DefaultImageViewerProps {
|
|||||||
pub onclose: Callback<()>,
|
pub onclose: Callback<()>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
#[function_component]
|
#[function_component]
|
||||||
pub fn DefaultImageViewer(props: &DefaultImageViewerProps) -> Html {
|
pub fn DefaultImageViewer(props: &DefaultImageViewerProps) -> Html {
|
||||||
let selected_image = use_state(|| 0);
|
let selected_image = use_state_eq(|| 0);
|
||||||
let (select_next, select_prev) = (selected_image.clone(), selected_image.clone());
|
let select_next = selected_image.setter();
|
||||||
|
let select_prev = selected_image.setter();
|
||||||
|
|
||||||
let (has_next, has_prev) = (props.infinite || *selected_image < props.images.len() - 1, props.infinite || *selected_image > 0);
|
let has_next = props.infinite || *selected_image < props.images.len() - 1;
|
||||||
let next = (*select_next + props.images.len() + 1) % props.images.len();
|
let has_prev = props.infinite || *selected_image > 0;
|
||||||
let prev = (*select_prev + props.images.len() - 1) % props.images.len();
|
let next = (*selected_image + props.images.len() + 1) % props.images.len();
|
||||||
|
let prev = (*selected_image + props.images.len() - 1) % props.images.len();
|
||||||
|
let onclose = props.onclose.clone();
|
||||||
|
|
||||||
html! {
|
html! {
|
||||||
<ImageViewer
|
<ImageViewer
|
||||||
image={props.images[*selected_image].clone()}
|
image={props.images[*selected_image].clone()}
|
||||||
open={props.open && !props.images.is_empty()}
|
open={props.open && !props.images.is_empty()}
|
||||||
onclose={props.onclose.clone()}
|
onclose={Callback::from(move |_| onclose.emit(()))}
|
||||||
has_next={has_next}
|
has_next={has_next}
|
||||||
has_prev={has_prev}
|
has_prev={has_prev}
|
||||||
onnext={Callback::from(move |_| if has_next {
|
onnext={Callback::from(move |_| if has_next {
|
||||||
@ -61,6 +63,70 @@ pub fn DefaultImageViewer(props: &DefaultImageViewerProps) -> Html {
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
pub enum DefaultImageViewerMessage {
|
||||||
|
Next,
|
||||||
|
Prev
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct DefaultImageViewer {
|
||||||
|
current: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component for DefaultImageViewer {
|
||||||
|
type Message = DefaultImageViewerMessage;
|
||||||
|
|
||||||
|
type Properties = DefaultImageViewerProps;
|
||||||
|
|
||||||
|
fn create(_ctx: &yew::Context<Self>) -> Self {
|
||||||
|
Self { current: 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn view(&self, ctx: &yew::Context<Self>) -> Html {
|
||||||
|
let DefaultImageViewerProps {
|
||||||
|
images,
|
||||||
|
open,
|
||||||
|
infinite,
|
||||||
|
onclose,
|
||||||
|
} = ctx.props();
|
||||||
|
|
||||||
|
let has_next = *infinite || self.current < images.len() - 1;
|
||||||
|
let has_prev = *infinite || self.current > 0;
|
||||||
|
let onclose = onclose.clone();
|
||||||
|
|
||||||
|
html! {
|
||||||
|
<ImageViewer
|
||||||
|
image={images[self.current].clone()}
|
||||||
|
open={*open && !images.is_empty()}
|
||||||
|
onclose={Callback::from(move |_| onclose.emit(()))}
|
||||||
|
has_next={has_next}
|
||||||
|
has_prev={has_prev}
|
||||||
|
onnext={if has_next { ctx.link().callback(|_| DefaultImageViewerMessage::Next)} else { Default::default() }}
|
||||||
|
onprev={if has_prev { ctx.link().callback(|_| DefaultImageViewerMessage::Prev)} else { Default::default() }}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(&mut self, ctx: &yew::Context<Self>, msg: Self::Message) -> bool {
|
||||||
|
let images = &ctx.props().images;
|
||||||
|
|
||||||
|
self.current = match msg {
|
||||||
|
DefaultImageViewerMessage::Next => {
|
||||||
|
if self.current == images.len() - 1 { 0 } else { self.current + 1 }
|
||||||
|
},
|
||||||
|
DefaultImageViewerMessage::Prev => {
|
||||||
|
if self.current == 0 { images.len() - 1 } else { self.current - 1 }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
images.len() > 1
|
||||||
|
}
|
||||||
|
|
||||||
|
fn changed(&mut self, ctx: &yew::Context<Self>, old_props: &Self::Properties) -> bool {
|
||||||
|
ctx.props() != old_props
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Properties, PartialEq)]
|
#[derive(Properties, PartialEq)]
|
||||||
pub struct ImageViewerProps {
|
pub struct ImageViewerProps {
|
||||||
@ -81,18 +147,27 @@ pub fn ImageViewer(props: &ImageViewerProps) -> Html {
|
|||||||
html! {
|
html! {
|
||||||
<>
|
<>
|
||||||
<Overlay
|
<Overlay
|
||||||
class="gallery"
|
class="overlay-gallery"
|
||||||
open={props.open && props.image.is_some()}
|
open={props.open && props.image.is_some()}
|
||||||
onclose={&props.onclose}>
|
onclick={Callback::from(move |_| onclose.emit(()))}>
|
||||||
<>
|
<>
|
||||||
<div onclick={Callback::from(move |_| onclose.emit(()))}></div>
|
<div></div>
|
||||||
{
|
{
|
||||||
if let Some(image_desc) = props.image.as_ref() {
|
if let Some(image_desc) = props.image.as_ref() {
|
||||||
let onclose = props.onclose.clone();
|
let onclose = props.onclose.clone();
|
||||||
Some(html! {
|
Some(html! {
|
||||||
<>
|
<>
|
||||||
<div onclick={Callback::from(move |_| onclose.emit(()))}>
|
<div>
|
||||||
<img src={if image_desc.link.starts_with("https?://") { image_desc.link.to_string() } else { format!("/img/{}", image_desc.link) }} />
|
<img
|
||||||
|
onclick={Callback::from(move |_| onclose.emit(()))}
|
||||||
|
src={
|
||||||
|
if image_desc.link.starts_with("https?://") {
|
||||||
|
image_desc.link.to_string()
|
||||||
|
} else {
|
||||||
|
format!("/img/{}", image_desc.link)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
{
|
{
|
||||||
if let Some(description) = &image_desc.description {
|
if let Some(description) = &image_desc.description {
|
||||||
Some(html! {
|
Some(html! {
|
||||||
@ -106,13 +181,13 @@ pub fn ImageViewer(props: &ImageViewerProps) -> Html {
|
|||||||
// Controls: Next
|
// Controls: Next
|
||||||
if props.has_next {
|
if props.has_next {
|
||||||
Some(html! {
|
Some(html! {
|
||||||
<Icon
|
<div class="overlay-gallery-next">
|
||||||
icon={yewprint::Icon::ChevronRight}
|
<FAIcon
|
||||||
intent={CHEVRON_TYPE}
|
icon={FontawesomeIcon::ChevronRight}
|
||||||
size={IconSize(CHEVRON_SIZE)}
|
size={FontawesomeSize::XL}
|
||||||
onclick={props.onnext.clone()}
|
onclick={props.onnext.clone()}
|
||||||
class="gallery-next"
|
/>
|
||||||
/>
|
</div>
|
||||||
})
|
})
|
||||||
} else { None }
|
} else { None }
|
||||||
}
|
}
|
||||||
@ -121,13 +196,13 @@ pub fn ImageViewer(props: &ImageViewerProps) -> Html {
|
|||||||
// Controls: Prev
|
// Controls: Prev
|
||||||
if props.has_prev {
|
if props.has_prev {
|
||||||
Some(html! {
|
Some(html! {
|
||||||
<Icon
|
<div class="overlay-gallery-prev">
|
||||||
icon={yewprint::Icon::ChevronLeft}
|
<FAIcon
|
||||||
intent={CHEVRON_TYPE}
|
icon={FontawesomeIcon::ChevronLeft}
|
||||||
size={IconSize(CHEVRON_SIZE)}
|
size={FontawesomeSize::XL}
|
||||||
onclick={props.onprev.clone()}
|
onclick={props.onprev.clone()}
|
||||||
class="gallery-prev"
|
/>
|
||||||
/>
|
</div>
|
||||||
})
|
})
|
||||||
} else { None }
|
} else { None }
|
||||||
}
|
}
|
||||||
|
@ -1,2 +1,7 @@
|
|||||||
pub mod actionbar;
|
pub mod actionbar;
|
||||||
pub mod image_viewer;
|
pub mod image_viewer;
|
||||||
|
pub mod cache;
|
||||||
|
pub mod fa_icon;
|
||||||
|
pub mod overlay;
|
||||||
|
pub mod card;
|
||||||
|
pub mod tag;
|
92
src/component/overlay.rs
Normal file
92
src/component/overlay.rs
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
use web_sys::MouseEvent;
|
||||||
|
use yew::{Html, html, Properties, Children, Classes, classes, Callback, Component};
|
||||||
|
|
||||||
|
#[derive(Properties, PartialEq, Debug)]
|
||||||
|
pub struct OverlayProps {
|
||||||
|
pub open: bool,
|
||||||
|
|
||||||
|
#[prop_or_default]
|
||||||
|
pub noshade: bool,
|
||||||
|
|
||||||
|
#[prop_or_default]
|
||||||
|
pub center: bool,
|
||||||
|
|
||||||
|
#[prop_or_default]
|
||||||
|
pub children: Children,
|
||||||
|
|
||||||
|
#[prop_or_default]
|
||||||
|
pub class: Classes,
|
||||||
|
|
||||||
|
#[prop_or_default]
|
||||||
|
pub onclick: Callback<()>
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum OverlayMessage {
|
||||||
|
ClickContent,
|
||||||
|
ClickRoot
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Overlay {
|
||||||
|
should_notify: bool,
|
||||||
|
on_root_click: Callback<MouseEvent>,
|
||||||
|
on_child_click: Callback<MouseEvent>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component for Overlay {
|
||||||
|
type Message = OverlayMessage;
|
||||||
|
type Properties = OverlayProps;
|
||||||
|
|
||||||
|
fn create(ctx: &yew::Context<Self>) -> Self {
|
||||||
|
Self {
|
||||||
|
should_notify: true,
|
||||||
|
on_root_click: ctx.link().callback(|_| OverlayMessage::ClickRoot),
|
||||||
|
on_child_click: ctx.link().callback(|_| OverlayMessage::ClickContent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn view(&self, ctx: &yew::Context<Self>) -> Html {
|
||||||
|
let OverlayProps {
|
||||||
|
open,
|
||||||
|
noshade,
|
||||||
|
center,
|
||||||
|
children,
|
||||||
|
class,
|
||||||
|
onclick: _,
|
||||||
|
} = ctx.props();
|
||||||
|
html! {
|
||||||
|
<div
|
||||||
|
onclick={self.on_root_click.clone()}
|
||||||
|
class={format!(
|
||||||
|
"overlay{}{}",
|
||||||
|
if *noshade { "" } else { " shade" },
|
||||||
|
if *open { "" } else { " hidden" }
|
||||||
|
)}>
|
||||||
|
<div
|
||||||
|
onclick={self.on_child_click.clone()}
|
||||||
|
class={classes!(if *center { "center" } else { "" }, class.clone())}>{children.clone()}</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(&mut self, ctx: &yew::Context<Self>, msg: Self::Message) -> bool {
|
||||||
|
let mut notified = false;
|
||||||
|
match msg {
|
||||||
|
OverlayMessage::ClickContent => self.should_notify = false,
|
||||||
|
OverlayMessage::ClickRoot => {
|
||||||
|
if self.should_notify {
|
||||||
|
ctx.props().onclick.emit(());
|
||||||
|
notified = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.should_notify = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
notified
|
||||||
|
}
|
||||||
|
|
||||||
|
fn changed(&mut self, ctx: &yew::Context<Self>, old_props: &Self::Properties) -> bool {
|
||||||
|
ctx.props() != old_props
|
||||||
|
}
|
||||||
|
}
|
35
src/component/tag.rs
Normal file
35
src/component/tag.rs
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
use yew::{function_component, Properties, Html, html, classes};
|
||||||
|
|
||||||
|
#[derive(PartialEq, Default)]
|
||||||
|
pub enum TagColor {
|
||||||
|
#[default]
|
||||||
|
Blue,
|
||||||
|
Red,
|
||||||
|
Orange,
|
||||||
|
Green
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TagColor {
|
||||||
|
fn as_class(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Blue => "blue",
|
||||||
|
Self::Red => "red",
|
||||||
|
Self::Orange => "orange",
|
||||||
|
Self::Green => "green",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Properties, PartialEq)]
|
||||||
|
pub struct TagProps {
|
||||||
|
#[prop_or_default]
|
||||||
|
pub color: TagColor,
|
||||||
|
pub text: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[function_component]
|
||||||
|
pub fn Tag(props: &TagProps) -> Html {
|
||||||
|
html! {
|
||||||
|
<span class={classes!("tag", props.color.as_class())}><span>{props.text.clone()}</span></span>
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,7 @@
|
|||||||
use web_sys::MouseEvent;
|
use web_sys::MouseEvent;
|
||||||
use yew::{function_component, html, Html, use_state, Callback};
|
use yew::{function_component, html, Html, Callback, use_state_eq};
|
||||||
|
|
||||||
use crate::{component::image_viewer::{ImageDescription, ImageViewer}};
|
use crate::component::image_viewer::{ImageDescription, ImageViewer};
|
||||||
|
|
||||||
const GALLERY: [&str; 13] = [
|
const GALLERY: [&str; 13] = [
|
||||||
"gallery-1.jpg",
|
"gallery-1.jpg",
|
||||||
@ -21,19 +21,19 @@ const GALLERY: [&str; 13] = [
|
|||||||
|
|
||||||
fn gallery_entry(link: &&str, onclick: Callback<MouseEvent>) -> Html {
|
fn gallery_entry(link: &&str, onclick: Callback<MouseEvent>) -> Html {
|
||||||
html! {
|
html! {
|
||||||
<div><img src={format!("/img/{}", link)} onclick={onclick}/></div>
|
<div><img src={format!("/img/{link}")} onclick={onclick}/></div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[function_component]
|
#[function_component]
|
||||||
pub fn Gallery() -> Html {
|
pub fn Gallery() -> Html {
|
||||||
let images = GALLERY.iter().map(|it| ImageDescription::new_blank(it.to_string())).collect::<Vec<ImageDescription>>();
|
let images = GALLERY.iter().map(|it| ImageDescription::new(it.to_string(), it)).collect::<Vec<ImageDescription>>();
|
||||||
let is_open = use_state(|| false);
|
let is_open = use_state_eq(|| false);
|
||||||
let selected_image = use_state(|| 0);
|
let selected_image = use_state_eq(|| 0);
|
||||||
let select_next = selected_image.clone();
|
let select_next = selected_image.setter();
|
||||||
let select_prev = selected_image.clone();
|
let select_prev = selected_image.setter();
|
||||||
let next = (*select_next + images.len() + 1) % images.len();
|
let next = (*selected_image + images.len() + 1) % images.len();
|
||||||
let prev = (*select_prev + images.len() - 1) % images.len();
|
let prev = (*selected_image + images.len() - 1) % images.len();
|
||||||
|
|
||||||
html! {
|
html! {
|
||||||
<>
|
<>
|
||||||
|
@ -2,39 +2,24 @@ use comrak::{markdown_to_html, ComrakOptions};
|
|||||||
use gloo::utils::document;
|
use gloo::utils::document;
|
||||||
use gloo_net::http::Request;
|
use gloo_net::http::Request;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use yew::{function_component, html, Html, UseStateHandle, use_state, use_effect_with_deps, Properties, Children, use_context, Callback};
|
use yew::{function_component, html, Html, UseStateHandle, use_effect_with_deps, Properties, Children, use_context, Callback, use_state_eq};
|
||||||
use yewprint::{Divider, Elevation, Card, Tag, Intent, Icon};
|
use crate::{util::log, theme::{ThemeContext, ThemeState}, component::{card::Card, tag::{Tag, TagColor}}};
|
||||||
|
|
||||||
use crate::{util::log, theme::{ThemeContext, ThemeState}, component::image_viewer::{ImageDescription, DefaultImageViewer}};
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
enum TagType {
|
enum TagType {
|
||||||
NaturalLanguage, CodeLanguage, Interest
|
NaturalLanguage, CodeLanguage, Interest
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(PartialEq)]
|
|
||||||
enum ImageSource {
|
|
||||||
Link(String),
|
|
||||||
Icon(Icon, Intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(PartialEq)]
|
#[derive(PartialEq)]
|
||||||
struct ImageResource {
|
struct ImageResource {
|
||||||
source: ImageSource,
|
source: String,
|
||||||
clickable: bool
|
clickable: bool
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ImageResource {
|
impl ImageResource {
|
||||||
pub fn new_link(link: String, clickable: bool) -> Self {
|
pub fn new_link(link: String, clickable: bool) -> Self {
|
||||||
Self {
|
Self {
|
||||||
source: ImageSource::Link(link),
|
source: link,
|
||||||
clickable
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn new_icon(icon: Icon, intent: Intent, clickable: bool) -> Self {
|
|
||||||
Self {
|
|
||||||
source: ImageSource::Icon(icon, intent),
|
|
||||||
clickable
|
clickable
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -65,7 +50,7 @@ pub fn Home() -> Html {
|
|||||||
html! {
|
html! {
|
||||||
<>
|
<>
|
||||||
<HomeTitle />
|
<HomeTitle />
|
||||||
<Divider />
|
<hr />
|
||||||
<div class="home-content">
|
<div class="home-content">
|
||||||
<Profile />
|
<Profile />
|
||||||
</div>
|
</div>
|
||||||
@ -109,14 +94,14 @@ fn get_json_resource<T>(file: &'static str, on_result: impl FnOnce(Vec<T>) -> ()
|
|||||||
|
|
||||||
#[function_component]
|
#[function_component]
|
||||||
fn ProfileTags() -> Html {
|
fn ProfileTags() -> Html {
|
||||||
let natural_languages: UseStateHandle<Vec<String>> = use_state(|| vec![]);
|
let natural_languages: UseStateHandle<Vec<String>> = use_state_eq(|| vec![]);
|
||||||
let code_languages: UseStateHandle<Vec<String>> = use_state(|| vec![]);
|
let code_languages: UseStateHandle<Vec<String>> = use_state_eq(|| vec![]);
|
||||||
let interests: UseStateHandle<Vec<String>> = use_state(|| vec![]);
|
let interests: UseStateHandle<Vec<String>> = use_state_eq(|| vec![]);
|
||||||
|
|
||||||
|
|
||||||
// TODO: Cache results
|
// TODO: Cache results
|
||||||
{
|
{
|
||||||
let natural_languages = natural_languages.clone();
|
let natural_languages = natural_languages.setter();
|
||||||
use_effect_with_deps(
|
use_effect_with_deps(
|
||||||
move |_| get_json_resource(
|
move |_| get_json_resource(
|
||||||
"/res/languages.json",
|
"/res/languages.json",
|
||||||
@ -127,7 +112,7 @@ fn ProfileTags() -> Html {
|
|||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
let code_languages = code_languages.clone();
|
let code_languages = code_languages.setter();
|
||||||
use_effect_with_deps(
|
use_effect_with_deps(
|
||||||
move |_| get_json_resource(
|
move |_| get_json_resource(
|
||||||
"/res/code.json",
|
"/res/code.json",
|
||||||
@ -138,7 +123,7 @@ fn ProfileTags() -> Html {
|
|||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
let interests = interests.clone();
|
let interests = interests.setter();
|
||||||
use_effect_with_deps(
|
use_effect_with_deps(
|
||||||
move |_| get_json_resource(
|
move |_| get_json_resource(
|
||||||
"/res/interests.json",
|
"/res/interests.json",
|
||||||
@ -155,18 +140,14 @@ fn ProfileTags() -> Html {
|
|||||||
].into_iter().flatten().map(|(tag, tag_type)| {
|
].into_iter().flatten().map(|(tag, tag_type)| {
|
||||||
html! {
|
html! {
|
||||||
<Tag
|
<Tag
|
||||||
interactive=true
|
text={tag.clone()}
|
||||||
minimal=true
|
color={
|
||||||
round=true
|
|
||||||
intent={
|
|
||||||
match tag_type {
|
match tag_type {
|
||||||
TagType::NaturalLanguage => Intent::Primary,
|
TagType::NaturalLanguage => TagColor::Blue,
|
||||||
TagType::CodeLanguage => Intent::Warning,
|
TagType::CodeLanguage => TagColor::Orange,
|
||||||
TagType::Interest => Intent::Success
|
TagType::Interest => TagColor::Green
|
||||||
}
|
}
|
||||||
}>
|
}/>
|
||||||
{tag}
|
|
||||||
</Tag>
|
|
||||||
}
|
}
|
||||||
}).collect::<Html>();
|
}).collect::<Html>();
|
||||||
|
|
||||||
@ -177,9 +158,9 @@ fn ProfileTags() -> Html {
|
|||||||
|
|
||||||
#[function_component]
|
#[function_component]
|
||||||
fn Profile() -> Html {
|
fn Profile() -> Html {
|
||||||
let profile_text = use_state(|| "".to_owned());
|
let profile_text = use_state_eq(|| "".to_owned());
|
||||||
{
|
{
|
||||||
let profile_text = profile_text.clone();
|
let profile_text = profile_text.setter();
|
||||||
use_effect_with_deps(move |_| {
|
use_effect_with_deps(move |_| {
|
||||||
get_text_resource("/res/profile.md".to_owned(), move |text| profile_text.set(markdown_to_html(&text, &ComrakOptions::default())));
|
get_text_resource("/res/profile.md".to_owned(), move |text| profile_text.set(markdown_to_html(&text, &ComrakOptions::default())));
|
||||||
}, ());
|
}, ());
|
||||||
@ -195,38 +176,19 @@ fn Profile() -> Html {
|
|||||||
|
|
||||||
#[function_component]
|
#[function_component]
|
||||||
fn HomeCard(props: &HomeCardProps) -> Html {
|
fn HomeCard(props: &HomeCardProps) -> Html {
|
||||||
let overlay_state = use_state(|| false);
|
|
||||||
let open_overlay_state = overlay_state.clone();
|
|
||||||
html! {
|
html! {
|
||||||
<Card elevation={Elevation::Level3} interactive=true class="home-card" onclick={Callback::from(move |_| open_overlay_state.set(true))}>
|
<Card class="home-card">
|
||||||
{
|
{
|
||||||
if let Some(image) = &props.image {
|
if let Some(image) = &props.image {
|
||||||
html! {
|
html! {
|
||||||
<div class="home-tag">
|
<div class="home-tag">
|
||||||
{
|
<img
|
||||||
match &image.image.source {
|
class="home-image circle"
|
||||||
ImageSource::Link(link) => html! {
|
src={if image.image.source.starts_with("https?://") {
|
||||||
<img class="home-image circle" src={if link.starts_with("https?://") { link.to_string() } else { format!("/img/{}", link) }} />
|
image.image.source.to_string()
|
||||||
},
|
|
||||||
ImageSource::Icon(icon, intent) => html! { <Icon {icon} {intent} size={20}/> }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
if image.image.clickable {
|
|
||||||
if let ImageSource::Link(link) = &image.image.source {
|
|
||||||
html! {
|
|
||||||
<DefaultImageViewer
|
|
||||||
images={vec![ ImageDescription::new_blank(link) ]}
|
|
||||||
open={*overlay_state}
|
|
||||||
onclose={Callback::from(move |_| overlay_state.set(false))} />
|
|
||||||
}
|
|
||||||
} else { html! {} }
|
|
||||||
} else {
|
} else {
|
||||||
html! {}
|
format!("/img/{}", image.image.source)
|
||||||
}
|
}} />
|
||||||
}
|
|
||||||
|
|
||||||
<b>{image.description}</b>
|
<b>{image.description}</b>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@ -241,7 +203,7 @@ fn HomeCard(props: &HomeCardProps) -> Html {
|
|||||||
|
|
||||||
#[function_component]
|
#[function_component]
|
||||||
fn Github() -> Html {
|
fn Github() -> Html {
|
||||||
let github_entries: UseStateHandle<Vec<GithubEntry>> = use_state(|| vec![]);
|
let github_entries: UseStateHandle<Vec<GithubEntry>> = use_state_eq(|| vec![]);
|
||||||
{
|
{
|
||||||
let github_entries = github_entries.clone();
|
let github_entries = github_entries.clone();
|
||||||
use_effect_with_deps(move |_| get_json_resource("/res/github.json", move |it| github_entries.set(it)), ());
|
use_effect_with_deps(move |_| get_json_resource("/res/github.json", move |it| github_entries.set(it)), ());
|
||||||
@ -257,7 +219,7 @@ fn Github() -> Html {
|
|||||||
github_entries.iter().map(|it| {
|
github_entries.iter().map(|it| {
|
||||||
let link = it.link.clone();
|
let link = it.link.clone();
|
||||||
html! {
|
html! {
|
||||||
<Card elevation={Elevation::Level3} interactive=true onclick={Callback::from(move |_| {
|
<Card onclick={Callback::from(move |_| {
|
||||||
if let Err(_) = document().location().unwrap().set_href(link.as_str()) {
|
if let Err(_) = document().location().unwrap().set_href(link.as_str()) {
|
||||||
log("Couldn't change href");
|
log("Couldn't change href");
|
||||||
}
|
}
|
||||||
|
13
src/theme.rs
13
src/theme.rs
@ -7,28 +7,15 @@ use crate::util::log;
|
|||||||
|
|
||||||
fn set_global_dark(is_dark: bool) {
|
fn set_global_dark(is_dark: bool) {
|
||||||
let root = document().get_elements_by_tag_name("html").get_with_index(0).expect("Root html tag");
|
let root = document().get_elements_by_tag_name("html").get_with_index(0).expect("Root html tag");
|
||||||
let class_list = root.get_elements_by_tag_name("body").get_with_index(0).expect("Body tag").class_list();
|
|
||||||
|
|
||||||
if is_dark {
|
if is_dark {
|
||||||
if let Err(_) = root.set_attribute("data-theme", "dark") {
|
if let Err(_) = root.set_attribute("data-theme", "dark") {
|
||||||
log("Couldn't set attribute 'data-theme' on root");
|
log("Couldn't set attribute 'data-theme' on root");
|
||||||
}
|
}
|
||||||
|
|
||||||
if !class_list.contains("bp3-dark") {
|
|
||||||
if let Err(_) = class_list.add_1("bp3-dark") {
|
|
||||||
log("Couldn't set class 'bp3-dark' on root")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if let Err(_) = root.remove_attribute("data-theme") {
|
if let Err(_) = root.remove_attribute("data-theme") {
|
||||||
log("Couldn't remove attribute 'data-theme' from root");
|
log("Couldn't remove attribute 'data-theme' from root");
|
||||||
}
|
}
|
||||||
|
|
||||||
if class_list.contains("bp3-dark") {
|
|
||||||
if let Err(_) = class_list.remove_1("bp3-dark") {
|
|
||||||
log("Couldn't remove class 'bp3-dark' from root")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,39 +1,49 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>Gabriel Tofvesson</title>
|
<meta charset="utf-8"/>
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com">
|
<title>Gabriel Tofvesson</title>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400&display=swap" rel="stylesheet">
|
<link rel="preconnect" href="https://fonts.gstatic.com">
|
||||||
<script type="text/javascript">
|
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400&display=swap" rel="stylesheet">
|
||||||
// Single Page Apps for GitHub Pages
|
<link rel="stylesheet" href="/style/app.css">
|
||||||
// MIT License
|
<link rel="stylesheet" href="/style/theme.css">
|
||||||
// https://github.com/rafgraph/spa-github-pages
|
<link rel="stylesheet" href="/style/tag.css">
|
||||||
// This script checks to see if a redirect is present in the query string,
|
<link rel="stylesheet" href="/style/navbar.css">
|
||||||
// converts it back into the correct url and adds it to the
|
<link rel="stylesheet" href="/style/card.css">
|
||||||
// browser's history using window.history.replaceState(...),
|
<link rel="stylesheet" href="/style/gallery.css">
|
||||||
// which won't cause the browser to attempt to load the new url.
|
<link rel="stylesheet" href="/style/overlay.css">
|
||||||
// When the single page app is loaded further down in this file,
|
<link rel="stylesheet" href="/style/overlay-gallery.css">
|
||||||
// the correct url will be waiting in the browser's history for
|
<link rel="stylesheet" href="/style/image-gallery.css">
|
||||||
// the single page app to route accordingly.
|
<script src="https://kit.fontawesome.com/d5cf53d9fb.js" crossorigin="anonymous"></script>
|
||||||
(function(l) {
|
<script type="text/javascript">
|
||||||
if (l.search[1] === '/' ) {
|
// Single Page Apps for GitHub Pages
|
||||||
var decoded = l.search.slice(1).split('&').map(function(s) {
|
// MIT License
|
||||||
return s.replace(/~and~/g, '&')
|
// https://github.com/rafgraph/spa-github-pages
|
||||||
}).join('?');
|
// This script checks to see if a redirect is present in the query string,
|
||||||
window.history.replaceState(null, null,
|
// converts it back into the correct url and adds it to the
|
||||||
l.pathname.slice(0, -1) + decoded + l.hash
|
// browser's history using window.history.replaceState(...),
|
||||||
);
|
// which won't cause the browser to attempt to load the new url.
|
||||||
}
|
// When the single page app is loaded further down in this file,
|
||||||
}(window.location))
|
// the correct url will be waiting in the browser's history for
|
||||||
</script>
|
// the single page app to route accordingly.
|
||||||
<meta charset="utf-8"/>
|
(function(l) {
|
||||||
<script type="module">
|
if (l.search[1] === '/' ) {
|
||||||
import init from "/app.js";
|
var decoded = l.search.slice(1).split('&').map(function(s) {
|
||||||
init(new URL('app.wasm', import.meta.url));
|
return s.replace(/~and~/g, '&')
|
||||||
</script>
|
}).join('?');
|
||||||
<link rel="stylesheet" href="/styles.css">
|
window.history.replaceState(
|
||||||
<link rel="stylesheet" href="/blueprint.css">
|
null,
|
||||||
</head>
|
null,
|
||||||
<body>
|
l.pathname.slice(0, -1) + decoded + l.hash
|
||||||
</body>
|
);
|
||||||
|
}
|
||||||
|
}(window.location))
|
||||||
|
</script>
|
||||||
|
<script type="module">
|
||||||
|
import init from "/app.js";
|
||||||
|
init(new URL('app.wasm', import.meta.url));
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
90
static/style/app.css
Normal file
90
static/style/app.css
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
.main {
|
||||||
|
margin-top: 6vh;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
font-size: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (min-width: 320px) and (orientation: portrait) {
|
||||||
|
:root {
|
||||||
|
font-size: 125%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (min-width: 640px) and (orientation: portrait) {
|
||||||
|
:root {
|
||||||
|
font-size: 175%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-title {
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-title > h2 {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-content {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 100%;
|
||||||
|
grid-template-rows: auto auto auto auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-card {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 10px;
|
||||||
|
flex-direction: row;
|
||||||
|
margin: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-tag {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
width: min-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-image {
|
||||||
|
width: 10%;
|
||||||
|
min-width: 100px;
|
||||||
|
height: auto;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
gap: 10px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profiletags {
|
||||||
|
display: flex;
|
||||||
|
gap: 5px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (orientation: portrait) {
|
||||||
|
.home-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-card {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.home-tag {
|
||||||
|
width: fit-content;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
}
|
17
static/style/card.css
Normal file
17
static/style/card.css
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
:root {
|
||||||
|
--card-shadow: var(--drop-shadow-dark);
|
||||||
|
--card-color: var(--color-primary);
|
||||||
|
--card-elevation: 5px;
|
||||||
|
--card-elevation-hover: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 0 var(--card-elevation) var(--card-shadow);
|
||||||
|
background-color: var(--card-color);
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card:hover {
|
||||||
|
box-shadow: 0 0 var(--card-elevation-hover) var(--card-shadow);
|
||||||
|
}
|
0
static/style/gallery.css
Normal file
0
static/style/gallery.css
Normal file
50
static/style/image-gallery.css
Normal file
50
static/style/image-gallery.css
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
|
||||||
|
.image-gallery {
|
||||||
|
line-height: 0;
|
||||||
|
column-count: 5;
|
||||||
|
column-gap: 5px;
|
||||||
|
transition-timing-function: cubic-bezier(0.1, 0.7, 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-gallery div {
|
||||||
|
width: 100% !important;
|
||||||
|
height: auto !important;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-gallery div img {
|
||||||
|
width: 98% !important;
|
||||||
|
height: auto !important;
|
||||||
|
filter: grayscale(var(--grayscale,100%)) /*blur(var(--blur,1px))*/;
|
||||||
|
transition: 250ms all;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-gallery div:hover {
|
||||||
|
--grayscale: 0%;
|
||||||
|
--blur: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.image-gallery {
|
||||||
|
column-count: 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1000px) {
|
||||||
|
.image-gallery {
|
||||||
|
column-count: 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 800px) {
|
||||||
|
.image-gallery {
|
||||||
|
column-count: 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 400px) {
|
||||||
|
.image-gallery {
|
||||||
|
column-count: 1;
|
||||||
|
}
|
||||||
|
}
|
124
static/style/navbar.css
Normal file
124
static/style/navbar.css
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
:root {
|
||||||
|
--navbar-color-dropshadow: var(--drop-shadow);
|
||||||
|
--navbar-color: var(--color-primary);
|
||||||
|
--navbar-color-selected: var(--color-accent);
|
||||||
|
--navbar-padding: 2vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Navigation bar */
|
||||||
|
.navbar {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar,
|
||||||
|
.navbar * {
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar > div:nth-child(1) {
|
||||||
|
display: none;
|
||||||
|
padding-top: var(--navbar-padding);
|
||||||
|
padding-bottom: var(--navbar-padding);
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar > div:nth-child(2) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar > div:nth-child(3) {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar > div:nth-child(3) > div {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: box-shadow 250ms, z-index 250ms;
|
||||||
|
background-color: var(--navbar-color);
|
||||||
|
padding-top: var(--navbar-padding);
|
||||||
|
padding-bottom: var(--navbar-padding);
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar > div:nth-child(3) > div > div {
|
||||||
|
gap: 5px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar > div:nth-child(3) > div:hover {
|
||||||
|
box-shadow: 0 0 11px var(--navbar-color-dropshadow);
|
||||||
|
z-index: 4;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar > div:nth-child(3) > div.current {
|
||||||
|
background-color: var(--navbar-color-selected);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (orientation: landscape) {
|
||||||
|
.navbar > div:nth-child(3) > div:nth-child(1) {
|
||||||
|
border-bottom-left-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar > div:nth-child(3) > div:nth-last-child(1) {
|
||||||
|
border-bottom-right-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar > div:nth-child(3) > div.small {
|
||||||
|
flex-grow: 0;
|
||||||
|
width: 5%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar > div:nth-child(3) > div.small > div {
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (orientation: portrait) {
|
||||||
|
.navbar {
|
||||||
|
display: grid;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar > div:nth-child(1) {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: box-shadow 250ms, z-index 250ms;
|
||||||
|
background-color: var(--navbar-color);
|
||||||
|
box-shadow: 0 0 11px var(--navbar-color-dropshadow);
|
||||||
|
z-index: 5;
|
||||||
|
cursor: pointer;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar > div:nth-child(3) {
|
||||||
|
position: relative;
|
||||||
|
top: -100%;
|
||||||
|
flex-direction: column;
|
||||||
|
transition: top 0.5s;
|
||||||
|
overflow: hidden;
|
||||||
|
border-bottom-left-radius: 10px;
|
||||||
|
border-bottom-right-radius: 10px;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar > div:nth-child(2).open + div:nth-child(3) {
|
||||||
|
top: 0;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
}
|
98
static/style/overlay-gallery.css
Normal file
98
static/style/overlay-gallery.css
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
:root {
|
||||||
|
--overlay-gallery-scale: 95vmin;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Overlay image viewer */
|
||||||
|
.overlay-gallery {
|
||||||
|
width: var(--overlay-gallery-scale);
|
||||||
|
height: var(--overlay-gallery-scale);
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay-gallery > div:nth-of-type(1) {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay-gallery > div:nth-of-type(2) {
|
||||||
|
transform: translate(calc(calc(var(--overlay-gallery-scale) / 2) - 50%), 0);
|
||||||
|
border-radius: 20px;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay-gallery > div:nth-of-type(2) > img {
|
||||||
|
float: left;
|
||||||
|
max-width: var(--overlay-gallery-scale);
|
||||||
|
max-height: var(--overlay-gallery-scale);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay-gallery > div:nth-of-type(2) > div {
|
||||||
|
bottom: 0;
|
||||||
|
padding: 60px 10px 10px 10px;
|
||||||
|
width: 100%;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
background: linear-gradient(to top, rgba(0, 75, 196, 0.699), rgba(133, 180, 255, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Gallery overlay transition properties */
|
||||||
|
.overlay-gallery > div:nth-of-type(2) > div,
|
||||||
|
.overlay-gallery-next,
|
||||||
|
.overlay-gallery-prev {
|
||||||
|
position: absolute;
|
||||||
|
transition: opacity 250ms;
|
||||||
|
transition-timing-function: cubic-bezier(0.1, 0.7, 1.0);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Image viewer overlay button positioning */
|
||||||
|
.overlay-gallery-next,
|
||||||
|
.overlay-gallery-prev {
|
||||||
|
top: 50%;
|
||||||
|
height: max(100%, 10px);
|
||||||
|
width: 12.5%;
|
||||||
|
display: flex !important;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay-gallery-next {
|
||||||
|
left: 100%;
|
||||||
|
transform: translate(-100%, -50%);
|
||||||
|
padding-right: 2.5%;
|
||||||
|
padding-left: 5%;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay-gallery-prev {
|
||||||
|
left: 0%;
|
||||||
|
transform: translate(-0%, -50%);
|
||||||
|
padding-left: 2.5%;
|
||||||
|
padding-right: 5%;;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay-gallery > div:nth-of-type(2):hover > div,
|
||||||
|
.overlay-gallery:hover .overlay-gallery-next,
|
||||||
|
.overlay-gallery:hover .overlay-gallery-prev {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (orientation: portrait) {
|
||||||
|
.overlay-gallery > div:nth-of-type(2) > div,
|
||||||
|
.overlay-gallery .overlay-gallery-next,
|
||||||
|
.overlay-gallery .overlay-gallery-prev {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay-gallery-next:hover,
|
||||||
|
.overlay-gallery-prev:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
29
static/style/overlay.css
Normal file
29
static/style/overlay.css
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
/* Pop-up overlay */
|
||||||
|
.overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
z-index: 25;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Background */
|
||||||
|
.overlay.shade {
|
||||||
|
background-color: rgba(10, 10, 10, 0.75);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Content */
|
||||||
|
.overlay > div.center {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
@ -1,31 +1,4 @@
|
|||||||
:root {
|
|
||||||
--bg-color: #f5f8fa;
|
|
||||||
--gallery-scale: 95vmin;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-theme='dark'] {
|
|
||||||
--bg-color: #00251a;
|
|
||||||
--color-primary: #004d40;
|
|
||||||
--color-accent: #39796b;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-theme='mauve'] {
|
|
||||||
--bg-color: #560027;
|
|
||||||
--color-primary: #880e4f;
|
|
||||||
--color-accent: #bc477b;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-theme='gray'] {
|
|
||||||
--bg-color: #455A64;
|
|
||||||
--color-primary: #607D8B;
|
|
||||||
--color-accent: #212121;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-theme='purple'] {
|
|
||||||
--bg-color: #512DA8;
|
|
||||||
--color-primary: #673AB7;
|
|
||||||
--color-accent: #009688;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bp3-card.bp3-dark, .bp3-dark .bp3-card {
|
.bp3-card.bp3-dark, .bp3-dark .bp3-card {
|
||||||
background-color: var(--color-primary) !important;
|
background-color: var(--color-primary) !important;
|
||||||
@ -53,11 +26,6 @@ body {
|
|||||||
font-family: 'Roboto', sans-serif !important;
|
font-family: 'Roboto', sans-serif !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.main {
|
|
||||||
margin-top: 41px;
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.actionbar {
|
.actionbar {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
@ -118,6 +86,7 @@ body {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
width: min-content;
|
width: min-content;
|
||||||
}
|
}
|
||||||
|
/*
|
||||||
|
|
||||||
@media only screen and (max-width: 1000px) {
|
@media only screen and (max-width: 1000px) {
|
||||||
.home-content {
|
.home-content {
|
||||||
@ -132,6 +101,7 @@ body {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
.home-image {
|
.home-image {
|
||||||
width: 10%;
|
width: 10%;
|
61
static/style/tag.css
Normal file
61
static/style/tag.css
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
:root {
|
||||||
|
--tag-color-blue: rgba(19, 124, 189, 0.3);
|
||||||
|
--tag-color-red: rgba(219, 55, 55, 0.25);
|
||||||
|
--tag-color-orange: rgba(217, 130, 43, 0.25);
|
||||||
|
--tag-color-green: rgba(15, 153, 96, 0.25);
|
||||||
|
|
||||||
|
--tag-shadow: var(--drop-shadow);
|
||||||
|
|
||||||
|
--tag-color-text-blue: #48aff0;
|
||||||
|
--tag-color-text-red: #ff7373;
|
||||||
|
--tag-color-text-orange: #ffb366;
|
||||||
|
--tag-color-text-green: #3dcc91;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme="dark"] {
|
||||||
|
--tag-color-blue: #303f9f4d;
|
||||||
|
--tag-color-red: #7f00004d;
|
||||||
|
--tag-color-orange: #e651004d;
|
||||||
|
--tag-color-green: #33691e4d;
|
||||||
|
|
||||||
|
--tag-color-text-blue: #448aff;
|
||||||
|
--tag-color-text-red: #EF9A9A;
|
||||||
|
--tag-color-text-orange: #FFB74D;
|
||||||
|
--tag-color-text-green: #AED581;
|
||||||
|
}
|
||||||
|
|
||||||
|
span.tag {
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 2.5px;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
span.tag:hover {
|
||||||
|
box-shadow: 0 0 5px var(--tag-shadow);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
span.tag > span {
|
||||||
|
padding: 2.5px;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
span.tag.blue {
|
||||||
|
background-color: var(--tag-color-blue);
|
||||||
|
color: var(--tag-color-text-blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
span.tag.red {
|
||||||
|
background-color: var(--tag-color-red);
|
||||||
|
color: var(--tag-color-text-red);
|
||||||
|
}
|
||||||
|
|
||||||
|
span.tag.orange {
|
||||||
|
background-color: var(--tag-color-orange);
|
||||||
|
color: var(--tag-color-text-orange);
|
||||||
|
}
|
||||||
|
|
||||||
|
span.tag.green {
|
||||||
|
background-color: var(--tag-color-green);
|
||||||
|
color: var(--tag-color-text-green);
|
||||||
|
}
|
54
static/style/theme.css
Normal file
54
static/style/theme.css
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
:root {
|
||||||
|
--drop-shadow: rgba(0, 0, 0, 0.61);
|
||||||
|
--drop-shadow-dark: rgba(255, 255, 255, 0.61);
|
||||||
|
--bg-color: #f5f8fa;
|
||||||
|
--color-primary: rgb(210, 210, 210);
|
||||||
|
--color-accent: rgb(160, 200, 160);
|
||||||
|
--color-text: rgb(0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme='dark'] {
|
||||||
|
--drop-shadow: rgba(255, 255, 255, 0.61);
|
||||||
|
--drop-shadow-dark: rgba(0, 0, 0, 0.61);
|
||||||
|
--bg-color: #00251a;
|
||||||
|
--color-primary: #004d40;
|
||||||
|
--color-accent: #39796b;
|
||||||
|
--color-text: #f5f8fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme='mauve'] {
|
||||||
|
--drop-shadow: rgba(255, 255, 255, 0.61);
|
||||||
|
--drop-shadow-dark: rgba(0, 0, 0, 0.61);
|
||||||
|
--bg-color: #560027;
|
||||||
|
--color-primary: #880e4f;
|
||||||
|
--color-accent: #bc477b;
|
||||||
|
--color-text: #f5f8fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme='gray'] {
|
||||||
|
--drop-shadow: rgba(0, 0, 0, 0.61);
|
||||||
|
--drop-shadow-dark: rgba(0, 0, 0, 0.61);
|
||||||
|
--bg-color: #455A64;
|
||||||
|
--color-primary: #607D8B;
|
||||||
|
--color-accent: #212121;
|
||||||
|
--color-text: #f5f8fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme='purple'] {
|
||||||
|
--drop-shadow: rgba(255, 255, 255, 0.61);
|
||||||
|
--drop-shadow-dark: rgba(0, 0, 0, 0.61);
|
||||||
|
--bg-color: #512DA8;
|
||||||
|
--color-primary: #673AB7;
|
||||||
|
--color-accent: #009688;
|
||||||
|
--color-text: rgb(0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
color: var(--color-text);
|
||||||
|
transition: color 100ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-theme="dark"] > body {
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
font-family: 'Roboto', sans-serif !important;
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user