added zoom + date selector
This commit is contained in:
@@ -59,7 +59,7 @@ public class FileScannerService(MediaService mediaService, IConfiguration config
|
||||
foreach (var chunk in files.Chunk(50))
|
||||
{
|
||||
total += await ScanFileChunkAsync(path, chunk, existingFiles, cancellationToken);
|
||||
logger.LogInformation("Added {updated} of {count}", total, files.Length);
|
||||
logger.LogInformation("Added {updated} of {count} [{percentage}%]", total, files.Length, Math.Round(((float)total/ files.Length) * 100));
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
73
client/Cargo.lock
generated
73
client/Cargo.lock
generated
@@ -170,11 +170,13 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"dioxus",
|
||||
"dioxus-primitives",
|
||||
"dotenv",
|
||||
"prost",
|
||||
"prost-types",
|
||||
"serde",
|
||||
"serde_repr",
|
||||
"time",
|
||||
"tokio",
|
||||
"tonic",
|
||||
"tonic-prost",
|
||||
@@ -913,7 +915,7 @@ dependencies = [
|
||||
"global-hotkey",
|
||||
"infer",
|
||||
"jni",
|
||||
"lazy-js-bundle",
|
||||
"lazy-js-bundle 0.7.3",
|
||||
"libc",
|
||||
"muda",
|
||||
"ndk",
|
||||
@@ -982,7 +984,7 @@ dependencies = [
|
||||
"futures-channel",
|
||||
"futures-util",
|
||||
"generational-box",
|
||||
"lazy-js-bundle",
|
||||
"lazy-js-bundle 0.7.3",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tracing",
|
||||
@@ -1132,7 +1134,7 @@ dependencies = [
|
||||
"futures-util",
|
||||
"generational-box",
|
||||
"keyboard-types",
|
||||
"lazy-js-bundle",
|
||||
"lazy-js-bundle 0.7.3",
|
||||
"rustversion",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -1162,7 +1164,7 @@ dependencies = [
|
||||
"dioxus-core-types",
|
||||
"dioxus-html",
|
||||
"js-sys",
|
||||
"lazy-js-bundle",
|
||||
"lazy-js-bundle 0.7.3",
|
||||
"rustc-hash 2.1.1",
|
||||
"serde",
|
||||
"sledgehammer_bindgen",
|
||||
@@ -1184,6 +1186,19 @@ dependencies = [
|
||||
"tracing-wasm",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dioxus-primitives"
|
||||
version = "0.0.1"
|
||||
source = "git+https://github.com/DioxusLabs/components#7943bed2eb59ee43d713f935e0ba17989c02b992"
|
||||
dependencies = [
|
||||
"dioxus",
|
||||
"dioxus-sdk-time",
|
||||
"lazy-js-bundle 0.6.2",
|
||||
"num-integer",
|
||||
"time",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dioxus-router"
|
||||
version = "0.7.3"
|
||||
@@ -1232,6 +1247,18 @@ dependencies = [
|
||||
"syn 2.0.114",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dioxus-sdk-time"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "80c25ae93a3f72e734873b97fbd09d9b1b6adff97205fb0ffd8543e3564fb78e"
|
||||
dependencies = [
|
||||
"dioxus",
|
||||
"futures",
|
||||
"gloo-timers",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dioxus-signals"
|
||||
version = "0.7.3"
|
||||
@@ -1292,7 +1319,7 @@ dependencies = [
|
||||
"generational-box",
|
||||
"gloo-timers",
|
||||
"js-sys",
|
||||
"lazy-js-bundle",
|
||||
"lazy-js-bundle 0.7.3",
|
||||
"rustc-hash 2.1.1",
|
||||
"send_wrapper",
|
||||
"serde",
|
||||
@@ -2241,9 +2268,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone"
|
||||
version = "0.1.64"
|
||||
version = "0.1.65"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb"
|
||||
checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470"
|
||||
dependencies = [
|
||||
"android_system_properties",
|
||||
"core-foundation-sys",
|
||||
@@ -2508,6 +2535,12 @@ dependencies = [
|
||||
"selectors",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy-js-bundle"
|
||||
version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e49596223b9d9d4947a14a25c142a6e7d8ab3f27eb3ade269d238bb8b5c267e2"
|
||||
|
||||
[[package]]
|
||||
name = "lazy-js-bundle"
|
||||
version = "0.7.3"
|
||||
@@ -2931,9 +2964,18 @@ checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb"
|
||||
|
||||
[[package]]
|
||||
name = "num-conv"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
|
||||
checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050"
|
||||
|
||||
[[package]]
|
||||
name = "num-integer"
|
||||
version = "0.1.46"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
@@ -4611,12 +4653,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.3.45"
|
||||
version = "0.3.46"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f9e442fc33d7fdb45aa9bfeb312c095964abdf596f7567261062b2a7107aaabd"
|
||||
checksum = "9da98b7d9b7dad93488a84b8248efc35352b0b2657397d4167e7ad67e5d535e5"
|
||||
dependencies = [
|
||||
"deranged",
|
||||
"itoa",
|
||||
"js-sys",
|
||||
"num-conv",
|
||||
"powerfmt",
|
||||
"serde_core",
|
||||
@@ -4626,15 +4669,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "time-core"
|
||||
version = "0.1.7"
|
||||
version = "0.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b36ee98fd31ec7426d599183e8fe26932a8dc1fb76ddb6214d05493377d34ca"
|
||||
checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca"
|
||||
|
||||
[[package]]
|
||||
name = "time-macros"
|
||||
version = "0.2.25"
|
||||
version = "0.2.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "71e552d1249bf61ac2a52db88179fd0673def1e1ad8243a00d9ec9ed71fee3dd"
|
||||
checksum = "78cc610bac2dcee56805c99642447d4c5dbde4d01f752ffea0199aee1f601dc4"
|
||||
dependencies = [
|
||||
"num-conv",
|
||||
"time-core",
|
||||
|
||||
@@ -19,6 +19,8 @@ tonic-web-wasm-client = "0.8"
|
||||
web-sys = { version = "0.3.77", features = ["Storage", "Window"] }
|
||||
tokio = "1.49.0"
|
||||
tonic-prost = "0.14.2"
|
||||
dioxus-primitives = { git = "https://github.com/DioxusLabs/components", version = "0.0.1", default-features = false }
|
||||
time = {version = "0.3.46", features= ["wasm-bindgen"]}
|
||||
chrono = "0.4.43"
|
||||
|
||||
[build-dependencies]
|
||||
|
||||
83
client/assets/dx-components-theme.css
Normal file
83
client/assets/dx-components-theme.css
Normal file
@@ -0,0 +1,83 @@
|
||||
/* This file contains the global styles for the styled dioxus components. You only
|
||||
* need to import this file once in your project root.
|
||||
*/
|
||||
@import url("https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap");
|
||||
|
||||
body {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
background-color: var(--primary-color);
|
||||
color: var(--secondary-color-4);
|
||||
font-family: Inter, sans-serif;
|
||||
font-optical-sizing: auto;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--dark: initial;
|
||||
--light: ;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
--dark: ;
|
||||
--light: initial;
|
||||
}
|
||||
}
|
||||
|
||||
:root {
|
||||
/* Primary colors */
|
||||
--primary-color: var(--dark, #000) var(--light, #fff);
|
||||
--primary-color-1: var(--dark, #0e0e0e) var(--light, #fbfbfb);
|
||||
--primary-color-2: var(--dark, #0a0a0a) var(--light, #fff);
|
||||
--primary-color-3: var(--dark, #141313) var(--light, #f8f8f8);
|
||||
--primary-color-4: var(--dark, #1a1a1a) var(--light, #f8f8f8);
|
||||
--primary-color-5: var(--dark, #262626) var(--light, #f5f5f5);
|
||||
--primary-color-6: var(--dark, #232323) var(--light, #e5e5e5);
|
||||
--primary-color-7: var(--dark, #3e3e3e) var(--light, #b0b0b0);
|
||||
|
||||
/* Secondary colors */
|
||||
--secondary-color: var(--dark, #fff) var(--light, #000);
|
||||
--secondary-color-1: var(--dark, #fafafa) var(--light, #000);
|
||||
--secondary-color-2: var(--dark, #e6e6e6) var(--light, #0d0d0d);
|
||||
--secondary-color-3: var(--dark, #dcdcdc) var(--light, #2b2b2b);
|
||||
--secondary-color-4: var(--dark, #d4d4d4) var(--light, #111);
|
||||
--secondary-color-5: var(--dark, #a1a1a1) var(--light, #848484);
|
||||
--secondary-color-6: var(--dark, #5d5d5d) var(--light, #d0d0d0);
|
||||
|
||||
/* Highlight colors */
|
||||
--focused-border-color: var(--dark, #2b7fff) var(--light, #2b7fff);
|
||||
--primary-success-color: var(--dark, #02271c) var(--light, #ecfdf5);
|
||||
--secondary-success-color: var(--dark, #b6fae3) var(--light, #10b981);
|
||||
--primary-warning-color: var(--dark, #342203) var(--light, #fffbeb);
|
||||
--secondary-warning-color: var(--dark, #feeac7) var(--light, #f59e0b);
|
||||
--primary-error-color: var(--dark, #a22e2e) var(--light, #dc2626);
|
||||
--secondary-error-color: var(--dark, #9b1c1c) var(--light, #ef4444);
|
||||
--contrast-error-color: var(--dark, var(--secondary-color-3))
|
||||
var(--light, var(--primary-color));
|
||||
--primary-info-color: var(--dark, var(--primary-color-5))
|
||||
var(--light, var(--primary-color));
|
||||
--secondary-info-color: var(--dark, var(--primary-color-7))
|
||||
var(--light, var(--secondary-color-3));
|
||||
}
|
||||
|
||||
/* Modern browsers with `scrollbar-*` support */
|
||||
@supports (scrollbar-width: auto) {
|
||||
:not(:hover) {
|
||||
scrollbar-color: rgb(0 0 0 / 0%) rgb(0 0 0 / 0%);
|
||||
}
|
||||
|
||||
:hover {
|
||||
scrollbar-color: var(--secondary-color-2) rgb(0 0 0 / 0%);
|
||||
}
|
||||
}
|
||||
|
||||
/* Legacy browsers with `::-webkit-scrollbar-*` support */
|
||||
@supports selector(::-webkit-scrollbar) {
|
||||
:root::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
@@ -4,12 +4,14 @@ use crate::route::Route;
|
||||
|
||||
const FAVICON: Asset = asset!("/assets/favicon.ico");
|
||||
const MAIN_CSS: Asset = asset!("/assets/styling/main.scss");
|
||||
const DX_COMPONENTS: Asset = asset!("/assets/dx-components-theme.css");
|
||||
|
||||
#[component]
|
||||
pub fn App() -> Element {
|
||||
rsx! {
|
||||
document::Link { rel: "icon", href: FAVICON }
|
||||
document::Link { rel: "stylesheet", href: MAIN_CSS }
|
||||
document::Link { rel: "stylesheet", href: DX_COMPONENTS }
|
||||
|
||||
document::Link { rel: "preconnect", href: "https://fonts.googleapis.com" }
|
||||
document::Link { rel: "preconnect", href: "https://fonts.gstatic.com" }
|
||||
|
||||
152
client/src/components/calendar/component.rs
Normal file
152
client/src/components/calendar/component.rs
Normal file
@@ -0,0 +1,152 @@
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_primitives::calendar::{
|
||||
self, CalendarDayProps, CalendarGridProps, CalendarHeaderProps, CalendarMonthTitleProps,
|
||||
CalendarNavigationProps, CalendarProps, CalendarSelectMonthProps, CalendarSelectYearProps,
|
||||
RangeCalendarProps,
|
||||
};
|
||||
|
||||
#[component]
|
||||
pub fn Calendar(props: CalendarProps) -> Element {
|
||||
rsx! {
|
||||
document::Link { rel: "stylesheet", href: asset!("./style.css") }
|
||||
calendar::Calendar {
|
||||
class: "calendar",
|
||||
selected_date: props.selected_date,
|
||||
on_date_change: props.on_date_change,
|
||||
on_format_weekday: props.on_format_weekday,
|
||||
on_format_month: props.on_format_month,
|
||||
view_date: props.view_date,
|
||||
today: props.today,
|
||||
on_view_change: props.on_view_change,
|
||||
disabled: props.disabled,
|
||||
first_day_of_week: props.first_day_of_week,
|
||||
min_date: props.min_date,
|
||||
max_date: props.max_date,
|
||||
month_count: props.month_count,
|
||||
disabled_ranges: props.disabled_ranges,
|
||||
attributes: props.attributes,
|
||||
{props.children}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn RangeCalendar(props: RangeCalendarProps) -> Element {
|
||||
rsx! {
|
||||
document::Link { rel: "stylesheet", href: asset!("./style.css") }
|
||||
calendar::RangeCalendar {
|
||||
class: "calendar",
|
||||
selected_range: props.selected_range,
|
||||
on_range_change: props.on_range_change,
|
||||
on_format_weekday: props.on_format_weekday,
|
||||
on_format_month: props.on_format_month,
|
||||
view_date: props.view_date,
|
||||
today: props.today,
|
||||
on_view_change: props.on_view_change,
|
||||
disabled: props.disabled,
|
||||
first_day_of_week: props.first_day_of_week,
|
||||
min_date: props.min_date,
|
||||
max_date: props.max_date,
|
||||
month_count: props.month_count,
|
||||
disabled_ranges: props.disabled_ranges,
|
||||
attributes: props.attributes,
|
||||
{props.children}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn CalendarView(
|
||||
#[props(extends = GlobalAttributes)] attributes: Vec<Attribute>,
|
||||
children: Element,
|
||||
) -> Element {
|
||||
rsx! {
|
||||
div {
|
||||
class: "calendar-view",
|
||||
..attributes,
|
||||
{children}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn CalendarHeader(props: CalendarHeaderProps) -> Element {
|
||||
rsx! {
|
||||
calendar::CalendarHeader { id: props.id, attributes: props.attributes, {props.children} }
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn CalendarNavigation(props: CalendarNavigationProps) -> Element {
|
||||
rsx! {
|
||||
calendar::CalendarNavigation { attributes: props.attributes, {props.children} }
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn CalendarPreviousMonthButton(
|
||||
#[props(extends = GlobalAttributes)] attributes: Vec<Attribute>,
|
||||
) -> Element {
|
||||
rsx! {
|
||||
calendar::CalendarPreviousMonthButton { attributes,
|
||||
svg {
|
||||
class: "calendar-previous-month-icon",
|
||||
view_box: "0 0 24 24",
|
||||
xmlns: "http://www.w3.org/2000/svg",
|
||||
polyline { points: "15 6 9 12 15 18" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn CalendarNextMonthButton(
|
||||
#[props(extends = GlobalAttributes)] attributes: Vec<Attribute>,
|
||||
) -> Element {
|
||||
rsx! {
|
||||
calendar::CalendarNextMonthButton { attributes,
|
||||
svg {
|
||||
class: "calendar-next-month-icon",
|
||||
view_box: "0 0 24 24",
|
||||
xmlns: "http://www.w3.org/2000/svg",
|
||||
polyline { points: "9 18 15 12 9 6" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn CalendarSelectMonth(props: CalendarSelectMonthProps) -> Element {
|
||||
rsx! {
|
||||
calendar::CalendarSelectMonth { class: "calendar-month-select", attributes: props.attributes }
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn CalendarSelectYear(props: CalendarSelectYearProps) -> Element {
|
||||
rsx! {
|
||||
calendar::CalendarSelectYear { class: "calendar-year-select", attributes: props.attributes }
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn CalendarGrid(props: CalendarGridProps) -> Element {
|
||||
rsx! {
|
||||
calendar::CalendarGrid {
|
||||
id: props.id,
|
||||
show_week_numbers: props.show_week_numbers,
|
||||
render_day: props.render_day,
|
||||
attributes: props.attributes,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn CalendarMonthTitle(props: CalendarMonthTitleProps) -> Element {
|
||||
calendar::CalendarMonthTitle(props)
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn CalendarDay(props: CalendarDayProps) -> Element {
|
||||
calendar::CalendarDay(props)
|
||||
}
|
||||
2
client/src/components/calendar/mod.rs
Normal file
2
client/src/components/calendar/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
mod component;
|
||||
pub use component::*;
|
||||
297
client/src/components/calendar/style.css
Normal file
297
client/src/components/calendar/style.css
Normal file
@@ -0,0 +1,297 @@
|
||||
/* Calendar Container */
|
||||
.calendar {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
border: 1px solid var(--primary-color-6);
|
||||
border-radius: 8px;
|
||||
background-color: var(--primary-color-2);
|
||||
box-shadow: 0 2px 10px rgb(0 0 0 / 10%);
|
||||
font-family:
|
||||
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial,
|
||||
sans-serif;
|
||||
}
|
||||
|
||||
/* Calendar Navigation */
|
||||
.calendar-navigation {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.75rem;
|
||||
padding: 0.75rem calc(0.75rem + 1.75rem + 0.5rem) 0.25rem;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.calendar-nav-title {
|
||||
color: var(--secondary-color-4);
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.calendar-nav-prev,
|
||||
.calendar-nav-next {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
width: 1.75rem;
|
||||
height: 1.75rem;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid var(--primary-color-6);
|
||||
border-radius: 0.5rem;
|
||||
background-color: var(--light, transparent)
|
||||
var(--dark, var(--primary-color-3));
|
||||
color: var(--secondary-color-5);
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.calendar-nav-prev {
|
||||
left: 0.75rem;
|
||||
}
|
||||
|
||||
.calendar-nav-next {
|
||||
right: 0.75rem;
|
||||
}
|
||||
|
||||
.calendar-nav-prev:hover,
|
||||
.calendar-nav-next:hover {
|
||||
border-color: var(--primary-color-7);
|
||||
background-color: var(--primary-color-4);
|
||||
color: var(--secondary-color-4);
|
||||
}
|
||||
|
||||
.calendar-nav-prev:focus-visible,
|
||||
.calendar-nav-next:focus-visible {
|
||||
box-shadow: 0 0 0 2px var(--focused-border-color);
|
||||
}
|
||||
|
||||
.calendar-nav-prev:disabled,
|
||||
.calendar-nav-next:disabled {
|
||||
border-color: var(--primary-color-5);
|
||||
background-color: var(--primary-color-2);
|
||||
color: var(--secondary-color-3);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.calendar-month-title {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 1.75rem;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* Calendar Grid */
|
||||
.calendar-view {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.calendar-grid {
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.calendar-grid-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.calendar-grid-day-header {
|
||||
flex: 1;
|
||||
color: var(--secondary-color-5);
|
||||
font-size: 12px;
|
||||
font-weight: 300;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.calendar-grid-body {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.calendar-grid-cell {
|
||||
width: 2rem;
|
||||
border: none;
|
||||
border-radius: 0.5rem;
|
||||
aspect-ratio: 1;
|
||||
background: none;
|
||||
color: var(--secondary-color-4);
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.calendar-grid-cell[data-month="current"]:not([data-disabled="true"]):hover {
|
||||
background-color: var(--primary-color-4);
|
||||
}
|
||||
|
||||
.calendar-grid-cell[data-month="current"]:focus-visible {
|
||||
outline: 2px solid var(--focused-border-color);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.calendar-grid-cell[data-month="last"],
|
||||
.calendar-grid-cell[data-month="next"],
|
||||
.calendar-grid-cell[data-disabled="true"] {
|
||||
color: var(--secondary-color-5);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.calendar-grid-cell[data-month="last"][data-selected="true"],
|
||||
.calendar-grid-cell[data-month="next"][data-selected="true"] {
|
||||
background-color: var(--secondary-color-6);
|
||||
}
|
||||
|
||||
.calendar-grid-cell[data-month="current"][data-selected="true"] {
|
||||
background-color: var(--secondary-color-2);
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.calendar-grid-cell[data-month="current"][data-unavailable="true"] {
|
||||
color: var(--secondary-color-6);
|
||||
cursor: not-allowed;
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.calendar-grid-week td {
|
||||
padding-right: 0;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.calendar-grid-week td:first-child .calendar-grid-cell {
|
||||
border-bottom-left-radius: 0.5rem;
|
||||
border-top-left-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.calendar-grid-week td:last-child .calendar-grid-cell {
|
||||
border-bottom-right-radius: 0.5rem;
|
||||
border-top-right-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.calendar-grid-cell[data-month="last"][data-selection-between="true"],
|
||||
.calendar-grid-cell[data-month="next"][data-selection-between="true"] {
|
||||
border-radius: 0;
|
||||
background-color: var(--primary-color-5);
|
||||
color: var(--secondary-color-5);
|
||||
}
|
||||
|
||||
.calendar-grid-cell[data-month="current"][data-selection-between="true"] {
|
||||
border-radius: 0;
|
||||
background-color: var(--primary-color-5);
|
||||
color: var(--secondary-color-4);
|
||||
}
|
||||
|
||||
td:has(.calendar-grid-cell[data-selection-start="true"]) {
|
||||
padding: 0;
|
||||
margin-top: 1px;
|
||||
margin-bottom: 1px;
|
||||
background-color: var(--primary-color-5);
|
||||
border-bottom-left-radius: 0.5rem;
|
||||
border-top-left-radius: 0.5rem;
|
||||
}
|
||||
|
||||
td:has(.calendar-grid-cell[data-selection-end="true"]) {
|
||||
padding: 0;
|
||||
margin-top: 1px;
|
||||
margin-bottom: 1px;
|
||||
background-color: var(--primary-color-5);
|
||||
border-bottom-right-radius: 0.5rem;
|
||||
border-top-right-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.calendar-grid-cell[data-month="current"][data-selected="true"]:hover {
|
||||
background-color: var(--light, var(--secondary-color-2))
|
||||
var(--dark, var(--primary-color-5));
|
||||
color: var(--light, var(--primary-color))
|
||||
var(--dark, var(--secondary-color-1));
|
||||
font-weight: var(--light, 550) var(--dark, inherit);
|
||||
}
|
||||
|
||||
.calendar-grid-cell[data-month="current"][data-today="true"]:not(
|
||||
[data-selected="true"]
|
||||
) {
|
||||
background-color: var(--primary-color-5);
|
||||
}
|
||||
|
||||
.calendar-grid-weeknum {
|
||||
border-radius: 0.5rem;
|
||||
background-color: var(--primary-color);
|
||||
color: var(--secondary-color-5);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* Calendar with week numbers */
|
||||
.calendar-grid-week {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
/* Calendar states */
|
||||
.calendar[data-disabled="true"] {
|
||||
opacity: 0.6;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.calendar-next-month-icon,
|
||||
.calendar-previous-month-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
fill: none;
|
||||
stroke: currentcolor;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
stroke-width: 2;
|
||||
}
|
||||
|
||||
.calendar-month-select-container,
|
||||
.calendar-year-select-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.calendar-month-select-container:has(:focus-visible),
|
||||
.calendar-year-select-container:has(:focus-visible) {
|
||||
border-radius: 0.5rem;
|
||||
outline: 2px solid var(--focused-border-color);
|
||||
}
|
||||
|
||||
.calendar-month-select,
|
||||
.calendar-year-select {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0.25rem;
|
||||
margin: 0;
|
||||
inset: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.calendar-month-select-value,
|
||||
.calendar-year-select-value {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.25rem;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
color: var(--secondary-color-4);
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
transition:
|
||||
background-color 0.2s ease,
|
||||
color 0.2s ease;
|
||||
}
|
||||
|
||||
.select-expand-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
fill: none;
|
||||
stroke: var(--secondary-color-4);
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
stroke-width: 2;
|
||||
}
|
||||
147
client/src/components/date_picker/component.rs
Normal file
147
client/src/components/date_picker/component.rs
Normal file
@@ -0,0 +1,147 @@
|
||||
use dioxus::prelude::*;
|
||||
|
||||
use dioxus_primitives::{
|
||||
date_picker::{self, DatePickerInputProps, DatePickerProps, DateRangePickerProps},
|
||||
popover::{PopoverContentProps, PopoverTriggerProps},
|
||||
ContentAlign,
|
||||
};
|
||||
|
||||
use super::super::calendar::*;
|
||||
use super::super::popover::*;
|
||||
|
||||
#[component]
|
||||
pub fn DatePicker(props: DatePickerProps) -> Element {
|
||||
rsx! {
|
||||
document::Link { rel: "stylesheet", href: asset!("./style.css") }
|
||||
div {
|
||||
date_picker::DatePicker {
|
||||
class: "date-picker",
|
||||
on_value_change: props.on_value_change,
|
||||
selected_date: props.selected_date,
|
||||
disabled: props.disabled,
|
||||
read_only: props.read_only,
|
||||
min_date: props.min_date,
|
||||
max_date: props.max_date,
|
||||
month_count: props.month_count,
|
||||
disabled_ranges: props.disabled_ranges,
|
||||
roving_loop: props.roving_loop,
|
||||
attributes: props.attributes,
|
||||
date_picker::DatePickerPopover {
|
||||
popover_root: PopoverRoot,
|
||||
{props.children}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn DateRangePicker(props: DateRangePickerProps) -> Element {
|
||||
rsx! {
|
||||
document::Link { rel: "stylesheet", href: asset!("./style.css") }
|
||||
div {
|
||||
date_picker::DateRangePicker {
|
||||
class: "date-picker",
|
||||
on_range_change: props.on_range_change,
|
||||
selected_range: props.selected_range,
|
||||
disabled: props.disabled,
|
||||
read_only: props.read_only,
|
||||
min_date: props.min_date,
|
||||
max_date: props.max_date,
|
||||
month_count: props.month_count,
|
||||
disabled_ranges: props.disabled_ranges,
|
||||
roving_loop: props.roving_loop,
|
||||
attributes: props.attributes,
|
||||
date_picker::DatePickerPopover { popover_root: PopoverRoot, {props.children} }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn DatePickerInput(props: DatePickerInputProps) -> Element {
|
||||
rsx! {
|
||||
date_picker::DatePickerInput {
|
||||
on_format_day_placeholder: props.on_format_day_placeholder,
|
||||
on_format_month_placeholder: props.on_format_month_placeholder,
|
||||
on_format_year_placeholder: props.on_format_year_placeholder,
|
||||
attributes: props.attributes,
|
||||
{props.children}
|
||||
DatePickerPopoverTrigger {}
|
||||
DatePickerPopoverContent { align: ContentAlign::Center,
|
||||
date_picker::DatePickerCalendar { calendar: Calendar,
|
||||
CalendarView {
|
||||
CalendarHeader {
|
||||
CalendarNavigation {
|
||||
CalendarPreviousMonthButton {}
|
||||
CalendarSelectMonth {}
|
||||
CalendarSelectYear {}
|
||||
CalendarNextMonthButton {}
|
||||
}
|
||||
}
|
||||
CalendarGrid {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn DateRangePickerInput(props: DatePickerInputProps) -> Element {
|
||||
rsx! {
|
||||
date_picker::DateRangePickerInput {
|
||||
on_format_day_placeholder: props.on_format_day_placeholder,
|
||||
on_format_month_placeholder: props.on_format_month_placeholder,
|
||||
on_format_year_placeholder: props.on_format_year_placeholder,
|
||||
attributes: props.attributes,
|
||||
{props.children}
|
||||
DatePickerPopoverTrigger {}
|
||||
DatePickerPopoverContent {
|
||||
align: ContentAlign::Center,
|
||||
date_picker::DateRangePickerCalendar {
|
||||
calendar: RangeCalendar,
|
||||
CalendarView {
|
||||
CalendarHeader {
|
||||
CalendarNavigation {
|
||||
CalendarPreviousMonthButton {}
|
||||
CalendarSelectMonth {}
|
||||
CalendarSelectYear {}
|
||||
CalendarNextMonthButton {}
|
||||
}
|
||||
}
|
||||
CalendarGrid {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn DatePickerPopoverTrigger(props: PopoverTriggerProps) -> Element {
|
||||
rsx! {
|
||||
PopoverTrigger { aria_label: "Show Calendar", attributes: props.attributes,
|
||||
svg {
|
||||
class: "date-picker-expand-icon",
|
||||
view_box: "0 0 24 24",
|
||||
xmlns: "http://www.w3.org/2000/svg",
|
||||
polyline { points: "6 9 12 15 18 9" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn DatePickerPopoverContent(props: PopoverContentProps) -> Element {
|
||||
rsx! {
|
||||
PopoverContent {
|
||||
class: "popover-content",
|
||||
id: props.id,
|
||||
side: props.side,
|
||||
align: props.align,
|
||||
attributes: props.attributes,
|
||||
{props.children}
|
||||
}
|
||||
}
|
||||
}
|
||||
2
client/src/components/date_picker/mod.rs
Normal file
2
client/src/components/date_picker/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
mod component;
|
||||
pub use component::*;
|
||||
79
client/src/components/date_picker/style.css
Normal file
79
client/src/components/date_picker/style.css
Normal file
@@ -0,0 +1,79 @@
|
||||
.date-picker {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.date-picker-group .popover-trigger {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
cursor: pointer;
|
||||
transition: rotate 150ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.popover[data-state="open"] div .date-picker-trigger {
|
||||
rotate: 180deg;
|
||||
}
|
||||
|
||||
.date-picker-expand-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
fill: none;
|
||||
stroke: var(--primary-color-7);
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
stroke-width: 2;
|
||||
}
|
||||
|
||||
[data-disabled="true"] {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.date-picker-group {
|
||||
display: flex;
|
||||
width: fit-content;
|
||||
min-width: 150px;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.5rem;
|
||||
border: none;
|
||||
border-radius: 0.5rem;
|
||||
background: none;
|
||||
background: var(--light, var(--primary-color))
|
||||
var(--dark, var(--primary-color-3));
|
||||
box-shadow: inset 0 0 0 1px var(--light, var(--primary-color-6))
|
||||
var(--dark, var(--primary-color-7));
|
||||
color: var(--secondary-color-4);
|
||||
gap: 0.25rem;
|
||||
transition: background-color 100ms ease-out;
|
||||
}
|
||||
|
||||
.date-picker-group .popover-content {
|
||||
max-width: unset;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.date-segment {
|
||||
caret-color: transparent;
|
||||
}
|
||||
|
||||
.date-segment[no-date="true"] {
|
||||
color: var(--secondary-color-5);
|
||||
}
|
||||
|
||||
.date-segment[is-separator="true"] {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.date-segment:focus-visible {
|
||||
border-radius: 0.25rem;
|
||||
background: var(--secondary-color-3);
|
||||
color: var(--primary-color);
|
||||
outline: none;
|
||||
}
|
||||
@@ -1,3 +1,7 @@
|
||||
mod calendar;
|
||||
pub mod calendar;
|
||||
pub mod playback;
|
||||
pub use calendar::*;
|
||||
mod playback_calendar;
|
||||
pub use playback_calendar::*;
|
||||
pub mod date_picker;
|
||||
pub mod popover;
|
||||
pub mod slider;
|
||||
|
||||
@@ -1,19 +1,31 @@
|
||||
use chrono::{Datelike, Local};
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_primitives::slider::{SliderTrack, SliderValue};
|
||||
use prost_types::Timestamp;
|
||||
use time::Date;
|
||||
|
||||
use crate::{
|
||||
components::playback::{Timeline, Viewport},
|
||||
components::{
|
||||
date_picker::{DatePicker, DatePickerInput},
|
||||
playback::{Timeline, Viewport},
|
||||
slider::{Slider, SliderRange, SliderThumb},
|
||||
},
|
||||
rpc::{azki::MediaPlaybackRequest, get_rpc_client},
|
||||
};
|
||||
const PLAYER_CSS: Asset = asset!("/assets/styling/player.scss");
|
||||
|
||||
#[component]
|
||||
pub fn Player() -> Element {
|
||||
let playbackResult = use_resource(|| async move {
|
||||
let mut client = get_rpc_client();
|
||||
let mut selected_date = use_signal(|| {
|
||||
let now = Local::now();
|
||||
let from = Timestamp::date(now.year() as i64, now.month() as u8, now.day() as u8 - 4).unwrap();
|
||||
Some(Date::from_ordinal_date(now.year(), now.ordinal() as u16).unwrap())
|
||||
});
|
||||
let mut zoom = use_signal(|| 1.0 as f32);
|
||||
let playbackResult = use_resource(use_reactive!(|(selected_date)| async move {
|
||||
let mut client = get_rpc_client();
|
||||
info!("Load data");
|
||||
let now = selected_date.cloned().unwrap();
|
||||
let from = Timestamp::date(now.year() as i64, now.month() as u8, now.day()).unwrap();
|
||||
let result = client
|
||||
.get_media_playback(MediaPlaybackRequest { date: Some(from) })
|
||||
.await;
|
||||
@@ -25,7 +37,7 @@ pub fn Player() -> Element {
|
||||
let msg = err.message();
|
||||
return Err(format!("Failed to load results: {msg}"));
|
||||
}
|
||||
});
|
||||
}));
|
||||
let info = match playbackResult.cloned() {
|
||||
Some(value) => match value {
|
||||
Ok(result) => Some(result),
|
||||
@@ -39,9 +51,31 @@ pub fn Player() -> Element {
|
||||
id: "player",
|
||||
div {
|
||||
id: "head",
|
||||
Slider{
|
||||
value: SliderValue::Single(zoom() as f64),
|
||||
horizontal: true,
|
||||
min: 1.0,
|
||||
max: 5.0,
|
||||
step: 0.1,
|
||||
on_value_change: move |val| {
|
||||
let SliderValue::Single(v) = val;
|
||||
zoom.set(v as f32);
|
||||
},
|
||||
SliderTrack{
|
||||
SliderRange{}
|
||||
SliderThumb{}
|
||||
}
|
||||
},
|
||||
DatePicker{
|
||||
selected_date,
|
||||
on_value_change: move |v| {
|
||||
selected_date.set(v);
|
||||
},
|
||||
DatePickerInput{}
|
||||
}
|
||||
}
|
||||
Viewport { }
|
||||
Timeline { playbackInfo: info }
|
||||
Timeline { playbackInfo: info, zoom }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,25 +3,25 @@ use dioxus::prelude::*;
|
||||
use crate::rpc::azki::{MediaChannel, MediaEntry, PlaybackInfo};
|
||||
|
||||
#[component]
|
||||
pub fn Timeline(playbackInfo: Option<PlaybackInfo>) -> Element {
|
||||
pub fn Timeline(playbackInfo: Option<PlaybackInfo>, zoom: Signal<f32>) -> Element {
|
||||
return match playbackInfo {
|
||||
Some(info) => rsx! {
|
||||
div{
|
||||
id: "timeline",
|
||||
TrackList { channels: info.channels, start: info.date.unwrap().seconds, zoom: 2.0 }
|
||||
TrackList { channels: info.channels, start: info.date.unwrap().seconds, zoom }
|
||||
}
|
||||
},
|
||||
None => rsx! {
|
||||
div{
|
||||
id: "timeline",
|
||||
TrackList { channels: Vec::new(), start: 0, zoom: 1.0 }
|
||||
TrackList { channels: Vec::new(), start: 0, zoom }
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn TrackList(channels: Vec<MediaChannel>, start: i64, zoom: f32) -> Element {
|
||||
fn TrackList(channels: Vec<MediaChannel>, start: i64, zoom: Signal<f32>) -> Element {
|
||||
rsx! {
|
||||
div {
|
||||
id: "tracklist",
|
||||
@@ -33,11 +33,11 @@ fn TrackList(channels: Vec<MediaChannel>, start: i64, zoom: f32) -> Element {
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn TimelineTrack(channel: MediaChannel, start: i64, zoom: f32) -> Element {
|
||||
fn TimelineTrack(channel: MediaChannel, start: i64, zoom: Signal<f32>) -> Element {
|
||||
rsx! {
|
||||
div{
|
||||
class: "track",
|
||||
style: "width: calc({zoom * 100.0}% - 100px);",
|
||||
style: "width: calc({zoom() * 100.0}% - 100px);",
|
||||
TrackLabel { channel: channel.clone() },
|
||||
{channel.videos.iter().map(|m|rsx!{Clip{media: m.clone(), start}})}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use dioxus::prelude::*;
|
||||
|
||||
#[component]
|
||||
pub fn Calendar() -> Element {
|
||||
pub fn PlaybackCalendar() -> Element {
|
||||
rsx! {}
|
||||
}
|
||||
41
client/src/components/popover/component.rs
Normal file
41
client/src/components/popover/component.rs
Normal file
@@ -0,0 +1,41 @@
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_primitives::popover::{
|
||||
self, PopoverContentProps, PopoverRootProps, PopoverTriggerProps,
|
||||
};
|
||||
|
||||
#[component]
|
||||
pub fn PopoverRoot(props: PopoverRootProps) -> Element {
|
||||
rsx! {
|
||||
document::Link { rel: "stylesheet", href: asset!("./style.css") }
|
||||
popover::PopoverRoot {
|
||||
class: "popover",
|
||||
is_modal: props.is_modal,
|
||||
open: props.open,
|
||||
default_open: props.default_open,
|
||||
on_open_change: props.on_open_change,
|
||||
attributes: props.attributes,
|
||||
{props.children}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn PopoverTrigger(props: PopoverTriggerProps) -> Element {
|
||||
rsx! {
|
||||
popover::PopoverTrigger { class: "popover-trigger", attributes: props.attributes, {props.children} }
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn PopoverContent(props: PopoverContentProps) -> Element {
|
||||
rsx! {
|
||||
popover::PopoverContent {
|
||||
class: "popover-content",
|
||||
id: props.id,
|
||||
side: props.side,
|
||||
align: props.align,
|
||||
attributes: props.attributes,
|
||||
{props.children}
|
||||
}
|
||||
}
|
||||
}
|
||||
2
client/src/components/popover/mod.rs
Normal file
2
client/src/components/popover/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
mod component;
|
||||
pub use component::*;
|
||||
228
client/src/components/popover/style.css
Normal file
228
client/src/components/popover/style.css
Normal file
@@ -0,0 +1,228 @@
|
||||
.popover {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.popover-content {
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
min-width: 200px;
|
||||
max-width: calc(100% - 2rem);
|
||||
box-sizing: border-box;
|
||||
flex-direction: column;
|
||||
padding: .25rem;
|
||||
border-radius: .5rem;
|
||||
margin-top: .5rem;
|
||||
background: var(--light, var(--primary-color))
|
||||
var(--dark, var(--primary-color-5));
|
||||
box-shadow: inset 0 0 0 1px var(--light, var(--primary-color-6)) var(--dark, var(--primary-color-7));
|
||||
text-align: center;
|
||||
transform: translate(-50%, -50%);
|
||||
transform-origin: top;
|
||||
will-change: transform, opacity;
|
||||
}
|
||||
|
||||
.popover-content[data-state="closed"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.popover-content[data-state="open"] {
|
||||
display: flex;
|
||||
animation: popover-fade-in .2s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes popover-fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Positioning based on side */
|
||||
.popover-content[data-side="top"] {
|
||||
position: absolute;
|
||||
bottom: 100%;
|
||||
left: 50%;
|
||||
margin-bottom: 8px;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.popover-content[data-side="top"]::after {
|
||||
top: calc(100% - 0.25rem);
|
||||
left: 50%;
|
||||
border-color: var(--secondary-color-4);
|
||||
border-radius: 0 0 0.1rem;
|
||||
}
|
||||
|
||||
.popover-content[data-side="right"] {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 100%;
|
||||
margin-left: 8px;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.popover-content[data-side="right"]::after {
|
||||
top: calc(50% - 0.25rem);
|
||||
left: 0;
|
||||
border-color: var(--secondary-color-4);
|
||||
border-radius: 0 0 0 0.1rem;
|
||||
}
|
||||
|
||||
.popover-content[data-side="bottom"] {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
margin-top: 8px;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.popover-content[data-side="bottom"]::after {
|
||||
bottom: calc(100% - 0.25rem);
|
||||
left: 50%;
|
||||
border-color: var(--secondary-color-4);
|
||||
border-radius: 0.1rem 0 0;
|
||||
}
|
||||
|
||||
.popover-content[data-side="left"] {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 100%;
|
||||
margin-right: 8px;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.popover-content[data-side="left"]::after {
|
||||
top: calc(50% - 0.25rem);
|
||||
right: -0.25rem;
|
||||
border-color: var(--secondary-color-4);
|
||||
border-radius: 0 0.1rem 0 0;
|
||||
}
|
||||
|
||||
/* Alignment styles for top and bottom */
|
||||
.popover-content[data-side="top"][data-align="start"],
|
||||
.popover-content[data-side="bottom"][data-align="start"] {
|
||||
left: 0;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.popover-content[data-side="top"][data-align="end"],
|
||||
.popover-content[data-side="bottom"][data-align="end"] {
|
||||
right: 0;
|
||||
left: auto;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
/* Alignment styles for left and right */
|
||||
.popover-content[data-side="left"][data-align="start"],
|
||||
.popover-content[data-side="right"][data-align="start"] {
|
||||
top: 0;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.popover-content[data-side="left"][data-align="center"],
|
||||
.popover-content[data-side="right"][data-align="center"] {
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.popover-content[data-side="left"][data-align="end"],
|
||||
.popover-content[data-side="right"][data-align="end"] {
|
||||
top: auto;
|
||||
bottom: 0;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.popover-content-title {
|
||||
margin: 0;
|
||||
color: var(--secondary-color-4);
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.popover-content-description {
|
||||
margin: 0;
|
||||
color: var(--secondary-color-5);
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.popover-content-actions {
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
@media (width >= 40rem) {
|
||||
.popover-content-actions {
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.popover-content {
|
||||
max-width: 32rem;
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
.popover-content-cancel {
|
||||
padding: 8px 18px;
|
||||
border: 1px solid var(--primary-color-6);
|
||||
border-radius: 0.5rem;
|
||||
background-color: var(--light, var(--primary-color))
|
||||
var(--dark, var(--primary-color-3));
|
||||
color: var(--secondary-color-4);
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.popover-content-cancel:hover {
|
||||
background-color: var(--primary-color-4);
|
||||
}
|
||||
|
||||
.popover-content-cancel:focus-visible {
|
||||
box-shadow: 0 0 0 2px var(--focused-border-color);
|
||||
}
|
||||
|
||||
.popover-content-action {
|
||||
padding: 8px 18px;
|
||||
border: 1px solid var(--primary-error-color);
|
||||
border-radius: 0.5rem;
|
||||
background-color: var(--primary-error-color);
|
||||
color: var(--contrast-error-color);
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.popover-content-action:hover {
|
||||
background-color: var(--secondary-error-color);
|
||||
}
|
||||
|
||||
.popover-content-action:focus-visible {
|
||||
box-shadow: 0 0 0 2px var(--focused-border-color);
|
||||
}
|
||||
|
||||
.popover-trigger {
|
||||
padding: 8px 18px;
|
||||
border: 1px solid var(--primary-color-6);
|
||||
border-radius: 0.5rem;
|
||||
background-color: var(--light, var(--primary-color))
|
||||
var(--dark, var(--primary-color-3));
|
||||
color: var(--secondary-color-4);
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.popover-trigger:hover {
|
||||
background-color: var(--primary-color-4);
|
||||
}
|
||||
|
||||
.popover-trigger:focus-visible {
|
||||
box-shadow: 0 0 0 2px var(--focused-border-color);
|
||||
}
|
||||
52
client/src/components/slider/component.rs
Normal file
52
client/src/components/slider/component.rs
Normal file
@@ -0,0 +1,52 @@
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_primitives::slider::{
|
||||
self, SliderProps, SliderRangeProps, SliderThumbProps, SliderTrackProps,
|
||||
};
|
||||
|
||||
#[component]
|
||||
pub fn Slider(props: SliderProps) -> Element {
|
||||
rsx! {
|
||||
document::Link { rel: "stylesheet", href: asset!("./style.css") }
|
||||
slider::Slider {
|
||||
class: "slider",
|
||||
value: props.value,
|
||||
default_value: props.default_value,
|
||||
min: props.min,
|
||||
max: props.max,
|
||||
step: props.step,
|
||||
disabled: props.disabled,
|
||||
horizontal: props.horizontal,
|
||||
inverted: props.inverted,
|
||||
on_value_change: props.on_value_change,
|
||||
label: props.label,
|
||||
attributes: props.attributes,
|
||||
{props.children}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn SliderTrack(props: SliderTrackProps) -> Element {
|
||||
rsx! {
|
||||
slider::SliderTrack { class: "slider-track", attributes: props.attributes, {props.children} }
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn SliderRange(props: SliderRangeProps) -> Element {
|
||||
rsx! {
|
||||
slider::SliderRange { class: "slider-range", attributes: props.attributes, {props.children} }
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn SliderThumb(props: SliderThumbProps) -> Element {
|
||||
rsx! {
|
||||
slider::SliderThumb {
|
||||
class: "slider-thumb",
|
||||
index: props.index,
|
||||
attributes: props.attributes,
|
||||
{props.children}
|
||||
}
|
||||
}
|
||||
}
|
||||
2
client/src/components/slider/mod.rs
Normal file
2
client/src/components/slider/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
mod component;
|
||||
pub use component::*;
|
||||
76
client/src/components/slider/style.css
Normal file
76
client/src/components/slider/style.css
Normal file
@@ -0,0 +1,76 @@
|
||||
.slider {
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: 200px;
|
||||
align-items: center;
|
||||
padding: 0.5rem 0;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.slider[data-orientation="vertical"] {
|
||||
width: auto;
|
||||
height: 200px;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.slider-track {
|
||||
position: relative;
|
||||
height: 0.5rem;
|
||||
box-sizing: border-box;
|
||||
flex-grow: 1;
|
||||
border-radius: 9999px;
|
||||
background: var(--primary-color-5);
|
||||
}
|
||||
|
||||
.slider[data-orientation="vertical"] .slider-track {
|
||||
width: 4px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.slider-range {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
border-radius: 9999px;
|
||||
background-color: var(--secondary-color-2);
|
||||
}
|
||||
|
||||
.slider[data-orientation="vertical"] .slider-range {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.slider-thumb {
|
||||
all: unset;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
display: block;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 1px solid var(--secondary-color-2);
|
||||
border-radius: 50%;
|
||||
background-color: var(--primary-color-1);
|
||||
cursor: pointer;
|
||||
transform: translate(-50%, -50%);
|
||||
transition: border-color 150ms;
|
||||
}
|
||||
|
||||
.slider[data-orientation="vertical"] .slider-thumb {
|
||||
left: 50%;
|
||||
transform: translate(-50%, 50%);
|
||||
}
|
||||
|
||||
.slider-thumb:focus-visible[data-dragging="true"],
|
||||
.slider-thumb:focus-visible,
|
||||
.slider-thumb:hover {
|
||||
box-shadow: 0 0 0 4px
|
||||
color-mix(in oklab, var(--primary-color-7) 50%, transparent);
|
||||
transition: box-shadow 150ms;
|
||||
}
|
||||
|
||||
.slider[data-disabled="true"] {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.slider[data-disabled="true"] .slider-thumb {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
Reference in New Issue
Block a user