video playback and timeline playhead

This commit is contained in:
2026-03-05 19:18:45 -05:00
parent 20874ecff7
commit 78651ca58d
8 changed files with 227 additions and 37 deletions

View File

@@ -7,18 +7,30 @@
background-color: $darkBackground;
display: grid;
grid-template-areas: "Head ControlPanel" "ViewPort ControlPanel" "Timeline Timeline";
grid-template-rows: auto 1fr 500px;
grid-template-rows: 50px calc(100dvh - 350px - 20px) 300px;
grid-template-columns: 1fr auto;
max-height: calc(100dvh - 20px);
}
#viewport {
grid-area: ViewPort;
border: 1px solid $lightBackground;
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-rows: repeat(3, 1fr);
overflow: hidden;
video {
width: 100%;
height: auto;
image-rendering: smooth;
}
}
#timeline {
grid-area: Timeline;
border: 1px solid $lightBackground;
$labelWidth: 100px;
#tracklist {
display: flex;
@@ -27,14 +39,34 @@
width: 100%;
gap: 1px;
}
.track {
display: block;
#playhead {
display: grid;
width: 100%;
position: relative;
padding: 2px 0;
height: 30px;
grid-template-columns: $labelWidth 1fr;
grid-template-areas: "Label Input";
input {
grid-area: Input;
width: 100%;
}
.indicator {
width: 1px;
background-color: $secondaryAccentLight;
height: 100%;
position: absolute;
pointer-events: none;
}
}
.track {
display: grid;
min-height: 30px;
background-color: $lightBackground;
grid-template-columns: $labelWidth 1fr;
/* overflow: hidden; */
margin-left: 100px;
/* margin-left: 100px; */
.label {
width: 100px;
@@ -42,11 +74,15 @@
display: flex;
justify-content: center;
align-items: center;
position: absolute;
left: -100px;
left: 0;
top: 0px;
background-color: $featureBackround;
}
.clips {
display: block;
position: relative;
padding: 0px 2px;
}
}
.clip {

View File

@@ -15,13 +15,14 @@ use crate::{
const PLAYER_CSS: Asset = asset!("/assets/styling/player.scss");
#[component]
pub fn Player() -> Element {
pub fn Player() -> Element
{
let mut selected_date = use_signal(|| {
let now = Local::now();
Some(Date::from_ordinal_date(now.year(), now.ordinal() as u16).unwrap())
});
let mut zoom = use_signal(|| 1.0 as f32);
let mut time = use_signal(|| 0 as i64);
let time = use_signal(|| 0 as i64);
let playbackResult = use_resource(use_reactive!(|(selected_date)| async move {
let mut client = get_rpc_client();
info!("Load data");
@@ -30,17 +31,22 @@ pub fn Player() -> Element {
let result = client
.get_media_playback(MediaPlaybackRequest { date: Some(from) })
.await;
if let Ok(entries) = result {
if let Ok(entries) = result
{
let res = entries.into_inner();
return Ok(res);
} else {
}
else
{
let err = result.err().unwrap();
let msg = err.message();
return Err(format!("Failed to load results: {msg}"));
}
}));
let info = match playbackResult.cloned() {
Some(value) => match value {
let info = match playbackResult.cloned()
{
Some(value) => match value
{
Ok(result) => Some(result),
Err(_) => None,
},
@@ -73,10 +79,10 @@ pub fn Player() -> Element {
selected_date.set(v);
},
DatePickerInput{}
}
},
}
Viewport { }
Timeline { playbackInfo: info, zoom, time }
Viewport { playback_info: info.clone(), time }
Timeline { playback_info: info, zoom, time }
}
}
}

View File

@@ -1,37 +1,67 @@
use chrono::DateTime;
use dioxus::prelude::*;
use crate::rpc::azki::{MediaChannel, MediaEntry, PlaybackInfo};
#[component]
pub fn Timeline(playbackInfo: Option<PlaybackInfo>, zoom: Signal<f32>, time: Signal<i64>) -> Element {
return match playbackInfo {
pub fn Timeline(playback_info: Option<PlaybackInfo>, zoom: Signal<f32>, time: Signal<i64>) -> Element
{
return match playback_info
{
Some(info) => rsx! {
div{
id: "timeline",
PlayHead { time, zoom }
TrackList { channels: info.channels, start: info.date.unwrap().seconds, zoom }
TrackList { channels: info.channels, start: info.date.unwrap().seconds, zoom, time }
}
},
None => rsx! {
div{
id: "timeline",
PlayHead { time, zoom }
TrackList { channels: Vec::new(), start: 0, zoom }
TrackList { channels: Vec::new(), start: 0, zoom, time }
}
},
};
}
#[component]
fn PlayHead(time: Signal<i64>, zoom: Signal<f32>) -> Element {
rsx! {}
fn PlayHead(start: i64, time: Signal<i64>, zoom: Signal<f32>) -> Element
{
let time_string = get_time_string(start, time());
let p = (time() as f32 / SEC_PER_DAY) * 100.0;
rsx! {
div {
id: "playhead",
input {
style: "width: calc({zoom() * 100.0}%);",
type: "range",
value: "{time()}",
max: "{SEC_PER_DAY}",
oninput: move |e|{
if let Ok(t) = e.value().parse() {
time.set(t);
}
}
},
div{
class: "time",
"{time_string}"
}
div{
class: "indicator",
style: "left: {p}%"
}
}
}
}
#[component]
fn TrackList(channels: Vec<MediaChannel>, start: i64, zoom: Signal<f32>) -> Element {
fn TrackList(channels: Vec<MediaChannel>, start: i64, zoom: Signal<f32>, time: Signal<i64>) -> Element
{
rsx! {
div {
id: "tracklist",
PlayHead { start, time, zoom }
{channels.iter().map(|c|rsx!{
TimelineTrack { channel: c.clone(), start, zoom }
})}
@@ -40,19 +70,24 @@ fn TrackList(channels: Vec<MediaChannel>, start: i64, zoom: Signal<f32>) -> Elem
}
#[component]
fn TimelineTrack(channel: MediaChannel, start: i64, zoom: Signal<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}%);",
TrackLabel { channel: channel.clone() },
{channel.videos.iter().map(|m|rsx!{Clip{media: m.clone(), start}})}
div{
class: "clips",
{channel.videos.iter().map(|m|rsx!{Clip{media: m.clone(), start}})}
}
}
}
}
#[component]
fn TrackLabel(channel: MediaChannel) -> Element {
fn TrackLabel(channel: MediaChannel) -> Element
{
rsx! {
div{
class: "label",
@@ -63,7 +98,8 @@ fn TrackLabel(channel: MediaChannel) -> Element {
const SEC_PER_DAY: f32 = 86400.0;
#[component]
fn Clip(media: MediaEntry, start: i64) -> Element {
fn Clip(media: MediaEntry, start: i64) -> Element
{
let meta = media.metadata.unwrap();
let date = media.date.unwrap();
let timestamp = date.seconds - start;
@@ -73,7 +109,13 @@ fn Clip(media: MediaEntry, start: i64) -> Element {
div{
class: "clip",
style: "width: {duration}%; left: {offset}%",
"{timestamp / 60 / 60}"
}
}
}
fn get_time_string(start: i64, time: i64) -> String
{
let date_time = DateTime::from_timestamp_secs(start + time).expect("Failed to convert time");
let local = date_time.naive_local();
return local.to_string();
}

View File

@@ -1,10 +1,76 @@
use dioxus::prelude::*;
use crate::{
rpc::azki::{MediaChannel, MediaEntry, PlaybackInfo},
MEDIA_HOST,
};
#[component]
pub fn Viewport() -> Element {
pub fn Viewport(playback_info: Option<PlaybackInfo>, time: Signal<i64>) -> Element
{
match playback_info
{
Some(info) => rsx! { ViewportFull{playback_info: info, time} },
None => rsx! { EmptyViewport{} },
}
}
#[component]
fn ViewportFull(playback_info: PlaybackInfo, time: Signal<i64>) -> Element
{
let start = playback_info.date.expect("Failed to get date of playback").seconds;
let cur_time = time();
rsx! {
div{
id: "viewport"
id: "viewport",
{playback_info.channels.iter().map(|c| {
let entry = get_entry(c, start, cur_time);
rsx!{
Video{ entry }
}
})}
}
}
}
#[component]
fn EmptyViewport() -> Element
{
rsx! {
div{
id: "viewport",
"Empty"
}
}
}
fn get_entry(channel: &MediaChannel, start: i64, time: i64) -> Option<MediaEntry>
{
return channel
.videos
.iter()
.find(|e| {
let meta = e.metadata.unwrap();
let date = e.date.unwrap();
let timestamp = date.seconds - start;
return timestamp <= time && timestamp + meta.duration as i64 >= time;
})
.map(|m| m.clone());
}
#[component]
fn Video(entry: Option<MediaEntry>) -> Element
{
match entry
{
Some(entry) => rsx! {
video{
autoplay: "true",
muted: "true",
src: "{MEDIA_HOST}/m/v/{entry.id}"
}
},
None => rsx! { div{ "No Video" }},
}
}

View File

@@ -7,11 +7,17 @@ mod route;
mod rpc;
mod views;
#[cfg(debug_assertions)]
pub const MEDIA_HOST: &'static str = "http://localhost:5177";
#[cfg(not(debug_assertions))]
pub const MEDIA_HOST: &'static str = "";
#[cfg(debug_assertions)]
pub const RPC_HOST: &'static str = "http://localhost:5177";
#[cfg(not(debug_assertions))]
pub const RPC_HOST: &'static str = "https://grpc.aoba.app:8443";
fn main() {
fn main()
{
dioxus::launch(App);
}