initialized empty playlist explorer view

This commit is contained in:
euzu
2025-07-29 18:14:43 +02:00
parent d691a29be1
commit 4ada26c207
25 changed files with 308 additions and 140 deletions
+5 -5
View File
@@ -8,7 +8,7 @@ use axum::{
response::IntoResponse,
};
use log::{error, info};
use shared::model::{ProtocolHandler, ProtocolHandlerMemory, ProtocolMessage, WsCloseCode, PROTOCOL_VERSION};
use shared::model::{ProtocolHandler, ProtocolHandlerMemory, ProtocolMessage, UserRole, WsCloseCode, PROTOCOL_VERSION};
use std::sync::Arc;
// WebSocket upgrade handler
@@ -114,11 +114,11 @@ async fn handle_protocol_message(
Ok(ProtocolMessage::Auth(auth_token)) => {
mem.token = None;
if verify_auth_admin_token(&auth_token, secret_key) {
mem.admin = true;
mem.role = UserRole::Admin;
mem.token = Some(auth_token);
Some(ProtocolMessage::Authorized)
} else if verify_auth_user_token(&auth_token, secret_key) {
mem.admin = false;
mem.role = UserRole::User;
mem.token = Some(auth_token);
Some(ProtocolMessage::Authorized)
} else {
@@ -127,7 +127,7 @@ async fn handle_protocol_message(
},
Ok(ProtocolMessage::StatusRequest(auth_token)) => {
if !auth || verify_auth_admin_token(&auth_token, secret_key) {
mem.admin = true;
mem.role = UserRole::Admin;
mem.token = Some(auth_token);
let status = create_status_check(app_state).await;
Some(ProtocolMessage::StatusResponse(status))
@@ -192,7 +192,7 @@ async fn handle_event_message(socket: &mut WebSocket, event: EventMessage, handl
match handler {
ProtocolHandler::Version(_) => {},
ProtocolHandler::Default(mem) => {
if mem.admin {
if mem.role.is_admin() {
match event {
EventMessage::ActiveUserChange(users, connections) => {
let msg = ProtocolMessage::ActiveUserResponse(users, connections)
+1 -1
View File
@@ -326,7 +326,7 @@ impl AppConfig {
}
match utils::read_file_as_bytes(&PathBuf::from(&file_path)) {
Ok(data) => Some(TransportStreamBuffer::new(data, )),
Ok(data) => Some(TransportStreamBuffer::new(data)),
Err(err) => {
error!("Failed to load a resource file: {} {err}", file_path.display());
None
+1 -1
View File
@@ -122,7 +122,7 @@ macros::from_impl!(Config);
impl From<&ConfigDto> for Config {
fn from(dto: &ConfigDto) -> Self {
Config {
threads: 0,
threads: dto.threads,
api: ConfigApi::from(&dto.api),
working_dir: dto.working_dir.to_string(),
backup_dir: dto.backup_dir.clone(),
+1 -4
View File
@@ -150,7 +150,7 @@ impl ConfigInput {
let mut aliases: Vec<ConfigInputAlias> = batch_aliases.into_iter()
.map(ConfigInputAlias::from)
.collect();
if let Some(mut first) = aliases.pop() {
if let Some(mut first) = aliases.remove(0) {
self.username = first.username.take();
self.password = first.password.take();
self.url = first.url.trim().to_string();
@@ -222,9 +222,6 @@ pub fn get_batch_aliases(input_type: InputType, url: &str) -> Result<Option<(Pat
if input_type == InputType::M3uBatch || input_type == InputType::XtreamBatch {
return match utils::csv_read_inputs(input_type, url) {
Ok((file_path, mut batch_aliases)) => {
if !batch_aliases.is_empty() {
batch_aliases.reverse();
}
Ok(Some((file_path, batch_aliases)))
}
Err(err) => {
-1
View File
@@ -14,7 +14,6 @@ macro_rules! apply_batch_aliases {
($target:expr, $batch_aliases:expr) => {{
if !$batch_aliases.is_empty() {
if !$batch_aliases.is_empty() {
$batch_aliases.reverse();
if let Some(aliases) = $target.aliases.as_mut() {
aliases.extend($batch_aliases);
} else {
+18 -9
View File
@@ -5,18 +5,27 @@ use serde::{Deserialize, Serialize};
pub const PROTOCOL_VERSION: u8 = 1;
pub struct ProtocolHandlerMemory {
pub token: Option<String>,
pub admin: bool,
#[derive(Default)]
pub enum UserRole {
#[default]
Unauthorized,
Admin,
User,
}
impl Default for ProtocolHandlerMemory {
fn default() -> Self {
Self {
token: None,
admin: false,
}
impl UserRole {
pub fn is_admin(&self) -> bool {
self.eq(&UserRole::Admin)
}
pub fn is_user(&self) -> bool {
self.eq(&UserRole::User)
}
}
#[derive(Default)]
pub struct ProtocolHandlerMemory {
pub token: Option<String>,
pub role: UserRole,
}
pub enum ProtocolHandler {
File diff suppressed because one or more lines are too long
-1
View File
@@ -1,6 +1,5 @@
{
"tabTitle": "tuliprox",
"appTitle": "tuliprox",
"appLogo": "/assets/tuliprox-logo.svg",
"api": {
"apiUrl": "/api/v1/",
+3
View File
@@ -13,6 +13,9 @@
width: 2.5rem;
height: 2.5rem;
}
.icon-AppTitle {
width: 100px !important;
}
&-toolbar {
display: flex;
align-items: center;
+2 -1
View File
@@ -19,7 +19,8 @@
@forward "components/dashboard/stats_view";
@forward "components/list_view";
@forward "components/userlist/userlist_view";
@forward "components/playlist/playlist_view";
@forward "components/playlist/playlist_editor_view";
@forward "components/playlist/playlist_explorer_view";
@forward "components/playlist/assistant/playlist_assistant";
@forward "components/reveal_content";
@forward "components/hide_content";
@@ -1,5 +1,5 @@
.tp__playlist-create,
.tp__playlist-list {
.tp__playlist-editor-create,
.tp__playlist-editor-list {
&__body {
@@ -0,0 +1,46 @@
.tp__playlist-editor-create,
.tp__playlist-editor-list {
&__body {
> :nth-child(even) {
border-left: 2px solid var(--output-xtream-color);
}
> :nth-child(odd) {
border-left: 2px solid var(--output-hdhomerun-color);
}
}
&__source {
display: flex;
flex-flow: column;
border-top: 1px solid var(--border-color);
border-bottom: 1px solid var(--border-color);
border-right: 1px solid var(--border-color);
box-sizing: border-box;
border-top-right-radius: var(--border-radius);
border-bottom-right-radius: var(--border-radius);
padding: var(--padding-mini);
gap: var(--gap-default);
.svg-icon {
width: 1.2rem;
min-width: 1.2rem;
min-height: 1.2rem;
}
&-label,
&-inputs > .tp__collapse-panel__header > .tp__collapse-panel__header-title {
display: flex;
flex-flow: row;
align-items: center;
gap: var(--gap-default);
font-size: 1.2rem;
}
&-label {
color: var(--modest-text-color);
fill: var(--modest-text-color);
}
}
}
-1
View File
@@ -34,4 +34,3 @@ body {
max-width: 100%;
background-color: inherit;
}
+49 -10
View File
@@ -1,18 +1,17 @@
use crate::app::components::{AppIcon, DashboardView, IconButton, InputRow, Panel, PlaylistEditorView, PlaylistExplorerView, Sidebar, StatsView, UserlistView};
use crate::app::context::{ConfigContext, PlaylistContext, StatusContext};
use crate::hooks::{use_server_status, use_service_context};
use crate::model::ViewType;
use crate::services::WsMessage;
use shared::model::{AppConfigDto, StatusCheck};
use std::future;
use std::rc::Rc;
use yew::prelude::*;
use yew::suspense::use_future;
use shared::model::{AppConfigDto, StatusCheck};
use crate::app::components::{IconButton, Sidebar, DashboardView, PlaylistView, Panel, UserlistView, StatsView};
use crate::app::context::{ConfigContext, StatusContext};
use crate::model::ViewType;
use crate::hooks::{use_server_status, use_service_context};
use crate::services::WsMessage;
#[function_component]
pub fn Home() -> Html {
let services = use_service_context();
let app_title = services.config.ui_config.app_title.as_ref().map_or("tuliprox", |v| v.as_str());
let config = use_state(|| None::<Rc<AppConfigDto>>);
let status = use_state(|| None::<Rc<StatusCheck>>);
@@ -65,6 +64,32 @@ pub fn Home() -> Html {
});
}
let sources = use_memo(config.clone(), |config_ctx| {
if let Some(cfg) = config_ctx.as_ref() {
let mut sources = vec![];
for source in &cfg.sources.sources {
let mut inputs = vec![];
for input_cfg in &source.inputs {
let input = Rc::new(input_cfg.clone());
inputs.push(Rc::new(InputRow::Input(Rc::clone(&input))));
if let Some(aliases) = input_cfg.aliases.as_ref() {
for alias in aliases {
inputs.push(Rc::new(InputRow::Alias(Rc::new(alias.clone()), Rc::clone(&input))));
}
}
}
let mut targets = vec![];
for target in &source.targets {
targets.push(Rc::new(target.clone()));
}
sources.push((inputs, targets));
}
Some(Rc::new(sources))
} else {
None
}
});
// {
// let services_ctx = services.clone();
// let status_signal = status.clone();
@@ -126,12 +151,15 @@ pub fn Home() -> Html {
// }
let config_context = ConfigContext {
config: (*config).clone(),
config: (*config).clone(),
};
let status_context = StatusContext {
status: (*status).clone(),
};
let playlist_context = PlaylistContext {
sources: sources.clone(),
};
//<div class={"app-header__toolbar"}><select onchange={handle_language} defaultValue={i18next.language}>{services.config().getUiConfig().languages.map(l => <option key={l} value={l}>{l}</option>)}</select></div>
// <div class={"app-header__toolbar"}><button data-tooltip={preferencesVisible ? "LABEL.PLAYLIST_BROWSER" : "LABEL.CONFIGURATION"} onClick={handlePreferences}>{getIconByName(preferencesVisible ? "Live" : "Config")}</button></div>
@@ -139,12 +167,19 @@ pub fn Home() -> Html {
html! {
<ContextProvider<ConfigContext> context={config_context}>
<ContextProvider<StatusContext> context={status_context}>
<ContextProvider<PlaylistContext> context={playlist_context}>
<div class="tp__app">
<Sidebar onview={handle_view_change}/>
<div class="tp__app-main">
<div class="tp__app-main__header tp__app-header">
{app_title}
{
if let Some(ref title) = services.config.ui_config.app_title {
html! { title.as_str() }
} else {
html! { <AppIcon name="AppTitle" /> }
}
}
<div class={"tp__app-header-toolbar"}>
// <select onchange={handle_language} defaultValue={i18next.language}>{
// services.config().getUiConfig().languages.map(l => <option key={l} value={l}>{l}</option>)
@@ -170,7 +205,10 @@ pub fn Home() -> Html {
<StatsView/>
</Panel>
<Panel class="tp__full-width" value={ViewType::PlaylistEditor.to_string()} active={view_visible.to_string()}>
<PlaylistView/>
<PlaylistEditorView/>
</Panel>
<Panel class="tp__full-width" value={ViewType::PlaylistExplorer.to_string()} active={view_visible.to_string()}>
<PlaylistExplorerView/>
</Panel>
<Panel class="tp__full-width" value={ViewType::Users.to_string()} active={view_visible.to_string()}>
<UserlistView/>
@@ -178,6 +216,7 @@ pub fn Home() -> Html {
</div>
</div>
</div>
</ContextProvider<PlaylistContext>>
</ContextProvider<StatusContext>>
</ContextProvider<ConfigContext>>
}
+3 -4
View File
@@ -1,16 +1,15 @@
use yew::prelude::*;
use yew_i18n::use_translation;
use crate::app::components::{PlaylistContext, PlaylistPage, TextButton};
use crate::app::components::{PlaylistEditorContext, PlaylistEditorPage, TextButton};
#[function_component]
pub fn PlaylistCreate() -> Html {
let translate = use_translation();
let playlist_ctx = use_context::<PlaylistContext>().expect("Playlist context not found");
let playlist_editor_ctx = use_context::<PlaylistEditorContext>().expect("PlaylistEditor context not found");
let handle_back = {
let playlist_ctx = playlist_ctx.clone();
Callback::from(move |_| {
playlist_ctx.active_page.set(PlaylistPage::List);
playlist_editor_ctx.active_page.set(PlaylistEditorPage::List);
})
};
+4 -3
View File
@@ -1,16 +1,17 @@
use yew::prelude::*;
use yew_i18n::use_translation;
use crate::app::components::{AppIcon, CollapsePanel, InputTable, PlaylistContext, PlaylistPage, TargetTable, TextButton};
use crate::app::components::{AppIcon, CollapsePanel, InputTable, PlaylistContext, PlaylistEditorPage, TargetTable, TextButton};
use crate::app::context::PlaylistEditorContext;
#[function_component]
pub fn PlaylistList() -> Html {
let translate = use_translation();
let playlist_ctx = use_context::<PlaylistContext>().expect("Playlist context not found");
let playlist_editor_ctx = use_context::<PlaylistEditorContext>().expect("PlaylistEditor context not found");
let handle_create = {
let playlist_ctx = playlist_ctx.clone();
Callback::from(move |_| {
playlist_ctx.active_page.set(PlaylistPage::Create);
playlist_editor_ctx.active_page.set(PlaylistEditorPage::Create);
})
};
+8 -4
View File
@@ -1,10 +1,12 @@
mod assistant;
mod list;
mod create;
mod page;
mod playlist_editor_page;
mod playlist_explorer_page;
mod target_table;
mod target;
mod playlist_view;
mod playlist_editor_view;
mod playlist_explorer_view;
mod processing;
mod mappings;
mod filter_view;
@@ -19,7 +21,8 @@ pub use self::assistant::*;
pub use self::list::*;
pub use self::create::*;
pub use crate::app::context::*;
pub use self::page::*;
pub use self::playlist_editor_page::*;
pub use self::playlist_explorer_page::*;
pub use self::target_table::*;
pub use self::target::*;
pub use self::processing::*;
@@ -27,7 +30,8 @@ pub use self::mappings::*;
pub use self::filter_view::*;
pub use self::input_table::*;
pub use self::input::*;
pub use self::playlist_view::*;
pub use self::playlist_editor_view::*;
pub use self::playlist_explorer_view::*;
pub fn make_tags(data: &[(bool, &str)], translate: &YewI18n) -> Vec<Rc<Tag>> {
data.iter()
@@ -4,24 +4,24 @@ use shared::error::{TuliproxError,TuliproxErrorKind};
use shared::info_err;
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum PlaylistPage {
pub enum PlaylistEditorPage {
List,
Create,
}
impl FromStr for PlaylistPage {
impl FromStr for PlaylistEditorPage {
type Err = TuliproxError;
fn from_str(s: &str) -> Result<Self, TuliproxError> {
match s.to_lowercase().as_str() {
"list" => Ok(PlaylistPage::List),
"create" => Ok(PlaylistPage::Create),
"list" => Ok(PlaylistEditorPage::List),
"create" => Ok(PlaylistEditorPage::Create),
_ => Err(info_err!(format!("Unknown page type: {s}"))),
}
}
}
impl Display for PlaylistPage {
impl Display for PlaylistEditorPage {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{}", match *self {
Self::List => "list",
@@ -0,0 +1,54 @@
use crate::app::components::{Breadcrumbs, Panel, PlaylistCreate, PlaylistEditorPage, PlaylistList};
use crate::app::context::PlaylistEditorContext;
use std::rc::Rc;
use yew::prelude::*;
use yew_i18n::use_translation;
#[function_component]
pub fn PlaylistEditorView() -> Html {
let translate = use_translation();
let breadcrumbs = use_state(|| Rc::new(vec![translate.t("LABEL.PLAYLISTS"), translate.t("LABEL.LIST")]));
let active_page = use_state(|| PlaylistEditorPage::List);
let handle_breadcrumb_select = {
let view_visible = active_page.clone();
Callback::from(move |(_name, index)| {
if index == 0 && *view_visible != PlaylistEditorPage::List {
view_visible.set(PlaylistEditorPage::List);
}
})
};
{
let breadcrumbs = breadcrumbs.clone();
let view_visible_dep = active_page.clone();
let view_visible = active_page.clone();
let translate = translate.clone();
use_effect_with(view_visible_dep, move |_| {
match *view_visible {
PlaylistEditorPage::List => breadcrumbs.set(Rc::new(vec![translate.t("LABEL.PLAYLISTS"), translate.t("LABEL.LIST")])),
PlaylistEditorPage::Create => breadcrumbs.set(Rc::new(vec![translate.t("LABEL.PLAYLISTS"), translate.t("LABEL.CREATE")])),
}
});
};
let context = PlaylistEditorContext {
active_page: active_page.clone(),
};
html! {
<ContextProvider<PlaylistEditorContext> context={context}>
<div class="tp__playlist-editor-view tp__list-view">
<Breadcrumbs items={&*breadcrumbs} onclick={ handle_breadcrumb_select }/>
<div class="tp__playlist-editor-view__body tp__list-view__body">
<Panel value={PlaylistEditorPage::List.to_string()} active={active_page.to_string()}>
<PlaylistList />
</Panel>
<Panel value={PlaylistEditorPage::Create.to_string()} active={active_page.to_string()}>
<PlaylistCreate />
</Panel>
</div>
</div>
</ContextProvider<PlaylistEditorContext>>
}
}
@@ -0,0 +1,31 @@
use std::fmt::Display;
use std::str::FromStr;
use shared::error::{TuliproxError,TuliproxErrorKind};
use shared::info_err;
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum PlaylistExplorerPage {
List,
Create,
}
impl FromStr for PlaylistExplorerPage {
type Err = TuliproxError;
fn from_str(s: &str) -> Result<Self, TuliproxError> {
match s.to_lowercase().as_str() {
"list" => Ok(PlaylistExplorerPage::List),
"create" => Ok(PlaylistExplorerPage::Create),
_ => Err(info_err!(format!("Unknown page type: {s}"))),
}
}
}
impl Display for PlaylistExplorerPage {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{}", match *self {
Self::List => "list",
Self::Create => "create",
})
}
}
@@ -0,0 +1,54 @@
use crate::app::components::{Breadcrumbs, Panel, PlaylistExplorerPage};
use crate::app::context::PlaylistExplorerContext;
use std::rc::Rc;
use yew::prelude::*;
use yew_i18n::use_translation;
#[function_component]
pub fn PlaylistExplorerView() -> Html {
let translate = use_translation();
let breadcrumbs = use_state(|| Rc::new(vec![translate.t("LABEL.PLAYLISTS"), translate.t("LABEL.LIST")]));
let active_page = use_state(|| PlaylistExplorerPage::List);
let handle_breadcrumb_select = {
let view_visible = active_page.clone();
Callback::from(move |(_name, index)| {
if index == 0 && *view_visible != PlaylistExplorerPage::List {
view_visible.set(PlaylistExplorerPage::List);
}
})
};
{
let breadcrumbs = breadcrumbs.clone();
let view_visible_dep = active_page.clone();
let view_visible = active_page.clone();
let translate = translate.clone();
use_effect_with(view_visible_dep, move |_| {
match *view_visible {
PlaylistExplorerPage::List => breadcrumbs.set(Rc::new(vec![translate.t("LABEL.PLAYLISTS"), translate.t("LABEL.LIST")])),
PlaylistExplorerPage::Create => breadcrumbs.set(Rc::new(vec![translate.t("LABEL.PLAYLISTS"), translate.t("LABEL.CREATE")])),
}
});
};
let context = PlaylistExplorerContext {
active_page: active_page.clone(),
};
html! {
<ContextProvider<PlaylistExplorerContext> context={context}>
<div class="tp__playlist-explorer-view tp__list-view">
<Breadcrumbs items={&*breadcrumbs} onclick={ handle_breadcrumb_select }/>
<div class="tp__playlist-explorer-view__body tp__list-view__body">
<Panel value={PlaylistExplorerPage::List.to_string()} active={active_page.to_string()}>
{"List"}
</Panel>
<Panel value={PlaylistExplorerPage::Create.to_string()} active={active_page.to_string()}>
{"Create"}
</Panel>
</div>
</div>
</ContextProvider<PlaylistExplorerContext>>
}
}
@@ -1,82 +0,0 @@
use crate::app::components::{Breadcrumbs, InputRow, Panel, PlaylistContext, PlaylistCreate, PlaylistList, PlaylistPage};
use std::rc::Rc;
use yew::prelude::*;
use yew_i18n::use_translation;
use crate::app::context::ConfigContext;
#[function_component]
pub fn PlaylistView() -> Html {
let translate = use_translation();
let breadcrumbs = use_state(|| Rc::new(vec![translate.t("LABEL.PLAYLISTS"), translate.t("LABEL.LIST")]));
let active_page = use_state(|| PlaylistPage::List);
let config_ctx = use_context::<ConfigContext>().expect("Config context not found");
let handle_breadcrumb_select = {
let view_visible = active_page.clone();
Callback::from(move |(_name, index)| {
if index == 0 && *view_visible != PlaylistPage::List {
view_visible.set(PlaylistPage::List);
}
})
};
{
let breadcrumbs = breadcrumbs.clone();
let view_visible_dep = active_page.clone();
let view_visible = active_page.clone();
let translate = translate.clone();
use_effect_with(view_visible_dep, move |_| {
match *view_visible {
PlaylistPage::List => breadcrumbs.set(Rc::new(vec![translate.t("LABEL.PLAYLISTS"), translate.t("LABEL.LIST")])),
PlaylistPage::Create => breadcrumbs.set(Rc::new(vec![translate.t("LABEL.PLAYLISTS"), translate.t("LABEL.CREATE")])),
}
});
};
let sources = use_memo(config_ctx.config.map(|c| c.sources.clone()), |cfg_sources_opt| {
if let Some(cfg_sources) = cfg_sources_opt {
let mut sources = vec![];
for source in &cfg_sources.sources {
let mut inputs = vec![];
for input_cfg in &source.inputs {
let input = Rc::new(input_cfg.clone());
inputs.push(Rc::new(InputRow::Input(Rc::clone(&input))));
if let Some(aliases) = input_cfg.aliases.as_ref() {
for alias in aliases {
inputs.push(Rc::new(InputRow::Alias(Rc::new(alias.clone()), Rc::clone(&input))));
}
}
}
let mut targets = vec![];
for target in &source.targets {
targets.push(Rc::new(target.clone()));
}
sources.push((inputs, targets));
}
Some(Rc::new(sources))
} else {
None
}
});
let context = PlaylistContext {
sources,
active_page: active_page.clone(),
};
html! {
<ContextProvider<PlaylistContext> context={context}>
<div class="tp__playlist-view tp__list-view">
<Breadcrumbs items={&*breadcrumbs} onclick={ handle_breadcrumb_select }/>
<div class="tp__playlist-view__body tp__list-view__body">
<Panel value={PlaylistPage::List.to_string()} active={active_page.to_string()}>
<PlaylistList />
</Panel>
<Panel value={PlaylistPage::Create.to_string()} active={active_page.to_string()}>
<PlaylistCreate />
</Panel>
</div>
</div>
</ContextProvider<PlaylistContext>>
}
}
+1 -1
View File
@@ -51,7 +51,7 @@ pub fn Sidebar(props: &SidebarProps) -> Html {
<CollapsePanel title={translate.t("LABEL.PLAYLIST")}>
<MenuItem icon="PlayArrowOutline" name={ViewType::PlaylistEditor.to_string()} label={translate.t("LABEL.PLAYLIST")}
onclick={&handle_menu_click}></MenuItem>
<MenuItem icon="Live" name={ViewType::PlaylistViewer.to_string()} label={translate.t("LABEL.PLAYLIST_VIEWER")}
<MenuItem icon="Live" name={ViewType::PlaylistExplorer.to_string()} label={translate.t("LABEL.PLAYLIST_VIEWER")}
onclick={&handle_menu_click}></MenuItem>
</CollapsePanel>
</div>
+12 -2
View File
@@ -1,16 +1,26 @@
use std::rc::Rc;
use yew::UseStateHandle;
use shared::model::{AppConfigDto, ConfigTargetDto, ProxyUserCredentialsDto, StatusCheck};
use crate::app::components::{InputRow, PlaylistPage, UserlistPage};
use crate::app::components::{InputRow, PlaylistEditorPage, PlaylistExplorerPage, UserlistPage};
type SingleSource = (Vec<Rc<InputRow>>, Vec<Rc<ConfigTargetDto>>);
#[derive(Clone, PartialEq)]
pub struct PlaylistContext {
pub sources: Rc<Option<Rc<Vec<SingleSource>>>>,
pub active_page: UseStateHandle<PlaylistPage>,
}
#[derive(Clone, PartialEq)]
pub struct PlaylistEditorContext {
pub active_page: UseStateHandle<PlaylistEditorPage>,
}
#[derive(Clone, PartialEq)]
pub struct PlaylistExplorerContext {
pub active_page: UseStateHandle<PlaylistExplorerPage>,
}
#[derive(Clone, PartialEq)]
pub struct TargetUser {
pub target: String,
+3 -3
View File
@@ -8,7 +8,7 @@ pub enum ViewType {
Stats,
Users,
PlaylistEditor,
PlaylistViewer
PlaylistExplorer
}
impl FromStr for ViewType {
@@ -20,7 +20,7 @@ impl FromStr for ViewType {
"stats" => Ok(ViewType::Stats),
"users" => Ok(ViewType::Users),
"playlist_editor" => Ok(ViewType::PlaylistEditor),
"playlist_viewer" => Ok(ViewType::PlaylistEditor),
"playlist_explorer" => Ok(ViewType::PlaylistExplorer),
_ => Err(info_err!(format!("Unknown view type: {s}"))),
}
}
@@ -33,7 +33,7 @@ impl fmt::Display for ViewType {
ViewType::Stats => "stats",
ViewType::Users => "users",
ViewType::PlaylistEditor => "playlist_editor",
ViewType::PlaylistViewer => "playlist_viewer",
ViewType::PlaylistExplorer => "playlist_explorer",
};
write!(f, "{s}")
}