Skip to content

Commit

Permalink
Feature/notifications (#9)
Browse files Browse the repository at this point in the history
* basic mock with no styles

* new media appears in list

* media inits properly

* mock with notifications about new media

* rename toast -> notification

* notifs with sse

* update TODOs

* rm unnused type def
  • Loading branch information
littlebenlittle authored Jul 25, 2024
1 parent 2d34897 commit 11b16bd
Show file tree
Hide file tree
Showing 12 changed files with 327 additions and 190 deletions.
28 changes: 15 additions & 13 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@
- [x] Select should preserve scroll
- [x] Filter media in selector by query
- [x] Media detail on hover
- [ ] Toaster notifications
- [ ] new media
- [x] notifications
- [x] new media
- [x] Uploads

- [ ] hightlight new media items
- [ ] improve notification tray look and feel

### API

- [ ] Persist storage on server
Expand All @@ -20,10 +22,18 @@
- [ ] on-disk kv storage
- [ ] sqlite storage
- [ ] garbage collection instead of deletion
- [ ] Improve metadata automation
- [x] Improve metadata automation
- [x] `ffprobe` to mine metadata
- ~~[ ] Use collections for nested directories~~
- [x] Recurse directories for media discovery

### Protocol

- [ ] Bidi communication
- [ ] Send ack for accepted updates
- [ ] Abstractions
- [ ] Transport (req+sse, ws, quic)
- [ ] Remotes (servers, peers)

## Maintenance

Expand All @@ -42,19 +52,11 @@
- [ ] Resumable
- [ ] Upload progress bars
- [ ] Use object storage
- [ ] Alternative protocols (ws,quic)
- ~~[ ] Alternative protocols (ws,quic)~~
- [ ] API tests

## PR

- [x] Dry demo
- [ ] User docs
- [x] GitHub pages automation

## Under Consideration

- [x] better data model and sync procedure (CRDT?)
- [x] database in browser storage
- [x] only pull out-of-date items from server
- [ ] ~~treat browser storage as write-back cache~~
- [ ] ~~Consider HTTP/2 for event streams~~
57 changes: 53 additions & 4 deletions api/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ import (
"bytes"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"os/exec"
"path"
"strings"
"sync"

"github.com/fsnotify/fsnotify"
"github.com/gin-gonic/gin"
Expand Down Expand Up @@ -39,7 +41,7 @@ func main() {

path_to_id := make(map[string]string)
media := NewMemCollection()
index := func(p string) {
index := func(p string) gin.H {
title, format := extract_title_format(p)
if format == "unknown" {
title, format = ffprobe(p)
Expand All @@ -48,25 +50,72 @@ func main() {
id, err := media.Create(url, title, format)
if err != nil {
log.Print(err)
return nil
}
path_to_id[p] = id
return gin.H{
"id": id,
"title": title,
"format": format,
"url": url,
}

}
walk("/data", index)

clients := make(map[chan gin.H]struct{})
mu := sync.Mutex{}
watch("/data", func(ev fsnotify.Event) {
switch ev.Op {
case fsnotify.Create:
log.Printf("create %s", ev.Name)
index(ev.Name)
item := index(ev.Name)
mu.Lock()
log.Printf("clients: %d", len(clients))
for client := range clients {
client <- item
}
mu.Unlock()
case fsnotify.Rename, fsnotify.Remove:
log.Printf("remove %s", ev.Name)
id := path_to_id[ev.Name]
media.Drop(id)
if err := media.Drop(id); err != nil {
log.Print(err)
}
}
})

router := gin.Default()
add_collection(router, media, "media")

router.GET("/events/media", func(c *gin.Context) {
c.Writer.Header().Set("Content-Type", "text/event-stream")
c.Writer.Header().Set("Cache-Control", "no-cache")
c.Writer.Header().Set("Connection", "keep-alive")
c.Writer.Header().Set("Transfer-Encoding", "chunked")
ch := make(chan gin.H)
log.Printf("new client")
mu.Lock()
clients[ch] = struct{}{}
mu.Unlock()
defer func() {
log.Printf("closing client channel")
mu.Lock()
close(ch)
delete(clients, ch)
mu.Unlock()
}()
c.Stream(func(_ io.Writer) bool {
if event, ok := <-ch; ok {
log.Printf("sending event to client")
c.SSEvent("message", event)
log.Printf("event sent")
return true
}
return false
})
})

log.Fatal(router.Run())
}

Expand Down Expand Up @@ -134,7 +183,7 @@ func add_collection(router *gin.Engine, coll Collection, name string) {
})
}

func walk(dir string, f func(string)) {
func walk(dir string, f func(string) gin.H) {
if entries, err := os.ReadDir(dir); err != nil {
log.Print(err)
} else {
Expand Down
8 changes: 7 additions & 1 deletion env.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
#!/bin/bash

function serve {
RUSTFLAGS=--cfg=web_sys_unstable_apis APP_BASE_PATH=media-manager trunk serve --features=demo
pushd ui || exit 1
RUSTFLAGS=--cfg=web_sys_unstable_apis APP_BASE_PATH=media-manager trunk serve --features=demo
popd || exit 1
}

function nginx-reload {
podman-compose -f compose.yaml exec nginx nginx -s reload
}
4 changes: 2 additions & 2 deletions nginx.conf
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,8 @@ http {
proxy_pass http://api:8080/;
}

location ~ /api/(media|jobs)/subscribe {
# proxy_pass http://api:8080/events;
location /api/events/ {
proxy_pass http://api:8080/events/;
# add_header 'Access-Control-Allow-Origin' '*' always;
proxy_http_version 1.1;
proxy_read_timeout 300s;
Expand Down
9 changes: 9 additions & 0 deletions ui/src/client/http.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use crate::{data::MediaItem, log};
use leptos::*;

#[inline]
fn origin() -> String {
Expand Down Expand Up @@ -64,3 +65,11 @@ pub async fn upload_file(file: web_sys::File) {
}
};
}

pub fn new_media() -> Signal<Option<MediaItem>> {
let event_source = leptos_use::use_event_source::<MediaItem, leptos_use::utils::JsonCodec>(
&format!("{}/api/events/media", origin()),
);
create_effect(move |_| log!("{:?}", event_source.data.get()));
event_source.data
}
39 changes: 35 additions & 4 deletions ui/src/client/mock.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! Generate fake data for faster debugging cycles.
use crate::{data::MediaItem, log};
use std::sync::Mutex;
use std::{array::IntoIter, sync::Mutex};

lazy_static::lazy_static! {
static ref MEDIA: Mutex<Option<Vec<MediaItem>>> = Mutex::new(None);
Expand All @@ -18,9 +18,11 @@ fn init_media() -> Option<Vec<MediaItem>> {
});
}
for i in 0..5 {
let id = (7 + i).to_string();
let title = format!("Big Buck Bunny {}", id);
m.push(MediaItem {
id: (7 + i).to_string(),
title: format!("Big Buck Bunny {}", i),
id,
title,
format: "webm".to_string(),
url: "https://dl6.webmfiles.org/big-buck-bunny_trailer.webm".to_owned(),
});
Expand Down Expand Up @@ -54,6 +56,35 @@ pub async fn update_media(id: String, field: String, value: String) -> anyhow::R
Ok(true)
}

pub async fn upload_file(file: web_sys::File) {
pub async fn upload_file(_file: web_sys::File) {
log!("File uploads not supported in demo mode!");
}

use leptos::*;

pub fn new_media() -> Signal<Option<MediaItem>> {
let (data, set_data) = create_signal(None::<MediaItem>);
let interval = leptos_use::use_interval(10_000);
create_effect(move |items| {
(interval.counter).track();
let mut items = if items.is_none() {
[1, 2, 3, 4].into_iter()
} else {
items.unwrap()
};
if let Some(i) = items.next() {
let id = (12 + i).to_string();
let title = format!("Big Buck Bunny {}", id);
set_data(Some(MediaItem {
id,
title,
format: "webm".to_string(),
url: "https://dl6.webmfiles.org/big-buck-bunny_trailer.webm".to_owned(),
}));
} else {
(interval.pause)()
}
items
});
return data.into();
}
33 changes: 1 addition & 32 deletions ui/src/components.rs
Original file line number Diff line number Diff line change
@@ -1,26 +1,8 @@
pub mod dashboard;
pub mod notification_tray;

use leptos::*;

#[component]
pub fn Loading(#[prop(optional)] what: Option<String>) -> impl IntoView {
view! {
<p>
"Loading"
{if let Some(what) = what {
view! {
" "
{what}
}
.into_view()
} else {
view! {}.into_view()
}}
"..."
</p>
}
}

#[component]
pub fn LoremIpsum() -> impl IntoView {
view! {
Expand All @@ -32,19 +14,6 @@ pub fn LoremIpsum() -> impl IntoView {
}
}

#[component]
pub fn SyncButton<T: 'static>(action: Action<(), T>, pending: ReadSignal<bool>) -> impl IntoView {
view! {
<button
on:click=move |_| action.dispatch(())
prop:disabled=move || pending.get()
class:disabled-button=move || pending.get()
>
{move || if pending.get() { "Syncing..." } else { "Sync" }}
</button>
}
}

#[component]
pub fn ClickToEdit<Cb>(value: String, onset: Cb) -> impl IntoView
where
Expand Down
Loading

0 comments on commit 11b16bd

Please sign in to comment.